diff --git a/apps/api/package.json b/apps/api/package.json index f6e3b3e67f6..591799cd2e7 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -8,6 +8,7 @@ "scripts": { "prebuild": "rimraf dist", "build": "pnpm build:metadata && nest build", + "build:watch": "pnpm build:metadata && nest build --watch", "format": "prettier --write \"src/**/*.ts\"", "docker:build": "pnpm --silent --workspace-root pnpm-context -- apps/api/Dockerfile | BULL_MQ_PRO_NPM_TOKEN=${BULL_MQ_PRO_NPM_TOKEN} docker buildx build --load -t novu-api --secret id=BULL_MQ_PRO_NPM_TOKEN --build-arg PACKAGE_PATH=apps/api - $DOCKER_BUILD_ARGUMENTS", "docker:build:depot": "pnpm --silent --workspace-root pnpm-context -- apps/api/Dockerfile | depot build --build-arg PACKAGE_PATH=apps/api - -t novu-api --load", 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 1e225806aaa..c3215401736 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 @@ -137,7 +137,7 @@ export class GeneratePreviewUsecase { } private sanitizeControlsForPreview(initialControlValues: Record, stepData: StepDataDto) { - const sanitizedValues = dashboardSanitizeControlValues(initialControlValues, stepData.type); + const sanitizedValues = dashboardSanitizeControlValues(this.logger, initialControlValues, stepData.type); return sanitizeControlValuesByOutputSchema(sanitizedValues || {}, stepData.type); } 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 fede52da51b..6aa16266519 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 @@ -11,7 +11,6 @@ import { } from '@novu/dal'; import { ContentIssue, - CreateWorkflowDto, JSONSchemaDto, DEFAULT_WORKFLOW_PREFERENCES, slugify, @@ -19,7 +18,6 @@ import { StepCreateDto, StepIssuesDto, StepUpdateDto, - UpdateWorkflowDto, UserSessionData, StepTypeEnum, WorkflowCreationSourceEnum, @@ -50,6 +48,7 @@ import { DeleteControlValuesUseCase, TierRestrictionsValidateCommand, dashboardSanitizeControlValues, + PinoLogger, } from '@novu/application-generic'; import { UpsertWorkflowCommand, UpsertWorkflowDataCommand } from './upsert-workflow.command'; @@ -70,7 +69,8 @@ export class UpsertWorkflowUseCase { private controlValuesRepository: ControlValuesRepository, private upsertControlValuesUseCase: UpsertControlValuesUseCase, private deleteControlValuesUseCase: DeleteControlValuesUseCase, - private tierRestrictionsValidateUsecase: TierRestrictionsValidateUsecase + private tierRestrictionsValidateUsecase: TierRestrictionsValidateUsecase, + private logger: PinoLogger ) {} @InstrumentUsecase() @@ -316,12 +316,12 @@ export class UpsertWorkflowUseCase { const sanitizedControlValues = controlValueLocal && workflowOrigin === WorkflowOriginEnum.NOVU_CLOUD - ? dashboardSanitizeControlValues(controlValueLocal, step.type) || {} - : convertEmptyStringsToNull(controlValueLocal) || {}; + ? dashboardSanitizeControlValues(this.logger, controlValueLocal, step.type) || {} + : frameworkSanitizeEmptyStringsToNull(controlValueLocal) || {}; const controlIssues = processControlValuesBySchema(controlSchemas?.schema, sanitizedControlValues || {}); const liquidTemplateIssues = processControlValuesByLiquid(variableSchema, controlValueLocal || {}); - const customIssues = await this.processControlValuesByRules(user, step.type, controlValueLocal || {}); + const customIssues = await this.processControlValuesByRules(user, step.type, sanitizedControlValues || {}); const customControlIssues = _.isEmpty(customIssues) ? {} : { controls: customIssues }; return _.merge(controlIssues, liquidTemplateIssues, customControlIssues); @@ -432,13 +432,11 @@ export class UpsertWorkflowUseCase { stepType: StepTypeEnum, controlValues: Record | null ): Promise { - const cleanedControlValues = controlValues ? cleanObject(controlValues) : {}; - const restrictionsErrors = await this.tierRestrictionsValidateUsecase.execute( TierRestrictionsValidateCommand.create({ - amount: cleanedControlValues.amount as string | undefined, - unit: cleanedControlValues.unit as string | undefined, - cron: cleanedControlValues.cron as string | undefined, + amount: controlValues?.amount as number | undefined, + unit: controlValues?.unit as string | undefined, + cron: controlValues?.cron as string | undefined, organizationId: user.organizationId, stepType, }) @@ -571,20 +569,9 @@ function getErrorPath(error: ErrorObject): string { return fullPath?.replace(/\//g, '.'); } -function cleanObject( - obj: Record, - valuesToClean: Array = ['', null, undefined] -) { - if (typeof obj !== 'object' || obj === null) return obj; - - return Object.fromEntries( - Object.entries(obj) - .filter(([unused, value]) => !valuesToClean.includes(value as string | null | undefined)) - .map(([key, value]) => [key, cleanObject(value, valuesToClean)]) - ); -} - -function convertEmptyStringsToNull(obj: Record | undefined): Record | undefined { +function frameworkSanitizeEmptyStringsToNull( + obj: Record | undefined | null +): Record | undefined | null { if (typeof obj !== 'object' || obj === null || obj === undefined) return obj; return Object.fromEntries( @@ -593,7 +580,7 @@ function convertEmptyStringsToNull(obj: Record | undefined): Re return [key, null]; } if (typeof value === 'object') { - return [key, convertEmptyStringsToNull(value as Record)]; + return [key, frameworkSanitizeEmptyStringsToNull(value as Record)]; } return [key, value]; diff --git a/apps/api/src/app/workflows-v2/usecases/validate-content/prepare-and-validate-content/prepare-and-validate-content.usecase.ts b/apps/api/src/app/workflows-v2/usecases/validate-content/prepare-and-validate-content/prepare-and-validate-content.usecase.ts index 5834d99832a..f3f49516f63 100644 --- a/apps/api/src/app/workflows-v2/usecases/validate-content/prepare-and-validate-content/prepare-and-validate-content.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/validate-content/prepare-and-validate-content/prepare-and-validate-content.usecase.ts @@ -394,7 +394,7 @@ export class PrepareAndValidateContentUsecase { ): Promise> { const restrictionsErrors = await this.tierRestrictionsValidateUsecase.execute( TierRestrictionsValidateCommand.create({ - amount: defaultControlValues.amount as string | undefined, + amount: defaultControlValues.amount as number | undefined, unit: defaultControlValues.unit as string | undefined, organizationId: user.organizationId, stepType, diff --git a/apps/dashboard/src/components/workflow-editor/step-utils.ts b/apps/dashboard/src/components/workflow-editor/step-utils.ts index 87a1880409e..5bdd6f6487d 100644 --- a/apps/dashboard/src/components/workflow-editor/step-utils.ts +++ b/apps/dashboard/src/components/workflow-editor/step-utils.ts @@ -1,14 +1,23 @@ import { flatten } from 'flat'; import type { ContentIssue, + StepCreateDto, StepIssuesDto, - StepTypeEnum, StepUpdateDto, UpdateWorkflowDto, WorkflowResponseDto, } from '@novu/shared'; -import { Step } from '@/utils/types'; -import { STEP_TYPE_LABELS } from '@/utils/constants'; +import { StepTypeEnum } from '@novu/shared'; +import { + DEFAULT_CONTROL_DELAY_AMOUNT, + DEFAULT_CONTROL_DELAY_TYPE, + DEFAULT_CONTROL_DELAY_UNIT, + DEFAULT_CONTROL_DIGEST_AMOUNT, + DEFAULT_CONTROL_DIGEST_CRON, + DEFAULT_CONTROL_DIGEST_DIGEST_KEY, + DEFAULT_CONTROL_DIGEST_UNIT, + STEP_TYPE_LABELS, +} from '@/utils/constants'; export const getFirstBodyErrorMessage = (issues?: StepIssuesDto) => { const stepIssuesArray = Object.entries({ ...issues?.body }); @@ -58,10 +67,25 @@ export const updateStepInWorkflow = ( }; }; -export const createStep = (type: StepTypeEnum): Step => ({ - name: STEP_TYPE_LABELS[type] + ' Step', - stepId: '', - slug: '_st_', - type, - _id: crypto.randomUUID(), -}); +export const createStep = (type: StepTypeEnum): StepCreateDto => { + const controlValue: Record = {}; + + if (type === StepTypeEnum.DIGEST) { + controlValue.amount = DEFAULT_CONTROL_DIGEST_AMOUNT; + controlValue.unit = DEFAULT_CONTROL_DIGEST_UNIT; + controlValue.digestKey = DEFAULT_CONTROL_DIGEST_DIGEST_KEY; + controlValue.cron = DEFAULT_CONTROL_DIGEST_CRON; + } + + if (type === StepTypeEnum.DELAY) { + controlValue.amount = DEFAULT_CONTROL_DELAY_AMOUNT; + controlValue.unit = DEFAULT_CONTROL_DELAY_UNIT; + controlValue.type = DEFAULT_CONTROL_DELAY_TYPE; + } + + return { + name: STEP_TYPE_LABELS[type] + ' Step', + type, + controlValues: controlValue, + }; +}; diff --git a/apps/dashboard/src/utils/constants.ts b/apps/dashboard/src/utils/constants.ts index 39977428470..6839b5243de 100644 --- a/apps/dashboard/src/utils/constants.ts +++ b/apps/dashboard/src/utils/constants.ts @@ -1,4 +1,4 @@ -import { StepTypeEnum } from '@novu/shared'; +import { StepTypeEnum, TimeUnitEnum } from '@novu/shared'; export const AUTOCOMPLETE_PASSWORD_MANAGERS_OFF = { autoComplete: 'off', @@ -27,3 +27,12 @@ export const STEP_TYPE_LABELS: Record = { [StepTypeEnum.TRIGGER]: 'Trigger', [StepTypeEnum.CUSTOM]: 'Custom', }; + +export const DEFAULT_CONTROL_DELAY_AMOUNT = 30; +export const DEFAULT_CONTROL_DELAY_UNIT = TimeUnitEnum.SECONDS; +export const DEFAULT_CONTROL_DELAY_TYPE = 'regular'; + +export const DEFAULT_CONTROL_DIGEST_AMOUNT = 30; +export const DEFAULT_CONTROL_DIGEST_UNIT = TimeUnitEnum.SECONDS; +export const DEFAULT_CONTROL_DIGEST_CRON = ''; +export const DEFAULT_CONTROL_DIGEST_DIGEST_KEY = ''; diff --git a/apps/worker/src/app/workflow/usecases/execute-bridge-job/execute-bridge-job.usecase.ts b/apps/worker/src/app/workflow/usecases/execute-bridge-job/execute-bridge-job.usecase.ts index 0985b455484..b1dbdc158bf 100644 --- a/apps/worker/src/app/workflow/usecases/execute-bridge-job/execute-bridge-job.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/execute-bridge-job/execute-bridge-job.usecase.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ControlValuesRepository, @@ -15,7 +15,6 @@ import { ExecutionDetailsStatusEnum, ITriggerPayload, JobStatusEnum, - StepTypeEnum, WorkflowOriginEnum, WorkflowTypeEnum, } from '@novu/shared'; @@ -30,6 +29,7 @@ import { ExecuteBridgeRequestCommand, Instrument, InstrumentUsecase, + PinoLogger, } from '@novu/application-generic'; import { ExecuteBridgeJobCommand } from './execute-bridge-job.command'; @@ -44,7 +44,8 @@ export class ExecuteBridgeJob { private environmentRepository: EnvironmentRepository, private controlValuesRepository: ControlValuesRepository, private createExecutionDetails: CreateExecutionDetails, - private executeBridgeRequest: ExecuteBridgeRequest + private executeBridgeRequest: ExecuteBridgeRequest, + private logger: PinoLogger ) {} @InstrumentUsecase() @@ -152,7 +153,7 @@ export class ExecuteBridgeJob { if (workflow?.origin === WorkflowOriginEnum.NOVU_CLOUD) { return controls?.controls - ? dashboardSanitizeControlValues(controls.controls, command.job?.step?.template?.type) + ? dashboardSanitizeControlValues(this.logger, controls.controls, command.job?.step?.template?.type) : {}; } diff --git a/libs/application-generic/src/schemas/control/delay-control.schema.ts b/libs/application-generic/src/schemas/control/delay-control.schema.ts index 53779ea5596..8e6f7676846 100644 --- a/libs/application-generic/src/schemas/control/delay-control.schema.ts +++ b/libs/application-generic/src/schemas/control/delay-control.schema.ts @@ -13,8 +13,8 @@ import { defaultOptions, skipStepUiSchema, skipZodSchema } from './shared'; export const delayControlZodSchema = z .object({ skip: skipZodSchema, - type: z.enum(['regular']).default('regular'), - amount: z.union([z.number().min(1), z.string()]), + type: z.enum(['regular']), + amount: z.number().min(1), unit: z.nativeEnum(TimeUnitEnum), }) .strict(); diff --git a/libs/application-generic/src/schemas/control/digest-control.schema.ts b/libs/application-generic/src/schemas/control/digest-control.schema.ts index 68752745f1a..348f0cdeb3f 100644 --- a/libs/application-generic/src/schemas/control/digest-control.schema.ts +++ b/libs/application-generic/src/schemas/control/digest-control.schema.ts @@ -20,7 +20,7 @@ const lookBackWindowZodSchema = z const digestRegularControlZodSchema = z .object({ skip: skipZodSchema, - amount: z.union([z.number().min(1), z.string().min(1)]), + amount: z.number().min(1), unit: z.nativeEnum(TimeUnitEnum), digestKey: z.string().optional(), lookBackWindow: lookBackWindowZodSchema.optional(), @@ -28,7 +28,7 @@ const digestRegularControlZodSchema = z .strict(); const digestTimedControlZodSchema = z .object({ - skip: z.object({}).catchall(z.unknown()).optional(), + skip: skipZodSchema, cron: z.string().min(1), digestKey: z.string().optional(), }) diff --git a/libs/application-generic/src/usecases/tier-restrictions-validate/tier-restrictions-validate.command.ts b/libs/application-generic/src/usecases/tier-restrictions-validate/tier-restrictions-validate.command.ts index b6344851820..b8f669cfce6 100644 --- a/libs/application-generic/src/usecases/tier-restrictions-validate/tier-restrictions-validate.command.ts +++ b/libs/application-generic/src/usecases/tier-restrictions-validate/tier-restrictions-validate.command.ts @@ -5,9 +5,9 @@ import { OrganizationLevelCommand } from '../../commands'; export class TierRestrictionsValidateCommand extends OrganizationLevelCommand { @IsOptional() - @Transform(({ value }) => (value ? String(value) : value)) - @IsString() - amount?: string; + @Transform(({ value }) => (value ? Number(value) : value)) + @IsNumber() + amount?: number; @IsString() @IsOptional() diff --git a/libs/application-generic/src/utils/sanitize-control-values.ts b/libs/application-generic/src/utils/sanitize-control-values.ts index c454d2bf61f..c1431d69da7 100644 --- a/libs/application-generic/src/utils/sanitize-control-values.ts +++ b/libs/application-generic/src/utils/sanitize-control-values.ts @@ -9,15 +9,22 @@ import { SmsControlType, InAppRedirectType, PushControlType, - isDigestTimedControl, DigestTimedControlType, DigestControlSchemaType, - isDigestRegularControl, DigestRegularControlType, LookBackWindowType, DelayControlType, ChatControlType, } from '../schemas/control'; +import { PinoLogger } from '../logging'; + +// Cast input T_Type to trigger Ajv validation errors - possible undefined +function sanitizeEmptyInput( + input: T_Type, + defaultValue: T_Type = undefined as unknown as T_Type, +): T_Type { + return isEmpty(input) ? defaultValue : input; +} export function sanitizeRedirect(redirect: InAppRedirectType | undefined) { if (!redirect?.url || redirect.url.length === 0 || !redirect?.target) { @@ -48,17 +55,14 @@ function sanitizeAction(action: InAppActionType) { function sanitizeInApp(controlValues: InAppControlType) { const normalized: InAppControlType = { - subject: controlValues.subject || undefined, - // Cast to string to trigger Ajv validation errors - body: isEmpty(controlValues.body) - ? (undefined as unknown as string) - : controlValues.body, - avatar: controlValues.avatar || undefined, + subject: controlValues.subject, + body: sanitizeEmptyInput(controlValues.body), + avatar: controlValues.avatar, primaryAction: undefined, secondaryAction: undefined, redirect: undefined, - data: controlValues.data || undefined, - skip: controlValues.skip || undefined, + data: controlValues.data, + skip: controlValues.skip, }; if (controlValues.primaryAction) { @@ -90,8 +94,8 @@ function sanitizeEmail(controlValues: EmailControlType) { const emailControls: EmailControlType = { subject: controlValues.subject, - body: isEmpty(controlValues.body) ? EMPTY_TIP_TAP : controlValues.body, - skip: controlValues.skip || undefined, + body: sanitizeEmptyInput(controlValues.body, EMPTY_TIP_TAP), + skip: controlValues.skip, }; return filterNullishValues(emailControls); @@ -99,8 +103,8 @@ function sanitizeEmail(controlValues: EmailControlType) { function sanitizeSms(controlValues: SmsControlType) { const mappedValues: SmsControlType = { - body: controlValues.body || '', - skip: controlValues.skip || undefined, + body: sanitizeEmptyInput(controlValues.body), + skip: controlValues.skip, }; return filterNullishValues(mappedValues); @@ -108,9 +112,9 @@ function sanitizeSms(controlValues: SmsControlType) { function sanitizePush(controlValues: PushControlType) { const mappedValues: PushControlType = { - subject: controlValues.subject || '', - body: controlValues.body || '', - skip: controlValues.skip || undefined, + subject: sanitizeEmptyInput(controlValues.subject), + body: sanitizeEmptyInput(controlValues.body), + skip: controlValues.skip, }; return filterNullishValues(mappedValues); @@ -118,37 +122,38 @@ function sanitizePush(controlValues: PushControlType) { function sanitizeChat(controlValues: ChatControlType) { const mappedValues: ChatControlType = { - body: controlValues.body || '', - skip: controlValues.skip || undefined, + body: sanitizeEmptyInput(controlValues.body), + skip: controlValues.skip, }; return filterNullishValues(mappedValues); } function sanitizeDigest(controlValues: DigestControlSchemaType) { - if (isDigestTimedControl(controlValues)) { + if (isTimedDigestControl(controlValues)) { const mappedValues: DigestTimedControlType = { - cron: controlValues.cron || '', - digestKey: controlValues.digestKey || '', - skip: controlValues.skip || undefined, + cron: controlValues.cron, + digestKey: controlValues.digestKey, + skip: controlValues.skip, }; return filterNullishValues(mappedValues); } - if (isDigestRegularControl(controlValues)) { + if (isRegularDigestControl(controlValues)) { + const lookBackAmount = (controlValues.lookBackWindow as LookBackWindowType) + ?.amount; const mappedValues: DigestRegularControlType = { - amount: controlValues.amount || 0, - unit: controlValues.unit || TimeUnitEnum.SECONDS, - digestKey: controlValues.digestKey || '', - skip: controlValues.skip || undefined, + // Cast to trigger Ajv validation errors - possible undefined + ...(parseAmount(controlValues.amount) as { amount?: number }), + unit: controlValues.unit, + digestKey: controlValues.digestKey, + skip: controlValues.skip, lookBackWindow: controlValues.lookBackWindow ? { - amount: - (controlValues.lookBackWindow as LookBackWindowType).amount || 0, - unit: - (controlValues.lookBackWindow as LookBackWindowType).unit || - TimeUnitEnum.SECONDS, + // Cast to trigger Ajv validation errors - possible undefined + ...(parseAmount(lookBackAmount) as { amount?: number }), + unit: (controlValues.lookBackWindow as LookBackWindowType).unit, } : undefined, }; @@ -157,19 +162,20 @@ function sanitizeDigest(controlValues: DigestControlSchemaType) { } const anyControlValues = controlValues as Record; + const lookBackWindow = (anyControlValues.lookBackWindow as LookBackWindowType) + ?.amount; return filterNullishValues({ - amount: anyControlValues.amount || 0, - unit: anyControlValues.unit || TimeUnitEnum.SECONDS, - digestKey: anyControlValues.digestKey || '', - skip: anyControlValues.skip || undefined, + // Cast to trigger Ajv validation errors - possible undefined + ...(parseAmount(anyControlValues.amount) as { amount?: number }), + unit: anyControlValues.unit, + digestKey: anyControlValues.digestKey, + skip: anyControlValues.skip, lookBackWindow: anyControlValues.lookBackWindow ? { - amount: - (anyControlValues.lookBackWindow as LookBackWindowType).amount || 0, - unit: - (anyControlValues.lookBackWindow as LookBackWindowType).unit || - TimeUnitEnum.SECONDS, + // Cast to trigger Ajv validation errors - possible undefined + ...(parseAmount(lookBackWindow) as { amount?: number }), + unit: (anyControlValues.lookBackWindow as LookBackWindowType).unit, } : undefined, }); @@ -177,15 +183,31 @@ function sanitizeDigest(controlValues: DigestControlSchemaType) { function sanitizeDelay(controlValues: DelayControlType) { const mappedValues: DelayControlType = { - type: controlValues.type || 'regular', - amount: controlValues.amount || 0, - unit: controlValues.unit || TimeUnitEnum.SECONDS, - skip: controlValues.skip || undefined, + // Cast to trigger Ajv validation errors - possible undefined + ...(parseAmount(controlValues.amount) as { amount?: number }), + type: controlValues.type, + unit: controlValues.unit, + skip: controlValues.skip, }; return filterNullishValues(mappedValues); } +function parseAmount(amount?: unknown) { + try { + if (!isNumber(amount)) { + return {}; + } + + const numberAmount = + typeof amount === 'string' ? parseInt(amount, 10) : amount; + + return { amount: numberAmount }; + } catch (error) { + return amount; + } +} + function filterNullishValues>(obj: T): T { if (typeof obj === 'object' && obj !== null) { return Object.fromEntries( @@ -219,40 +241,64 @@ function filterNullishValues>(obj: T): T { * */ export function dashboardSanitizeControlValues( + logger: PinoLogger, controlValues: Record, stepType: StepTypeEnum | unknown, ): (Record & { skip?: Record }) | null { - if (!controlValues) { - return null; - } - let normalizedValues: Record; - switch (stepType) { - case StepTypeEnum.IN_APP: - normalizedValues = sanitizeInApp(controlValues as InAppControlType); - break; - case StepTypeEnum.EMAIL: - normalizedValues = sanitizeEmail(controlValues as EmailControlType); - break; - case StepTypeEnum.SMS: - normalizedValues = sanitizeSms(controlValues as SmsControlType); - break; - case StepTypeEnum.PUSH: - normalizedValues = sanitizePush(controlValues as PushControlType); - break; - case StepTypeEnum.CHAT: - normalizedValues = sanitizeChat(controlValues as ChatControlType); - break; - case StepTypeEnum.DIGEST: - normalizedValues = sanitizeDigest( - controlValues as DigestControlSchemaType, - ); - break; - case StepTypeEnum.DELAY: - normalizedValues = sanitizeDelay(controlValues as DelayControlType); - break; - default: - normalizedValues = filterNullishValues(controlValues); + try { + if (!controlValues) { + return null; + } + + let normalizedValues: Record; + switch (stepType) { + case StepTypeEnum.IN_APP: + normalizedValues = sanitizeInApp(controlValues as InAppControlType); + break; + case StepTypeEnum.EMAIL: + normalizedValues = sanitizeEmail(controlValues as EmailControlType); + break; + case StepTypeEnum.SMS: + normalizedValues = sanitizeSms(controlValues as SmsControlType); + break; + case StepTypeEnum.PUSH: + normalizedValues = sanitizePush(controlValues as PushControlType); + break; + case StepTypeEnum.CHAT: + normalizedValues = sanitizeChat(controlValues as ChatControlType); + break; + case StepTypeEnum.DIGEST: + normalizedValues = sanitizeDigest( + controlValues as DigestControlSchemaType, + ); + break; + case StepTypeEnum.DELAY: + normalizedValues = sanitizeDelay(controlValues as DelayControlType); + break; + default: + normalizedValues = filterNullishValues(controlValues); + } + + return normalizedValues; + } catch (error) { + logger.error('Error sanitizing control values', error); + + return controlValues; } +} + +function isNumber(value: unknown): value is number { + return !Number.isNaN(Number.parseInt(value as string, 10)); +} + +function isTimedDigestControl( + controlValues: unknown, +): controlValues is DigestTimedControlType { + return !isEmpty((controlValues as DigestTimedControlType)?.cron); +} - return normalizedValues; +function isRegularDigestControl( + controlValues: unknown, +): controlValues is DigestRegularControlType { + return !isTimedDigestControl(controlValues); }