From 4e502b8df0126fb7ff82ea018086597f379020d9 Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Mon, 23 Dec 2024 22:42:49 +0200 Subject: [PATCH] fix(api-service,dashboard): Crate of fixes for variable suggestions (#7360) --- .../build-payload-schema.usecase.ts | 94 ++++++++++++++----- .../src/app/workflows-v2/util/jsonToSchema.ts | 71 +------------- .../workflow-editor/steps/email/maily.tsx | 9 +- .../parseStepVariablesToLiquidVariables.ts | 1 - pnpm-lock.yaml | 1 - 5 files changed, 81 insertions(+), 95 deletions(-) diff --git a/apps/api/src/app/workflows-v2/usecases/build-payload-schema/build-payload-schema.usecase.ts b/apps/api/src/app/workflows-v2/usecases/build-payload-schema/build-payload-schema.usecase.ts index 33f841a8f0f..b57d38aef89 100644 --- a/apps/api/src/app/workflows-v2/usecases/build-payload-schema/build-payload-schema.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/build-payload-schema/build-payload-schema.usecase.ts @@ -5,7 +5,6 @@ import { Instrument, InstrumentUsecase } from '@novu/application-generic'; import { flattenObjectValues } from '../../util/utils'; import { pathsToObject } from '../../util/path-to-object'; import { extractLiquidTemplateVariables } from '../../util/template-parser/liquid-parser'; -import { convertJsonToSchemaWithDefaults, emptyJsonSchema } from '../../util/jsonToSchema'; import { BuildPayloadSchemaCommand } from './build-payload-schema.command'; import { transformMailyContentToLiquid } from '../generate-preview/transform-maily-content-to-liquid'; import { isStringTipTapNode } from '../../util/tip-tap.util'; @@ -16,26 +15,13 @@ export class BuildPayloadSchema { @InstrumentUsecase() async execute(command: BuildPayloadSchemaCommand): Promise { - const controlValues = await this.buildControlValues(command); + const controlValues = await this.getControlValues(command); + const extractedVariables = await this.extractAllVariables(controlValues); - if (!controlValues.length) { - return emptyJsonSchema(); - } - - const templateVars = await this.processControlValues(controlValues); - if (templateVars.length === 0) { - return emptyJsonSchema(); - } - - const variablesExample = pathsToObject(templateVars, { - valuePrefix: '{{', - valueSuffix: '}}', - }).payload; - - return convertJsonToSchemaWithDefaults(variablesExample); + return this.buildVariablesSchema(extractedVariables); } - private async buildControlValues(command: BuildPayloadSchemaCommand) { + private async getControlValues(command: BuildPayloadSchemaCommand) { let controlValues = command.controlValues ? [command.controlValues] : []; if (!controlValues.length) { @@ -56,15 +42,15 @@ export class BuildPayloadSchema { ).map((item) => item.controls); } - return controlValues; + return controlValues.flat(); } @Instrument() - private async processControlValues(controlValues: Record[]): Promise { + private async extractAllVariables(controlValues: Record[]): Promise { const allVariables: string[] = []; for (const controlValue of controlValues) { - const processedControlValue = await this.processControlValue(controlValue); + const processedControlValue = await this.extractVariables(controlValue); const controlValuesString = flattenObjectValues(processedControlValue).join(' '); const templateVariables = extractLiquidTemplateVariables(controlValuesString); allVariables.push(...templateVariables.validVariables.map((variable) => variable.name)); @@ -74,7 +60,7 @@ export class BuildPayloadSchema { } @Instrument() - private async processControlValue(controlValue: Record): Promise> { + private async extractVariables(controlValue: Record): Promise> { const processedValue: Record = {}; for (const [key, value] of Object.entries(controlValue)) { @@ -87,4 +73,68 @@ export class BuildPayloadSchema { return processedValue; } + + private async buildVariablesSchema(variables: string[]) { + // TODO: Update typings in this as .payload can be null or undefined + const variablesObject = pathsToObject(variables, { + valuePrefix: '{{', + valueSuffix: '}}', + }).payload; + + const schema: JSONSchemaDto = { + type: 'object', + properties: {}, + required: [], + additionalProperties: true, + }; + + if (variablesObject) { + for (const [key, value] of Object.entries(variablesObject)) { + if (schema.properties && schema.required) { + schema.properties[key] = determineSchemaType(value); + schema.required.push(key); + } + } + } + + return schema; + } +} + +function determineSchemaType(value: unknown): JSONSchemaDto { + if (value === null) { + return { type: 'null' }; + } + + if (Array.isArray(value)) { + return { + type: 'array', + items: value.length > 0 ? determineSchemaType(value[0]) : { type: 'null' }, + }; + } + + switch (typeof value) { + case 'string': + return { type: 'string', default: value }; + case 'number': + return { type: 'number', default: value }; + case 'boolean': + return { type: 'boolean', default: value }; + case 'object': + return { + type: 'object', + properties: Object.entries(value).reduce( + (acc, [key, val]) => { + acc[key] = determineSchemaType(val); + + return acc; + }, + {} as { [key: string]: JSONSchemaDto } + ), + required: Object.keys(value), + }; + + default: + return { type: 'null' }; + } } diff --git a/apps/api/src/app/workflows-v2/util/jsonToSchema.ts b/apps/api/src/app/workflows-v2/util/jsonToSchema.ts index d3617cfd730..a65bc01a04e 100644 --- a/apps/api/src/app/workflows-v2/util/jsonToSchema.ts +++ b/apps/api/src/app/workflows-v2/util/jsonToSchema.ts @@ -1,5 +1,7 @@ import { JSONSchemaDefinition, JSONSchemaDto } from '@novu/shared'; +export { JSONSchemaDto } from '@novu/shared'; + export function emptyJsonSchema(): JSONSchemaDto { return { type: 'object', @@ -8,70 +10,7 @@ export function emptyJsonSchema(): JSONSchemaDto { }; } -export function convertJsonToSchemaWithDefaults(unknownObject?: Record) { - if (!unknownObject) { - return {}; - } - - return generateJsonSchema(unknownObject) as unknown as JSONSchemaDto; -} - -function generateJsonSchema(jsonObject: Record): JSONSchemaDto { - const schema: JSONSchemaDto = { - type: 'object', - properties: {}, - required: [], - }; - - for (const [key, value] of Object.entries(jsonObject)) { - if (schema.properties && schema.required) { - schema.properties[key] = determineSchemaType(value); - schema.required.push(key); - } - } - - return schema; -} - -function determineSchemaType(value: unknown): JSONSchemaDto { - if (value === null) { - return { type: 'null' }; - } - - if (Array.isArray(value)) { - return { - type: 'array', - items: value.length > 0 ? determineSchemaType(value[0]) : { type: 'null' }, - }; - } - - switch (typeof value) { - case 'string': - return { type: 'string', default: value }; - case 'number': - return { type: 'number', default: value }; - case 'boolean': - return { type: 'boolean', default: value }; - case 'object': - return { - type: 'object', - properties: Object.entries(value).reduce( - (acc, [key, val]) => { - acc[key] = determineSchemaType(val); - - return acc; - }, - {} as { [key: string]: JSONSchemaDto } - ), - required: Object.keys(value), - }; - - default: - return { type: 'null' }; - } -} - -function isMatchingJsonSchema(schema: JSONSchemaDefinition, obj?: Record | null): boolean { +export function isMatchingJsonSchema(schema: JSONSchemaDefinition, obj?: Record | null): boolean { // Ensure the schema is an object with properties if (!obj || !schema || typeof schema !== 'object' || schema.type !== 'object' || !schema.properties) { return false; // If schema is not structured or no properties are defined, assume match @@ -101,7 +40,7 @@ function isMatchingJsonSchema(schema: JSONSchemaDefinition, obj?: Record { +export function extractMinValuesFromSchema(schema: JSONSchemaDefinition): Record { const result = {}; if (typeof schema === 'object' && schema.type === 'object') { @@ -121,5 +60,3 @@ function extractMinValuesFromSchema(schema: JSONSchemaDefinition): Record { variableTriggerCharacter="{{" variables={({ query, editor, from }) => { const queryWithoutSuffix = query.replace(/}+$/, ''); + const filteredVariables: { name: string; required: boolean }[] = []; function addInlineVariable() { if (!query.endsWith('}}')) { @@ -121,9 +122,7 @@ export const Maily = (props: MailyProps) => { }); } - const filteredVariables: { name: string; required: boolean }[] = []; - - if (from === 'for') { + if (from === 'for-variable') { filteredVariables.push(...arrays, ...namespaces); if (namespaces.some((namespace) => queryWithoutSuffix.includes(namespace.name))) { filteredVariables.push({ name: queryWithoutSuffix, required: false }); @@ -142,7 +141,9 @@ export const Maily = (props: MailyProps) => { filteredVariables.push({ name: queryWithoutSuffix, required: false }); } - addInlineVariable(); + if (from === 'content-variable') { + addInlineVariable(); + } return dedupAndSortVariables(filteredVariables, queryWithoutSuffix); }} contentJson={field.value ? JSON.parse(field.value) : undefined} diff --git a/apps/dashboard/src/utils/parseStepVariablesToLiquidVariables.ts b/apps/dashboard/src/utils/parseStepVariablesToLiquidVariables.ts index 309e8676126..9131554009d 100644 --- a/apps/dashboard/src/utils/parseStepVariablesToLiquidVariables.ts +++ b/apps/dashboard/src/utils/parseStepVariablesToLiquidVariables.ts @@ -33,7 +33,6 @@ export function parseStepVariables(schema: JSONSchemaDefinition): ParsedVariable type: 'variable', label: path, }); - return; } if (!obj.properties) return; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0264c6ce15b..f4aaa06efb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30234,7 +30234,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qs@6.10.4: