schema-shield 0.0.6 → 1.0.1

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.
Files changed (39) hide show
  1. package/README.md +219 -65
  2. package/dist/formats.d.ts.map +1 -1
  3. package/dist/index.d.ts +25 -6
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1837 -484
  6. package/dist/index.min.js +1 -1
  7. package/dist/index.min.js.map +1 -1
  8. package/dist/index.mjs +1837 -484
  9. package/dist/keywords/array-keywords.d.ts.map +1 -1
  10. package/dist/keywords/object-keywords.d.ts.map +1 -1
  11. package/dist/keywords/other-keywords.d.ts.map +1 -1
  12. package/dist/keywords/string-keywords.d.ts.map +1 -1
  13. package/dist/types.d.ts.map +1 -1
  14. package/dist/utils/deep-freeze.d.ts +5 -0
  15. package/dist/utils/deep-freeze.d.ts.map +1 -0
  16. package/dist/utils/has-changed.d.ts +2 -0
  17. package/dist/utils/has-changed.d.ts.map +1 -0
  18. package/dist/utils/index.d.ts +5 -0
  19. package/dist/utils/index.d.ts.map +1 -0
  20. package/dist/{utils.d.ts → utils/main-utils.d.ts} +7 -9
  21. package/dist/utils/main-utils.d.ts.map +1 -0
  22. package/dist/utils/pattern-matcher.d.ts +3 -0
  23. package/dist/utils/pattern-matcher.d.ts.map +1 -0
  24. package/lib/formats.ts +468 -155
  25. package/lib/index.ts +702 -107
  26. package/lib/keywords/array-keywords.ts +260 -52
  27. package/lib/keywords/number-keywords.ts +1 -1
  28. package/lib/keywords/object-keywords.ts +295 -88
  29. package/lib/keywords/other-keywords.ts +263 -70
  30. package/lib/keywords/string-keywords.ts +123 -7
  31. package/lib/types.ts +5 -18
  32. package/lib/utils/deep-freeze.ts +208 -0
  33. package/lib/utils/has-changed.ts +51 -0
  34. package/lib/utils/index.ts +4 -0
  35. package/lib/{utils.ts → utils/main-utils.ts} +63 -77
  36. package/lib/utils/pattern-matcher.ts +66 -0
  37. package/package.json +2 -2
  38. package/tsconfig.json +4 -4
  39. package/dist/utils.d.ts.map +0 -1
@@ -1,8 +1,102 @@
1
- import { isCompiledSchema, isObject } from "../utils";
1
+ import { isCompiledSchema } from "../utils/main-utils";
2
2
 
3
3
  import { KeywordFunction } from "../index";
4
+ import { hasChanged } from "../utils/has-changed";
5
+
6
+ function isUniquePrimitive(value: any) {
7
+ return (
8
+ value === null ||
9
+ typeof value === "string" ||
10
+ typeof value === "number" ||
11
+ typeof value === "boolean"
12
+ );
13
+ }
14
+
15
+ function getArrayBucketKey(value: any[]): string {
16
+ const length = value.length;
17
+ if (length === 0) {
18
+ return "0";
19
+ }
20
+
21
+ const first = value[0];
22
+ const last = value[length - 1];
23
+ const firstType = first === null ? "null" : typeof first;
24
+ const lastType = last === null ? "null" : typeof last;
25
+
26
+ let firstArrayMarker = "";
27
+ if (Array.isArray(first)) {
28
+ const firstSignature = getPrimitiveArraySignature(first);
29
+ firstArrayMarker = firstSignature === null ? `a:${first.length}` : firstSignature;
30
+ }
31
+
32
+ let lastArrayMarker = "";
33
+ if (Array.isArray(last)) {
34
+ const lastSignature = getPrimitiveArraySignature(last);
35
+ lastArrayMarker = lastSignature === null ? `a:${last.length}` : lastSignature;
36
+ }
37
+
38
+ return `${length}:${firstType}:${firstArrayMarker}:${lastType}:${lastArrayMarker}`;
39
+ }
40
+
41
+ function getObjectShapeKey(value: Record<string, any>): string {
42
+ const keys = Object.keys(value).sort();
43
+ return `${keys.length}:${keys.join("\u0001")}`;
44
+ }
45
+
46
+ function getPrimitiveArraySignature(value: any[]): string | null {
47
+ const length = value.length;
48
+
49
+ if (length === 0) {
50
+ return "a:0";
51
+ }
52
+
53
+ if (!isUniquePrimitive(value[0]) || !isUniquePrimitive(value[length - 1])) {
54
+ return null;
55
+ }
56
+
57
+ let signature = `a:${length}:`;
58
+
59
+ for (let i = 0; i < length; i++) {
60
+ const item = value[i];
61
+
62
+ if (item === null) {
63
+ signature += "l;";
64
+ continue;
65
+ }
66
+
67
+ if (typeof item === "string") {
68
+ signature += `s${item.length}:${item};`;
69
+ continue;
70
+ }
71
+
72
+ if (typeof item === "number") {
73
+ if (Number.isNaN(item)) {
74
+ signature += "n:NaN;";
75
+ continue;
76
+ }
77
+
78
+ if (Object.is(item, -0)) {
79
+ signature += "n:-0;";
80
+ continue;
81
+ }
82
+
83
+ signature += `n:${item};`;
84
+ continue;
85
+ }
86
+
87
+ if (typeof item === "boolean") {
88
+ signature += item ? "b:1;" : "b:0;";
89
+ continue;
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ return signature;
96
+ }
4
97
 
5
98
  export const ArrayKeywords: Record<string, KeywordFunction> = {
99
+ // lib/keywords/array-keywords.ts
6
100
  items(schema, data, defineError) {
7
101
  if (!Array.isArray(data)) {
8
102
  return;
@@ -15,17 +109,19 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
15
109
  if (schemaItems === false && dataLength > 0) {
16
110
  return defineError("Array items are not allowed", { data });
17
111
  }
18
-
19
112
  return;
20
113
  }
21
114
 
22
115
  if (Array.isArray(schemaItems)) {
23
116
  const schemaItemsLength = schemaItems.length;
24
- const itemsLength = Math.min(schemaItemsLength, dataLength);
117
+ const itemsLength =
118
+ schemaItemsLength < dataLength ? schemaItemsLength : dataLength;
119
+
25
120
  for (let i = 0; i < itemsLength; i++) {
26
121
  const schemaItem = schemaItems[i];
122
+
27
123
  if (typeof schemaItem === "boolean") {
28
- if (schemaItem === false && typeof data[i] !== "undefined") {
124
+ if (schemaItem === false && data[i] !== undefined) {
29
125
  return defineError("Array item is not allowed", {
30
126
  item: i,
31
127
  data: data[i]
@@ -34,8 +130,9 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
34
130
  continue;
35
131
  }
36
132
 
37
- if (isCompiledSchema(schemaItem)) {
38
- const error = schemaItem.$validate(data[i]);
133
+ const validate = schemaItem && schemaItem.$validate;
134
+ if (typeof validate === "function") {
135
+ const error = validate(data[i]);
39
136
  if (error) {
40
137
  return defineError("Array item is invalid", {
41
138
  item: i,
@@ -49,29 +146,36 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
49
146
  return;
50
147
  }
51
148
 
52
- if (isCompiledSchema(schemaItems)) {
53
- for (let i = 0; i < dataLength; i++) {
54
- const error = schemaItems.$validate(data[i]);
55
- if (error) {
56
- return defineError("Array item is invalid", {
57
- item: i,
58
- cause: error,
59
- data: data[i]
60
- });
61
- }
62
- }
149
+ const validate = schemaItems && schemaItems.$validate;
150
+ if (typeof validate !== "function") {
151
+ return;
63
152
  }
64
153
 
65
- return;
154
+ for (let i = 0; i < dataLength; i++) {
155
+ const error = validate(data[i]);
156
+ if (error) {
157
+ return defineError("Array item is invalid", {
158
+ item: i,
159
+ cause: error,
160
+ data: data[i]
161
+ });
162
+ }
163
+ }
66
164
  },
67
165
 
68
166
  elements(schema, data, defineError) {
69
- if (!Array.isArray(data) || !isCompiledSchema(schema.elements)) {
167
+ if (!Array.isArray(data)) {
168
+ return;
169
+ }
170
+
171
+ const elementsSchema = schema.elements;
172
+ const validate = elementsSchema && elementsSchema.$validate;
173
+ if (typeof validate !== "function") {
70
174
  return;
71
175
  }
72
176
 
73
177
  for (let i = 0; i < data.length; i++) {
74
- const error = schema.elements.$validate(data[i]);
178
+ const error = validate(data[i]);
75
179
  if (error) {
76
180
  return defineError("Array item is invalid", {
77
181
  item: i,
@@ -80,8 +184,6 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
80
184
  });
81
185
  }
82
186
  }
83
-
84
- return;
85
187
  },
86
188
 
87
189
  minItems(schema, data, defineError) {
@@ -101,20 +203,36 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
101
203
  },
102
204
 
103
205
  additionalItems(schema, data, defineError) {
104
- if (!schema.items || isObject(schema.items)) {
206
+ if (!Array.isArray(data) || !Array.isArray(schema.items)) {
105
207
  return;
106
208
  }
107
209
 
108
- if (schema.additionalItems === false) {
109
- if (data.length > schema.items.length) {
110
- return defineError("Array is too long", { data });
111
- }
210
+ let tupleLength = (schema as any)._tupleItemsLength as number | undefined;
211
+ if (tupleLength === undefined) {
212
+ tupleLength = schema.items.length;
213
+ Object.defineProperty(schema, "_tupleItemsLength", {
214
+ value: tupleLength,
215
+ enumerable: false,
216
+ configurable: false,
217
+ writable: false
218
+ });
219
+ }
220
+
221
+ if (data.length <= tupleLength) {
112
222
  return;
113
223
  }
114
224
 
115
- if (isObject(schema.additionalItems)) {
225
+ if (schema.additionalItems === false) {
226
+ return defineError("Array is too long", { data });
227
+ }
228
+
229
+ if (
230
+ schema.additionalItems &&
231
+ typeof schema.additionalItems === "object" &&
232
+ !Array.isArray(schema.additionalItems)
233
+ ) {
116
234
  if (isCompiledSchema(schema.additionalItems)) {
117
- for (let i = schema.items.length; i < data.length; i++) {
235
+ for (let i = tupleLength; i < data.length; i++) {
118
236
  const error = schema.additionalItems.$validate(data[i]);
119
237
  if (error) {
120
238
  return defineError("Array item is invalid", {
@@ -138,34 +256,123 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
138
256
  return;
139
257
  }
140
258
 
141
- const unique = new Set();
259
+ const len = data.length;
260
+ if (len <= 1) {
261
+ return;
262
+ }
142
263
 
143
- for (const item of data) {
144
- let itemStr;
264
+ if (len <= 8) {
265
+ for (let i = 0; i < len; i++) {
266
+ const left = data[i];
145
267
 
146
- // Change string to "string" to avoid false positives
147
- if (typeof item === "string") {
148
- itemStr = `s:${item}`;
149
- // Sort object keys to avoid false positives
150
- } else if (isObject(item)) {
151
- itemStr = `o:${JSON.stringify(
152
- Object.fromEntries(
153
- Object.entries(item).sort(([a], [b]) => a.localeCompare(b))
154
- )
155
- )}`;
156
- } else if (Array.isArray(item)) {
157
- itemStr = JSON.stringify(item);
158
- } else {
159
- itemStr = String(item);
160
- }
268
+ for (let j = i + 1; j < len; j++) {
269
+ const right = data[j];
161
270
 
162
- if (unique.has(itemStr)) {
163
- return defineError("Array items are not unique", { data: item });
271
+ if (left === right) {
272
+ return defineError("Array items are not unique", { data: right });
273
+ }
274
+
275
+ if (
276
+ typeof left === "number" &&
277
+ typeof right === "number" &&
278
+ Number.isNaN(left) &&
279
+ Number.isNaN(right)
280
+ ) {
281
+ return defineError("Array items are not unique", { data: right });
282
+ }
283
+
284
+ if (
285
+ left &&
286
+ right &&
287
+ typeof left === "object" &&
288
+ typeof right === "object" &&
289
+ !hasChanged(left, right)
290
+ ) {
291
+ return defineError("Array items are not unique", { data: right });
292
+ }
293
+ }
164
294
  }
165
- unique.add(itemStr);
295
+
296
+ return;
166
297
  }
167
298
 
168
- return;
299
+ const primitiveSeen = new Set<any>();
300
+ let primitiveArraySignatures: Set<string> | undefined;
301
+ let arrayBuckets: Map<string, any[]> | undefined;
302
+ let objectBuckets: Map<string, any[]> | undefined;
303
+
304
+ for (let i = 0; i < len; i++) {
305
+ const item = data[i];
306
+
307
+ if (isUniquePrimitive(item)) {
308
+ if (primitiveSeen.has(item)) {
309
+ return defineError("Array items are not unique", { data: item });
310
+ }
311
+ primitiveSeen.add(item);
312
+ continue;
313
+ }
314
+
315
+ if (!item || typeof item !== "object") {
316
+ continue;
317
+ }
318
+
319
+ if (Array.isArray(item)) {
320
+ const signature = getPrimitiveArraySignature(item);
321
+ if (signature !== null) {
322
+ if (!primitiveArraySignatures) {
323
+ primitiveArraySignatures = new Set<string>();
324
+ }
325
+
326
+ if (primitiveArraySignatures.has(signature)) {
327
+ return defineError("Array items are not unique", { data: item });
328
+ }
329
+
330
+ primitiveArraySignatures.add(signature);
331
+ continue;
332
+ }
333
+
334
+ if (!arrayBuckets) {
335
+ arrayBuckets = new Map<string, any[]>();
336
+ }
337
+
338
+ const bucketKey = getArrayBucketKey(item);
339
+ let candidates = arrayBuckets.get(bucketKey);
340
+
341
+ if (!candidates) {
342
+ candidates = [];
343
+ arrayBuckets.set(bucketKey, candidates);
344
+ }
345
+
346
+ for (let j = 0; j < candidates.length; j++) {
347
+ if (!hasChanged(candidates[j], item)) {
348
+ return defineError("Array items are not unique", { data: item });
349
+ }
350
+ }
351
+
352
+ candidates.push(item);
353
+ continue;
354
+ }
355
+
356
+ if (!objectBuckets) {
357
+ objectBuckets = new Map<string, any[]>();
358
+ }
359
+
360
+ const bucketKey = getObjectShapeKey(item);
361
+ let candidates = objectBuckets.get(bucketKey);
362
+
363
+ if (!candidates) {
364
+ candidates = [];
365
+ objectBuckets.set(bucketKey, candidates);
366
+ }
367
+
368
+ for (let j = 0; j < candidates.length; j++) {
369
+ if (!hasChanged(candidates[j], item)) {
370
+ return defineError("Array items are not unique", { data: item });
371
+ }
372
+ }
373
+
374
+ candidates.push(item);
375
+ }
169
376
  },
170
377
 
171
378
  contains(schema, data, defineError) {
@@ -183,8 +390,9 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
183
390
  return defineError("Array must not contain any items", { data });
184
391
  }
185
392
 
393
+ const containsValidate = schema.contains.$validate;
186
394
  for (let i = 0; i < data.length; i++) {
187
- const error = schema.contains.$validate(data[i]);
395
+ const error = containsValidate(data[i]);
188
396
  if (!error) {
189
397
  return;
190
398
  }
@@ -1,5 +1,5 @@
1
1
  import { KeywordFunction } from "../index";
2
- import { areCloseEnough } from "../utils";
2
+ import { areCloseEnough } from "../utils/index";
3
3
 
4
4
  export const NumberKeywords: Record<string, KeywordFunction> = {
5
5
  minimum(schema, data, defineError, instance) {