Skip to content

Commit

Permalink
Merge branch 'next' into nv-4965-e2e-testing-happy-path-notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
tatarco authored Dec 23, 2024
2 parents 194bb9a + 2d7fd8e commit 3879bcd
Show file tree
Hide file tree
Showing 56 changed files with 1,293 additions and 946 deletions.
2 changes: 2 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@
"twilio": "^4.14.1",
"uuid": "^8.3.2",
"zod": "^3.23.8",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"zod-to-json-schema": "^3.23.3"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@ import { Injectable } from '@nestjs/common';
import { DelayRenderOutput } from '@novu/shared';
import { InstrumentUsecase } from '@novu/application-generic';
import { RenderCommand } from './render-command';
import {
DelayTimeControlType,
DelayTimeControlZodSchema,
} from '../../../workflows-v2/shared/schemas/delay-control.schema';
import { delayControlZodSchema, DelayControlType } from '../../../workflows-v2/shared/schemas/delay-control.schema';

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

return {
amount: delayTimeControlType.amount as number,
type: delayTimeControlType.type,
unit: delayTimeControlType.unit,
amount: delayControlType.amount as number,
type: delayControlType.type,
unit: delayControlType.unit,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { InstrumentUsecase } from '@novu/application-generic';
import { RenderCommand } from './render-command';
import {
DigestControlSchemaType,
DigestControlZodSchema,
digestControlZodSchema,
isDigestRegularControl,
isDigestTimedControl,
} from '../../../workflows-v2/shared/schemas/digest-control.schema';
Expand All @@ -13,7 +13,7 @@ import {
export class DigestOutputRendererUsecase {
@InstrumentUsecase()
execute(renderCommand: RenderCommand): DigestRenderOutput {
const parse: DigestControlSchemaType = DigestControlZodSchema.parse(renderCommand.controlValues);
const parse: DigestControlSchemaType = digestControlZodSchema.parse(renderCommand.controlValues);
if (
isDigestRegularControl(parse) &&
parse.amount &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,8 @@ export class HydrateEmailSchemaUseCase {
node,
placeholderAggregation: PlaceholderAggregation
) {
const { fallback } = node.attrs;
const variableName = node.attrs.id;
const buildLiquidJSDefault = (mailyFallback: string) => (mailyFallback ? ` | default: '${mailyFallback}'` : '');
const finalValue = `{{ ${variableName} ${buildLiquidJSDefault(fallback)} }}`;
const { fallback, id: variableName } = node.attrs;
const finalValue = buildLiquidJSDefault(variableName, fallback);

placeholderAggregation.regularPlaceholdersToDefaultValue[`{{${node.attrs.id}}}`] = finalValue;

Expand Down Expand Up @@ -234,3 +232,6 @@ export const TipTapSchema = z.object({
text: z.string().optional(),
attrs: z.record(z.unknown()).optional(),
});

const buildLiquidJSDefault = (variableName: string, fallback?: string) =>
`{{ ${variableName}${fallback ? ` | default: '${fallback}'` : ''} }}`;
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import { InAppRenderOutput, RedirectTargetEnum } from '@novu/shared';
import { Injectable } from '@nestjs/common';
import { Instrument, InstrumentUsecase } from '@novu/application-generic';
import { RenderCommand } from './render-command';
import { isValidUrlForActionButton } from '../../../workflows-v2/util/url-utils';
import {
InAppActionType,
InAppControlType,
InAppControlZodSchema,
inAppControlZodSchema,
InAppRedirectType,
} from '../../../workflows-v2/shared';
import { isValidUrlForActionButton } from '../../../workflows-v2/util/url-utils';
} from '../../../workflows-v2/shared/schemas/in-app-control.schema';

@Injectable()
export class InAppOutputRendererUsecase {
@InstrumentUsecase()
execute(renderCommand: RenderCommand): InAppRenderOutput {
const inApp: InAppControlType = InAppControlZodSchema.parse(renderCommand.controlValues);
const inApp: InAppControlType = inAppControlZodSchema.parse(renderCommand.controlValues);
if (!inApp) {
throw new Error('Invalid in-app control value data');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { EmailRenderOutput, TipTapNode } from '@novu/shared';
import { Injectable } from '@nestjs/common';
import { render as mailyRender } from '@maily-to/render';
import { Instrument, InstrumentUsecase } from '@novu/application-generic';
import isEmpty from 'lodash/isEmpty';
import { Injectable } from '@nestjs/common';
import { Liquid } from 'liquidjs';

import { EmailRenderOutput, TipTapNode } from '@novu/shared';
import { Instrument, InstrumentUsecase } from '@novu/application-generic';

import { FullPayloadForRender, RenderCommand } from './render-command';
import { ExpandEmailEditorSchemaUsecase } from './expand-email-editor-schema.usecase';
import { emailStepControlZodSchema } from '../../../workflows-v2/shared';
import { emailControlZodSchema } from '../../../workflows-v2/shared/schemas/email-control.schema';

export class RenderEmailOutputCommand extends RenderCommand {}

Expand All @@ -16,25 +18,29 @@ export class RenderEmailOutputUsecase {

@InstrumentUsecase()
async execute(renderCommand: RenderEmailOutputCommand): Promise<EmailRenderOutput> {
const { body, subject } = emailStepControlZodSchema.parse(renderCommand.controlValues);
const { body, subject } = emailControlZodSchema.parse(renderCommand.controlValues);

if (isEmpty(body)) {
return { subject, body: '' };
}

const expandedMailyContent = this.transformForAndShowLogic(body, renderCommand.fullPayloadForRender);
const expandedMailyContent = this.transformMailyDynamicBlocks(body, renderCommand.fullPayloadForRender);
const parsedTipTap = await this.parseTipTapNodeByLiquid(expandedMailyContent, renderCommand);
const renderedHtml = await this.renderEmail(parsedTipTap);

return { subject, body: renderedHtml };
}

private async parseTipTapNodeByLiquid(
value: TipTapNode,
tiptapNode: TipTapNode,
renderCommand: RenderEmailOutputCommand
): Promise<TipTapNode> {
const client = new Liquid();
const templateString = client.parse(JSON.stringify(value));
const client = new Liquid({
outputEscape: (output) => {
return stringifyDataStructureWithSingleQuotes(output);
},
});
const templateString = client.parse(JSON.stringify(tiptapNode));
const parsedTipTap = await client.render(templateString, {
payload: renderCommand.fullPayloadForRender.payload,
subscriber: renderCommand.fullPayloadForRender.subscriber,
Expand All @@ -50,7 +56,19 @@ export class RenderEmailOutputUsecase {
}

@Instrument()
private transformForAndShowLogic(body: string, fullPayloadForRender: FullPayloadForRender) {
private transformMailyDynamicBlocks(body: string, fullPayloadForRender: FullPayloadForRender) {
return this.expandEmailEditorSchemaUseCase.execute({ emailEditorJson: body, fullPayloadForRender });
}
}

export const stringifyDataStructureWithSingleQuotes = (value: unknown, spaces: number = 0): string => {
if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
const valueStringified = JSON.stringify(value, null, spaces);
const valueSingleQuotes = valueStringified.replace(/"/g, "'");
const valueEscapedNewLines = valueSingleQuotes.replace(/\n/g, '\\n');

return valueEscapedNewLines;
} else {
return String(value);
}
};
12 changes: 6 additions & 6 deletions apps/api/src/app/workflows-v2/generate-preview.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import {
} from '@novu/shared';
import { buildCreateWorkflowDto } from './workflow.controller.e2e';
import { forSnippet, fullCodeSnippet } from './maily-test-data';
import { InAppControlType } from './shared';
import { EmailStepControlType } from './shared/schemas/email-control.schema';
import { InAppControlType } from './shared/schemas/in-app-control.schema';
import { EmailControlType } from './shared/schemas/email-control.schema';

const SUBJECT_TEST_PAYLOAD = '{{payload.subject.test.payload}}';
const PLACEHOLDER_SUBJECT_INAPP = '{{payload.subject}}';
Expand Down Expand Up @@ -421,7 +421,7 @@ describe('Generate Preview', () => {

channelTypes.forEach(({ type, description }) => {
// TODO: We need to get back to the drawing board on this one to make the preview action of the framework more forgiving
it(`[${type}] catches the 400 error returned by the Bridge Preview action`, async () => {
it(`[${type}] will generate gracefully the preview if the control values are missing`, async () => {
const { stepDatabaseId, workflowId, stepId } = await createWorkflowAndReturnId(workflowsClient, type);
const requestDto = buildDtoWithMissingControlValues(type, stepId);

Expand All @@ -433,7 +433,7 @@ describe('Generate Preview', () => {
description
);

expect(previewResponseDto.result).to.eql({ preview: {} });
expect(previewResponseDto.result).to.not.eql({ preview: {} });
});
});
});
Expand Down Expand Up @@ -514,13 +514,13 @@ function buildDtoNoPayload(stepTypeEnum: StepTypeEnum, stepId?: string): Generat
};
}

function buildEmailControlValuesPayload(stepId?: string): EmailStepControlType {
function buildEmailControlValuesPayload(stepId?: string): EmailControlType {
return {
subject: `Hello, World! ${SUBJECT_TEST_PAYLOAD}`,
body: JSON.stringify(fullCodeSnippet(stepId)),
};
}
function buildSimpleForEmail(): EmailStepControlType {
function buildSimpleForEmail(): EmailControlType {
return {
subject: `Hello, World! ${SUBJECT_TEST_PAYLOAD}`,
body: JSON.stringify(forSnippet),
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/app/workflows-v2/shared/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './step-type-to-control.mapper';
export * from './map-step-type-to-result.mapper';
export * from './schemas';
Loading

0 comments on commit 3879bcd

Please sign in to comment.