Skip to content

Commit

Permalink
fix(api-service,dashboard): Crate of fixes for variable suggestions (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
SokratisVidros authored Dec 23, 2024
1 parent 557b237 commit 4e502b8
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,26 +15,13 @@ export class BuildPayloadSchema {

@InstrumentUsecase()
async execute(command: BuildPayloadSchemaCommand): Promise<JSONSchemaDto> {
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) {
Expand All @@ -56,15 +42,15 @@ export class BuildPayloadSchema {
).map((item) => item.controls);
}

return controlValues;
return controlValues.flat();
}

@Instrument()
private async processControlValues(controlValues: Record<string, unknown>[]): Promise<string[]> {
private async extractAllVariables(controlValues: Record<string, unknown>[]): Promise<string[]> {
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));
Expand All @@ -74,7 +60,7 @@ export class BuildPayloadSchema {
}

@Instrument()
private async processControlValue(controlValue: Record<string, unknown>): Promise<Record<string, unknown>> {
private async extractVariables(controlValue: Record<string, unknown>): Promise<Record<string, unknown>> {
const processedValue: Record<string, unknown> = {};

for (const [key, value] of Object.entries(controlValue)) {
Expand All @@ -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' };
}
}
71 changes: 4 additions & 67 deletions apps/api/src/app/workflows-v2/util/jsonToSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { JSONSchemaDefinition, JSONSchemaDto } from '@novu/shared';

export { JSONSchemaDto } from '@novu/shared';

export function emptyJsonSchema(): JSONSchemaDto {
return {
type: 'object',
Expand All @@ -8,70 +10,7 @@ export function emptyJsonSchema(): JSONSchemaDto {
};
}

export function convertJsonToSchemaWithDefaults(unknownObject?: Record<string, unknown>) {
if (!unknownObject) {
return {};
}

return generateJsonSchema(unknownObject) as unknown as JSONSchemaDto;
}

function generateJsonSchema(jsonObject: Record<string, unknown>): 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<string, unknown> | null): boolean {
export function isMatchingJsonSchema(schema: JSONSchemaDefinition, obj?: Record<string, unknown> | 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
Expand Down Expand Up @@ -101,7 +40,7 @@ function isMatchingJsonSchema(schema: JSONSchemaDefinition, obj?: Record<string,
return true;
}

function extractMinValuesFromSchema(schema: JSONSchemaDefinition): Record<string, number> {
export function extractMinValuesFromSchema(schema: JSONSchemaDefinition): Record<string, number> {
const result = {};

if (typeof schema === 'object' && schema.type === 'object') {
Expand All @@ -121,5 +60,3 @@ function extractMinValuesFromSchema(schema: JSONSchemaDefinition): Record<string

return result;
}

export { generateJsonSchema, isMatchingJsonSchema, extractMinValuesFromSchema, JSONSchemaDto };
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const Maily = (props: MailyProps) => {
variableTriggerCharacter="{{"
variables={({ query, editor, from }) => {
const queryWithoutSuffix = query.replace(/}+$/, '');
const filteredVariables: { name: string; required: boolean }[] = [];

function addInlineVariable() {
if (!query.endsWith('}}')) {
Expand All @@ -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 });
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export function parseStepVariables(schema: JSONSchemaDefinition): ParsedVariable
type: 'variable',
label: path,
});
return;
}

if (!obj.properties) return;
Expand Down
1 change: 0 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4e502b8

Please sign in to comment.