@cloudflare/cabidela 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.2] - 2025-03-07
6
+
7
+ ### Added
8
+
9
+ - $merge support - see README for more information
10
+
5
11
  ## [0.1.1] - 2025-02-26
6
12
 
7
13
  ### Added
package/README.md CHANGED
@@ -71,6 +71,8 @@ Cabidela takes a JSON-Schema and optional configuration flags:
71
71
  - `applyDefaults`: boolean - If true, the validator will apply default values to the input object. Default is false.
72
72
  - `errorMessages`: boolean - If true, the validator will use custom `errorMessage` messages from the schema. Default is false.
73
73
  - `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.
74
+ - `useMerge`: boolean - Set to true if you want to use the `$merge` keyword. Default is false. See below for more information.
75
+ - `subSchema`: any[] - An optional array of sub-schemas that can be used with `$id` and `$ref`. See below for more information.
74
76
 
75
77
  Returns a validation object.
76
78
 
@@ -249,6 +251,49 @@ cabidela.validate({
249
251
  });
250
252
  ```
251
253
 
254
+ ## Combined schemas and $merge
255
+
256
+ The standard way of combining and extending schemas is by using the [`allOf`](https://json-schema.org/understanding-json-schema/reference/combining#allOf) (AND), [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf) (OR), [`oneOf`](https://json-schema.org/understanding-json-schema/reference/combining#oneOf) (XOR) and [`not`](https://json-schema.org/understanding-json-schema/reference/combining#not) keywords, all supported by this library.
257
+
258
+ Cabidela supports an additional keyword `$merge` (inspired by [Ajv](https://ajv.js.org/guide/combining-schemas.html#merge-and-patch-keywords)) that allows you to merge two objects. This is useful when you want to extend a schema with additional properties and `allOf`` is not enough.
259
+
260
+ Here's how it works:
261
+
262
+ ```json
263
+ {
264
+ "$merge": {
265
+ "source": {
266
+ "type": "object",
267
+ "properties": { "p": { "type": "string" } },
268
+ "additionalProperties": false
269
+ },
270
+ "with": {
271
+ "properties": { "q": { "type": "number" } }
272
+ }
273
+ }
274
+ }
275
+ ```
276
+
277
+ Resolves to:
278
+
279
+ ```json
280
+ {
281
+ "type": "object",
282
+ "properties": {
283
+ "q": {
284
+ "type": "number"
285
+ }
286
+ },
287
+ "additionalProperties": false
288
+ }
289
+ ```
290
+
291
+ To use `$merge` set the `useMerge` flag to true when creating the instance.
292
+
293
+ ```js
294
+ new Cabidela(schema, { useMerge: true });
295
+ ```
296
+
252
297
  ## Custom errors
253
298
 
254
299
  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.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  type CabidelaOptions = {
2
2
  applyDefaults?: boolean;
3
+ useMerge?: boolean;
3
4
  errorMessages?: boolean;
4
5
  fullErrors?: boolean;
5
6
  subSchemas?: Array<any>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  type CabidelaOptions = {
2
2
  applyDefaults?: boolean;
3
+ useMerge?: boolean;
3
4
  errorMessages?: boolean;
4
5
  fullErrors?: boolean;
5
6
  subSchemas?: Array<any>;
package/dist/index.js CHANGED
@@ -31,25 +31,45 @@ var parse$ref = (ref) => {
31
31
  const $path = parts[1].split("/").filter((part) => part !== "");
32
32
  return { $id, $path };
33
33
  };
34
- var traverseSchema = (definitions, obj, cb = () => {
35
- }) => {
36
- Object.keys(obj).forEach((key) => {
37
- if (obj[key] !== null && typeof obj[key] === "object") {
38
- traverseSchema(definitions, obj[key], (value) => {
39
- obj[key] = value;
40
- });
41
- } else {
42
- if (key === "$ref") {
43
- const { $id, $path } = parse$ref(obj[key]);
44
- const { resolvedObject } = resolvePayload($path, definitions[$id]);
45
- if (resolvedObject) {
46
- cb(resolvedObject);
47
- } else {
48
- throw new Error(`Could not resolve '${obj[key]}' $ref`);
34
+ function deepMerge(target, source) {
35
+ const result = Array.isArray(target) && Array.isArray(source) ? target.concat(source) : { ...target, ...source };
36
+ for (const key of Object.keys(result)) {
37
+ result[key] = typeof target[key] == "object" && typeof source[key] == "object" ? deepMerge(target[key], source[key]) : structuredClone(result[key]);
38
+ }
39
+ return result;
40
+ }
41
+ var traverseSchema = (options, definitions, obj, cb) => {
42
+ let hits;
43
+ do {
44
+ hits = 0;
45
+ Object.keys(obj).forEach((key) => {
46
+ if (obj[key] !== null && typeof obj[key] === "object") {
47
+ traverseSchema(options, definitions, obj[key], (value) => {
48
+ hits++;
49
+ obj[key] = value;
50
+ });
51
+ if (options.useMerge && key === "$merge") {
52
+ const merge = deepMerge(obj[key].source, obj[key].with);
53
+ if (cb) {
54
+ cb(merge);
55
+ } else {
56
+ Object.assign(obj, merge);
57
+ delete obj[key];
58
+ }
59
+ }
60
+ } else {
61
+ if (key === "$ref") {
62
+ const { $id, $path } = parse$ref(obj[key]);
63
+ const { resolvedObject } = resolvePayload($path, definitions[$id]);
64
+ if (resolvedObject) {
65
+ cb(resolvedObject);
66
+ } else {
67
+ throw new Error(`Could not resolve '${obj[key]}' $ref`);
68
+ }
49
69
  }
50
70
  }
51
- }
52
- });
71
+ });
72
+ } while (hits > 0);
53
73
  };
54
74
  var resolvePayload = (path, obj) => {
55
75
  let resolvedObject = path.reduce(function(prev, curr) {
@@ -114,7 +134,9 @@ var Cabidela = class {
114
134
  for (const subSchema of this.options.subSchemas) {
115
135
  this.addSchema(subSchema, false);
116
136
  }
117
- traverseSchema(this.definitions, this.schema);
137
+ }
138
+ if (this.options.useMerge || this.options.subSchemas.length > 0) {
139
+ traverseSchema(this.options, this.definitions, this.schema);
118
140
  }
119
141
  }
120
142
  setSchema(schema) {
@@ -133,7 +155,7 @@ var Cabidela = class {
133
155
  } else {
134
156
  throw new Error("subSchemas need $id https://json-schema.org/understanding-json-schema/structuring#id");
135
157
  }
136
- if (combine == true) traverseSchema(this.definitions, this.schema);
158
+ if (combine == true) traverseSchema(this.options, this.definitions, this.schema);
137
159
  }
138
160
  getSchema() {
139
161
  return this.schema;
package/dist/index.mjs CHANGED
@@ -5,25 +5,45 @@ var parse$ref = (ref) => {
5
5
  const $path = parts[1].split("/").filter((part) => part !== "");
6
6
  return { $id, $path };
7
7
  };
8
- var traverseSchema = (definitions, obj, cb = () => {
9
- }) => {
10
- Object.keys(obj).forEach((key) => {
11
- if (obj[key] !== null && typeof obj[key] === "object") {
12
- traverseSchema(definitions, obj[key], (value) => {
13
- obj[key] = value;
14
- });
15
- } else {
16
- if (key === "$ref") {
17
- const { $id, $path } = parse$ref(obj[key]);
18
- const { resolvedObject } = resolvePayload($path, definitions[$id]);
19
- if (resolvedObject) {
20
- cb(resolvedObject);
21
- } else {
22
- throw new Error(`Could not resolve '${obj[key]}' $ref`);
8
+ function deepMerge(target, source) {
9
+ const result = Array.isArray(target) && Array.isArray(source) ? target.concat(source) : { ...target, ...source };
10
+ for (const key of Object.keys(result)) {
11
+ result[key] = typeof target[key] == "object" && typeof source[key] == "object" ? deepMerge(target[key], source[key]) : structuredClone(result[key]);
12
+ }
13
+ return result;
14
+ }
15
+ var traverseSchema = (options, definitions, obj, cb) => {
16
+ let hits;
17
+ do {
18
+ hits = 0;
19
+ Object.keys(obj).forEach((key) => {
20
+ if (obj[key] !== null && typeof obj[key] === "object") {
21
+ traverseSchema(options, definitions, obj[key], (value) => {
22
+ hits++;
23
+ obj[key] = value;
24
+ });
25
+ if (options.useMerge && key === "$merge") {
26
+ const merge = deepMerge(obj[key].source, obj[key].with);
27
+ if (cb) {
28
+ cb(merge);
29
+ } else {
30
+ Object.assign(obj, merge);
31
+ delete obj[key];
32
+ }
33
+ }
34
+ } else {
35
+ if (key === "$ref") {
36
+ const { $id, $path } = parse$ref(obj[key]);
37
+ const { resolvedObject } = resolvePayload($path, definitions[$id]);
38
+ if (resolvedObject) {
39
+ cb(resolvedObject);
40
+ } else {
41
+ throw new Error(`Could not resolve '${obj[key]}' $ref`);
42
+ }
23
43
  }
24
44
  }
25
- }
26
- });
45
+ });
46
+ } while (hits > 0);
27
47
  };
28
48
  var resolvePayload = (path, obj) => {
29
49
  let resolvedObject = path.reduce(function(prev, curr) {
@@ -88,7 +108,9 @@ var Cabidela = class {
88
108
  for (const subSchema of this.options.subSchemas) {
89
109
  this.addSchema(subSchema, false);
90
110
  }
91
- traverseSchema(this.definitions, this.schema);
111
+ }
112
+ if (this.options.useMerge || this.options.subSchemas.length > 0) {
113
+ traverseSchema(this.options, this.definitions, this.schema);
92
114
  }
93
115
  }
94
116
  setSchema(schema) {
@@ -107,7 +129,7 @@ var Cabidela = class {
107
129
  } else {
108
130
  throw new Error("subSchemas need $id https://json-schema.org/understanding-json-schema/structuring#id");
109
131
  }
110
- if (combine == true) traverseSchema(this.definitions, this.schema);
132
+ if (combine == true) traverseSchema(this.options, this.definitions, this.schema);
111
133
  }
112
134
  getSchema() {
113
135
  return this.schema;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/cabidela",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Cabidela is a small, fast, eval-less, Cloudflare Workers compatible, dynamic JSON Schema validator",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -27,6 +27,9 @@
27
27
  "README.md",
28
28
  "CHANGELOG.md"
29
29
  ],
30
+ "prettier": {
31
+ "embeddedLanguageFormatting": "auto"
32
+ },
30
33
  "repository": {
31
34
  "type": "git",
32
35
  "url": "https://github.com/cloudflare/cabidela.git"
@@ -39,6 +42,7 @@
39
42
  "@vitest/ui": "^3.0.3",
40
43
  "ajv": "^8.17.1",
41
44
  "ajv-errors": "^3.0.0",
45
+ "ajv-merge-patch": "^5.0.1",
42
46
  "tsup": "^8.3.6",
43
47
  "typescript": "^5.7.3",
44
48
  "vitest": "^3.0.3"