Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dashboard): nested payload gen #7240

Merged
merged 3 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 || {});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the change was needed because now createMockObjectFromSchema is a more generic function and not payload object based.

}

@Instrument()
Expand Down
55 changes: 0 additions & 55 deletions apps/api/src/app/workflows-v2/util/utils.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was moved to shared and refactored to be a more generic function and not payload object based.

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was deleted, less is more?

now we reuse the createMockObjectFromSchema from shared the same one that BE used.

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

util from the API.

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;
}, {});
}
Loading