diff --git a/libs/application-generic/src/utils/issues.spec.ts b/libs/application-generic/src/utils/issues.spec.ts index 7a26ccfc805..c351ece5db8 100644 --- a/libs/application-generic/src/utils/issues.spec.ts +++ b/libs/application-generic/src/utils/issues.spec.ts @@ -2,6 +2,7 @@ import { JsonSchemaTypeEnum } from '@novu/dal'; import { ContentIssueEnum, StepTypeEnum } from '@novu/shared'; import type { PinoLogger } from 'nestjs-pino'; import { describe, expect, it } from 'vitest'; +import { JSONSchemaDto } from '../dtos/json-schema.dto'; import { chatControlSchema } from '../schemas/control'; import { processControlValuesBySchema } from './issues'; import { dashboardSanitizeControlValues } from './sanitize-control-values'; @@ -62,6 +63,24 @@ describe('processControlValuesBySchema', () => { expect(issues.controls?.editorType).toBeUndefined(); }); + it('validates schemas using JSON Schema draft 2020-12', () => { + const issues = processControlValuesBySchema({ + controlSchema: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: JsonSchemaTypeEnum.OBJECT, + properties: { + subject: { type: JsonSchemaTypeEnum.STRING, minLength: 1 }, + }, + required: ['subject'], + additionalProperties: false, + } as JSONSchemaDto, + controlValues: {}, + }); + + expect(issues.controls?.subject).toHaveLength(1); + expect(issues.controls?.subject?.[0].message).toBe('Subject is required'); + }); + it('does not flag editorType for an empty chat step after sanitize', () => { const sanitized = dashboardSanitizeControlValues(logger, { body: '', editorType: '' }, StepTypeEnum.CHAT); diff --git a/libs/application-generic/src/utils/issues.ts b/libs/application-generic/src/utils/issues.ts index d6fafca6a64..aaf74e009c9 100644 --- a/libs/application-generic/src/utils/issues.ts +++ b/libs/application-generic/src/utils/issues.ts @@ -1,5 +1,6 @@ import { ContentIssueEnum, RuntimeIssue, StepTypeEnum } from '@novu/shared'; -import Ajv, { ErrorObject } from 'ajv'; +import Ajv, { type AnySchema, ErrorObject } from 'ajv'; +import Ajv2020 from 'ajv/dist/2020'; import addFormats from 'ajv-formats'; import { JSONSchemaDto } from '../dtos/json-schema.dto'; import { capitalize } from '../services/helper-service'; @@ -90,9 +91,28 @@ export type ControlIssues = { controls?: Record; }; +const DRAFT_2020_12_SCHEMA_ID = 'https://json-schema.org/draft/2020-12/schema'; + +function usesDraft2020Schema(schema: AnySchema): boolean { + if (typeof schema !== 'object' || schema === null || !('$schema' in schema)) { + return false; + } + + const schemaId = (schema as { $schema?: string }).$schema; + + return schemaId === DRAFT_2020_12_SCHEMA_ID || schemaId?.includes('2020-12') === true; +} + /** Single AJV configuration for every control-value schema check, so issues stay consistent. */ -export const createSchemaValidationAjv = ({ verbose = false }: { verbose?: boolean } = {}): Ajv => { - const ajv = new Ajv({ allErrors: true, strict: false, verbose }); +export const createSchemaValidationAjv = ({ + verbose = false, + schema, +}: { verbose?: boolean; schema?: AnySchema } = {}): Ajv => { + const ajv = + schema && usesDraft2020Schema(schema) + ? new Ajv2020({ allErrors: true, strict: false, verbose }) + : new Ajv({ allErrors: true, strict: false, verbose }); + addFormats(ajv); return ajv; @@ -182,7 +202,7 @@ export const processControlValuesBySchema = ({ return {}; } - const validate = createSchemaValidationAjv().compile(controlSchema); + const validate = createSchemaValidationAjv({ schema: controlSchema }).compile(controlSchema); const isValid = validate(controlValues); const errors = validate.errors as null | ErrorObject[]; diff --git a/libs/application-generic/src/utils/liquid-tolerant-validator.ts b/libs/application-generic/src/utils/liquid-tolerant-validator.ts index abbf0f4ab29..d93916171b0 100644 --- a/libs/application-generic/src/utils/liquid-tolerant-validator.ts +++ b/libs/application-generic/src/utils/liquid-tolerant-validator.ts @@ -154,7 +154,7 @@ function dedupe(errors: ErrorObject[]): ErrorObject[] { * so callers are expected to build one per schema and keep it. */ export function createLiquidTolerantValidator(schema: JSONSchemaDto): (value: unknown) => ErrorObject[] { - const validate = createSchemaValidationAjv({ verbose: true }).compile(schema); + const validate = createSchemaValidationAjv({ verbose: true, schema }).compile(schema); const pathIndex = buildSchemaPathIndex(schema); return (value: unknown) => {