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.
- package/README.md +219 -65
- package/dist/formats.d.ts.map +1 -1
- package/dist/index.d.ts +25 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1837 -484
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.mjs +1837 -484
- package/dist/keywords/array-keywords.d.ts.map +1 -1
- package/dist/keywords/object-keywords.d.ts.map +1 -1
- package/dist/keywords/other-keywords.d.ts.map +1 -1
- package/dist/keywords/string-keywords.d.ts.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/deep-freeze.d.ts +5 -0
- package/dist/utils/deep-freeze.d.ts.map +1 -0
- package/dist/utils/has-changed.d.ts +2 -0
- package/dist/utils/has-changed.d.ts.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/{utils.d.ts → utils/main-utils.d.ts} +7 -9
- package/dist/utils/main-utils.d.ts.map +1 -0
- package/dist/utils/pattern-matcher.d.ts +3 -0
- package/dist/utils/pattern-matcher.d.ts.map +1 -0
- package/lib/formats.ts +468 -155
- package/lib/index.ts +702 -107
- package/lib/keywords/array-keywords.ts +260 -52
- package/lib/keywords/number-keywords.ts +1 -1
- package/lib/keywords/object-keywords.ts +295 -88
- package/lib/keywords/other-keywords.ts +263 -70
- package/lib/keywords/string-keywords.ts +123 -7
- package/lib/types.ts +5 -18
- package/lib/utils/deep-freeze.ts +208 -0
- package/lib/utils/has-changed.ts +51 -0
- package/lib/utils/index.ts +4 -0
- package/lib/{utils.ts → utils/main-utils.ts} +63 -77
- package/lib/utils/pattern-matcher.ts +66 -0
- package/package.json +2 -2
- package/tsconfig.json +4 -4
- package/dist/utils.d.ts.map +0 -1
|
@@ -1,8 +1,102 @@
|
|
|
1
|
-
import { isCompiledSchema
|
|
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 =
|
|
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 &&
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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)
|
|
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 =
|
|
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 (!
|
|
206
|
+
if (!Array.isArray(data) || !Array.isArray(schema.items)) {
|
|
105
207
|
return;
|
|
106
208
|
}
|
|
107
209
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 (
|
|
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 =
|
|
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
|
|
259
|
+
const len = data.length;
|
|
260
|
+
if (len <= 1) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
142
263
|
|
|
143
|
-
|
|
144
|
-
let
|
|
264
|
+
if (len <= 8) {
|
|
265
|
+
for (let i = 0; i < len; i++) {
|
|
266
|
+
const left = data[i];
|
|
145
267
|
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
295
|
+
|
|
296
|
+
return;
|
|
166
297
|
}
|
|
167
298
|
|
|
168
|
-
|
|
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 =
|
|
395
|
+
const error = containsValidate(data[i]);
|
|
188
396
|
if (!error) {
|
|
189
397
|
return;
|
|
190
398
|
}
|