Skip to content

Commit

Permalink
bug(api): api compiles
Browse files Browse the repository at this point in the history
bug(api): api compiles

bug(api): tests passing
  • Loading branch information
tatarco committed Nov 12, 2024
1 parent b9185ed commit 479a8b7
Show file tree
Hide file tree
Showing 23 changed files with 652 additions and 370 deletions.
4 changes: 3 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@
"twilio": "^4.14.1",
"uuid": "^8.3.2",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.23.3"
"zod-to-json-schema": "^3.23.3",
"json-schema-to-ts": "^3.0.0"
},
"devDependencies": {
"@faker-js/faker": "^6.0.0",
Expand All @@ -122,6 +123,7 @@
"tsconfig-paths": "~4.1.0",
"typescript": "5.6.2"
},

"optionalDependencies": {
"@novu/ee-auth": "workspace:*",
"@novu/ee-billing": "workspace:*",
Expand Down
11 changes: 6 additions & 5 deletions apps/api/src/app/bridge/usecases/sync/sync.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import {
UpsertWorkflowPreferencesCommand,
} from '@novu/application-generic';
import {
JSONSchemaDto,
WorkflowCreationSourceEnum,
WorkflowOriginEnum,
WorkflowTypeEnum,
WorkflowPreferencesPartial,
WorkflowTypeEnum,
} from '@novu/shared';
import { DiscoverOutput, DiscoverStepOutput, DiscoverWorkflowOutput, GetActionEnum } from '@novu/framework/internal';

Expand Down Expand Up @@ -208,10 +209,10 @@ export class Sync {
__source: WorkflowCreationSourceEnum.BRIDGE,
steps: this.mapSteps(workflow.steps),
controls: {
schema: workflow.controls?.schema,
schema: workflow.controls?.schema as JSONSchemaDto,
},
rawData: workflow as unknown as Record<string, unknown>,
payloadSchema: workflow.payload?.schema,
payloadSchema: workflow.payload?.schema as JSONSchemaDto,
active: isWorkflowActive,
description: this.getWorkflowDescription(workflow),
data: this.castToAnyNotSupportedParam(workflow)?.data,
Expand All @@ -237,10 +238,10 @@ export class Sync {
workflowId: workflow.workflowId,
steps: this.mapSteps(workflow.steps, workflowExist),
controls: {
schema: workflow.controls?.schema,
schema: workflow.controls?.schema as JSONSchemaDto,
},
rawData: workflow,
payloadSchema: workflow.payload?.schema,
payloadSchema: workflow.payload?.schema as unknown as JSONSchemaDto,
type: WorkflowTypeEnum.BRIDGE,
description: this.getWorkflowDescription(workflow),
data: this.castToAnyNotSupportedParam(workflow)?.data,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { workflow } from '@novu/framework/express';
import {
ActionStep,
Expand Down Expand Up @@ -45,18 +45,17 @@ export class ConstructFrameworkWorkflow {
return this.constructFrameworkWorkflow(dbWorkflow);
}

private constructFrameworkWorkflow(newWorkflow: NotificationTemplateEntity) {
private constructFrameworkWorkflow(newWorkflow: NotificationTemplateEntity): Workflow {
return workflow(
newWorkflow.triggers[0].identifier,
async ({ step, payload, subscriber }) => {
const fullPayloadForRender: FullPayloadForRender = { payload, subscriber, steps: {} };
for await (const staticStep of newWorkflow.steps) {
try {
const stepOutputs = await this.constructStep(step, staticStep, fullPayloadForRender);
fullPayloadForRender.steps[staticStep.stepId || staticStep._templateId] = stepOutputs;
} catch (e) {
Logger.log(`Cannot Construct Step ${staticStep.stepId || staticStep._templateId}`, e);
}
fullPayloadForRender.steps[staticStep.stepId || staticStep._templateId] = await this.constructStep(
step,
staticStep,
fullPayloadForRender
);
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class RenderEmailOutputUsecase {

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

Expand Down
80 changes: 63 additions & 17 deletions apps/api/src/app/workflows-v2/generate-preview.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ChannelTypeEnum,
createWorkflowClient,
CreateWorkflowDto,
CronExpressionEnum,
EmailStepControlSchemaDto,
GeneratePreviewRequestDto,
GeneratePreviewResponseDto,
Expand Down Expand Up @@ -36,16 +37,26 @@ describe('Generate Preview', () => {
});
describe('Generate Preview', () => {
describe('Hydration testing', () => {
it(` should hydrate previous step`, async () => {
const { workflowId, emailStepDatabaseId, digestStepId } = await createWorkflowWithDigest();
it(` should hydrate previous step in iterator email --> digest`, async () => {
const { workflowId, emailStepDatabaseId, digestStepId } = await createWorkflowWithEmailLookingAtDigestResult();
const requestDto = buildDtoWithPayload(StepTypeEnum.EMAIL, digestStepId);
const previewResponseDto = await generatePreview(workflowId, emailStepDatabaseId, requestDto, 'testing steps');
expect(previewResponseDto.result!.preview).to.exist;
expect(previewResponseDto.previewPayloadExample).to.exist;
console.log(previewResponseDto.previewPayloadExample);
expect(previewResponseDto.previewPayloadExample?.steps?.digeststep).to.be.ok;
expect(previewResponseDto.result!.preview.body).to.contain('{{item.payload.country}}');
});
it(` should hydrate previous step in iterator sms looking at inApp`, async () => {
const { workflowId, smsDatabaseStepId, inAppStepId } = await createWorkflowWithSmsLookingAtInAppResult();
const requestDto = buildDtoNoPayload(StepTypeEnum.SMS, inAppStepId);
const previewResponseDto = await generatePreview(workflowId, smsDatabaseStepId, requestDto, 'testing steps');
expect(previewResponseDto.result!.preview).to.exist;
expect(previewResponseDto.previewPayloadExample).to.exist;
expect(previewResponseDto.previewPayloadExample?.steps).to.be.ok;
if (previewResponseDto.result?.type === 'sms' && previewResponseDto.result?.preview.body) {
expect(previewResponseDto.result!.preview.body).to.contain('[[{{steps.inappstep.seen}}]]');
}
});

const channelTypes = [{ type: StepTypeEnum.IN_APP, description: 'InApp' }];

channelTypes.forEach(({ type, description }) => {
Expand All @@ -54,8 +65,8 @@ describe('Generate Preview', () => {
const requestDto = buildDtoWithPayload(type, stepId);
const previewResponseDto = await generatePreview(workflowId, stepDatabaseId, requestDto, description);
expect(previewResponseDto.result!.preview).to.exist;
const expectedRenderedResult = buildInAppControlValues(stepId);
expectedRenderedResult.subject = buildInAppControlValues(stepId).subject!.replace(
const expectedRenderedResult = buildInAppControlValues();
expectedRenderedResult.subject = buildInAppControlValues().subject!.replace(
PLACEHOLDER_SUBJECT_INAPP,
PLACEHOLDER_SUBJECT_INAPP_PAYLOAD_VALUE
);
Expand All @@ -75,7 +86,7 @@ describe('Generate Preview', () => {
channelTypes.forEach(({ type, description }) => {
it(`${type}:should match the body in the preview response`, async () => {
const { stepDatabaseId, workflowId, stepId } = await createWorkflowAndReturnId(type);
const requestDto = buildDtoNoPayload(type, stepId);
const requestDto = buildDtoNoPayload(type);
const previewResponseDto = await generatePreview(workflowId, stepDatabaseId, requestDto, description);
expect(previewResponseDto.result!.preview).to.exist;
expect(previewResponseDto.issues).to.exist;
Expand All @@ -84,7 +95,7 @@ describe('Generate Preview', () => {
.exist;

if (type !== StepTypeEnum.EMAIL) {
expect(previewResponseDto.result!.preview).to.deep.equal(getTestControlValues(stepId)[type]);
expect(previewResponseDto.result!.preview).to.deep.equal(getTestControlValues()[type]);
} else {
assertEmail(previewResponseDto);
}
Expand Down Expand Up @@ -235,7 +246,7 @@ describe('Generate Preview', () => {
stepId: workflowResult.value.steps[0].stepId,
};
}
async function createWorkflowWithDigest() {
async function createWorkflowWithEmailLookingAtDigestResult() {
const createWorkflowDto: CreateWorkflowDto = {
tags: [],
__source: WorkflowCreationSourceEnum.EDITOR,
Expand All @@ -258,17 +269,46 @@ describe('Generate Preview', () => {
if (!workflowResult.isSuccessResult()) {
throw new Error(`Failed to create workflow ${JSON.stringify(workflowResult.error)}`);
}
console.log(workflowResult.value);

return {
workflowId: workflowResult.value._id,
emailStepDatabaseId: workflowResult.value.steps[1]._id,
digestStepId: workflowResult.value.steps[0].stepId,
};
}
async function createWorkflowWithSmsLookingAtInAppResult() {
const createWorkflowDto: CreateWorkflowDto = {
tags: [],
__source: WorkflowCreationSourceEnum.EDITOR,
name: 'John',
workflowId: `john:${randomUUID()}`,
description: 'This is a test workflow',
active: true,
steps: [
{
name: 'InAppStep',
type: StepTypeEnum.IN_APP,
},
{
name: 'SmsStep',
type: StepTypeEnum.SMS,
},
],
};
const workflowResult = await workflowsClient.createWorkflow(createWorkflowDto);
if (!workflowResult.isSuccessResult()) {
throw new Error(`Failed to create workflow ${JSON.stringify(workflowResult.error)}`);
}

return {
workflowId: workflowResult.value._id,
smsDatabaseStepId: workflowResult.value.steps[1]._id,
inAppStepId: workflowResult.value.steps[0].stepId,
};
}
});

function buildDtoNoPayload(stepTypeEnum: StepTypeEnum, stepId: string): GeneratePreviewRequestDto {
function buildDtoNoPayload(stepTypeEnum: StepTypeEnum, stepId?: string): GeneratePreviewRequestDto {
return {
controlValues: getTestControlValues(stepId)[stepTypeEnum],
};
Expand Down Expand Up @@ -307,10 +347,10 @@ function buildSimpleForEmail(): EmailStepControlSchemaDto {
emailEditor: JSON.stringify(forSnippet),
};
}
function buildInAppControlValues(stepId?: string) {
function buildInAppControlValues() {
return {
subject: `{{subscriber.firstName}} Hello, World! ${PLACEHOLDER_SUBJECT_INAPP}`,
body: `${stepId ? `steps.${stepId}.origins` : '{{payload.origins}}'} Hello, World! {{payload.placeholder.body}}`,
body: `Hello, World! {{payload.placeholder.body}}`,
avatar: 'https://www.example.com/avatar.png',
primaryAction: {
label: '{{payload.secondaryUrl}}',
Expand All @@ -336,9 +376,9 @@ function buildInAppControlValues(stepId?: string) {
};
}

function buildSmsControlValuesPayload() {
function buildSmsControlValuesPayload(stepId: string | undefined) {
return {
body: 'Hello, World! {{subscriber.firstName}}',
body: `${stepId ? ` [[{{steps.${stepId}.seen}}]]` : ''} Hello, World! {{subscriber.firstName}}`,
};
}

Expand All @@ -354,13 +394,19 @@ function buildChatControlValuesPayload() {
body: 'Hello, World! {{subscriber.firstName}}',
};
}
function buildDigestControlValuesPayload() {
return {
cron: CronExpressionEnum.EVERY_DAY_AT_8AM,
};
}

export const getTestControlValues = (stepId?: string) => ({
[StepTypeEnum.SMS]: buildSmsControlValuesPayload(),
[StepTypeEnum.SMS]: buildSmsControlValuesPayload(stepId),
[StepTypeEnum.EMAIL]: buildEmailControlValuesPayload(stepId) as unknown as Record<string, unknown>,
[StepTypeEnum.PUSH]: buildPushControlValuesPayload(),
[StepTypeEnum.CHAT]: buildChatControlValuesPayload(),
[StepTypeEnum.IN_APP]: buildInAppControlValues(stepId),
[StepTypeEnum.IN_APP]: buildInAppControlValues(),
[StepTypeEnum.DIGEST]: buildDigestControlValuesPayload(),
});

async function assertHttpError(
Expand Down
18 changes: 16 additions & 2 deletions apps/api/src/app/workflows-v2/maily-test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export function fullCodeSnippet(stepId?: string) {
{
type: 'for',
attrs: {
each: stepId ? `steps.${stepId}.origins` : 'payload.origins',
each: stepId ? `steps.${stepId}.events` : 'payload.origins',
isUpdatingKey: false,
},
content: [
Expand Down Expand Up @@ -337,7 +337,21 @@ export function fullCodeSnippet(stepId?: string) {
{
type: 'payloadValue',
attrs: {
id: 'origin.country',
id: stepId ? 'payload.country' : 'origin.country',
label: null,
},
},
{
type: 'payloadValue',
attrs: {
id: 'id',
label: null,
},
},
{
type: 'payloadValue',
attrs: {
id: 'time',
label: null,
},
},
Expand Down
20 changes: 0 additions & 20 deletions apps/api/src/app/workflows-v2/shared/build-string-schema.ts
Original file line number Diff line number Diff line change
@@ -1,20 +0,0 @@
import { JSONSchemaDto } from '@novu/shared';

/**
* Builds a JSON schema object where each variable becomes a string property.
*/
export function buildJSONSchema(variables: Record<string, unknown>): JSONSchemaDto {
const properties: Record<string, JSONSchemaDto> = {};

for (const [variableKey, variableValue] of Object.entries(variables)) {
properties[variableKey] = {
type: 'string',
default: variableValue,
};
}

return {
type: 'object',
properties,
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ActionStepEnum, actionStepSchemas, ChannelStepEnum, channelStepSchemas } from '@novu/framework/internal';
import { ControlSchemas } from '@novu/shared';
import { ControlSchemas, JSONSchemaDto } from '@novu/shared';
import { EmailStepControlSchema, EmailStepUiSchema, inAppControlSchema, InAppUiSchema } from './schemas';

export const PERMISSIVE_EMPTY_SCHEMA = {
Expand All @@ -19,22 +19,22 @@ export const stepTypeToDefaultDashboardControlSchema: Record<ChannelStepEnum | A
uiSchema: EmailStepUiSchema,
},
[ChannelStepEnum.SMS]: {
schema: channelStepSchemas[ChannelStepEnum.SMS].output,
schema: channelStepSchemas[ChannelStepEnum.SMS].output as unknown as JSONSchemaDto,
},
[ChannelStepEnum.PUSH]: {
schema: channelStepSchemas[ChannelStepEnum.PUSH].output,
schema: channelStepSchemas[ChannelStepEnum.PUSH].output as unknown as JSONSchemaDto,
},
[ChannelStepEnum.CHAT]: {
schema: channelStepSchemas[ChannelStepEnum.CHAT].output,
schema: channelStepSchemas[ChannelStepEnum.CHAT].output as unknown as JSONSchemaDto,
},

[ActionStepEnum.DELAY]: {
schema: actionStepSchemas[ActionStepEnum.DELAY].output,
schema: actionStepSchemas[ActionStepEnum.DELAY].output as unknown as JSONSchemaDto,
},
[ActionStepEnum.DIGEST]: {
schema: actionStepSchemas[ActionStepEnum.DIGEST].output,
schema: actionStepSchemas[ActionStepEnum.DIGEST].output as unknown as JSONSchemaDto,
},
[ActionStepEnum.CUSTOM]: {
schema: PERMISSIVE_EMPTY_SCHEMA,
schema: PERMISSIVE_EMPTY_SCHEMA as unknown as JSONSchemaDto,
},
};
Loading

0 comments on commit 479a8b7

Please sign in to comment.