From b9c95284866535ca160ac668b81f1b5af7f19b3f Mon Sep 17 00:00:00 2001 From: Celso Martinho Date: Wed, 26 Mar 2025 16:33:16 +0000 Subject: [PATCH 1/4] improves error handling --- src/index.ts | 6 ++---- tests/60-error-messages.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 58f67e3..46c561c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -207,7 +207,7 @@ export class Cabidela { absorvErrors: true, deferredApplyDefaults: true, }); - rounds += matches; + rounds++; if (breakCondition && breakCondition(rounds)) break; defaultsCallbacks.push(...needle.defaultsCallbacks); needle.defaultsCallbacks = []; @@ -247,9 +247,7 @@ export class Cabidela { if (needle.schema.hasOwnProperty("oneOf")) { const rounds = this.parseList(needle.schema.oneOf, needle, (r: number) => r !== 1); if (rounds !== 1) { - if (needle.path.length == 0) { - this.throw(`oneOf at '${pathToString(needle.path)}' not met, ${rounds} matches`, needle); - } + this.throw(`oneOf at '${pathToString(needle.path)}' not met, ${rounds} matches found`, needle); return 0; } return 1; diff --git a/tests/60-error-messages.test.ts b/tests/60-error-messages.test.ts index 42166b6..17ea347 100644 --- a/tests/60-error-messages.test.ts +++ b/tests/60-error-messages.test.ts @@ -89,13 +89,13 @@ describe("errorMessages oneOf", () => { cabidela.validate({ missing: "property", }), - ).toThrowError(/oneOf at '.' not met, 0 matches: prompt required, messages required/); + ).toThrowError(/oneOf at '\/' not met, 0 matches found: prompt required, messages required/); }); test.skipIf(process.env.AJV)("messages need role and content", () => { expect(() => cabidela.validate({ messages: [{ role: "user" }], }), - ).toThrowError(/oneOf at '.' not met, 0 matches: prompt required, messages need both role and content/); + ).toThrowError(/oneOf at '\/' not met, 0 matches found: prompt required, messages need both role and content/); }); }); From 0120a56d63dd7f1aa81186777dddd24b5b5f4fbe Mon Sep 17 00:00:00 2001 From: Joaquin Gimenez Date: Fri, 20 Jun 2025 16:21:01 -0500 Subject: [PATCH 2/4] Fix typo on error message --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 58f67e3..d4ddf7e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -139,7 +139,7 @@ export class Cabidela { if (needle.schema.hasOwnProperty("maxProperties")) { if (Object.keys(needle.payload).length > needle.schema.maxProperties) { this.throw( - `maxProperties at '${pathToString(needle.path)}' is ${needle.schema.minProperties}, got ${Object.keys(needle.payload).length}`, + `maxProperties at '${pathToString(needle.path)}' is ${needle.schema.maxProperties}, got ${Object.keys(needle.payload).length}`, needle, ); } From 5b984cdd00a04850e929a9f92c6c281d50323063 Mon Sep 17 00:00:00 2001 From: Celso Martinho Date: Tue, 1 Jul 2025 15:54:57 +0100 Subject: [PATCH 3/4] $patch --- CHANGELOG.md | 6 ++++++ README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/helpers.ts | 29 ++++++++++++++++++++++++++++- src/index.ts | 5 ++++- 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eea56e0..4724cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [0.2.5] - 2025-03-27 + +### Changed + +- Improved error handling for oneOf, anyOf, allOf + ## [0.2.4] - 2025-03-24 ### Changed diff --git a/README.md b/README.md index ec56530..d78db8c 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Cabidela takes a JSON-Schema and optional configuration flags: - `errorMessages`: boolean - If true, the validator will use custom `errorMessage` messages from the schema. Default is false. - `fullErrors`: boolean - If true, the validator will be more verbose when throwing errors for complex schemas (example: anyOf, oneOf's), set to false for shorter exceptions. Default is true. - `useMerge`: boolean - Set to true if you want to use the `$merge` keyword. Default is false. See below for more information. +- `usePatch`: boolean - Set to true if you want to use the `$patch` keyword. Default is false. See below for more information. - `subSchemas`: any[] - An optional array of sub-schemas that can be used with `$id` and `$ref`. See below for more information. Returns a validation object. @@ -299,6 +300,52 @@ new Cabidela(schema, { useMerge: true }); You can combine `$merge` with `$id` and `$ref` keywords, which get resolved first, for even more flexibility. +## $patch + +Use can use `$patch` to remove properties from an object. + +Here's how it works: + +```json +{ + "$patch": { + "source": { + "type": "object", + "properties": { + "p": { "type": "string" }, + "q": { "type": "number" } + }, + "additionalProperties": false + }, + "with": { + "properties": { "q": null } + } + } +} +``` + +Resolves to: + +```json +{ + "type": "object", + "properties": { + "p": { + "type": "string" } + }, + }, + "additionalProperties": false +} +``` + +To use `$patch` set the `usePatch` flag to true when creating the instance. + +```js +new Cabidela(schema, { usePatch: true }); +``` + +Like `$merge`, you can combine `$patch` with `$id` and `$ref` keywords, which get resolved first, for even more flexibility. + ## Custom errors If the new instance options has the `errorMessages` flag set to true, you can use the property `errorMessage` in the schema to define custom error messages. diff --git a/src/helpers.ts b/src/helpers.ts index 0b05883..749bc30 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -35,9 +35,26 @@ function deepMerge(target: any, source: any) { return result; } +function deepPatch(target: any, source: any) { + const result = { ...target, ...source }; + console.log(result); + for (const key of Object.keys(result)) { + if (typeof target[key] == "object" && typeof source[key] == "object") { + result[key] = deepMerge(target[key], source[key]); + } else if (target[key] == null) { + delete result[key]; + } else { + result[key] = structuredClone(result[key]); + } + } + console.log(result); + return result; +} + export const traverseSchema = (options: CabidelaOptions, definitions: any, obj: any) => { const ts = (obj: any, cb?: any) => { let hits: number; + if (!obj) return; do { hits = 0; for (const key of Object.keys(obj)) { @@ -57,6 +74,16 @@ export const traverseSchema = (options: CabidelaOptions, definitions: any, obj: delete obj[key]; } } + if (options.usePatch && key == "$patch") { + const merge = deepPatch(obj[key].source, obj[key].with); + if (cb) { + cb(merge); + } else { + // root level + Object.assign(obj, merge); + delete obj[key]; + } + } } else { if (key == "$ref") { const { $id, $path } = parse$ref(obj[key]); @@ -76,7 +103,7 @@ export const traverseSchema = (options: CabidelaOptions, definitions: any, obj: } } } - } while (hits > 0); + } while (obj && hits > 0); }; ts(obj); }; diff --git a/src/index.ts b/src/index.ts index 46c561c..d3af9b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { resolvePayload, pathToString, traverseSchema } from "./helpers"; export type CabidelaOptions = { applyDefaults?: boolean; useMerge?: boolean; + usePatch?: boolean; errorMessages?: boolean; fullErrors?: boolean; subSchemas?: Array; @@ -30,6 +31,8 @@ export class Cabidela { this.options = { fullErrors: true, subSchemas: [], + useMerge: false, + usePatch: false, applyDefaults: false, errorMessages: false, ...(options || {}), @@ -43,7 +46,7 @@ export class Cabidela { this.addSchema(subSchema, false); } } - if (this.options.useMerge || (this.options.subSchemas as []).length > 0) { + if (this.options.useMerge || this.options.usePatch || (this.options.subSchemas as []).length > 0) { traverseSchema(this.options, this.definitions, this.schema); } } From 41256832149fa2e2b5bf05a92ecd334707f6abc9 Mon Sep 17 00:00:00 2001 From: Celso Martinho Date: Tue, 8 Sep 2026 15:38:04 +0100 Subject: [PATCH 4/4] Improves patch --- README.md | 4 +--- src/helpers.ts | 14 +++++++------- tests/11-patch.test.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 tests/11-patch.test.ts diff --git a/README.md b/README.md index d78db8c..de8165d 100644 --- a/README.md +++ b/README.md @@ -330,9 +330,7 @@ Resolves to: { "type": "object", "properties": { - "p": { - "type": "string" } - }, + "p": { "type": "string" }, }, "additionalProperties": false } diff --git a/src/helpers.ts b/src/helpers.ts index 749bc30..2e7ac54 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -36,18 +36,18 @@ function deepMerge(target: any, source: any) { } function deepPatch(target: any, source: any) { - const result = { ...target, ...source }; - console.log(result); - for (const key of Object.keys(result)) { + const result = { ...target }; + for (const key of Object.keys(target)) { if (typeof target[key] == "object" && typeof source[key] == "object") { - result[key] = deepMerge(target[key], source[key]); - } else if (target[key] == null) { - delete result[key]; + const patch = deepPatch(target[key], source[key]); + if (patch) result[key] = patch; + else delete result[key]; + } else if (source === null) { + return null; } else { result[key] = structuredClone(result[key]); } } - console.log(result); return result; } diff --git a/tests/11-patch.test.ts b/tests/11-patch.test.ts new file mode 100644 index 0000000..067e2dc --- /dev/null +++ b/tests/11-patch.test.ts @@ -0,0 +1,31 @@ +import { expect, describe, test } from "vitest"; +import { FakeCabidela } from "./lib/fake-cabidela"; + +describe("$patch", () => { + test.skipIf(process.env.AJV)("two objects", () => { + let schema = { + $patch: { + source: { + type: "object", + properties: { + p: { type: "string" }, + q: { type: "number" }, + }, + additionalProperties: false, + }, + with: { + properties: { q: null }, + }, + }, + }; + const cabidela = new FakeCabidela(schema, { usePatch: true }); + schema = cabidela.getSchema(); + expect(schema).toStrictEqual({ + type: "object", + properties: { + p: { type: "string" }, + }, + additionalProperties: false, + }); + }); +});