Skip to content

Commit

Permalink
fix(dashboard): nested payload gen
Browse files Browse the repository at this point in the history
  • Loading branch information
djabarovgeorge committed Dec 8, 2024
1 parent 64a1c99 commit 1ba4b5a
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common
import _ from 'lodash';
import {
ChannelTypeEnum,
createMockObjectFromSchema,
GeneratePreviewResponseDto,
JobStatusEnum,
JSONSchemaDto,
Expand All @@ -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';
Expand Down Expand Up @@ -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<string, unknown>,
{ payload: examplePayloadSchema },
commandVariablesExample || {}
);
return _.merge(finalPayload as Record<string, unknown>, examplePayloadSchema, commandVariablesExample || {});
}

@Instrument()
Expand Down
55 changes: 0 additions & 55 deletions apps/api/src/app/workflows-v2/util/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,61 +73,6 @@ export function flattenObjectValues(obj: Record<string, unknown>): 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<string, unknown> {
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.
Expand Down
15 changes: 0 additions & 15 deletions apps/dashboard/src/components/workflow-editor/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,6 @@ export const buildDynamicFormSchema = ({

export type TestWorkflowFormType = z.infer<ReturnType<typeof buildDynamicFormSchema>>;

export const makeObjectFromSchema = ({
properties,
}: {
properties: Readonly<Record<string, JSONSchemaDefinition>>;
}) => {
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),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ 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';
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';
Expand All @@ -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<TestWorkflowFormType>({
mode: 'onSubmit',
resolver: zodResolver(buildDynamicFormSchema({ to: testData?.to ?? {} })),
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
58 changes: 58 additions & 0 deletions packages/shared/src/utils/schema/create-mock-object-from-schema.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
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;
}, {});
}

0 comments on commit 1ba4b5a

Please sign in to comment.