Skip to content

Commit

Permalink
Merge branch 'next' into fix-workflow-editor-name-change-updates-slug…
Browse files Browse the repository at this point in the history
…-url
  • Loading branch information
LetItRock committed Nov 19, 2024
2 parents 40545d6 + 5afb77b commit 7aab670
Show file tree
Hide file tree
Showing 23 changed files with 533 additions and 196 deletions.
4 changes: 4 additions & 0 deletions apps/api/src/app/environments-v1/novu-bridge.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
RenderEmailOutputUsecase,
SmsOutputRendererUsecase,
} from './usecases/output-renderers';
import { DelayOutputRendererUsecase } from './usecases/output-renderers/delay-output-renderer.usecase';
import { DigestOutputRendererUsecase } from './usecases/output-renderers/digest-output-renderer.usecase';

@Module({
controllers: [NovuBridgeController],
Expand All @@ -36,6 +38,8 @@ import {
RenderEmailOutputUsecase,
ExpandEmailEditorSchemaUsecase,
HydrateEmailSchemaUseCase,
DelayOutputRendererUsecase,
DigestOutputRendererUsecase,
],
})
export class NovuBridgeModule {}
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { workflow } from '@novu/framework/express';
import {
ActionStep,
ChannelStep,
DelayOutput,
DigestOutput,
JsonSchema,
Step,
StepOptions,
StepOutput,
Workflow,
} from '@novu/framework/internal';
import { ActionStep, ChannelStep, JsonSchema, Step, StepOptions, StepOutput, Workflow } from '@novu/framework/internal';
import { NotificationStepEntity, NotificationTemplateEntity, NotificationTemplateRepository } from '@novu/dal';
import { StepTypeEnum } from '@novu/shared';
import { ConstructFrameworkWorkflowCommand } from './construct-framework-workflow.command';
Expand All @@ -22,6 +12,8 @@ import {
RenderEmailOutputUsecase,
SmsOutputRendererUsecase,
} from '../output-renderers';
import { DelayOutputRendererUsecase } from '../output-renderers/delay-output-renderer.usecase';
import { DigestOutputRendererUsecase } from '../output-renderers/digest-output-renderer.usecase';

@Injectable()
export class ConstructFrameworkWorkflow {
Expand All @@ -31,7 +23,9 @@ export class ConstructFrameworkWorkflow {
private emailOutputRendererUseCase: RenderEmailOutputUsecase,
private smsOutputRendererUseCase: SmsOutputRendererUsecase,
private chatOutputRendererUseCase: ChatOutputRendererUsecase,
private pushOutputRendererUseCase: PushOutputRendererUsecase
private pushOutputRendererUseCase: PushOutputRendererUsecase,
private delayOutputRendererUseCase: DelayOutputRendererUsecase,
private digestOutputRendererUseCase: DigestOutputRendererUsecase
) {}

async execute(command: ConstructFrameworkWorkflowCommand): Promise<Workflow> {
Expand Down Expand Up @@ -143,15 +137,15 @@ export class ConstructFrameworkWorkflow {
return step.digest(
stepId,
async (controlValues) => {
return controlValues as DigestOutput;
return this.digestOutputRendererUseCase.execute({ controlValues, fullPayloadForRender });
},
this.constructActionStepOptions(staticStep)
);
case StepTypeEnum.DELAY:
return step.delay(
stepId,
async (controlValues) => {
return controlValues as DelayOutput;
return this.delayOutputRendererUseCase.execute({ controlValues, fullPayloadForRender });
},
this.constructActionStepOptions(staticStep)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { DelayRenderOutput } from '@novu/shared';
import { RenderCommand } from './render-command';
import {
DelayTimeControlType,
DelayTimeControlZodSchema,
} from '../../../workflows-v2/shared/schemas/delay-control.schema';

@Injectable()
export class DelayOutputRendererUsecase {
execute(renderCommand: RenderCommand): DelayRenderOutput {
const delayTimeControlType: DelayTimeControlType = DelayTimeControlZodSchema.parse(renderCommand.controlValues);

return {
amount: delayTimeControlType.amount,
type: delayTimeControlType.type,
unit: delayTimeControlType.unit,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { DigestRenderOutput } from '@novu/shared';
import { RenderCommand } from './render-command';
import {
DigestControlSchemaType,
DigestControlZodSchema,
isDigestRegularControl,
isDigestTimedControl,
} from '../../../workflows-v2/shared/schemas/digest-control.schema';

@Injectable()
export class DigestOutputRendererUsecase {
execute(renderCommand: RenderCommand): DigestRenderOutput {
const parse: DigestControlSchemaType = DigestControlZodSchema.parse(renderCommand.controlValues);
if (
isDigestRegularControl(parse) &&
parse.amount &&
parse.unit &&
parse.lookBackWindow &&
parse.lookBackWindow.amount &&
parse.lookBackWindow.unit
) {
return {
amount: parse.amount,
unit: parse.unit,
digestKey: parse.digestKey,
lookBackWindow: {
amount: parse.lookBackWindow.amount,
unit: parse.lookBackWindow.unit,
},
};
}
if (isDigestTimedControl(parse) && parse.cron) {
return {
cron: parse.cron,
digestKey: parse.digestKey,
};
}
throw new InternalServerErrorException({
message: `Invalid digest control value data sent for rendering`,
controlValues: renderCommand.controlValues,
});
}
}
Original file line number Diff line number Diff line change
@@ -1,87 +1,66 @@
// Concrete Renderer for In-App Message Preview
import { InAppRenderOutput, RedirectTargetEnum } from '@novu/shared';
import { z } from 'zod';
import { Injectable } from '@nestjs/common';
import { RenderCommand } from './render-command';
import {
InAppActionType,
InAppControlType,
InAppControlZodSchema,
InAppRedirectType,
} from '../../../workflows-v2/shared';

@Injectable()
export class InAppOutputRendererUsecase {
execute(renderCommand: RenderCommand): InAppRenderOutput {
const inApp = InAppRenderOutputSchema.optional().parse(renderCommand.controlValues);
const inApp: InAppControlType = InAppControlZodSchema.parse(renderCommand.controlValues);
if (!inApp) {
throw new Error('Invalid in-app control value data');
}

const { primaryAction, secondaryAction, redirect } = inApp;

return {
subject: inApp.subject,
body: inApp.body,
avatar: inApp.avatar,
primaryAction:
inApp.primaryAction &&
inApp.primaryAction.label &&
inApp.primaryAction.redirect &&
inApp.primaryAction.redirect.url
? {
label: inApp.primaryAction.label,
redirect: {
url: inApp.primaryAction.redirect.url,
target: inApp.primaryAction.redirect.target as RedirectTargetEnum,
},
}
: undefined,
secondaryAction:
inApp.secondaryAction &&
inApp.secondaryAction.label &&
inApp.secondaryAction.redirect &&
inApp.secondaryAction.redirect.url
? {
label: inApp.secondaryAction?.label,
redirect: {
url: inApp.secondaryAction?.redirect.url,
target: inApp.secondaryAction?.redirect.target as RedirectTargetEnum,
},
}
: undefined,
redirect:
inApp.redirect && inApp.redirect.url
? {
url: inApp.redirect.url,
target: inApp.redirect.target as RedirectTargetEnum,
}
: undefined,
primaryAction: this.buildActionIfAllPartsAvailable(primaryAction),
secondaryAction: this.buildActionIfAllPartsAvailable(secondaryAction),
redirect: this.buildRedirect(redirect),
data: inApp.data as Record<string, unknown>,
};
}

private buildRedirect(redirect?: InAppRedirectType) {
if (!(redirect && redirect.url && isValidURL(redirect.url))) {
return undefined;
}

return {
url: redirect.url,
target: redirect.target as RedirectTargetEnum,
};
}

private buildActionIfAllPartsAvailable(action?: InAppActionType) {
if (!(action && action.label && action.redirect && action.redirect.url && isValidURL(action.redirect.url))) {
return undefined;
}

return {
label: action.label,
redirect: {
url: action.redirect.url.toLowerCase().trim(),
target: action.redirect.target as RedirectTargetEnum,
},
};
}
}
const RedirectTargetEnumSchema = z.enum(['_self', '_blank', '_parent', '_top', '_unfencedTop']);
function isValidURL(urlString: string): boolean {
try {
const url = new URL(urlString);

const InAppRenderOutputSchema = z.object({
subject: z.string().optional(),
body: z.string(),
avatar: z.string().optional(),
primaryAction: z
.object({
label: z.string().optional(),
redirect: z.object({
url: z.string().optional(),
target: RedirectTargetEnumSchema.optional(), // Optional target
}),
})
.optional(),
secondaryAction: z
.object({
label: z.string().optional(),
redirect: z.object({
url: z.string().optional(),
target: RedirectTargetEnumSchema.optional(), // Optional target
}),
})
.optional(), // Optional secondary action
data: z.record(z.unknown()).optional(), // Optional data
redirect: z
.object({
url: z.string().optional(),
target: RedirectTargetEnumSchema.optional(), // Optional target
})
.optional(),
});
return url.protocol === 'http:' || url.protocol === 'https:'; // Ensure it's HTTP or HTTPS
} catch (error) {
return false; // The URL is invalid
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { EmailRenderOutput } from '@novu/shared';
import { z } from 'zod';
import { Injectable } from '@nestjs/common';
import { render } from '@maily-to/render';
import { FullPayloadForRender, RenderCommand } from './render-command';
import { ExpandEmailEditorSchemaUsecase } from './expand-email-editor-schema.usecase';
import { EmailStepControlZodSchema } from '../../../workflows-v2/shared';

export class RenderEmailOutputCommand extends RenderCommand {}

Expand All @@ -12,7 +12,7 @@ export class RenderEmailOutputUsecase {
constructor(private expendEmailEditorSchemaUseCase: ExpandEmailEditorSchemaUsecase) {}

async execute(renderCommand: RenderEmailOutputCommand): Promise<EmailRenderOutput> {
const { emailEditor, subject } = EmailStepControlSchema.parse(renderCommand.controlValues);
const { emailEditor, subject } = EmailStepControlZodSchema.parse(renderCommand.controlValues);
const expandedSchema = this.transformForAndShowLogic(emailEditor, renderCommand.fullPayloadForRender);
const htmlRendered = await render(expandedSchema);

Expand All @@ -23,10 +23,3 @@ export class RenderEmailOutputUsecase {
return this.expendEmailEditorSchemaUseCase.execute({ emailEditorJson: body, fullPayloadForRender });
}
}

export const EmailStepControlSchema = z
.object({
emailEditor: z.string(),
subject: z.string(),
})
.strict();
Loading

0 comments on commit 7aab670

Please sign in to comment.