schema-shield 1.0.5 → 1.1.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.
@@ -1,6 +1,14 @@
1
- import { isCompiledSchema } from "../utils/main-utils";
2
-
3
- import { KeywordFunction } from "../index";
1
+ import {
2
+ definePropertyOrThrow,
3
+ isCompiledSchema
4
+ } from "../utils/main-utils";
5
+ import type {
6
+ KeywordFunction,
7
+ Result,
8
+ ValidateFunction,
9
+ ValidateSubschemaFunction
10
+ } from "../index";
11
+ import type { DefineErrorFunction } from "../utils/main-utils";
4
12
  import { hasChanged } from "../utils/has-changed";
5
13
 
6
14
  type BranchEntry =
@@ -40,7 +48,7 @@ function getBranchEntries(schema: any, key: "allOf" | "anyOf" | "oneOf") {
40
48
  entries.push(toBranchEntry(source[i]));
41
49
  }
42
50
 
43
- Object.defineProperty(schema, cacheKey, {
51
+ definePropertyOrThrow(schema, cacheKey, {
44
52
  value: entries,
45
53
  enumerable: false,
46
54
  configurable: false,
@@ -50,6 +58,192 @@ function getBranchEntries(schema: any, key: "allOf" | "anyOf" | "oneOf") {
50
58
  return entries;
51
59
  }
52
60
 
61
+ type CombinatorKey = "allOf" | "anyOf" | "oneOf";
62
+ type DefaultMutation = { target: Record<string, any>; key: string; value: any };
63
+ type TransactionHooks = {
64
+ savepoint: () => number;
65
+ rollback: (savepoint: number) => void;
66
+ capture: (savepoint: number) => DefaultMutation[];
67
+ restore: (mutations: DefaultMutation[]) => void;
68
+ };
69
+
70
+ function evaluateAllOf(
71
+ branches: BranchEntry[],
72
+ data: any,
73
+ defineError: DefineErrorFunction
74
+ ): Result {
75
+ for (let i = 0; i < branches.length; i++) {
76
+ const branch = branches[i];
77
+ if (branch.kind === "validate") {
78
+ const error = branch.validate(data);
79
+ if (error) {
80
+ return defineError("Value is not valid", { cause: error, data });
81
+ }
82
+ continue;
83
+ }
84
+ if (branch.kind === "alwaysValid") {
85
+ continue;
86
+ }
87
+ if (branch.kind === "alwaysInvalid" || data !== branch.value) {
88
+ return defineError("Value is not valid", { data });
89
+ }
90
+ }
91
+ }
92
+
93
+ function evaluateAnyOf(
94
+ branches: BranchEntry[],
95
+ data: any,
96
+ defineError: DefineErrorFunction
97
+ ): Result {
98
+ for (let i = 0; i < branches.length; i++) {
99
+ const branch = branches[i];
100
+ if (branch.kind === "validate") {
101
+ if (!branch.validate(data)) {
102
+ return;
103
+ }
104
+ continue;
105
+ }
106
+ if (branch.kind === "alwaysValid") {
107
+ return;
108
+ }
109
+ if (branch.kind === "literal" && data === branch.value) {
110
+ return;
111
+ }
112
+ }
113
+ return defineError("Value is not valid", { data });
114
+ }
115
+
116
+ function evaluateOneOf(
117
+ branches: BranchEntry[],
118
+ data: any,
119
+ defineError: DefineErrorFunction
120
+ ): { error: Result; winnerIndex: number } {
121
+ let validCount = 0;
122
+ let winnerIndex = -1;
123
+ for (let i = 0; i < branches.length; i++) {
124
+ const branch = branches[i];
125
+ let isValid = false;
126
+ if (branch.kind === "validate") {
127
+ isValid = !branch.validate(data);
128
+ } else if (branch.kind === "alwaysValid") {
129
+ isValid = true;
130
+ } else if (branch.kind === "literal") {
131
+ isValid = data === branch.value;
132
+ }
133
+ if (isValid) {
134
+ validCount++;
135
+ winnerIndex = i;
136
+ if (validCount > 1) {
137
+ return {
138
+ error: defineError("Value is not valid", { data }),
139
+ winnerIndex: -1
140
+ };
141
+ }
142
+ }
143
+ }
144
+ return {
145
+ error:
146
+ validCount === 1
147
+ ? undefined
148
+ : defineError("Value is not valid", { data }),
149
+ winnerIndex: validCount === 1 ? winnerIndex : -1
150
+ };
151
+ }
152
+
153
+ export function createCombinatorValidator(
154
+ key: CombinatorKey,
155
+ schema: any,
156
+ defineError: DefineErrorFunction,
157
+ validateSubschema?: ValidateSubschemaFunction,
158
+ transactions?: TransactionHooks
159
+ ): ValidateFunction {
160
+ const sourceBranches = getBranchEntries(schema, key);
161
+ const branches = validateSubschema
162
+ ? sourceBranches.map((branch, index): BranchEntry =>
163
+ branch.kind === "validate"
164
+ ? {
165
+ kind: "validate",
166
+ validate: (data) => validateSubschema(schema[key][index], data)
167
+ }
168
+ : branch
169
+ )
170
+ : sourceBranches;
171
+
172
+ if (!transactions) {
173
+ if (key === "allOf") {
174
+ return (data) => evaluateAllOf(branches, data, defineError);
175
+ }
176
+ if (key === "anyOf") {
177
+ return (data) => evaluateAnyOf(branches, data, defineError);
178
+ }
179
+ return (data) => evaluateOneOf(branches, data, defineError).error;
180
+ }
181
+
182
+ if (key === "allOf") {
183
+ return (data) => {
184
+ const savepoint = transactions.savepoint();
185
+ try {
186
+ const error = evaluateAllOf(branches, data, defineError);
187
+ if (error) {
188
+ transactions.rollback(savepoint);
189
+ }
190
+ return error;
191
+ } catch (error) {
192
+ transactions.rollback(savepoint);
193
+ throw error;
194
+ }
195
+ };
196
+ }
197
+
198
+ if (key === "anyOf") {
199
+ return (data) => evaluateAnyOf(branches, data, defineError);
200
+ }
201
+
202
+ return (data) => {
203
+ const savepoint = transactions.savepoint();
204
+ const defaultsByBranch: DefaultMutation[][] = [];
205
+ const isolatedBranches = branches.map((branch, index): BranchEntry =>
206
+ branch.kind === "validate"
207
+ ? {
208
+ kind: "validate",
209
+ validate: (value) => {
210
+ const branchSavepoint = transactions.savepoint();
211
+ const error = branch.validate(value);
212
+ if (!error) {
213
+ defaultsByBranch[index] = transactions.capture(branchSavepoint);
214
+ }
215
+ return error;
216
+ }
217
+ }
218
+ : branch
219
+ );
220
+ try {
221
+ const result = evaluateOneOf(isolatedBranches, data, defineError);
222
+ if (result.error) {
223
+ transactions.rollback(savepoint);
224
+ return result.error;
225
+ }
226
+ transactions.restore(defaultsByBranch[result.winnerIndex] || []);
227
+ return;
228
+ } catch (error) {
229
+ transactions.rollback(savepoint);
230
+ throw error;
231
+ }
232
+ };
233
+ }
234
+
235
+ export function prepareCombinatorEntries(schema: any) {
236
+ if (Array.isArray(schema.allOf)) {
237
+ getBranchEntries(schema, "allOf");
238
+ }
239
+ if (Array.isArray(schema.anyOf)) {
240
+ getBranchEntries(schema, "anyOf");
241
+ }
242
+ if (Array.isArray(schema.oneOf)) {
243
+ getBranchEntries(schema, "oneOf");
244
+ }
245
+ }
246
+
53
247
  export const OtherKeywords: Record<string, KeywordFunction> = {
54
248
  enum(schema, data, defineError) {
55
249
  let enumCache = (schema as any)._enumCache as
@@ -71,7 +265,7 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
71
265
  }
72
266
 
73
267
  enumCache = { primitiveSet, objectValues };
74
- Object.defineProperty(schema, "_enumCache", {
268
+ definePropertyOrThrow(schema, "_enumCache", {
75
269
  value: enumCache,
76
270
  enumerable: false,
77
271
  configurable: false,
@@ -100,175 +294,15 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
100
294
  },
101
295
 
102
296
  allOf(schema, data, defineError) {
103
- const branches = getBranchEntries(schema, "allOf");
104
-
105
- if (branches.length === 1) {
106
- const onlyBranch = branches[0];
107
-
108
- if (onlyBranch.kind === "validate") {
109
- const error = onlyBranch.validate(data);
110
- if (error) {
111
- return defineError("Value is not valid", { cause: error, data });
112
- }
113
- return;
114
- }
115
-
116
- if (onlyBranch.kind === "alwaysValid") {
117
- return;
118
- }
119
-
120
- if (onlyBranch.kind === "alwaysInvalid") {
121
- return defineError("Value is not valid", { data });
122
- }
123
-
124
- if (data !== onlyBranch.value) {
125
- return defineError("Value is not valid", { data });
126
- }
127
-
128
- return;
129
- }
130
-
131
- for (let i = 0; i < branches.length; i++) {
132
- const branch = branches[i];
133
-
134
- if (branch.kind === "validate") {
135
- const error = branch.validate(data);
136
- if (error) {
137
- return defineError("Value is not valid", { cause: error, data });
138
- }
139
- continue;
140
- }
141
-
142
- if (branch.kind === "alwaysValid") {
143
- continue;
144
- }
145
-
146
- if (branch.kind === "alwaysInvalid") {
147
- return defineError("Value is not valid", { data });
148
- }
149
-
150
- if (data !== branch.value) {
151
- return defineError("Value is not valid", { data });
152
- }
153
- }
154
-
155
- return;
297
+ return createCombinatorValidator("allOf", schema, defineError)(data);
156
298
  },
157
299
 
158
300
  anyOf(schema, data, defineError) {
159
- const branches = getBranchEntries(schema, "anyOf");
160
-
161
- if (branches.length === 1) {
162
- const onlyBranch = branches[0];
163
-
164
- if (onlyBranch.kind === "validate") {
165
- const error = onlyBranch.validate(data);
166
- if (!error) {
167
- return;
168
- }
169
- return defineError("Value is not valid", { data });
170
- }
171
-
172
- if (onlyBranch.kind === "alwaysValid") {
173
- return;
174
- }
175
-
176
- if (onlyBranch.kind === "alwaysInvalid") {
177
- return defineError("Value is not valid", { data });
178
- }
179
-
180
- if (data === onlyBranch.value) {
181
- return;
182
- }
183
-
184
- return defineError("Value is not valid", { data });
185
- }
186
-
187
- for (let i = 0; i < branches.length; i++) {
188
- const branch = branches[i];
189
-
190
- if (branch.kind === "validate") {
191
- const error = branch.validate(data);
192
- if (!error) {
193
- return;
194
- }
195
- continue;
196
- }
197
-
198
- if (branch.kind === "alwaysValid") {
199
- return;
200
- }
201
-
202
- if (branch.kind === "alwaysInvalid") {
203
- continue;
204
- }
205
-
206
- if (data === branch.value) {
207
- return;
208
- }
209
- }
210
-
211
- return defineError("Value is not valid", { data });
301
+ return createCombinatorValidator("anyOf", schema, defineError)(data);
212
302
  },
213
303
 
214
304
  oneOf(schema, data, defineError) {
215
- const branches = getBranchEntries(schema, "oneOf");
216
-
217
- if (branches.length === 1) {
218
- const onlyBranch = branches[0];
219
-
220
- if (onlyBranch.kind === "validate") {
221
- const error = onlyBranch.validate(data);
222
- if (!error) {
223
- return;
224
- }
225
- return defineError("Value is not valid", { data });
226
- }
227
-
228
- if (onlyBranch.kind === "alwaysValid") {
229
- return;
230
- }
231
-
232
- if (onlyBranch.kind === "alwaysInvalid") {
233
- return defineError("Value is not valid", { data });
234
- }
235
-
236
- if (data === onlyBranch.value) {
237
- return;
238
- }
239
-
240
- return defineError("Value is not valid", { data });
241
- }
242
-
243
- let validCount = 0;
244
-
245
- for (let i = 0; i < branches.length; i++) {
246
- const branch = branches[i];
247
- let isValid = false;
248
-
249
- if (branch.kind === "validate") {
250
- isValid = !branch.validate(data);
251
- } else if (branch.kind === "alwaysValid") {
252
- isValid = true;
253
- } else if (branch.kind === "alwaysInvalid") {
254
- isValid = false;
255
- } else {
256
- isValid = data === branch.value;
257
- }
258
-
259
- if (isValid) {
260
- validCount++;
261
- if (validCount > 1) {
262
- return defineError("Value is not valid", { data });
263
- }
264
- }
265
- }
266
-
267
- if (validCount === 1) {
268
- return;
269
- }
270
-
271
- return defineError("Value is not valid", { data });
305
+ return createCombinatorValidator("oneOf", schema, defineError)(data);
272
306
  },
273
307
 
274
308
  const(schema, data, defineError) {
@@ -352,32 +386,11 @@ export const OtherKeywords: Record<string, KeywordFunction> = {
352
386
  return defineError("Value is not valid", { data });
353
387
  },
354
388
 
355
- $ref(schema, data, defineError, instance) {
356
- if (schema._resolvedRef) {
357
- if (schema.$validate !== schema._resolvedRef) {
358
- schema.$validate = schema._resolvedRef;
359
- }
360
-
389
+ $ref(schema, data, defineError) {
390
+ if (typeof schema._resolvedRef === "function") {
361
391
  return schema._resolvedRef(data);
362
392
  }
363
393
 
364
- const refPath = schema.$ref;
365
- let targetSchema = instance.getSchemaRef(refPath);
366
-
367
- if (!targetSchema) {
368
- targetSchema = instance.getSchemaById(refPath);
369
- }
370
-
371
- if (!targetSchema) {
372
- return defineError(`Missing reference: ${refPath}`);
373
- }
374
-
375
- if (!targetSchema.$validate) {
376
- return;
377
- }
378
-
379
- schema._resolvedRef = targetSchema.$validate;
380
- schema.$validate = schema._resolvedRef;
381
- return schema._resolvedRef(data);
394
+ return defineError(`Missing reference: ${schema.$ref}`);
382
395
  }
383
396
  };
@@ -1,12 +1,46 @@
1
1
  import { FormatFunction, KeywordFunction } from "../index";
2
2
  import { compilePatternMatcher } from "../utils/pattern-matcher";
3
+ import { definePropertyOrThrow } from "../utils/main-utils";
3
4
 
4
5
  const PATTERN_MATCH_CACHE_LIMIT = 512;
5
6
  const FORMAT_RESULT_CACHE_LIMIT = 512;
6
7
 
8
+ function hasAtLeastCodePoints(value: string, limit: number): boolean {
9
+ let count = 0;
10
+ for (let index = 0; index < value.length; index++) {
11
+ const unit = value.charCodeAt(index);
12
+ if (
13
+ unit >= 0xd800 &&
14
+ unit <= 0xdbff &&
15
+ index + 1 < value.length
16
+ ) {
17
+ const nextUnit = value.charCodeAt(index + 1);
18
+ if (nextUnit >= 0xdc00 && nextUnit <= 0xdfff) {
19
+ index++;
20
+ }
21
+ }
22
+
23
+ count++;
24
+ if (count >= limit) {
25
+ return true;
26
+ }
27
+ }
28
+
29
+ return count >= limit;
30
+ }
31
+
7
32
  export const StringKeywords: Record<string, KeywordFunction> = {
8
33
  minLength(schema, data, defineError) {
9
- if (typeof data !== "string" || data.length >= schema.minLength) {
34
+ if (typeof data !== "string") {
35
+ return;
36
+ }
37
+
38
+ const units = data.length;
39
+ const limit = schema.minLength;
40
+ if (units < limit) {
41
+ return defineError("Value is shorter than the minimum length", { data });
42
+ }
43
+ if (units - limit >= limit || hasAtLeastCodePoints(data, limit)) {
10
44
  return;
11
45
  }
12
46
 
@@ -14,11 +48,20 @@ export const StringKeywords: Record<string, KeywordFunction> = {
14
48
  },
15
49
 
16
50
  maxLength(schema, data, defineError) {
17
- if (typeof data !== "string" || data.length <= schema.maxLength) {
51
+ if (typeof data !== "string") {
18
52
  return;
19
53
  }
20
54
 
21
- return defineError("Value is longer than the maximum length", { data });
55
+ const units = data.length;
56
+ const limit = schema.maxLength;
57
+ if (units <= limit) {
58
+ return;
59
+ }
60
+ if (units - limit > limit || hasAtLeastCodePoints(data, limit + 1)) {
61
+ return defineError("Value is longer than the maximum length", { data });
62
+ }
63
+
64
+ return;
22
65
  },
23
66
 
24
67
  pattern(schema, data, defineError) {
@@ -42,7 +85,7 @@ export const StringKeywords: Record<string, KeywordFunction> = {
42
85
  ? (value: string) => compiled.test(value)
43
86
  : compiled;
44
87
 
45
- Object.defineProperty(schema, "_patternMatch", {
88
+ definePropertyOrThrow(schema, "_patternMatch", {
46
89
  value: patternMatch,
47
90
  enumerable: false,
48
91
  configurable: false,
@@ -58,7 +101,7 @@ export const StringKeywords: Record<string, KeywordFunction> = {
58
101
 
59
102
  if (!patternMatchCache) {
60
103
  patternMatchCache = new Map<string, boolean>();
61
- Object.defineProperty(schema, "_patternMatchCache", {
104
+ definePropertyOrThrow(schema, "_patternMatchCache", {
62
105
  value: patternMatchCache,
63
106
  enumerable: false,
64
107
  configurable: false,
@@ -104,7 +147,7 @@ export const StringKeywords: Record<string, KeywordFunction> = {
104
147
 
105
148
  if (formatValidate === undefined) {
106
149
  formatValidate = instance.getFormat(schema.format);
107
- Object.defineProperty(schema, "_formatValidate", {
150
+ definePropertyOrThrow(schema, "_formatValidate", {
108
151
  value: formatValidate,
109
152
  enumerable: false,
110
153
  configurable: false,
@@ -122,7 +165,7 @@ export const StringKeywords: Record<string, KeywordFunction> = {
122
165
  formatValidate
123
166
  );
124
167
 
125
- Object.defineProperty(schema, "_formatResultCacheEnabled", {
168
+ definePropertyOrThrow(schema, "_formatResultCacheEnabled", {
126
169
  value: formatResultCacheEnabled,
127
170
  enumerable: false,
128
171
  configurable: false,
@@ -140,7 +183,7 @@ export const StringKeywords: Record<string, KeywordFunction> = {
140
183
 
141
184
  if (!formatResultCache) {
142
185
  formatResultCache = new Map<string, boolean>();
143
- Object.defineProperty(schema, "_formatResultCache", {
186
+ definePropertyOrThrow(schema, "_formatResultCache", {
144
187
  value: formatResultCache,
145
188
  enumerable: false,
146
189
  configurable: false,
package/lib/types.ts CHANGED
@@ -11,10 +11,10 @@ export const Types: Record<string, TypeFunction | false> = {
11
11
  return typeof data === "string";
12
12
  },
13
13
  number(data) {
14
- return typeof data === "number";
14
+ return typeof data === "number" && Number.isFinite(data);
15
15
  },
16
16
  integer(data) {
17
- return typeof data === "number" && data % 1 === 0;
17
+ return typeof data === "number" && Number.isFinite(data) && data % 1 === 0;
18
18
  },
19
19
  boolean(data) {
20
20
  return typeof data === "boolean";
@@ -1,3 +1,5 @@
1
+ import { definePropertyOrThrow } from "./main-utils";
2
+
1
3
  export function deepFreeze(
2
4
  obj: any,
3
5
  freezeClassInstances: boolean = false,
@@ -26,7 +28,7 @@ export function deepFreeze(
26
28
 
27
29
  // If the object is an instance of a class (not a plain object or array) we need to freeze the prototype
28
30
  if (freezeClassInstances) {
29
- const proto = Object.getPrototypeOf(obj);
31
+ const proto = Reflect.getPrototypeOf(obj);
30
32
  if (proto && proto !== Object.prototype) {
31
33
  deepFreeze(proto, freezeClassInstances, seen);
32
34
  }
@@ -43,7 +45,7 @@ function isPlainObject(value: any): boolean {
43
45
  return false;
44
46
  }
45
47
 
46
- const proto = Object.getPrototypeOf(value);
48
+ const proto = Reflect.getPrototypeOf(value);
47
49
  return proto === Object.prototype || proto === null;
48
50
  }
49
51
 
@@ -168,7 +170,7 @@ export function deepCloneUnfreeze<T>(
168
170
  seen.set(source, clone);
169
171
  return clone;
170
172
  }
171
- clone = Object.create(Object.getPrototypeOf(source));
173
+ clone = Object.create(Reflect.getPrototypeOf(source));
172
174
  seen.set(source, clone);
173
175
  break;
174
176
  }
@@ -201,7 +203,7 @@ export function deepCloneUnfreeze<T>(
201
203
  seen
202
204
  );
203
205
  }
204
- Object.defineProperty(clone, key, descriptor);
206
+ definePropertyOrThrow(clone, key, descriptor);
205
207
  }
206
208
 
207
209
  return clone;