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
package/lib/index.ts CHANGED
@@ -1,21 +1,21 @@
1
+ /****************** Path: lib/index.ts ******************/
1
2
  import {
2
3
  DefineErrorFunction,
3
4
  ValidationError,
4
- deepClone,
5
5
  getDefinedErrorFunctionForKey,
6
6
  getNamedFunction,
7
- isObject
8
- } from "./utils";
7
+ resolvePath
8
+ } from "./utils/main-utils";
9
9
 
10
10
  import { Formats } from "./formats";
11
11
  import { Types } from "./types";
12
12
  import { keywords } from "./keywords";
13
+ import { deepCloneUnfreeze } from "./utils/deep-freeze";
13
14
 
14
- export { ValidationError } from "./utils";
15
+ export { ValidationError } from "./utils/main-utils";
16
+ export { deepCloneUnfreeze as deepClone } from "./utils/deep-freeze";
15
17
 
16
- export { deepClone } from "./utils";
17
-
18
- export type Result = void | ValidationError;
18
+ export type Result = void | ValidationError | true;
19
19
 
20
20
  export interface KeywordFunction {
21
21
  (
@@ -44,22 +44,37 @@ export interface CompiledSchema {
44
44
  }
45
45
 
46
46
  export interface Validator {
47
- (data: any): { data: any; error: ValidationError | null; valid: boolean };
47
+ (data: any): {
48
+ data: any;
49
+ error: ValidationError | null | true;
50
+ valid: boolean;
51
+ };
48
52
  compiledSchema: CompiledSchema;
49
53
  }
50
54
 
55
+ interface ValidatorItem {
56
+ name: string;
57
+ validate: ValidateFunction;
58
+ }
59
+
51
60
  export class SchemaShield {
52
61
  private types: Record<string, TypeFunction | false> = {};
53
62
  private formats: Record<string, FormatFunction | false> = {};
54
63
  private keywords: Record<string, KeywordFunction | false> = {};
55
64
  private immutable = false;
65
+ private rootSchema: CompiledSchema | null = null;
66
+ private idRegistry: Map<string, CompiledSchema> = new Map();
67
+ private failFast: boolean = true;
56
68
 
57
69
  constructor({
58
- immutable = false
70
+ immutable = false,
71
+ failFast = true
59
72
  }: {
60
73
  immutable?: boolean;
74
+ failFast?: boolean;
61
75
  } = {}) {
62
76
  this.immutable = immutable;
77
+ this.failFast = failFast;
63
78
 
64
79
  for (const [type, validator] of Object.entries(Types)) {
65
80
  if (validator) {
@@ -100,6 +115,10 @@ export class SchemaShield {
100
115
  return this.formats[format];
101
116
  }
102
117
 
118
+ isDefaultFormatValidator(format: string, validator: FormatFunction): boolean {
119
+ return (Formats as Record<string, FormatFunction | false>)[format] === validator;
120
+ }
121
+
103
122
  addKeyword(name: string, validator: KeywordFunction, overwrite = false) {
104
123
  if (this.keywords[name] && !overwrite) {
105
124
  throw new ValidationError(`Keyword "${name}" already exists`);
@@ -111,175 +130,690 @@ export class SchemaShield {
111
130
  return this.keywords[keyword];
112
131
  }
113
132
 
133
+ getSchemaRef(path: string): CompiledSchema | undefined {
134
+ if (!this.rootSchema) {
135
+ return;
136
+ }
137
+ return resolvePath(this.rootSchema, path);
138
+ }
139
+
140
+ getSchemaById(id: string): CompiledSchema | undefined {
141
+ return this.idRegistry.get(id);
142
+ }
143
+
114
144
  compile(schema: any): Validator {
145
+ this.idRegistry.clear();
115
146
  const compiledSchema = this.compileSchema(schema);
147
+ this.rootSchema = compiledSchema;
148
+ if ((compiledSchema as any)._hasRef === true) {
149
+ this.linkReferences(compiledSchema);
150
+ }
151
+
116
152
  if (!compiledSchema.$validate) {
117
- if (this.isSchemaLike(schema) === false) {
153
+ if (schema === false) {
154
+ const defineError = getDefinedErrorFunctionForKey(
155
+ "oneOf",
156
+ compiledSchema,
157
+ this.failFast
158
+ );
159
+
160
+ compiledSchema.$validate = getNamedFunction<ValidateFunction>(
161
+ "Validate_False",
162
+ (data) => defineError("Value is not valid", { data })
163
+ );
164
+ } else if (schema === true) {
165
+ compiledSchema.$validate = getNamedFunction<ValidateFunction>(
166
+ "Validate_Any",
167
+ () => {}
168
+ );
169
+ } else if (this.isSchemaLike(schema) === false) {
118
170
  throw new ValidationError("Invalid schema");
171
+ } else {
172
+ compiledSchema.$validate = getNamedFunction<ValidateFunction>(
173
+ "Validate_Any",
174
+ () => {}
175
+ );
119
176
  }
120
-
121
- compiledSchema.$validate = getNamedFunction<ValidateFunction>(
122
- "any",
123
- () => {}
124
- );
125
177
  }
126
178
 
127
179
  const validate: Validator = (data: any) => {
128
- const clonedData = this.immutable ? deepClone(data) : data;
129
- const error = compiledSchema.$validate(clonedData);
180
+ this.rootSchema = compiledSchema;
130
181
 
131
- return {
132
- data: clonedData,
133
- error: error ? error : null,
134
- valid: !error
135
- };
182
+ const clonedData = this.immutable ? deepCloneUnfreeze(data) : data;
183
+ const res = compiledSchema.$validate!(clonedData);
184
+
185
+ if (res) {
186
+ return { data: clonedData, error: res, valid: false };
187
+ }
188
+
189
+ return { data: clonedData, error: null, valid: true };
136
190
  };
137
191
 
138
192
  validate.compiledSchema = compiledSchema;
139
-
140
193
  return validate;
141
194
  }
142
195
 
196
+ private isPlainObject(value: any): value is Record<string, any> {
197
+ return !!value && typeof value === "object" && !Array.isArray(value);
198
+ }
199
+
200
+ private isTrivialAlwaysValidSubschema(value: any): boolean {
201
+ return (
202
+ value === true ||
203
+ (this.isPlainObject(value) && Object.keys(value).length === 0)
204
+ );
205
+ }
206
+
207
+ private shallowArrayEquals(a: any[], b: any[]): boolean {
208
+ if (a === b) {
209
+ return true;
210
+ }
211
+
212
+ if (a.length !== b.length) {
213
+ return false;
214
+ }
215
+
216
+ for (let i = 0; i < a.length; i++) {
217
+ if (a[i] !== b[i]) {
218
+ return false;
219
+ }
220
+ }
221
+
222
+ return true;
223
+ }
224
+
225
+ private flattenAssociativeBranches(
226
+ key: "allOf" | "anyOf",
227
+ branches: any[]
228
+ ): any[] {
229
+ const out: any[] = [];
230
+
231
+ for (let i = 0; i < branches.length; i++) {
232
+ const item = branches[i];
233
+ if (
234
+ this.isPlainObject(item) &&
235
+ Object.keys(item).length === 1 &&
236
+ Array.isArray(item[key])
237
+ ) {
238
+ const nested = this.flattenAssociativeBranches(key, item[key]);
239
+ for (let j = 0; j < nested.length; j++) {
240
+ out.push(nested[j]);
241
+ }
242
+ continue;
243
+ }
244
+ out.push(item);
245
+ }
246
+
247
+ return out;
248
+ }
249
+
250
+ private flattenSingleWrapperOneOf(branches: any[]): any[] {
251
+ let current = branches;
252
+
253
+ while (current.length === 1) {
254
+ const item = current[0];
255
+ if (
256
+ this.isPlainObject(item) &&
257
+ Object.keys(item).length === 1 &&
258
+ Array.isArray(item.oneOf)
259
+ ) {
260
+ current = item.oneOf;
261
+ continue;
262
+ }
263
+ break;
264
+ }
265
+
266
+ return current;
267
+ }
268
+
269
+ private normalizeSchemaForCompile(schema: Record<string, any>): Record<string, any> {
270
+ let normalized = schema;
271
+ const schemaKeys = Object.keys(schema);
272
+ const hasOnlyKey = (key: string) =>
273
+ schemaKeys.length === 1 && schemaKeys[0] === key;
274
+
275
+ const setNormalized = (key: string, value: any) => {
276
+ if (normalized === schema) {
277
+ normalized = { ...schema };
278
+ }
279
+ normalized[key] = value;
280
+ };
281
+
282
+ if (Array.isArray(schema.allOf)) {
283
+ const flattenedAllOf = this.flattenAssociativeBranches(
284
+ "allOf",
285
+ schema.allOf
286
+ ).filter(
287
+ (item) =>
288
+ !(
289
+ this.isPlainObject(item) && Object.keys(item).length === 0
290
+ )
291
+ );
292
+
293
+ if (
294
+ hasOnlyKey("allOf") &&
295
+ flattenedAllOf.length === 1 &&
296
+ this.isPlainObject(flattenedAllOf[0])
297
+ ) {
298
+ return flattenedAllOf[0];
299
+ }
300
+
301
+ if (!this.shallowArrayEquals(flattenedAllOf, schema.allOf)) {
302
+ setNormalized("allOf", flattenedAllOf);
303
+ }
304
+ }
305
+
306
+ if (Array.isArray(schema.anyOf)) {
307
+ const flattenedAnyOf = this.flattenAssociativeBranches(
308
+ "anyOf",
309
+ schema.anyOf
310
+ );
311
+
312
+ if (
313
+ hasOnlyKey("anyOf") &&
314
+ flattenedAnyOf.length === 1 &&
315
+ this.isPlainObject(flattenedAnyOf[0])
316
+ ) {
317
+ return flattenedAnyOf[0];
318
+ }
319
+
320
+ if (!this.shallowArrayEquals(flattenedAnyOf, schema.anyOf)) {
321
+ setNormalized("anyOf", flattenedAnyOf);
322
+ }
323
+ }
324
+
325
+ if (Array.isArray(schema.oneOf)) {
326
+ const flattenedOneOf = this.flattenSingleWrapperOneOf(schema.oneOf);
327
+
328
+ if (
329
+ hasOnlyKey("oneOf") &&
330
+ flattenedOneOf.length === 1 &&
331
+ this.isPlainObject(flattenedOneOf[0])
332
+ ) {
333
+ return flattenedOneOf[0];
334
+ }
335
+
336
+ if (!this.shallowArrayEquals(flattenedOneOf, schema.oneOf)) {
337
+ setNormalized("oneOf", flattenedOneOf);
338
+ }
339
+ }
340
+
341
+ return normalized;
342
+ }
343
+
344
+ private markSchemaHasRef(schema: CompiledSchema) {
345
+ if ((schema as any)._hasRef === true) {
346
+ return;
347
+ }
348
+
349
+ Object.defineProperty(schema, "_hasRef", {
350
+ value: true,
351
+ enumerable: false,
352
+ configurable: false,
353
+ writable: false
354
+ });
355
+ }
356
+
357
+ private shouldSkipKeyword(schema: Record<string, any>, key: string): boolean {
358
+ const value = schema[key];
359
+
360
+ switch (key) {
361
+ case "required":
362
+ return Array.isArray(value) && value.length === 0;
363
+ case "uniqueItems":
364
+ return value === false;
365
+ case "properties":
366
+ case "patternProperties":
367
+ case "dependencies":
368
+ return (
369
+ this.isPlainObject(value) &&
370
+ Object.keys(value).length === 0
371
+ );
372
+ case "propertyNames":
373
+ case "items":
374
+ return value === true;
375
+ case "additionalProperties":
376
+ if (value === true) {
377
+ return true;
378
+ }
379
+
380
+ return (
381
+ value === false &&
382
+ this.isPlainObject(schema.patternProperties) &&
383
+ Object.keys(schema.patternProperties).length > 0
384
+ );
385
+ case "additionalItems":
386
+ return value === true || !Array.isArray(schema.items);
387
+ case "allOf": {
388
+ if (!Array.isArray(value)) {
389
+ return false;
390
+ }
391
+
392
+ if (value.length === 0) {
393
+ return true;
394
+ }
395
+
396
+ for (let i = 0; i < value.length; i++) {
397
+ if (this.isTrivialAlwaysValidSubschema(value[i])) {
398
+ continue;
399
+ }
400
+
401
+ return false;
402
+ }
403
+
404
+ return true;
405
+ }
406
+ case "anyOf": {
407
+ if (!Array.isArray(value)) {
408
+ return false;
409
+ }
410
+
411
+ for (let i = 0; i < value.length; i++) {
412
+ if (this.isTrivialAlwaysValidSubschema(value[i])) {
413
+ return true;
414
+ }
415
+ }
416
+
417
+ return false;
418
+ }
419
+ default:
420
+ return false;
421
+ }
422
+ }
423
+
424
+ private hasRequiredDefaults(schema: Record<string, any>): boolean {
425
+ const properties = schema.properties;
426
+ if (!this.isPlainObject(properties)) {
427
+ return false;
428
+ }
429
+
430
+ const keys = Object.keys(properties);
431
+ for (let i = 0; i < keys.length; i++) {
432
+ const subSchema = properties[keys[i]];
433
+ if (this.isPlainObject(subSchema) && "default" in subSchema) {
434
+ return true;
435
+ }
436
+ }
437
+
438
+ return false;
439
+ }
440
+
441
+ private isDefaultTypeValidator(type: string, validator: TypeFunction): boolean {
442
+ return (Types as Record<string, TypeFunction | false>)[type] === validator;
443
+ }
444
+
143
445
  private compileSchema(schema: Partial<CompiledSchema> | any): CompiledSchema {
144
- if (!isObject(schema)) {
446
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
145
447
  if (schema === true) {
146
- schema = {
147
- anyOf: [{}]
148
- };
448
+ schema = { anyOf: [{}] }; // Always valid
149
449
  } else if (schema === false) {
150
- schema = {
151
- oneOf: []
152
- };
450
+ schema = { oneOf: [] }; // Always invalid
153
451
  } else {
154
- schema = {
155
- oneOf: [schema]
156
- };
452
+ schema = { oneOf: [schema] };
157
453
  }
158
454
  }
159
455
 
160
- const compiledSchema: CompiledSchema = deepClone(schema) as CompiledSchema;
161
- const defineTypeError = getDefinedErrorFunctionForKey("type", schema);
162
- const typeValidations: TypeFunction[] = [];
456
+ schema = this.normalizeSchemaForCompile(schema);
457
+
458
+ const compiledSchema: CompiledSchema = deepCloneUnfreeze(
459
+ schema
460
+ ) as CompiledSchema;
163
461
 
164
- let methodName = "";
462
+ let schemaHasRef = false;
463
+
464
+ if (typeof schema.$id === "string") {
465
+ this.idRegistry.set(schema.$id, compiledSchema);
466
+ }
467
+
468
+ if ("$ref" in schema) {
469
+ schemaHasRef = true;
470
+ const refValidator = this.getKeyword("$ref");
471
+ if (refValidator) {
472
+ const defineError = getDefinedErrorFunctionForKey(
473
+ "$ref",
474
+ schema["$ref"],
475
+ this.failFast
476
+ );
477
+
478
+ compiledSchema.$validate = getNamedFunction<ValidateFunction>(
479
+ "Validate_Reference",
480
+ (data) =>
481
+ (refValidator as KeywordFunction)(
482
+ compiledSchema,
483
+ data,
484
+ defineError,
485
+ this
486
+ )
487
+ );
488
+ }
489
+
490
+ this.markSchemaHasRef(compiledSchema);
491
+ return compiledSchema;
492
+ }
493
+
494
+ const validators: ValidatorItem[] = [];
495
+ const activeNames: string[] = [];
165
496
 
166
497
  if ("type" in schema) {
498
+ const defineTypeError = getDefinedErrorFunctionForKey(
499
+ "type",
500
+ schema,
501
+ this.failFast
502
+ );
167
503
  const types = Array.isArray(schema.type)
168
504
  ? schema.type
169
- : schema.type.split(",").map((t) => t.trim());
505
+ : schema.type.split(",").map((t: string) => t.trim());
506
+
507
+ const typeFunctions: TypeFunction[] = [];
508
+ const typeNames: string[] = [];
509
+ const defaultTypeNames: string[] = [];
510
+ let allTypesDefault = true;
170
511
 
171
512
  for (const type of types) {
172
513
  const validator = this.getType(type);
173
514
  if (validator) {
174
- typeValidations.push(validator);
175
- methodName += (methodName ? "_OR_" : "") + validator.name;
515
+ typeFunctions.push(validator);
516
+ typeNames.push(validator.name);
517
+ if (this.isDefaultTypeValidator(type, validator)) {
518
+ defaultTypeNames.push(type);
519
+ } else {
520
+ allTypesDefault = false;
521
+ }
176
522
  }
177
523
  }
178
524
 
179
- const typeValidationsLength = typeValidations.length;
180
-
181
- if (typeValidationsLength === 0) {
182
- throw defineTypeError("Invalid type for schema", { data: schema.type });
525
+ if (typeFunctions.length === 0) {
526
+ throw getDefinedErrorFunctionForKey(
527
+ "type",
528
+ schema,
529
+ this.failFast
530
+ )("Invalid type for schema", { data: schema.type });
183
531
  }
184
532
 
185
- if (typeValidationsLength === 1) {
186
- const typeValidation = typeValidations[0];
187
- compiledSchema.$validate = getNamedFunction<ValidateFunction>(
188
- methodName,
189
- (data) => {
190
- if (!typeValidation(data)) {
191
- return defineTypeError("Invalid type", { data });
533
+ let combinedTypeValidator: ValidateFunction;
534
+ let typeMethodName = "";
535
+
536
+ if (typeFunctions.length === 1 && allTypesDefault) {
537
+ const singleTypeName = defaultTypeNames[0];
538
+ typeMethodName = singleTypeName;
539
+
540
+ switch (singleTypeName) {
541
+ case "object":
542
+ combinedTypeValidator = (data) => {
543
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
544
+ return defineTypeError("Invalid type", { data });
545
+ }
546
+ };
547
+ break;
548
+ case "array":
549
+ combinedTypeValidator = (data) => {
550
+ if (!Array.isArray(data)) {
551
+ return defineTypeError("Invalid type", { data });
552
+ }
553
+ };
554
+ break;
555
+ case "string":
556
+ combinedTypeValidator = (data) => {
557
+ if (typeof data !== "string") {
558
+ return defineTypeError("Invalid type", { data });
559
+ }
560
+ };
561
+ break;
562
+ case "number":
563
+ combinedTypeValidator = (data) => {
564
+ if (typeof data !== "number") {
565
+ return defineTypeError("Invalid type", { data });
566
+ }
567
+ };
568
+ break;
569
+ case "integer":
570
+ combinedTypeValidator = (data) => {
571
+ if (typeof data !== "number" || !Number.isInteger(data)) {
572
+ return defineTypeError("Invalid type", { data });
573
+ }
574
+ };
575
+ break;
576
+ case "boolean":
577
+ combinedTypeValidator = (data) => {
578
+ if (typeof data !== "boolean") {
579
+ return defineTypeError("Invalid type", { data });
580
+ }
581
+ };
582
+ break;
583
+ case "null":
584
+ combinedTypeValidator = (data) => {
585
+ if (data !== null) {
586
+ return defineTypeError("Invalid type", { data });
587
+ }
588
+ };
589
+ break;
590
+ default: {
591
+ const singleTypeFn = typeFunctions[0];
592
+ combinedTypeValidator = (data) => {
593
+ if (!singleTypeFn(data)) {
594
+ return defineTypeError("Invalid type", { data });
595
+ }
596
+ };
597
+ }
598
+ }
599
+ } else if (typeFunctions.length > 1 && allTypesDefault) {
600
+ typeMethodName = defaultTypeNames.join("_OR_");
601
+
602
+ const allowsObject = defaultTypeNames.includes("object");
603
+ const allowsArray = defaultTypeNames.includes("array");
604
+ const allowsString = defaultTypeNames.includes("string");
605
+ const allowsNumber = defaultTypeNames.includes("number");
606
+ const allowsInteger = defaultTypeNames.includes("integer");
607
+ const allowsBoolean = defaultTypeNames.includes("boolean");
608
+ const allowsNull = defaultTypeNames.includes("null");
609
+
610
+ combinedTypeValidator = (data) => {
611
+ const dataType = typeof data;
612
+
613
+ if (dataType === "number") {
614
+ if (allowsNumber || (allowsInteger && Number.isInteger(data))) {
615
+ return;
192
616
  }
617
+
618
+ return defineTypeError("Invalid type", { data });
193
619
  }
194
- );
195
- } else if (typeValidationsLength > 1) {
196
- compiledSchema.$validate = getNamedFunction<ValidateFunction>(
197
- methodName,
198
- (data) => {
199
- for (let i = 0; i < typeValidationsLength; i++) {
200
- if (typeValidations[i](data)) {
620
+
621
+ if (dataType === "string") {
622
+ if (allowsString) {
623
+ return;
624
+ }
625
+
626
+ return defineTypeError("Invalid type", { data });
627
+ }
628
+
629
+ if (dataType === "boolean") {
630
+ if (allowsBoolean) {
631
+ return;
632
+ }
633
+
634
+ return defineTypeError("Invalid type", { data });
635
+ }
636
+
637
+ if (dataType === "object") {
638
+ if (data === null) {
639
+ if (allowsNull) {
201
640
  return;
202
641
  }
642
+
643
+ return defineTypeError("Invalid type", { data });
203
644
  }
645
+
646
+ if (Array.isArray(data)) {
647
+ if (allowsArray) {
648
+ return;
649
+ }
650
+
651
+ return defineTypeError("Invalid type", { data });
652
+ }
653
+
654
+ if (allowsObject) {
655
+ return;
656
+ }
657
+
204
658
  return defineTypeError("Invalid type", { data });
205
659
  }
206
- );
660
+
661
+ return defineTypeError("Invalid type", { data });
662
+ };
663
+ } else if (typeFunctions.length === 1) {
664
+ typeMethodName = typeNames[0];
665
+ const singleTypeFn = typeFunctions[0];
666
+ combinedTypeValidator = (data) => {
667
+ if (!singleTypeFn(data)) {
668
+ return defineTypeError("Invalid type", { data });
669
+ }
670
+ };
671
+ } else {
672
+ typeMethodName = typeNames.join("_OR_");
673
+ combinedTypeValidator = (data) => {
674
+ for (let i = 0; i < typeFunctions.length; i++) {
675
+ if (typeFunctions[i](data)) {
676
+ return;
677
+ }
678
+ }
679
+ return defineTypeError("Invalid type", { data });
680
+ };
207
681
  }
682
+
683
+ validators.push({
684
+ name: typeMethodName,
685
+ validate: getNamedFunction(typeMethodName, combinedTypeValidator)
686
+ });
687
+ activeNames.push(typeMethodName);
208
688
  }
209
689
 
210
- for (const key of Object.keys(schema)) {
211
- if (key === "type") {
212
- compiledSchema.type = schema.type;
690
+ const { type, $id, $ref, $validate, required, ...otherKeys } = schema; // Exclude handled keys
691
+
692
+ // In here we create an array of keys putting the require keyword last
693
+ // This is to ensure required properties are checked after defaults are applied
694
+ const keyOrder = required
695
+ ? this.hasRequiredDefaults(schema)
696
+ ? [...Object.keys(otherKeys), "required"]
697
+ : ["required", ...Object.keys(otherKeys)]
698
+ : Object.keys(otherKeys);
699
+
700
+ for (const key of keyOrder) {
701
+ const keywordFn = this.getKeyword(key);
702
+
703
+ if (!keywordFn) {
213
704
  continue;
214
705
  }
215
706
 
216
- const keywordValidator = this.getKeyword(key);
217
- if (keywordValidator) {
218
- const defineError = getDefinedErrorFunctionForKey(key, schema[key]);
219
- if (compiledSchema.$validate) {
220
- const prevValidator = compiledSchema.$validate;
221
- methodName += `_AND_${keywordValidator.name}`;
222
- compiledSchema.$validate = getNamedFunction<ValidateFunction>(
223
- methodName,
224
- (data) => {
225
- const error = prevValidator(data);
226
- if (error) {
227
- return error;
228
- }
229
- return (keywordValidator as KeywordFunction)(
230
- compiledSchema,
231
- data,
232
- defineError,
233
- this
234
- );
235
- }
236
- );
237
- } else {
238
- methodName = keywordValidator.name;
239
- compiledSchema.$validate = getNamedFunction<ValidateFunction>(
240
- methodName,
241
- (data) =>
242
- (keywordValidator as KeywordFunction)(
243
- compiledSchema,
244
- data,
245
- defineError,
246
- this
247
- )
248
- );
249
- }
707
+ if (this.shouldSkipKeyword(schema, key)) {
708
+ continue;
250
709
  }
251
710
 
252
- if (isObject(schema[key])) {
253
- // If the key is properties go through each property and try to compile it as a schema
711
+ const defineError = getDefinedErrorFunctionForKey(
712
+ key,
713
+ schema[key],
714
+ this.failFast
715
+ );
716
+ const fnName = keywordFn.name || key;
717
+
718
+ validators.push({
719
+ name: fnName,
720
+ validate: getNamedFunction<ValidateFunction>(fnName, (data) =>
721
+ (keywordFn as KeywordFunction)(compiledSchema, data, defineError, this)
722
+ )
723
+ });
724
+
725
+ activeNames.push(fnName);
726
+ }
727
+
728
+ const literalKeywords = ["enum", "const", "default", "examples"];
729
+ for (const key of keyOrder) {
730
+ if (literalKeywords.includes(key)) {
731
+ continue;
732
+ }
733
+
734
+ if (
735
+ schema[key] &&
736
+ typeof schema[key] === "object" &&
737
+ !Array.isArray(schema[key])
738
+ ) {
254
739
  if (key === "properties") {
255
740
  for (const subKey of Object.keys(schema[key])) {
256
- compiledSchema[key][subKey] = this.compileSchema(
741
+ const compiledSubSchema = this.compileSchema(
257
742
  schema[key][subKey]
258
743
  );
744
+
745
+ if ((compiledSubSchema as any)._hasRef === true) {
746
+ schemaHasRef = true;
747
+ }
748
+
749
+ compiledSchema[key][subKey] = compiledSubSchema;
259
750
  }
260
751
  continue;
261
752
  }
262
- compiledSchema[key] = this.compileSchema(schema[key]);
753
+ const compiledSubSchema = this.compileSchema(schema[key]);
754
+ if ((compiledSubSchema as any)._hasRef === true) {
755
+ schemaHasRef = true;
756
+ }
757
+
758
+ compiledSchema[key] = compiledSubSchema;
263
759
  continue;
264
760
  }
265
761
 
266
762
  if (Array.isArray(schema[key])) {
267
- compiledSchema[key] = schema[key].map((subSchema, index) =>
268
- this.isSchemaLike(subSchema)
269
- ? this.compileSchema(subSchema)
270
- : subSchema
271
- );
763
+ for (let i = 0; i < schema[key].length; i++) {
764
+ if (this.isSchemaLike(schema[key][i])) {
765
+ const compiledSubSchema = this.compileSchema(schema[key][i]);
766
+ if ((compiledSubSchema as any)._hasRef === true) {
767
+ schemaHasRef = true;
768
+ }
769
+
770
+ compiledSchema[key][i] = compiledSubSchema;
771
+ }
772
+ }
272
773
  continue;
273
774
  }
775
+ }
776
+
777
+ if (schemaHasRef) {
778
+ this.markSchemaHasRef(compiledSchema);
779
+ }
274
780
 
275
- compiledSchema[key] = schema[key];
781
+ if (validators.length === 0) {
782
+ return compiledSchema;
783
+ }
784
+
785
+ if (validators.length === 1) {
786
+ const v = validators[0];
787
+ compiledSchema.$validate = getNamedFunction(v.name, v.validate);
788
+ } else {
789
+ const compositeName = "Validate_" + activeNames.join("_AND_");
790
+
791
+ const masterValidator: ValidateFunction = (data) => {
792
+ for (let i = 0; i < validators.length; i++) {
793
+ const v = validators[i];
794
+ const error = v.validate(data);
795
+ if (error) {
796
+ return error;
797
+ }
798
+ }
799
+ return;
800
+ };
801
+
802
+ compiledSchema.$validate = getNamedFunction(
803
+ compositeName,
804
+ masterValidator
805
+ );
276
806
  }
277
807
 
278
808
  return compiledSchema as CompiledSchema;
279
809
  }
280
810
 
281
811
  isSchemaLike(subSchema: any): boolean {
282
- if (isObject(subSchema)) {
812
+ if (
813
+ subSchema &&
814
+ typeof subSchema === "object" &&
815
+ !Array.isArray(subSchema)
816
+ ) {
283
817
  if ("type" in subSchema) {
284
818
  return true;
285
819
  }
@@ -292,4 +826,65 @@ export class SchemaShield {
292
826
  }
293
827
  return false;
294
828
  }
829
+
830
+ private linkReferences(root: CompiledSchema) {
831
+ const stack: any[] = [root];
832
+
833
+ while (stack.length > 0) {
834
+ const node = stack.pop();
835
+
836
+ if (!node || typeof node !== "object") continue;
837
+
838
+ if (
839
+ typeof node.$ref === "string" &&
840
+ typeof node.$validate === "function" &&
841
+ node.$validate.name === "Validate_Reference"
842
+ ) {
843
+ const refPath = node.$ref as string;
844
+
845
+ let target: any = this.getSchemaRef(refPath);
846
+ if (typeof target === "undefined") {
847
+ target = this.getSchemaById(refPath);
848
+ }
849
+
850
+ if (typeof target === "boolean") {
851
+ if (target === true) {
852
+ node.$validate = getNamedFunction("Validate_Ref_True", () => {});
853
+ } else {
854
+ const defineError = getDefinedErrorFunctionForKey(
855
+ "$ref",
856
+ node as any,
857
+ this.failFast
858
+ );
859
+
860
+ node.$validate = getNamedFunction(
861
+ "Validate_Ref_False",
862
+ (_data: any) => defineError("Value is not valid")
863
+ );
864
+ }
865
+ continue;
866
+ }
867
+
868
+ if (target && typeof target.$validate === "function") {
869
+ node.$validate = target.$validate;
870
+ }
871
+ }
872
+
873
+ for (const key in node) {
874
+ const value = node[key];
875
+ if (!value) continue;
876
+
877
+ if (Array.isArray(value)) {
878
+ for (let i = 0; i < value.length; i++) {
879
+ const v = value[i];
880
+ if (v && typeof v === "object") {
881
+ stack.push(v);
882
+ }
883
+ }
884
+ } else if (typeof value === "object") {
885
+ stack.push(value);
886
+ }
887
+ }
888
+ }
889
+ }
295
890
  }