diff --git a/apps/api/src/app/workflows-v2/exceptions/step-not-found-exception.ts b/apps/api/src/app/workflows-v2/exceptions/step-not-found-exception.ts index bc59a05d452d..0e2e8438ce56 100644 --- a/apps/api/src/app/workflows-v2/exceptions/step-not-found-exception.ts +++ b/apps/api/src/app/workflows-v2/exceptions/step-not-found-exception.ts @@ -10,3 +10,8 @@ export class StepMissingControlsException extends InternalServerErrorException { super({ message: 'Step cannot be found using the UUID Supplied', stepDatabaseId, step }); } } +export class StepMissingStepIdException extends InternalServerErrorException { + constructor(stepDatabaseId: string, step: any) { + super({ message: 'Step Missing StepId', stepDatabaseId, step }); + } +} diff --git a/apps/api/src/app/workflows-v2/mappers/notification-template-mapper.ts b/apps/api/src/app/workflows-v2/mappers/notification-template-mapper.ts index 34c720ef626c..33d262e5ec48 100644 --- a/apps/api/src/app/workflows-v2/mappers/notification-template-mapper.ts +++ b/apps/api/src/app/workflows-v2/mappers/notification-template-mapper.ts @@ -2,9 +2,11 @@ import { DEFAULT_WORKFLOW_PREFERENCES, PreferencesResponseDto, PreferencesTypeEnum, + RuntimeIssue, ShortIsPrefixEnum, StepResponseDto, StepTypeEnum, + WorkflowCreateAndUpdateKeys, WorkflowListResponseDto, WorkflowOriginEnum, WorkflowResponseDto, @@ -39,6 +41,7 @@ export function toResponseWorkflowDto( updatedAt: template.updatedAt || 'Missing Updated At', createdAt: template.createdAt || 'Missing Create At', status: WorkflowStatusEnum.ACTIVE, + issues: template.issues as unknown as Record, }; } @@ -73,15 +76,16 @@ export function toWorkflowsMinifiedDtos(templates: NotificationTemplateEntity[]) return templates.map(toMinifiedWorkflowDto); } -function toStepResponseDto(step: NotificationStepEntity): StepResponseDto { - const stepName = step.name || 'Missing Name'; +function toStepResponseDto(persistedStep: NotificationStepEntity): StepResponseDto { + const stepName = persistedStep.name || 'Missing Name'; return { - _id: step._templateId, - slug: buildSlug(stepName, ShortIsPrefixEnum.STEP, step._templateId), + _id: persistedStep._templateId, + slug: buildSlug(stepName, ShortIsPrefixEnum.STEP, persistedStep._templateId), name: stepName, - stepId: step.stepId || 'Missing Step Id', - type: step.template?.type || StepTypeEnum.EMAIL, + stepId: persistedStep.stepId || 'Missing Step Id', + type: persistedStep.template?.type || StepTypeEnum.EMAIL, + issues: persistedStep.issues, } satisfies StepResponseDto; } diff --git a/apps/api/src/app/workflows-v2/usecases/build-payload-from-placeholder/build-default-payload-use-case.service.ts b/apps/api/src/app/workflows-v2/usecases/build-payload-from-placeholder/build-default-payload-use-case.service.ts index 5bd1d6e9f7c0..a71ebaf8d043 100644 --- a/apps/api/src/app/workflows-v2/usecases/build-payload-from-placeholder/build-default-payload-use-case.service.ts +++ b/apps/api/src/app/workflows-v2/usecases/build-payload-from-placeholder/build-default-payload-use-case.service.ts @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ import { Injectable } from '@nestjs/common'; -import { ControlPreviewIssue, ControlPreviewIssueTypeEnum, PreviewPayload } from '@novu/shared'; +import { ContentIssue, PreviewPayload, StepContentIssueEnum } from '@novu/shared'; import { BaseCommand } from '@novu/application-generic'; import _ = require('lodash'); import { CreateMockPayloadForSingleControlValueUseCase } from '../placeholder-enrichment/payload-preview-value-generator.usecase'; @@ -16,7 +16,7 @@ export class BuildDefaultPayloadUseCase { execute(command: BuildDefaultPayloadCommand): { previewPayload: PreviewPayload; - issues: Record; + issues: Record; } { let aggregatedDefaultValues = {}; const aggregatedDefaultValuesForControl: Record> = {}; @@ -96,13 +96,13 @@ export class BuildDefaultPayloadUseCase { private buildPayloadIssues( missingVariables: string[], variableToControlValueKeys: Record - ): Record { - const record: Record = {}; + ): Record { + const record: Record = {}; missingVariables.forEach((missingVariable) => { variableToControlValueKeys[missingVariable].forEach((controlValueKey) => { record[controlValueKey] = [ { - issueType: ControlPreviewIssueTypeEnum.MISSING_VARIABLE_IN_PAYLOAD, + issueType: StepContentIssueEnum.MISSING_VARIABLE_IN_PAYLOAD, message: `Variable payload.${missingVariable} is missing in payload`, variableName: `payload.${missingVariable}`, }, diff --git a/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts b/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts index 27b9ad5c577f..4781c689add2 100644 --- a/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts @@ -1,13 +1,12 @@ import { Injectable } from '@nestjs/common'; import { ChannelTypeEnum, - ControlPreviewIssue, - ControlPreviewIssueTypeEnum, + ContentIssue, ControlSchemas, GeneratePreviewResponseDto, JobStatusEnum, - JSONSchemaDto, PreviewPayload, + StepContentIssueEnum, StepTypeEnum, WorkflowOriginEnum, } from '@novu/shared'; @@ -16,19 +15,19 @@ import _ = require('lodash'); import { GeneratePreviewCommand } from './generate-preview-command'; import { PreviewStep, PreviewStepCommand } from '../../../bridge/usecases/preview-step'; import { StepMissingControlsException, StepNotFoundException } from '../../exceptions/step-not-found-exception'; -import { ExtractDefaultsUsecase } from '../get-default-values-from-schema/extract-defaults.usecase'; import { GetWorkflowByIdsUseCase } from '../get-workflow-by-ids/get-workflow-by-ids.usecase'; import { OriginMissingException, StepIdMissingException } from './step-id-missing.exception'; import { BuildDefaultPayloadUseCase } from '../build-payload-from-placeholder'; import { FrameworkPreviousStepsOutputState } from '../../../bridge/usecases/preview-step/preview-step.command'; +import { ValidateControlValuesAndConstructPassableStructureUsecase } from '../validate-control-values/build-default-control-values-usecase.service'; @Injectable() export class GeneratePreviewUsecase { constructor( private legacyPreviewStepUseCase: PreviewStep, private getWorkflowByIdsUseCase: GetWorkflowByIdsUseCase, - private extractDefaultsUseCase: ExtractDefaultsUsecase, - private constructPayloadUseCase: BuildDefaultPayloadUseCase + private constructPayloadUseCase: BuildDefaultPayloadUseCase, + private controlValuesUsecase: ValidateControlValuesAndConstructPassableStructureUsecase ) {} async execute(command: GeneratePreviewCommand): Promise { @@ -63,16 +62,11 @@ export class GeneratePreviewUsecase { return { previewPayload, issues }; } - 3; private addMissingValuesToControlValues(command: GeneratePreviewCommand, stepControlSchema: ControlSchemas) { - const defaultValues = this.extractDefaultsUseCase.execute({ - jsonSchemaDto: stepControlSchema.schema as JSONSchemaDto, + return this.controlValuesUsecase.execute({ + controlSchema: stepControlSchema, + controlValues: command.generatePreviewRequestDto.controlValues || {}, }); - - return { - augmentedControlValues: merge(defaultValues, command.generatePreviewRequestDto.controlValues), - issuesMissingValues: this.buildMissingControlValuesIssuesList(defaultValues, command), - }; } private buildMissingControlValuesIssuesList(defaultValues: Record, command: GeneratePreviewCommand) { @@ -81,16 +75,16 @@ export class GeneratePreviewUsecase { command.generatePreviewRequestDto.controlValues || {} ); - return this.buildControlPreviewIssues(missingRequiredControlValues); + return this.buildContentIssues(missingRequiredControlValues); } - private buildControlPreviewIssues(keys: string[]): Record { - const record: Record = {}; + private buildContentIssues(keys: string[]): Record { + const record: Record = {}; keys.forEach((key) => { record[key] = [ { - issueType: ControlPreviewIssueTypeEnum.MISSING_VALUE, + issueType: StepContentIssueEnum.MISSING_VALUE, message: `Value is missing on a required control`, }, ]; @@ -115,7 +109,6 @@ export class GeneratePreviewUsecase { } const state = buildState(hydratedPayload.steps); - console.log('state', JSON.stringify(state, null, 2)); return await this.legacyPreviewStepUseCase.execute( PreviewStepCommand.create({ @@ -158,8 +151,8 @@ export class GeneratePreviewUsecase { } function buildResponse( - missingValuesIssue: Record, - missingPayloadVariablesIssue: Record, + missingValuesIssue: Record, + missingPayloadVariablesIssue: Record, executionOutput, stepType: StepTypeEnum, augmentedPayload: PreviewPayload diff --git a/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.usecase.ts b/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.usecase.ts index d31a30094dbf..96392730e7fd 100644 --- a/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.usecase.ts @@ -45,6 +45,7 @@ import { toResponseWorkflowDto } from '../../mappers/notification-template-mappe import { GetWorkflowByIdsUseCase } from '../get-workflow-by-ids/get-workflow-by-ids.usecase'; import { GetWorkflowByIdsCommand } from '../get-workflow-by-ids/get-workflow-by-ids.command'; import { stepTypeToDefaultDashboardControlSchema } from '../../shared'; +import { ValidateAndPersistWorkflowIssuesUsecase } from './validate-and-persist-workflow-issues.usecase'; function buildUpsertControlValuesCommand( command: UpsertWorkflowCommand, @@ -69,16 +70,24 @@ export class UpsertWorkflowUseCase { private notificationGroupRepository: NotificationGroupRepository, private upsertPreferencesUsecase: UpsertPreferences, private upsertControlValuesUseCase: UpsertControlValuesUseCase, + private validateWorkflowUsecase: ValidateAndPersistWorkflowIssuesUsecase, private getWorkflowByIdsUseCase: GetWorkflowByIdsUseCase, private getPreferencesUseCase: GetPreferences ) {} async execute(command: UpsertWorkflowCommand): Promise { const workflowForUpdate = await this.queryWorkflow(command); + const workflow = await this.createOrUpdateWorkflow(workflowForUpdate, command); - await this.upsertControlValues(workflow, command); + const stepIdToControlValuesMap = await this.upsertControlValues(workflow, command); const preferences = await this.upsertPreference(command, workflow); - - return toResponseWorkflowDto(workflow, preferences); + const validatedWorkflowWithIssues = await this.validateWorkflowUsecase.execute({ + user: command.user, + workflow, + preferences, + stepIdToControlValuesMap, + }); + + return toResponseWorkflowDto(validatedWorkflowWithIssues, preferences); } private async queryWorkflow(command: UpsertWorkflowCommand): Promise { diff --git a/apps/api/src/app/workflows-v2/usecases/upsert-workflow/validate-and-persist-workflow-issues.usecase.ts b/apps/api/src/app/workflows-v2/usecases/upsert-workflow/validate-and-persist-workflow-issues.usecase.ts new file mode 100644 index 000000000000..d0b6fa3e5b1a --- /dev/null +++ b/apps/api/src/app/workflows-v2/usecases/upsert-workflow/validate-and-persist-workflow-issues.usecase.ts @@ -0,0 +1,182 @@ +import { + ContentIssue, + RuntimeIssue, + StepIssueEnum, + StepIssues, + StepIssuesDto, + WorkflowIssueTypeEnum, + WorkflowResponseDto, +} from '@novu/shared'; +import { + ControlValuesEntity, + NotificationStepEntity, + NotificationTemplateEntity, + NotificationTemplateRepository, +} from '@novu/dal'; +import { Injectable } from '@nestjs/common'; +import { ValidateWorkflowCommand } from './validate-workflow.command'; +import { WorkflowNotFoundException } from '../../exceptions/workflow-not-found-exception'; +import { ValidateControlValuesAndConstructPassableStructureUsecase } from '../validate-control-values/build-default-control-values-usecase.service'; + +@Injectable() +export class ValidateAndPersistWorkflowIssuesUsecase { + constructor( + private notificationTemplateRepository: NotificationTemplateRepository, + private buildDefaultControlValuesUsecase: ValidateControlValuesAndConstructPassableStructureUsecase + ) {} + + async execute(command: ValidateWorkflowCommand): Promise { + const workflowIssues = await this.validateWorkflow(command); + const stepIssues = this.validateSteps(command.workflow.steps, command.stepIdToControlValuesMap); + const workflowWithIssues = this.updateIssuesOnWorkflow(command.workflow, workflowIssues, stepIssues); + await this.persistWorkflow(command, workflowWithIssues); + + return await this.getWorkflow(command); + } + + private async persistWorkflow(command: ValidateWorkflowCommand, workflowWithIssues) { + await this.notificationTemplateRepository.update( + { + _id: command.workflow._id, + _environmentId: command.user.environmentId, + }, + { + ...workflowWithIssues, + } + ); + } + private async getWorkflow(command: ValidateWorkflowCommand) { + const entity = await this.notificationTemplateRepository.findById(command.workflow._id, command.user.environmentId); + if (entity == null) { + throw new WorkflowNotFoundException(command.workflow._id); + } + + return entity; + } + + private validateSteps( + steps: NotificationStepEntity[], + stepIdToControlValuesMap: { [p: string]: ControlValuesEntity } + ): Record { + const stepIdToIssues: Record = {}; + for (const step of steps) { + // @ts-ignore + const stepIssues: Required = { body: {}, controls: {} }; + this.addControlIssues(step, stepIdToControlValuesMap, stepIssues); + this.addStepBodyIssues(step, stepIssues); + stepIdToIssues[step._templateId] = stepIssues; + } + + return stepIdToIssues; + } + + private addControlIssues( + step: NotificationStepEntity, + stepIdToControlValuesMap: { + [p: string]: ControlValuesEntity; + }, + stepIssues: StepIssuesDto + ) { + if (step.template?.controls) { + const { issuesMissingValues } = this.buildDefaultControlValuesUsecase.execute({ + controlSchema: step.template?.controls, + controlValues: stepIdToControlValuesMap, + }); + // eslint-disable-next-line no-param-reassign + stepIssues.controls = issuesMissingValues; + } + } + private async validateWorkflow( + command: ValidateWorkflowCommand + ): Promise> { + // @ts-ignore + const issues: Record = {}; + await this.addTriggerIdentifierNotUnuiqeIfApplicable(command, issues); + this.addNameMissingIfApplicable(command, issues); + this.addDescriptionTooLongIfApplicable(command, issues); + + return issues; + } + + private addNameMissingIfApplicable( + command: ValidateWorkflowCommand, + issues: Record + ) { + if (!command.workflow.name || command.workflow.name.trim() === '') { + // eslint-disable-next-line no-param-reassign + issues.name = [{ issueType: WorkflowIssueTypeEnum.MISSING_VALUE, message: 'Name is missing' }]; + } + } + private addDescriptionTooLongIfApplicable( + command: ValidateWorkflowCommand, + issues: Record + ) { + if (command.workflow.description && command.workflow.description.length > 160) { + // eslint-disable-next-line no-param-reassign + issues.description = [ + { issueType: WorkflowIssueTypeEnum.MAX_LENGTH_ACCESSED, message: 'Description is too long' }, + ]; + } + } + private async addTriggerIdentifierNotUnuiqeIfApplicable( + command: ValidateWorkflowCommand, + issues: Record< + | '_id' + | 'slug' + | 'updatedAt' + | 'createdAt' + | 'steps' + | 'origin' + | 'preferences' + | 'status' + | 'issues' + | 'workflowId' + | 'tags' + | 'active' + | 'name' + | 'description', + RuntimeIssue[] + > + ) { + const findAllByTriggerIdentifier = await this.notificationTemplateRepository.findAllByTriggerIdentifier( + command.user.environmentId, + command.workflow.triggers[0].identifier + ); + if (findAllByTriggerIdentifier && findAllByTriggerIdentifier.length > 1) { + // eslint-disable-next-line no-param-reassign + command.workflow.triggers[0].identifier = `${command.workflow.triggers[0].identifier}-${command.workflow._id}`; + // eslint-disable-next-line no-param-reassign + issues.workflowId = [ + { + issueType: WorkflowIssueTypeEnum.WORKFLOW_ID_ALREADY_EXISTS, + message: 'Trigger identifier is not unique', + }, + ]; + } + } + + private addStepBodyIssues(step: NotificationStepEntity, stepIssues: Required) { + if (!step.name || step.name.trim() === '') { + // eslint-disable-next-line no-param-reassign + stepIssues.body.name = { + issueType: StepIssueEnum.MISSING_REQUIRED_VALUE, + message: 'Step name is missing', + }; + } + } + + private updateIssuesOnWorkflow( + workflow: NotificationTemplateEntity, + workflowIssues: Record, + stepIssuesMap: Record + ): NotificationTemplateEntity { + const issues = workflowIssues as unknown as Record; + for (const step of workflow.steps) { + if (stepIssuesMap[step._templateId]) { + step.issues = stepIssuesMap[step._templateId]; + } + } + + return { ...workflow, issues }; + } +} diff --git a/apps/api/src/app/workflows-v2/usecases/upsert-workflow/validate-workflow.command.ts b/apps/api/src/app/workflows-v2/usecases/upsert-workflow/validate-workflow.command.ts new file mode 100644 index 000000000000..0f9689c099e4 --- /dev/null +++ b/apps/api/src/app/workflows-v2/usecases/upsert-workflow/validate-workflow.command.ts @@ -0,0 +1,8 @@ +import { EnvironmentWithUserObjectCommand, GetPreferencesResponseDto } from '@novu/application-generic'; +import { ControlValuesEntity, NotificationTemplateEntity } from '@novu/dal'; + +export class ValidateWorkflowCommand extends EnvironmentWithUserObjectCommand { + workflow: NotificationTemplateEntity; + preferences?: GetPreferencesResponseDto; + stepIdToControlValuesMap: { [p: string]: ControlValuesEntity }; +} diff --git a/apps/api/src/app/workflows-v2/usecases/validate-control-values/build-default-control-values-usecase.service.ts b/apps/api/src/app/workflows-v2/usecases/validate-control-values/build-default-control-values-usecase.service.ts new file mode 100644 index 000000000000..45256be91538 --- /dev/null +++ b/apps/api/src/app/workflows-v2/usecases/validate-control-values/build-default-control-values-usecase.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { ContentIssue, JSONSchemaDto, StepContentIssueEnum } from '@novu/shared'; +import _ = require('lodash'); +import { ExtractDefaultsUsecase } from '../get-default-values-from-schema/extract-defaults.usecase'; +import { BuildDefaultControlValuesCommand } from './build-default-control-values.command'; +import { findMissingKeys } from '../../util/utils'; + +@Injectable() +export class ValidateControlValuesAndConstructPassableStructureUsecase { + constructor(private extractDefaultsUseCase: ExtractDefaultsUsecase) {} + + execute(command: BuildDefaultControlValuesCommand): { + augmentedControlValues: Record; + issuesMissingValues: Record; + } { + const defaultValues = this.extractDefaultsUseCase.execute({ + jsonSchemaDto: command.controlSchema.schema as JSONSchemaDto, + }); + + return { + augmentedControlValues: _.merge(defaultValues, command.controlValues), + issuesMissingValues: this.buildMissingControlValuesIssuesList(defaultValues, command.controlValues), + }; + } + + private buildMissingControlValuesIssuesList(defaultValues: Record, controlValues: Record) { + const missingRequiredControlValues = findMissingKeys(defaultValues, controlValues); + + return this.buildContentIssues(missingRequiredControlValues); + } + + private buildContentIssues(keys: string[]): Record { + const record: Record = {}; + + keys.forEach((key) => { + record[key] = [ + { + issueType: StepContentIssueEnum.MISSING_VALUE, + message: `Value is missing on a required control`, // Custom message for the issue + }, + ]; + }); + + return record; + } +} diff --git a/apps/api/src/app/workflows-v2/usecases/validate-control-values/build-default-control-values.command.ts b/apps/api/src/app/workflows-v2/usecases/validate-control-values/build-default-control-values.command.ts new file mode 100644 index 000000000000..06da962c65f2 --- /dev/null +++ b/apps/api/src/app/workflows-v2/usecases/validate-control-values/build-default-control-values.command.ts @@ -0,0 +1,6 @@ +import { ControlsSchema } from '@novu/shared'; + +export class BuildDefaultControlValuesCommand { + controlSchema: ControlsSchema; + controlValues: Record; +} diff --git a/apps/api/src/app/workflows-v2/util/utils.ts b/apps/api/src/app/workflows-v2/util/utils.ts new file mode 100644 index 000000000000..97536bafabe3 --- /dev/null +++ b/apps/api/src/app/workflows-v2/util/utils.ts @@ -0,0 +1,25 @@ +import _ = require('lodash'); + +export function findMissingKeys(requiredRecord: Record, actualRecord: Record) { + const requiredKeys = collectKeys(requiredRecord); + const actualKeys = collectKeys(actualRecord); + + return _.difference(requiredKeys, actualKeys); +} + +export function collectKeys(obj, prefix = '') { + return _.reduce( + obj, + (result, value, key) => { + const newKey = prefix ? `${prefix}.${key}` : key; + if (_.isObject(value) && !_.isArray(value)) { + result.push(...collectKeys(value, newKey)); + } else { + result.push(newKey); + } + + return result; + }, + [] + ); +} diff --git a/apps/api/src/app/workflows-v2/workflow.controller.e2e.ts b/apps/api/src/app/workflows-v2/workflow.controller.e2e.ts index 7094dd0f2126..801907b95e7a 100644 --- a/apps/api/src/app/workflows-v2/workflow.controller.e2e.ts +++ b/apps/api/src/app/workflows-v2/workflow.controller.e2e.ts @@ -7,11 +7,12 @@ import { DEFAULT_WORKFLOW_PREFERENCES, isStepUpdateBody, ListWorkflowResponse, + PreferencesRequestDto, ShortIsPrefixEnum, - Slug, slugify, + StepContentIssueEnum, StepCreateDto, - StepDto, + StepIssueEnum, StepResponseDto, StepTypeEnum, StepUpdateDto, @@ -19,8 +20,11 @@ import { UpdateWorkflowDto, UpsertStepBody, UpsertWorkflowBody, + WorkflowCommonsFields, WorkflowCreationSourceEnum, + WorkflowIssueTypeEnum, WorkflowListResponseDto, + WorkflowOriginEnum, WorkflowResponseDto, } from '@novu/shared'; @@ -35,6 +39,9 @@ const TEST_WORKFLOW_NAME = 'Test Workflow Name'; const TEST_TAGS = ['test']; let session: UserSession; +const LONG_DESCRIPTION = `XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`; describe('Workflow Controller E2E API Testing', () => { let workflowsClient: ReturnType; @@ -63,6 +70,48 @@ describe('Workflow Controller E2E API Testing', () => { await deleteWorkflowAndValidateDeletion(workflowCreated._id); }); + describe('Error Handling', () => { + describe('Workflow Body Issues', () => { + it('should show description issue when too long', async () => { + const issues = await createWorkflowAndReturnIssues({ description: LONG_DESCRIPTION }); + expect(issues?.description).to.be.ok; + if (issues?.description) { + expect(issues?.description[0]?.issueType, JSON.stringify(issues)).to.be.equal( + WorkflowIssueTypeEnum.MAX_LENGTH_ACCESSED + ); + } + }); + }); + describe('Workflow Step Body Issues', () => { + it('should show name issue when missing', async () => { + const issues = await createWorkflowAndReturnStepIssues({ steps: [{ ...buildEmailStep(), name: '' }] }, 0); + + expect(issues?.body).to.be.ok; + if (issues?.body) { + expect(issues?.body).to.be.ok; + expect(issues?.body.name).to.be.ok; + expect(issues?.body.name?.issueType, JSON.stringify(issues?.body.name)).to.be.equal( + StepIssueEnum.MISSING_REQUIRED_VALUE + ); + } + }); + }); + describe('Workflow Step content Issues', () => { + it('should show control value required when missing', async () => { + const issues = await createWorkflowAndReturnStepIssues( + { steps: [{ ...buildEmailStep(), controlValues: {} }] }, + 0 + ); + expect(issues?.controls).to.be.ok; + if (issues?.controls) { + expect(issues?.controls.emailEditor).to.be.ok; + if (issues.controls.emailEditor) { + expect(issues.controls.emailEditor[0].issueType).to.be.equal(StepContentIssueEnum.MISSING_VALUE); + } + } + }); + }); + }); describe('Create Workflow Permutations', () => { it('should allow creating two workflows for the same user with the same name', async () => { const nameSuffix = `Test Workflow${new Date().toString()}`; @@ -598,11 +647,7 @@ describe('Workflow Controller E2E API Testing', () => { return value; } - async function getWorkflowStepControlValues( - workflow: WorkflowResponseDto, - step: StepDto & { _id: string; slug: Slug; stepId: string }, - envId: string - ) { + async function getWorkflowStepControlValues(workflow: WorkflowResponseDto, step: StepResponseDto, envId: string) { const value = await getStepData(workflow._id, step._id, envId); return value.controls.values; @@ -646,44 +691,98 @@ describe('Workflow Controller E2E API Testing', () => { } } } -}); + async function create10Workflows(prefix: string) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 10; i++) { + await createWorkflowAndValidate(`${prefix}-ABC${i}`); + } + } + async function createWorkflowAndValidate(nameSuffix: string = ''): Promise { + const createWorkflowDto: CreateWorkflowDto = buildCreateWorkflowDto(nameSuffix); + const res = await workflowsClient.createWorkflow(createWorkflowDto); + if (!res.isSuccessResult()) { + throw new Error(res.error!.responseText); + } + validateCreateWorkflowResponse(res.value, createWorkflowDto); -async function createWorkflowAndValidate(nameSuffix: string = ''): Promise { - const createWorkflowDto: CreateWorkflowDto = buildCreateWorkflowDto(nameSuffix); - const res = await session.testAgent.post(`${v2Prefix}/workflows`).send(createWorkflowDto); - const workflowResponseDto: WorkflowResponseDto = res.body.data; - const errorMessageOnFailure = JSON.stringify(res, null, 2); - expect(workflowResponseDto, errorMessageOnFailure).to.be.ok; - expect(workflowResponseDto._id, errorMessageOnFailure).to.be.ok; - expect(workflowResponseDto.updatedAt, errorMessageOnFailure).to.be.ok; - expect(workflowResponseDto.createdAt, errorMessageOnFailure).to.be.ok; - expect(workflowResponseDto.preferences, errorMessageOnFailure).to.be.ok; - expect(workflowResponseDto.status, errorMessageOnFailure).to.be.ok; - expect(workflowResponseDto.origin, errorMessageOnFailure).to.be.eq('novu-cloud'); - for (const step of workflowResponseDto.steps) { - expect(step._id, errorMessageOnFailure).to.be.ok; - expect(step.slug, errorMessageOnFailure).to.be.ok; + return res.value; + } + function workflowAsString(workflowResponseDto: any) { + return JSON.stringify(workflowResponseDto, null, 2); } - const createdWorkflowWithoutUpdateDate = removeFields( - workflowResponseDto, - '_id', - 'origin', - 'preferences', - 'updatedAt', - 'createdAt', - 'status', - 'slug' - ); - createdWorkflowWithoutUpdateDate.steps = createdWorkflowWithoutUpdateDate.steps.map((step) => - removeFields(step, '_id', 'slug', 'slug', 'stepId') - ); - expect(createdWorkflowWithoutUpdateDate).to.deep.equal( - removeFields(createWorkflowDto, '__source') - // buildErrorMsg(createWorkflowDto, createdWorkflowWithoutUpdateDate) - ); - return workflowResponseDto; -} + function assertWorkflowResponseBodyData(workflowResponseDto: WorkflowResponseDto) { + expect(workflowResponseDto, workflowAsString(workflowResponseDto)).to.be.ok; + expect(workflowResponseDto._id, workflowAsString(workflowResponseDto)).to.be.ok; + expect(workflowResponseDto.updatedAt, workflowAsString(workflowResponseDto)).to.be.ok; + expect(workflowResponseDto.createdAt, workflowAsString(workflowResponseDto)).to.be.ok; + expect(workflowResponseDto.preferences, workflowAsString(workflowResponseDto)).to.be.ok; + expect(workflowResponseDto.status, workflowAsString(workflowResponseDto)).to.be.ok; + expect(workflowResponseDto.origin, workflowAsString(workflowResponseDto)).to.be.eq(WorkflowOriginEnum.NOVU_CLOUD); + expect(Object.keys(workflowResponseDto.issues || {}).length, workflowAsString(workflowResponseDto)).to.be.equal(0); + } + + function assertStepResponse(workflowResponseDto: WorkflowResponseDto, createWorkflowDto: CreateWorkflowDto) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < workflowResponseDto.steps.length; i++) { + const stepInRequest = createWorkflowDto.steps[i]; + const step = workflowResponseDto.steps[i]; + expect(step._id, workflowAsString(step)).to.be.ok; + expect(step.slug, workflowAsString(step)).to.be.ok; + expect(step.name, workflowAsString(step)).to.be.equal(stepInRequest.name); + expect(step.type, workflowAsString(step)).to.be.equal(stepInRequest.type); + expect(Object.keys(step.issues?.body || {}).length, workflowAsString(step)).to.be.eq(0); + } + } + async function createWorkflowAndReturnIssues(overrideDto: Partial) { + const workflowCreated = await createWorkflowAndReturn(overrideDto); + const { issues } = workflowCreated; + expect(issues, JSON.stringify(workflowCreated)).to.be.ok; + + return issues; + } + async function createWorkflowAndReturn( + overrideDto: Partial< + WorkflowCommonsFields & { + workflowId: string; + steps: StepCreateDto[]; + __source: WorkflowCreationSourceEnum; + preferences?: PreferencesRequestDto; + } + > + ) { + const createWorkflowDto: CreateWorkflowDto = buildCreateWorkflowDto('nameSuffix'); + const dtoWithoutName = { ...createWorkflowDto, ...overrideDto }; + + const res = await workflowsClient.createWorkflow(dtoWithoutName); + if (!res.isSuccessResult()) { + throw new Error(res.error!.responseText); + } + const workflowCreated: WorkflowResponseDto = res.value; + + return workflowCreated; + } + + async function createWorkflowAndReturnStepIssues(overrideDto: Partial, stepIndex: number) { + const workflowCreated = await createWorkflowAndReturn(overrideDto); + const { steps } = workflowCreated; + expect(steps, JSON.stringify(workflowCreated)).to.be.ok; + const step = steps[stepIndex]; + const { issues } = step; + expect(issues, JSON.stringify(step)).to.be.ok; + if (issues) { + return issues; + } + throw new Error('Issues not found'); + } + function validateCreateWorkflowResponse( + workflowResponseDto: WorkflowResponseDto, + createWorkflowDto: CreateWorkflowDto + ) { + assertWorkflowResponseBodyData(workflowResponseDto); + assertStepResponse(workflowResponseDto, createWorkflowDto); + } +}); function buildEmailStep(): StepCreateDto { return { @@ -847,13 +946,6 @@ function buildIdSet( return new Set([...extractIDs(listWorkflowResponse1), ...extractIDs(listWorkflowResponse2)]); } -async function create10Workflows(prefix: string) { - // eslint-disable-next-line no-plusplus - for (let i = 0; i < 10; i++) { - await createWorkflowAndValidate(`${prefix}-ABC${i}`); - } -} - function removeFields(obj: T, ...keysToRemove: (keyof T)[]): T { const objCopy = JSON.parse(JSON.stringify(obj)); keysToRemove.forEach((key) => { diff --git a/apps/api/src/app/workflows-v2/workflow.module.ts b/apps/api/src/app/workflows-v2/workflow.module.ts index c70cb692e997..f973a3a745f5 100644 --- a/apps/api/src/app/workflows-v2/workflow.module.ts +++ b/apps/api/src/app/workflows-v2/workflow.module.ts @@ -13,7 +13,6 @@ import { AuthModule } from '../auth/auth.module'; import { IntegrationModule } from '../integrations/integrations.module'; import { WorkflowController } from './workflow.controller'; import { UpsertWorkflowUseCase } from './usecases/upsert-workflow/upsert-workflow.usecase'; -import { GetWorkflowUseCase } from './usecases/get-workflow/get-workflow.usecase'; import { ListWorkflowsUseCase } from './usecases/list-workflows/list-workflow.usecase'; import { DeleteWorkflowUseCase } from './usecases/delete-workflow/delete-workflow.usecase'; import { GetWorkflowByIdsUseCase } from './usecases/get-workflow-by-ids/get-workflow-by-ids.usecase'; @@ -25,8 +24,11 @@ import { ExtractDefaultsUsecase } from './usecases/get-default-values-from-schem import { HydrateEmailSchemaUseCase } from '../environments-v1/usecases/output-renderers'; import { WorkflowTestDataUseCase } from './usecases/test-data/test-data.usecase'; import { GetStepDataUsecase } from './usecases/get-step-schema/get-step-data.usecase'; +import { ValidateAndPersistWorkflowIssuesUsecase } from './usecases/upsert-workflow/validate-and-persist-workflow-issues.usecase'; import { BuildPayloadNestedStructureUsecase } from './usecases/placeholder-enrichment/buildPayloadNestedStructureUsecase'; -import { BuildDefaultPayloadUseCase } from './usecases/build-payload-from-placeholder/build-default-payload-use-case.service'; +import { GetWorkflowUseCase } from './usecases/get-workflow/get-workflow.usecase'; +import { BuildDefaultPayloadUseCase } from './usecases/build-payload-from-placeholder'; +import { ValidateControlValuesAndConstructPassableStructureUsecase } from './usecases/validate-control-values/build-default-control-values-usecase.service'; @Module({ imports: [SharedModule, MessageTemplateModule, ChangeModule, AuthModule, BridgeModule, IntegrationModule], @@ -35,7 +37,6 @@ import { BuildDefaultPayloadUseCase } from './usecases/build-payload-from-placeh CreateWorkflow, UpdateWorkflow, UpsertWorkflowUseCase, - GetWorkflowUseCase, ListWorkflowsUseCase, DeleteWorkflowUseCase, UpsertPreferences, @@ -49,8 +50,11 @@ import { BuildDefaultPayloadUseCase } from './usecases/build-payload-from-placeh ExtractDefaultsUsecase, BuildPayloadNestedStructureUsecase, WorkflowTestDataUseCase, - BuildDefaultPayloadUseCase, + GetWorkflowUseCase, HydrateEmailSchemaUseCase, + ValidateAndPersistWorkflowIssuesUsecase, + BuildDefaultPayloadUseCase, + ValidateControlValuesAndConstructPassableStructureUsecase, ], }) export class WorkflowModule implements NestModule { diff --git a/apps/api/src/config/env.validators.ts b/apps/api/src/config/env.validators.ts index e50279dcc6e5..c69013e775a5 100644 --- a/apps/api/src/config/env.validators.ts +++ b/apps/api/src/config/env.validators.ts @@ -52,8 +52,7 @@ export const envValidators = { NOVU_INVITE_TEAM_MEMBER_NUDGE_TRIGGER_IDENTIFIER: str({ default: undefined }), HUBSPOT_INVITE_NUDGE_EMAIL_USER_LIST_ID: str({ default: undefined }), HUBSPOT_PRIVATE_APP_ACCESS_TOKEN: str({ default: undefined }), - PLAIN_SUPPORT_KEY: str(), - // Feature Flags + PLAIN_SUPPORT_KEY: str({ default: undefined }), // Feature Flags ...Object.keys(FeatureFlagsKeysEnum).reduce( (acc, key) => { return { diff --git a/libs/dal/src/repositories/notification-template/notification-template.entity.ts b/libs/dal/src/repositories/notification-template/notification-template.entity.ts index c04f8aee5400..71e9a6bf7075 100644 --- a/libs/dal/src/repositories/notification-template/notification-template.entity.ts +++ b/libs/dal/src/repositories/notification-template/notification-template.entity.ts @@ -2,6 +2,7 @@ import { Types } from 'mongoose'; import { BuilderFieldType, BuilderGroupValues, + ContentIssue, ControlSchemas, ControlsDto, FilterParts, @@ -16,6 +17,7 @@ import { ITriggerReservedVariable, IWorkflowStepMetadata, NotificationTemplateCustomData, + StepIssues, TriggerTypeEnum, WorkflowOriginEnum, WorkflowTypeEnum, @@ -83,6 +85,8 @@ export class NotificationTemplateEntity implements INotificationTemplate { rawData?: any; payloadSchema?: any; + + issues: Record; } export type NotificationTemplateDBModel = ChangePropsValueType< @@ -111,6 +115,8 @@ export class StepVariantEntity implements IStepVariant { stepId?: string; + issues?: StepIssues; + name?: string; _templateId: string; diff --git a/libs/dal/src/repositories/notification-template/notification-template.repository.ts b/libs/dal/src/repositories/notification-template/notification-template.repository.ts index d19fa027cd67..5753c87782eb 100644 --- a/libs/dal/src/repositories/notification-template/notification-template.repository.ts +++ b/libs/dal/src/repositories/notification-template/notification-template.repository.ts @@ -38,6 +38,16 @@ export class NotificationTemplateRepository extends BaseRepository< return this.mapEntity(item); } + async findAllByTriggerIdentifier(environmentId: string, identifier: string): Promise { + const requestQuery: NotificationTemplateQuery = { + _environmentId: environmentId, + 'triggers.identifier': identifier, + }; + + const query = await this._model.find(requestQuery, { _id: 1, 'triggers.identifier': 1 }); + + return this.mapEntities(query); + } async findById(id: string, environmentId: string) { return this.findByIdQuery({ id, environmentId }); diff --git a/libs/dal/src/repositories/notification-template/notification-template.schema.ts b/libs/dal/src/repositories/notification-template/notification-template.schema.ts index d47c5f9ce454..22c61d28e6e0 100644 --- a/libs/dal/src/repositories/notification-template/notification-template.schema.ts +++ b/libs/dal/src/repositories/notification-template/notification-template.schema.ts @@ -19,6 +19,7 @@ const variantSchemePart = { type: Schema.Types.Boolean, default: false, }, + issues: Schema.Types.Mixed, uuid: Schema.Types.String, stepId: Schema.Types.String, name: Schema.Types.String, @@ -219,6 +220,7 @@ const notificationTemplateSchema = new Schema( data: Schema.Types.Mixed, rawData: Schema.Types.Mixed, payloadSchema: Schema.Types.Mixed, + issues: Schema.Types.Mixed, }, { ...schemaOptions, minimize: false } ); diff --git a/packages/framework/src/client.ts b/packages/framework/src/client.ts index ebd0ce6a4a4d..e604585a61cb 100644 --- a/packages/framework/src/client.ts +++ b/packages/framework/src/client.ts @@ -9,14 +9,14 @@ import { ExecutionStateCorruptError, ExecutionStateOutputInvalidError, ExecutionStateResultInvalidError, + isFrameworkError, ProviderExecutionFailedError, ProviderNotFoundError, StepControlCompilationFailedError, + StepExecutionFailedError, StepNotFoundError, WorkflowAlreadyExistsError, WorkflowNotFoundError, - StepExecutionFailedError, - isFrameworkError, } from './errors'; import type { ActionStep, diff --git a/packages/shared/src/clients/workflows-client.ts b/packages/shared/src/clients/workflows-client.ts index 47828c16f1b8..06026aeb99af 100644 --- a/packages/shared/src/clients/workflows-client.ts +++ b/packages/shared/src/clients/workflows-client.ts @@ -1,7 +1,7 @@ import { createNovuBaseClient, HttpError, NovuRestResult } from './novu-base-client'; import { CreateWorkflowDto, - GeneratePreviewResponseDto, + GeneratePreviewRequestDto, GetListQueryParams, ListWorkflowResponse, StepDataDto, @@ -10,7 +10,7 @@ import { WorkflowResponseDto, WorkflowTestDataResponseDto, } from '../dto'; -import { GeneratePreviewRequestDto } from '../dto/workflows/generate-preview-request.dto'; +import { GeneratePreviewResponseDto } from '../dto/workflows/preview-step-response.dto'; // Define the WorkflowClient as a function that utilizes the base client export const createWorkflowClient = (baseUrl: string, headers: HeadersInit = {}) => { diff --git a/packages/shared/src/dto/workflows/index.ts b/packages/shared/src/dto/workflows/index.ts index f21be1f4d29d..565d6345636c 100644 --- a/packages/shared/src/dto/workflows/index.ts +++ b/packages/shared/src/dto/workflows/index.ts @@ -12,7 +12,8 @@ export * from './workflow-status-enum'; export * from './get-list-query-params'; export * from './workflow-test-data-response-dto'; export * from './step-data.dto'; -export * from './preview-step-response.dto'; export * from './generate-preview-request.dto'; export * from './control-schemas'; export * from './json-schema-dto'; +export * from './preview-step-response.dto'; +export * from './step-content-issue.enum'; diff --git a/packages/shared/src/dto/workflows/preview-step-response.dto.ts b/packages/shared/src/dto/workflows/preview-step-response.dto.ts index f9a9f7b8ccde..c125bfd1f598 100644 --- a/packages/shared/src/dto/workflows/preview-step-response.dto.ts +++ b/packages/shared/src/dto/workflows/preview-step-response.dto.ts @@ -1,5 +1,6 @@ import { ChannelTypeEnum } from '../../types'; import { SubscriberDto } from '../subscriber'; +import { ContentIssue } from './workflow-commons-fields'; export class RenderOutput {} @@ -53,17 +54,7 @@ export class InAppRenderOutput extends RenderOutput { target?: RedirectTargetEnum; }; } -export enum ControlPreviewIssueTypeEnum { - MISSING_VARIABLE_IN_PAYLOAD = 'MISSING_VARIABLE_IN_PAYLOAD', - VARIABLE_TYPE_MISMATCH = 'VARIABLE_TYPE_MISMATCH', - MISSING_VALUE = 'MISSING_VALUE', -} -export class ControlPreviewIssue { - issueType: ControlPreviewIssueTypeEnum; - variableName?: string; - message: string; -} export class PreviewPayload { subscriber?: Partial; payload?: Record; @@ -72,7 +63,7 @@ export class PreviewPayload { export class GeneratePreviewResponseDto { previewPayloadExample: PreviewPayload; - issues: Record; + issues: Record; result?: | { type: ChannelTypeEnum.EMAIL; diff --git a/packages/shared/src/dto/workflows/step-content-issue.enum.ts b/packages/shared/src/dto/workflows/step-content-issue.enum.ts new file mode 100644 index 000000000000..a5eb75d07c3b --- /dev/null +++ b/packages/shared/src/dto/workflows/step-content-issue.enum.ts @@ -0,0 +1,9 @@ +export enum StepContentIssueEnum { + MISSING_VARIABLE_IN_PAYLOAD = 'MISSING_VARIABLE_IN_PAYLOAD', + VARIABLE_TYPE_MISMATCH = 'VARIABLE_TYPE_MISMATCH', + MISSING_VALUE = 'MISSING_VALUE', +} +export enum StepIssueEnum { + STEP_ID_EXISTS = 'STEP_ID_EXISTS', + MISSING_REQUIRED_VALUE = 'MISSING_REQUIRED_VALUE', +} diff --git a/packages/shared/src/dto/workflows/workflow-commons-fields.ts b/packages/shared/src/dto/workflows/workflow-commons-fields.ts index 50e787cc53ff..5bbdd2ea631d 100644 --- a/packages/shared/src/dto/workflows/workflow-commons-fields.ts +++ b/packages/shared/src/dto/workflows/workflow-commons-fields.ts @@ -1,13 +1,37 @@ import { IsArray, IsBoolean, IsDefined, IsOptional, IsString } from 'class-validator'; +import { JSONSchema } from 'json-schema-to-ts'; import { WorkflowResponseDto } from './workflow-response-dto'; import { Slug, StepTypeEnum, WorkflowPreferences } from '../../types'; +import { StepContentIssueEnum, StepIssueEnum } from './step-content-issue.enum'; +export class ControlsSchema { + schema: JSONSchema; +} +export type StepCreateAndUpdateKeys = keyof StepCreateDto | keyof StepUpdateDto; + +export class StepIssuesDto { + body?: Record; + controls?: Record; +} +// eslint-disable-next-line @typescript-eslint/naming-convention +interface Issue { + issueType: T; + variableName?: string; + message: string; +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface ContentIssue extends Issue {} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface StepIssue extends Issue {} export type IdentifierOrInternalId = string; export type StepResponseDto = StepDto & { _id: string; slug: Slug; stepId: string; + issues?: StepIssuesDto; }; export type StepUpdateDto = StepCreateDto & { @@ -50,8 +74,6 @@ export class WorkflowCommonsFields { @IsBoolean() active?: boolean; - @IsString() - @IsDefined() name: string; @IsString() diff --git a/packages/shared/src/dto/workflows/workflow-response-dto.ts b/packages/shared/src/dto/workflows/workflow-response-dto.ts index 7654d6a62acd..c92fe3ea78bb 100644 --- a/packages/shared/src/dto/workflows/workflow-response-dto.ts +++ b/packages/shared/src/dto/workflows/workflow-response-dto.ts @@ -2,6 +2,8 @@ import { IsArray, IsDefined, IsEnum, IsObject, IsOptional, IsString } from 'clas import { PreferencesResponseDto, StepResponseDto, WorkflowCommonsFields } from './workflow-commons-fields'; import { Slug, WorkflowOriginEnum } from '../../types'; import { WorkflowStatusEnum } from './workflow-status-enum'; +import { CreateWorkflowDto } from './create-workflow-dto'; +import { UpdateWorkflowDto } from './update-workflow-dto'; export class WorkflowResponseDto extends WorkflowCommonsFields { @IsString() @@ -36,12 +38,22 @@ export class WorkflowResponseDto extends WorkflowCommonsFields { @IsDefined() status: WorkflowStatusEnum; - // TODO: provide better types for issues @IsObject() @IsOptional() - issues?: Record; + issues?: Record; @IsString() @IsDefined() workflowId: string; } +export type WorkflowCreateAndUpdateKeys = keyof CreateWorkflowDto | keyof UpdateWorkflowDto; +export class RuntimeIssue { + issueType: WorkflowIssueTypeEnum; + variableName?: string; + message: string; +} +export enum WorkflowIssueTypeEnum { + MISSING_VALUE = 'MISSING_VALUE', + MAX_LENGTH_ACCESSED = 'MAX_LENGTH_ACCESSED', + WORKFLOW_ID_ALREADY_EXISTS = 'WORKFLOW_ID_ALREADY_EXISTS', +} diff --git a/packages/shared/src/entities/notification-template/notification-template.interface.ts b/packages/shared/src/entities/notification-template/notification-template.interface.ts index b3d4661aba11..548ad1e9080f 100644 --- a/packages/shared/src/entities/notification-template/notification-template.interface.ts +++ b/packages/shared/src/entities/notification-template/notification-template.interface.ts @@ -12,7 +12,7 @@ import { ControlSchemas, IMessageTemplate } from '../message-template'; import { IPreferenceChannels } from '../subscriber-preference'; import { IWorkflowStepMetadata } from '../step'; import { INotificationGroup } from '../notification-group'; -import { ControlsDto } from '../../index'; +import { ContentIssue, ControlsDto, StepIssue } from '../../index'; export interface INotificationTemplate { _id?: string; @@ -82,11 +82,15 @@ export interface INotificationTriggerVariable { value?: any; type?: TemplateVariableTypeEnum; } - +export class StepIssues { + body?: Record; + controls?: Record; +} export interface IStepVariant { _id?: string; uuid?: string; stepId?: string; + issues?: StepIssues; name?: string; filters?: IMessageFilter[]; _templateId?: string;