Skip to content

Commit

Permalink
feat(api): add ui scehma (#6764)
Browse files Browse the repository at this point in the history
Co-authored-by: Gosha <[email protected]>
  • Loading branch information
tatarco and djabarovgeorge authored Oct 29, 2024
1 parent f068ac9 commit 8f5cc6e
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 83 deletions.
98 changes: 97 additions & 1 deletion apps/api/src/app/step-schemas/dtos/step-schema.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,102 @@
import { JSONSchema } from 'json-schema-to-ts';
import { StepType } from '@novu/shared';

import { ActionStepEnum, ChannelStepEnum } from '@novu/framework/internal';

const chatProperties = {
body: { type: 'chatBody' },
};

const emailProperties = {
subject: { type: 'emailSubject' },
body: { type: 'emailBody' },
};

const inAppProperties = {
subject: { type: 'inAppSubject' },
body: { type: 'inAppBody' },
avatar: { type: 'inAppAvatar' },
primaryAction: { type: 'inAppPrimaryAction' },
secondaryAction: { type: 'inAppSecondaryAction' },
data: { type: 'inAppData' },
redirect: { type: 'inAppRedirect' },
};

const pushProperties = {
subject: { type: 'pushSubject' },
body: { type: 'pushBody' },
};

const smsProperties = {
body: { type: 'smsBody' },
};

const delayProperties = {
type: { type: 'delayType' },
amount: { type: 'delayAmount' },
unit: { type: 'delayUnit' },
};

const digestProperties = {
amount: { type: 'digestAmount' },
unit: { type: 'digestUnit' },
digestKey: { type: 'digestKey' },
lookBackWindow: { type: 'digestLookBackWindow' },
};

const customProperties = {};

export type UiSchema =
| {
type: 'email';
properties: typeof emailProperties;
}
| {
type: 'sms';
properties: typeof smsProperties;
}
| {
type: 'push';
properties: typeof pushProperties;
}
| {
type: 'chat';
properties: typeof chatProperties;
}
| {
type: 'in_app';
properties: typeof inAppProperties;
}
| {
type: 'delay';
properties: typeof delayProperties;
}
| {
type: 'digest';
properties: typeof digestProperties;
}
| {
type: 'custom';
properties: typeof customProperties;
};

export const mapStepTypeToUiSchema = {
[ChannelStepEnum.SMS]: { type: 'sms', properties: smsProperties },
[ChannelStepEnum.EMAIL]: { type: 'email', properties: emailProperties },
[ChannelStepEnum.PUSH]: { type: 'push', properties: pushProperties },
[ChannelStepEnum.CHAT]: { type: 'chat', properties: chatProperties },
[ChannelStepEnum.IN_APP]: { type: 'in_app', properties: inAppProperties },
[ActionStepEnum.DELAY]: { type: 'delay', properties: delayProperties },
[ActionStepEnum.DIGEST]: { type: 'digest', properties: digestProperties },
[ActionStepEnum.CUSTOM]: { type: 'custom', properties: customProperties },
} as const satisfies Record<ChannelStepEnum | ActionStepEnum, UiSchema>;

export class ControlsDto {
schema: JSONSchema;
uiSchema: UiSchema;
}

export type StepSchemaDto = {
controls: JSONSchema;
controls: ControlsDto;
variables: JSONSchema;
};
119 changes: 65 additions & 54 deletions apps/api/src/app/step-schemas/e2e/get-step-schema.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect } from 'chai';
import { UserSession } from '@novu/testing';
import { CreateWorkflowDto, StepTypeEnum, WorkflowCreationSourceEnum, WorkflowResponseDto } from '@novu/shared';

describe('Get Step Schema - /step-schemas?workflowId=:workflowId&stepId=:stepId&stepType=:stepType (GET)', async () => {
describe('Get Step Schema - /steps?workflowId=:workflowId&stepId=:stepId&stepType=:stepType (GET)', async () => {
let session: UserSession;
let createdWorkflow: WorkflowResponseDto;

Expand All @@ -16,64 +16,75 @@ describe('Get Step Schema - /step-schemas?workflowId=:workflowId&stepId=:stepId&
it('should get step schema for in app step type', async function () {
const { data } = (await getStepSchema({ session, stepType: StepTypeEnum.IN_APP })).body;

// in app output schema
expect(data.controls.type).to.equal('object');
expect(data.controls.properties).to.have.property('subject');
expect(data.controls.properties.subject.type).to.equal('string');
expect(data.controls.properties).to.have.property('body');
expect(data.controls.properties.body.type).to.equal('string');
expect(data.controls.properties).to.have.property('avatar');
expect(data.controls.properties.avatar.type).to.equal('string');
expect(data.controls.properties.avatar.format).to.equal('uri');
expect(data.controls.properties).to.have.property('primaryAction');
expect(data.controls.properties.primaryAction.type).to.equal('object');
expect(data.controls.properties.primaryAction.properties).to.have.property('label');
expect(data.controls.properties.primaryAction.properties.label.type).to.equal('string');
expect(data.controls.properties.primaryAction.properties).to.have.property('redirect');
expect(data.controls.properties.primaryAction.properties.redirect.type).to.equal('object');
expect(data.controls.properties.primaryAction.required).to.deep.equal(['label']);
expect(data.controls.properties).to.have.property('secondaryAction');
expect(data.controls.properties.secondaryAction.type).to.equal('object');
expect(data.controls.properties.secondaryAction.properties).to.have.property('label');
expect(data.controls.properties.secondaryAction.properties.label.type).to.equal('string');
expect(data.controls.properties.secondaryAction.properties).to.have.property('redirect');
expect(data.controls.properties.secondaryAction.properties.redirect.type).to.equal('object');
expect(data.controls.properties.secondaryAction.required).to.deep.equal(['label']);
expect(data.controls.properties).to.have.property('data');
expect(data.controls.properties.data.type).to.equal('object');
expect(data.controls.properties.data.additionalProperties).to.be.true;
expect(data.controls.properties).to.have.property('redirect');
expect(data.controls.properties.redirect.type).to.equal('object');
expect(data.controls.properties.redirect.properties).to.have.property('url');
expect(data.controls.properties.redirect.properties.url.type).to.equal('string');
expect(data.controls.properties.redirect.properties).to.have.property('target');
expect(data.controls.properties.redirect.properties.target.type).to.equal('string');
expect(data.controls.required).to.deep.equal(['body']);
expect(data.controls.additionalProperties).to.be.false;
expect(data.controls.description).to.not.be.empty;
const { controls, variables } = data;
const { schema, uiSchema } = controls;

expect(data.variables.type).to.equal('object');
expect(data.variables.description).to.not.be.empty;
expect(data.variables.properties).to.have.property('subscriber');
expect(data.variables.properties.subscriber.type).to.equal('object');
expect(data.variables.properties.subscriber.description).to.not.be.empty;
expect(data.variables.properties.subscriber.properties).to.have.property('firstName');
expect(data.variables.properties.subscriber.properties.firstName.type).to.equal('string');
expect(data.variables.properties.subscriber.properties).to.have.property('lastName');
expect(data.variables.properties.subscriber.properties.lastName.type).to.equal('string');
expect(data.variables.properties.subscriber.properties).to.have.property('email');
expect(data.variables.properties.subscriber.properties.email.type).to.equal('string');
expect(data.variables.properties.subscriber.required).to.deep.equal([
expect(schema.type).to.equal('object');
expect(schema.properties).to.have.property('subject');
expect(schema.properties.subject.type).to.equal('string');
expect(schema.properties).to.have.property('body');
expect(schema.properties.body.type).to.equal('string');
expect(schema.properties).to.have.property('avatar');
expect(schema.properties.avatar.type).to.equal('string');
expect(schema.properties.avatar.format).to.equal('uri');
expect(schema.properties).to.have.property('primaryAction');
expect(schema.properties.primaryAction.type).to.equal('object');
expect(schema.properties.primaryAction.properties).to.have.property('label');
expect(schema.properties.primaryAction.properties.label.type).to.equal('string');
expect(schema.properties.primaryAction.properties).to.have.property('redirect');
expect(schema.properties.primaryAction.properties.redirect.type).to.equal('object');
expect(schema.properties.primaryAction.required).to.deep.equal(['label']);
expect(schema.properties).to.have.property('secondaryAction');
expect(schema.properties.secondaryAction.type).to.equal('object');
expect(schema.properties.secondaryAction.properties).to.have.property('label');
expect(schema.properties.secondaryAction.properties.label.type).to.equal('string');
expect(schema.properties.secondaryAction.properties).to.have.property('redirect');
expect(schema.properties.secondaryAction.properties.redirect.type).to.equal('object');
expect(schema.properties.secondaryAction.required).to.deep.equal(['label']);
expect(schema.properties).to.have.property('data');
expect(schema.properties.data.type).to.equal('object');
expect(schema.properties.data.additionalProperties).to.be.true;
expect(schema.properties).to.have.property('redirect');
expect(schema.properties.redirect.type).to.equal('object');
expect(schema.properties.redirect.properties).to.have.property('url');
expect(schema.properties.redirect.properties.url.type).to.equal('string');
expect(schema.properties.redirect.properties).to.have.property('target');
expect(schema.properties.redirect.properties.target.type).to.equal('string');
expect(schema.required).to.deep.equal(['body']);
expect(schema.additionalProperties).to.be.false;
expect(schema.description).to.not.be.empty;

expect(uiSchema.type).to.equal('in_app');
expect(uiSchema.properties.subject.type).to.equal('inAppSubject');
expect(uiSchema.properties.body.type).to.equal('inAppBody');
expect(uiSchema.properties.avatar.type).to.equal('inAppAvatar');
expect(uiSchema.properties.primaryAction.type).to.equal('inAppPrimaryAction');
expect(uiSchema.properties.secondaryAction.type).to.equal('inAppSecondaryAction');
expect(uiSchema.properties.data.type).to.equal('inAppData');
expect(uiSchema.properties.redirect.type).to.equal('inAppRedirect');

expect(variables.type).to.equal('object');
expect(variables.description).to.not.be.empty;
expect(variables.properties).to.have.property('subscriber');
expect(variables.properties.subscriber.type).to.equal('object');
expect(variables.properties.subscriber.description).to.not.be.empty;
expect(variables.properties.subscriber.properties).to.have.property('firstName');
expect(variables.properties.subscriber.properties.firstName.type).to.equal('string');
expect(variables.properties.subscriber.properties).to.have.property('lastName');
expect(variables.properties.subscriber.properties.lastName.type).to.equal('string');
expect(variables.properties.subscriber.properties).to.have.property('email');
expect(variables.properties.subscriber.properties.email.type).to.equal('string');
expect(variables.properties.subscriber.required).to.deep.equal([
'firstName',
'lastName',
'email',
'subscriberId',
]);
expect(data.variables.properties).to.have.property('steps');
expect(data.variables.properties.steps.type).to.equal('object');
expect(data.variables.properties.steps.description).to.not.be.empty;
expect(data.variables.required).to.deep.equal(['subscriber']);
expect(data.variables.additionalProperties).to.be.false;
expect(variables.properties).to.have.property('steps');
expect(variables.properties.steps.type).to.equal('object');
expect(variables.properties.steps.description).to.not.be.empty;
expect(variables.required).to.deep.equal(['subscriber']);
expect(variables.additionalProperties).to.be.false;
});

it('should get error for invalid step type', async function () {
Expand Down Expand Up @@ -130,5 +141,5 @@ const getStepSchema = async ({
if (stepId) queryParams.append('stepId', stepId);
if (stepType) queryParams.append('stepType', stepType);

return await session.testAgent.get(`/v1/step-schemas?${queryParams.toString()}`);
return await session.testAgent.get(`/v1/steps?${queryParams.toString()}`);
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,45 @@
import { ActionStepEnum, actionStepSchemas, ChannelStepEnum, channelStepSchemas } from '@novu/framework/internal';
import { EmailStepControlSchema } from '@novu/shared';
import { ControlsDto, mapStepTypeToUiSchema } from '../../dtos/step-schema.dto';

export const mapStepTypeToOutput = {
[ChannelStepEnum.SMS]: channelStepSchemas[ChannelStepEnum.SMS].output,
[ChannelStepEnum.EMAIL]: EmailStepControlSchema,
[ChannelStepEnum.PUSH]: channelStepSchemas[ChannelStepEnum.PUSH].output,
[ChannelStepEnum.CHAT]: channelStepSchemas[ChannelStepEnum.CHAT].output,
[ChannelStepEnum.IN_APP]: channelStepSchemas[ChannelStepEnum.IN_APP].output,
[ActionStepEnum.DELAY]: actionStepSchemas[ActionStepEnum.DELAY].output,
[ActionStepEnum.DIGEST]: actionStepSchemas[ActionStepEnum.DIGEST].output,
export const PERMISSIVE_EMPTY_SCHEMA = {
type: 'object',
properties: {},
required: [],
additionalProperties: true,
} as const;

export const mapStepTypeToControlSchema: Record<ChannelStepEnum | ActionStepEnum, ControlsDto> = {
[ChannelStepEnum.SMS]: {
schema: channelStepSchemas[ChannelStepEnum.SMS].output,
uiSchema: mapStepTypeToUiSchema[ChannelStepEnum.SMS],
},
[ChannelStepEnum.EMAIL]: {
schema: EmailStepControlSchema,
uiSchema: mapStepTypeToUiSchema[ChannelStepEnum.EMAIL],
},
[ChannelStepEnum.PUSH]: {
schema: channelStepSchemas[ChannelStepEnum.PUSH].output,
uiSchema: mapStepTypeToUiSchema[ChannelStepEnum.PUSH],
},
[ChannelStepEnum.CHAT]: {
schema: channelStepSchemas[ChannelStepEnum.CHAT].output,
uiSchema: mapStepTypeToUiSchema[ChannelStepEnum.CHAT],
},
[ChannelStepEnum.IN_APP]: {
schema: channelStepSchemas[ChannelStepEnum.IN_APP].output,
uiSchema: mapStepTypeToUiSchema[ChannelStepEnum.IN_APP],
},
[ActionStepEnum.DELAY]: {
schema: actionStepSchemas[ActionStepEnum.DELAY].output,
uiSchema: mapStepTypeToUiSchema[ActionStepEnum.DELAY],
},
[ActionStepEnum.DIGEST]: {
schema: actionStepSchemas[ActionStepEnum.DIGEST].output,
uiSchema: mapStepTypeToUiSchema[ActionStepEnum.DIGEST],
},
[ActionStepEnum.CUSTOM]: {
schema: PERMISSIVE_EMPTY_SCHEMA,
uiSchema: mapStepTypeToUiSchema[ActionStepEnum.CUSTOM],
},
};
2 changes: 1 addition & 1 deletion apps/api/src/app/step-schemas/step-schemas.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { GetStepSchemaUseCase } from './usecases/get-step-schema/get-step-schema
import { StepSchemaDto } from './dtos/step-schema.dto';
import { ParseSlugIdPipe } from '../workflows-v2/pipes/parse-slug-id.pipe';

@Controller('/step-schemas')
@Controller('/steps')
@UserAuthentication()
@UseInterceptors(ClassSerializerInterceptor)
export class StepSchemasController {
Expand Down
Loading

0 comments on commit 8f5cc6e

Please sign in to comment.