From e72be267e1bb5989b336ecadade1eacc0e1f2ec9 Mon Sep 17 00:00:00 2001 From: Kostiantyn Dvornik Date: Tue, 19 Mar 2024 00:55:27 +0200 Subject: [PATCH] update: add transformDate keyword --- src/js/utils/ajv.ts | 63 +++++++++++++++++++++++++++++++++++++++++--- tests/js/validate.ts | 34 ++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/js/utils/ajv.ts b/src/js/utils/ajv.ts index 5e2b30b4b..668198b91 100644 --- a/src/js/utils/ajv.ts +++ b/src/js/utils/ajv.ts @@ -1,4 +1,4 @@ -import type {} from "ajv"; // @see https://github.com/microsoft/TypeScript/issues/47663 +import type { FuncKeywordDefinition } from "ajv"; import Ajv, { SchemaObject } from "ajv"; import { AnyValidateFunction } from "ajv/dist/core"; @@ -25,6 +25,59 @@ function addAdditionalPropertiesToSchema(schema: JSONSchema, additionalPropertie }); } +function addTransformDateKeywordToSchema(schema: JSONSchema) { + return mapObjectDeep(schema, (object) => { + const localSchema = object as JSONSchema; + + if ( + typeof object === "object" && + localSchema?.type === "string" && + localSchema?.format === "date-time" + ) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { type, format, ...restSchema } = localSchema; + return { + ...restSchema, + transformDate: true, + }; + } + }); +} + +/** + * This function defines custom transformDate AJV keyword + * + * Problem: + * There's no way to define dates except as a string with "date-time" or "date" format in JsonSchema: + * { + * type: "string", + * format: "date-time" + * } + * But we need the Date object to be stored in databases or to perform correct date comparison + * + * Solution: + * The keyword will convert string with "date-time" format to Date object + */ +function transformDateKeyword(): FuncKeywordDefinition { + return { + keyword: "transformDate", + schemaType: "boolean", + modifying: true, + validate(transformDate, date, _metadata, dataCxt) { + if (transformDate && dataCxt && typeof date === "string") { + const dateObject = new Date(date); + if (dateObject.toString() === "Invalid Date") { + return false; + } + + dataCxt.parentData[dataCxt.parentDataProperty] = dateObject; + } + + return true; + }, + }; +} + const ajvConfig = { strict: false, // TODO: adjust schemas and enable strict mode useDefaults: true, @@ -33,6 +86,7 @@ const ajvConfig = { * @see https://ajv.js.org/guide/modifying-data.html#assigning-defaults */ discriminator: true, + keywords: [transformDateKeyword()], }; const ajvValidator = new Ajv({ ...ajvConfig }); @@ -69,8 +123,11 @@ export function getValidator( let validate = ajv.getSchema(schemaKey); if (!validate) { - // properties that were not defined in schema will be ignored when clean = false - const patchedSchema = clean ? addAdditionalPropertiesToSchema(jsonSchema) : jsonSchema; + // replace "date-time" format with "transformDate" keyword + const patchedSchema = addTransformDateKeywordToSchema( + // properties that were not defined in schema will be ignored when clean = false + clean ? addAdditionalPropertiesToSchema(jsonSchema) : jsonSchema, + ); ajv.addSchema(patchedSchema, schemaKey); validate = ajv.getSchema(schemaKey); } diff --git a/tests/js/validate.ts b/tests/js/validate.ts index 94ca6a627..a17e37d8e 100644 --- a/tests/js/validate.ts +++ b/tests/js/validate.ts @@ -4,6 +4,7 @@ import groupBy from "lodash/groupBy"; import path from "path"; import JSONSchemasInterface from "../../src/js/esse/JSONSchemasInterfaceServer"; +import { AnyObject } from "../../src/js/esse/types"; import * as ajv from "../../src/js/utils/ajv"; import { walkDirSync } from "../../src/js/utils/filesystem"; @@ -35,6 +36,39 @@ describe("validate all examples", () => { }); }); +interface Example extends AnyObject { + property: string | Date; +} + +describe("validate Date object", () => { + const schema = { + $id: "validate-date-object", + type: "object", + properties: { + property: { + type: "string", + format: "date-time", + }, + }, + }; + + const example1: Example = { property: new Date() }; + const example2: Example = { property: "December 17, 1995 03:24:00" }; + const example3: Example = { property: "Invalid Date" }; + + const result1 = ajv.validate(example1, schema); + const result2 = ajv.validate(example2, schema); + const result3 = ajv.validate(example3, schema); + + expect(result1.isValid).to.be.equal(true); + expect(result2.isValid).to.be.equal(true); + expect(result3.isValid).to.be.equal(false); + + expect(example1.property instanceof Date).to.be.equal(true); + expect(example2.property instanceof Date).to.be.equal(true); + expect(typeof example3.property === "string").to.be.equal(true); +}); + describe("schema titles must be unique or empty", () => { JSONSchemasInterface.setSchemaFolder(schemasPath); const schemas = JSONSchemasInterface.schemasCache.values();