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 472ba83708d..1aa4d771024 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 @@ -2,6 +2,7 @@ import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common import _ from 'lodash'; import { ChannelTypeEnum, + createMockObjectFromSchema, GeneratePreviewResponseDto, JobStatusEnum, JSONSchemaDto, @@ -22,7 +23,6 @@ import { PreviewStep, PreviewStepCommand } from '../../../bridge/usecases/previe import { FrameworkPreviousStepsOutputState } from '../../../bridge/usecases/preview-step/preview-step.command'; import { BuildStepDataUsecase } from '../build-step-data'; import { GeneratePreviewCommand } from './generate-preview.command'; -import { createMockPayloadFromSchema } from '../../util/utils'; import { PrepareAndValidateContentUsecase } from '../validate-content/prepare-and-validate-content/prepare-and-validate-content.usecase'; import { BuildPayloadSchemaCommand } from '../build-payload-schema/build-payload-schema.command'; import { BuildPayloadSchema } from '../build-payload-schema/build-payload-schema.usecase'; @@ -134,17 +134,19 @@ export class GeneratePreviewUsecase { return finalPayload; } - const examplePayloadSchema = createMockPayloadFromSchema(workflow.payloadSchema); + const examplePayloadSchema = createMockObjectFromSchema( + { + type: 'object', + properties: { payload: workflow.payloadSchema }, + }, + true + ); if (!examplePayloadSchema || Object.keys(examplePayloadSchema).length === 0) { return finalPayload; } - return _.merge( - finalPayload as Record, - { payload: examplePayloadSchema }, - commandVariablesExample || {} - ); + return _.merge(finalPayload as Record, examplePayloadSchema, commandVariablesExample || {}); } @Instrument() diff --git a/apps/api/src/app/workflows-v2/util/utils.ts b/apps/api/src/app/workflows-v2/util/utils.ts index 1332899a38d..69dc4008686 100644 --- a/apps/api/src/app/workflows-v2/util/utils.ts +++ b/apps/api/src/app/workflows-v2/util/utils.ts @@ -73,61 +73,6 @@ export function flattenObjectValues(obj: Record): string[] { }); } -/** - * Generates a payload based solely on the schema. - * Supports nested schemas and applies defaults where defined. - * @param JSONSchemaDto - Defining the structure. example: - * { - * firstName: { type: 'string', default: 'John' }, - * lastName: { type: 'string' } - * } - * @returns - Generated payload. example: { firstName: 'John', lastName: '{{payload.lastName}}' } - */ -export function createMockPayloadFromSchema( - schema: JSONSchemaDto, - path = 'payload', - depth = 0, - safe = true -): Record { - const MAX_DEPTH = 10; - if (depth >= MAX_DEPTH) { - if (safe) { - return {}; - } - throw new BadRequestException({ - message: 'Schema has surpassed the maximum allowed depth. Please specify a more shallow payload schema.', - maxDepth: MAX_DEPTH, - }); - } - - if (schema?.type !== 'object' || !schema?.properties) { - if (safe) { - return {}; - } - throw new BadRequestException({ - message: 'Schema must define an object with properties.', - }); - } - - return Object.entries(schema.properties).reduce((acc, [key, definition]) => { - if (typeof definition === 'boolean') { - return acc; - } - - const currentPath = `${path}.${key}`; - - if (definition.default) { - acc[key] = definition.default; - } else if (definition.type === 'object' && definition.properties) { - acc[key] = createMockPayloadFromSchema(definition, currentPath, depth + 1); - } else { - acc[key] = `{{${currentPath}}}`; - } - - return acc; - }, {}); -} - /** * Recursively adds missing defaults for properties in a JSON schema object. * For properties without defaults, adds interpolated path as the default value. diff --git a/apps/dashboard/src/components/workflow-editor/schema.ts b/apps/dashboard/src/components/workflow-editor/schema.ts index 7c570ba3b1d..c775d7c99ca 100644 --- a/apps/dashboard/src/components/workflow-editor/schema.ts +++ b/apps/dashboard/src/components/workflow-editor/schema.ts @@ -82,21 +82,6 @@ export const buildDynamicFormSchema = ({ export type TestWorkflowFormType = z.infer>; -export const makeObjectFromSchema = ({ - properties, -}: { - properties: Readonly>; -}) => { - return Object.keys(properties).reduce((acc, key) => { - const value = properties[key]; - if (typeof value !== 'object') { - return acc; - } - - return { ...acc, [key]: value.default }; - }, {}); -}; - const ChannelPreferenceSchema = z.object({ enabled: z.boolean().default(true), }); diff --git a/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-tabs.tsx b/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-tabs.tsx index b748f34efb5..861ee5ee654 100644 --- a/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-tabs.tsx +++ b/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-tabs.tsx @@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form'; // eslint-disable-next-line // @ts-ignore import { zodResolver } from '@hookform/resolvers/zod'; -import type { WorkflowTestDataResponseDto } from '@novu/shared'; +import { createMockObjectFromSchema, type WorkflowTestDataResponseDto } from '@novu/shared'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../primitives/tabs'; import { buildRoute, LEGACY_ROUTES, ROUTES } from '@/utils/routes'; import { useFetchWorkflow } from '@/hooks/use-fetch-workflow'; @@ -13,7 +13,7 @@ import { Form } from '../../primitives/form/form'; import { Button } from '../../primitives/button'; import { useTriggerWorkflow } from '@/hooks/use-trigger-workflow'; import { showToast } from '../../primitives/sonner-helpers'; -import { buildDynamicFormSchema, makeObjectFromSchema, TestWorkflowFormType } from '../schema'; +import { buildDynamicFormSchema, TestWorkflowFormType } from '../schema'; import { TestWorkflowForm } from './test-workflow-form'; import { toast } from 'sonner'; import { ToastClose, ToastIcon } from '@/components/primitives/sonner'; @@ -23,17 +23,8 @@ export const TestWorkflowTabs = ({ testData }: { testData: WorkflowTestDataRespo const { workflow } = useFetchWorkflow({ workflowSlug, }); - const to = useMemo( - () => (typeof testData.to === 'object' ? makeObjectFromSchema({ properties: testData.to.properties ?? {} }) : {}), - [testData] - ); - const payload = useMemo( - () => - typeof testData.payload === 'object' - ? makeObjectFromSchema({ properties: testData.payload.properties ?? {} }) - : {}, - [testData] - ); + const to = useMemo(() => createMockObjectFromSchema(testData.to, true), [testData]); + const payload = useMemo(() => createMockObjectFromSchema(testData.payload, true), [testData]); const form = useForm({ mode: 'onSubmit', resolver: zodResolver(buildDynamicFormSchema({ to: testData?.to ?? {} })), diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index c0003def3b4..224686340b2 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -4,3 +4,4 @@ export * from './normalizeEmail'; export * from './bridge.utils'; export * from './buildWorkflowPreferences'; export { slugify } from './slugify'; +export { createMockObjectFromSchema } from './schema/create-mock-object-from-schema'; diff --git a/packages/shared/src/utils/schema/create-mock-object-from-schema.ts b/packages/shared/src/utils/schema/create-mock-object-from-schema.ts new file mode 100644 index 00000000000..5a78122f7f0 --- /dev/null +++ b/packages/shared/src/utils/schema/create-mock-object-from-schema.ts @@ -0,0 +1,58 @@ +import { JSONSchemaDto } from '../../dto'; + +/** + * Generates a payload based solely on the schema. + * Supports nested schemas and applies defaults where defined. + * @param JSONSchemaDto - Defining the structure. example: + * { + * type: 'object', + * properties: { + * payload: { + * firstName: { type: 'string', default: 'John' }, + * lastName: { type: 'string' } + * } + * } + * } + * @returns - Generated payload. example: { payload: { firstName: 'John', lastName: '{{payload.lastName}}' }} + */ +export function createMockObjectFromSchema( + schema: JSONSchemaDto, + safe = true, + path = '', + depth = 0 +): Record { + const MAX_DEPTH = 10; + if (depth >= MAX_DEPTH) { + if (safe) { + return {}; + } + throw new Error( + `Schema has surpassed the maximum allowed depth. Please specify a more shallow payload schema. Max depth: ${MAX_DEPTH}` + ); + } + + if (schema?.type !== 'object' || !schema?.properties) { + if (safe) { + return {}; + } + throw new Error('Schema must define an object with properties.'); + } + + return Object.entries(schema.properties).reduce((acc, [key, definition]) => { + if (typeof definition === 'boolean') { + return acc; + } + + const currentPath = path && path.length > 0 ? `${path}.${key}` : key; + + if (definition.default) { + acc[key] = definition.default; + } else if (definition.type === 'object' && definition.properties) { + acc[key] = createMockObjectFromSchema(definition, safe, currentPath, depth + 1); + } else { + acc[key] = `{{${currentPath}}}`; + } + + return acc; + }, {}); +}