schema-shield 0.0.6 → 1.0.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/lib/index.ts CHANGED
@@ -1,10 +1,12 @@
1
+ /****************** Path: lib/index.ts ******************/
1
2
  import {
2
3
  DefineErrorFunction,
3
4
  ValidationError,
4
5
  deepClone,
5
6
  getDefinedErrorFunctionForKey,
6
7
  getNamedFunction,
7
- isObject
8
+ isObject,
9
+ resolvePath
8
10
  } from "./utils";
9
11
 
10
12
  import { Formats } from "./formats";
@@ -12,10 +14,9 @@ import { Types } from "./types";
12
14
  import { keywords } from "./keywords";
13
15
 
14
16
  export { ValidationError } from "./utils";
15
-
16
17
  export { deepClone } from "./utils";
17
18
 
18
- export type Result = void | ValidationError;
19
+ export type Result = void | ValidationError | true;
19
20
 
20
21
  export interface KeywordFunction {
21
22
  (
@@ -44,22 +45,37 @@ export interface CompiledSchema {
44
45
  }
45
46
 
46
47
  export interface Validator {
47
- (data: any): { data: any; error: ValidationError | null; valid: boolean };
48
+ (data: any): {
49
+ data: any;
50
+ error: ValidationError | null | true;
51
+ valid: boolean;
52
+ };
48
53
  compiledSchema: CompiledSchema;
49
54
  }
50
55
 
56
+ interface ValidatorItem {
57
+ fn: KeywordFunction;
58
+ defineError: DefineErrorFunction;
59
+ }
60
+
51
61
  export class SchemaShield {
52
62
  private types: Record<string, TypeFunction | false> = {};
53
63
  private formats: Record<string, FormatFunction | false> = {};
54
64
  private keywords: Record<string, KeywordFunction | false> = {};
55
65
  private immutable = false;
66
+ private rootSchema: CompiledSchema | null = null;
67
+ private idRegistry: Map<string, CompiledSchema> = new Map();
68
+ private failFast: boolean = true;
56
69
 
57
70
  constructor({
58
- immutable = false
71
+ immutable = false,
72
+ failFast = true
59
73
  }: {
60
74
  immutable?: boolean;
75
+ failFast?: boolean;
61
76
  } = {}) {
62
77
  this.immutable = immutable;
78
+ this.failFast = failFast;
63
79
 
64
80
  for (const [type, validator] of Object.entries(Types)) {
65
81
  if (validator) {
@@ -111,146 +127,189 @@ export class SchemaShield {
111
127
  return this.keywords[keyword];
112
128
  }
113
129
 
130
+ getSchemaRef(path: string): CompiledSchema | undefined {
131
+ if (!this.rootSchema) {
132
+ return;
133
+ }
134
+ return resolvePath(this.rootSchema, path);
135
+ }
136
+
137
+ getSchemaById(id: string): CompiledSchema | undefined {
138
+ return this.idRegistry.get(id);
139
+ }
140
+
114
141
  compile(schema: any): Validator {
142
+ this.idRegistry.clear();
115
143
  const compiledSchema = this.compileSchema(schema);
144
+ this.rootSchema = compiledSchema;
145
+ this.linkReferences(compiledSchema);
146
+
116
147
  if (!compiledSchema.$validate) {
117
148
  if (this.isSchemaLike(schema) === false) {
118
149
  throw new ValidationError("Invalid schema");
119
150
  }
120
151
 
121
152
  compiledSchema.$validate = getNamedFunction<ValidateFunction>(
122
- "any",
153
+ "Validate_Any",
123
154
  () => {}
124
155
  );
125
156
  }
126
157
 
127
158
  const validate: Validator = (data: any) => {
159
+ this.rootSchema = compiledSchema;
160
+
128
161
  const clonedData = this.immutable ? deepClone(data) : data;
129
- const error = compiledSchema.$validate(clonedData);
162
+ const res = compiledSchema.$validate!(clonedData);
130
163
 
131
- return {
132
- data: clonedData,
133
- error: error ? error : null,
134
- valid: !error
135
- };
164
+ if (res) {
165
+ return { data: clonedData, error: res, valid: false };
166
+ }
167
+
168
+ return { data: clonedData, error: null, valid: true };
136
169
  };
137
170
 
138
171
  validate.compiledSchema = compiledSchema;
139
-
140
172
  return validate;
141
173
  }
142
174
 
143
175
  private compileSchema(schema: Partial<CompiledSchema> | any): CompiledSchema {
144
176
  if (!isObject(schema)) {
145
177
  if (schema === true) {
146
- schema = {
147
- anyOf: [{}]
148
- };
178
+ schema = { anyOf: [{}] }; // Always valid
149
179
  } else if (schema === false) {
150
- schema = {
151
- oneOf: []
152
- };
180
+ schema = { oneOf: [] }; // Always invalid
153
181
  } else {
154
- schema = {
155
- oneOf: [schema]
156
- };
182
+ schema = { oneOf: [schema] };
157
183
  }
158
184
  }
159
185
 
160
186
  const compiledSchema: CompiledSchema = deepClone(schema) as CompiledSchema;
161
- const defineTypeError = getDefinedErrorFunctionForKey("type", schema);
162
- const typeValidations: TypeFunction[] = [];
163
187
 
164
- let methodName = "";
188
+ if (typeof schema.$id === "string") {
189
+ this.idRegistry.set(schema.$id, compiledSchema);
190
+ }
191
+
192
+ if ("$ref" in schema) {
193
+ const refValidator = this.getKeyword("$ref");
194
+ if (refValidator) {
195
+ const defineError = getDefinedErrorFunctionForKey(
196
+ "$ref",
197
+ schema["$ref"],
198
+ this.failFast
199
+ );
200
+
201
+ compiledSchema.$validate = getNamedFunction<ValidateFunction>(
202
+ "Validate_Reference",
203
+ (data) =>
204
+ (refValidator as KeywordFunction)(
205
+ compiledSchema,
206
+ data,
207
+ defineError,
208
+ this
209
+ )
210
+ );
211
+ }
212
+ return compiledSchema;
213
+ }
214
+
215
+ const validators: ValidatorItem[] = [];
216
+ const activeNames: string[] = [];
165
217
 
166
218
  if ("type" in schema) {
219
+ const defineTypeError = getDefinedErrorFunctionForKey(
220
+ "type",
221
+ schema,
222
+ this.failFast
223
+ );
167
224
  const types = Array.isArray(schema.type)
168
225
  ? schema.type
169
- : schema.type.split(",").map((t) => t.trim());
226
+ : schema.type.split(",").map((t: string) => t.trim());
227
+
228
+ const typeFunctions: TypeFunction[] = [];
229
+ const typeNames: string[] = [];
170
230
 
171
231
  for (const type of types) {
172
232
  const validator = this.getType(type);
173
233
  if (validator) {
174
- typeValidations.push(validator);
175
- methodName += (methodName ? "_OR_" : "") + validator.name;
234
+ typeFunctions.push(validator);
235
+ typeNames.push(validator.name);
176
236
  }
177
237
  }
178
238
 
179
- const typeValidationsLength = typeValidations.length;
180
-
181
- if (typeValidationsLength === 0) {
182
- throw defineTypeError("Invalid type for schema", { data: schema.type });
239
+ if (typeFunctions.length === 0) {
240
+ throw getDefinedErrorFunctionForKey(
241
+ "type",
242
+ schema,
243
+ this.failFast
244
+ )("Invalid type for schema", { data: schema.type });
183
245
  }
184
246
 
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 });
192
- }
247
+ let combinedTypeValidator: ValidateFunction;
248
+ let typeMethodName = "";
249
+
250
+ if (typeFunctions.length === 1) {
251
+ typeMethodName = typeNames[0];
252
+ const singleTypeFn = typeFunctions[0];
253
+ combinedTypeValidator = (data) => {
254
+ if (!singleTypeFn(data)) {
255
+ return defineTypeError("Invalid type", { data });
193
256
  }
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)) {
201
- return;
202
- }
257
+ };
258
+ } else {
259
+ typeMethodName = typeNames.join("_OR_");
260
+ combinedTypeValidator = (data) => {
261
+ for (let i = 0; i < typeFunctions.length; i++) {
262
+ if (typeFunctions[i](data)) {
263
+ return;
203
264
  }
204
- return defineTypeError("Invalid type", { data });
205
265
  }
206
- );
266
+ return defineTypeError("Invalid type", { data });
267
+ };
207
268
  }
269
+
270
+ const typeAdapter: KeywordFunction = (_s, data) =>
271
+ combinedTypeValidator(data);
272
+
273
+ validators.push({
274
+ fn: getNamedFunction(typeMethodName, typeAdapter),
275
+ defineError: defineTypeError
276
+ });
277
+ activeNames.push(typeMethodName);
208
278
  }
209
279
 
210
- for (const key of Object.keys(schema)) {
211
- if (key === "type") {
212
- compiledSchema.type = schema.type;
213
- continue;
214
- }
280
+ const { type, $id, $ref, $validate, required, ...otherKeys } = schema; // Exclude handled keys
281
+
282
+ // In here we create an array of keys putting the require keyword last
283
+ // This is to ensure required properties are checked after defaults are applied
284
+ const keyOrder = required
285
+ ? [...Object.keys(otherKeys), "required"]
286
+ : Object.keys(otherKeys);
287
+ for (const key of keyOrder) {
288
+ const keywordFn = this.getKeyword(key);
289
+
290
+ if (keywordFn) {
291
+ const defineError = getDefinedErrorFunctionForKey(
292
+ key,
293
+ schema[key],
294
+ this.failFast
295
+ );
296
+ const fnName = keywordFn.name || key;
215
297
 
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
- }
298
+ validators.push({
299
+ fn: keywordFn as KeywordFunction,
300
+ defineError
301
+ });
302
+
303
+ activeNames.push(fnName);
250
304
  }
305
+ }
251
306
 
307
+ const literalKeywords = ["enum", "const", "default", "examples"];
308
+ for (const key of keyOrder) {
309
+ if (literalKeywords.includes(key)) {
310
+ continue;
311
+ }
252
312
  if (isObject(schema[key])) {
253
- // If the key is properties go through each property and try to compile it as a schema
254
313
  if (key === "properties") {
255
314
  for (const subKey of Object.keys(schema[key])) {
256
315
  compiledSchema[key][subKey] = this.compileSchema(
@@ -264,15 +323,42 @@ export class SchemaShield {
264
323
  }
265
324
 
266
325
  if (Array.isArray(schema[key])) {
267
- compiledSchema[key] = schema[key].map((subSchema, index) =>
268
- this.isSchemaLike(subSchema)
269
- ? this.compileSchema(subSchema)
270
- : subSchema
271
- );
326
+ for (let i = 0; i < schema[key].length; i++) {
327
+ if (this.isSchemaLike(schema[key][i])) {
328
+ compiledSchema[key][i] = this.compileSchema(schema[key][i]);
329
+ }
330
+ }
272
331
  continue;
273
332
  }
333
+ }
274
334
 
275
- compiledSchema[key] = schema[key];
335
+ if (validators.length === 0) {
336
+ return compiledSchema;
337
+ }
338
+
339
+ if (validators.length === 1) {
340
+ const v = validators[0];
341
+ compiledSchema.$validate = getNamedFunction(activeNames[0], (data) =>
342
+ v.fn(compiledSchema, data, v.defineError, this)
343
+ );
344
+ } else {
345
+ const compositeName = "Validate_" + activeNames.join("_AND_");
346
+
347
+ const masterValidator: ValidateFunction = (data) => {
348
+ for (let i = 0; i < validators.length; i++) {
349
+ const v = validators[i];
350
+ const error = v.fn(compiledSchema, data, v.defineError, this);
351
+ if (error) {
352
+ return error;
353
+ }
354
+ }
355
+ return;
356
+ };
357
+
358
+ compiledSchema.$validate = getNamedFunction(
359
+ compositeName,
360
+ masterValidator
361
+ );
276
362
  }
277
363
 
278
364
  return compiledSchema as CompiledSchema;
@@ -292,4 +378,65 @@ export class SchemaShield {
292
378
  }
293
379
  return false;
294
380
  }
381
+
382
+ private linkReferences(root: CompiledSchema) {
383
+ const stack: any[] = [root];
384
+
385
+ while (stack.length > 0) {
386
+ const node = stack.pop();
387
+
388
+ if (!node || typeof node !== "object") continue;
389
+
390
+ if (
391
+ typeof node.$ref === "string" &&
392
+ typeof node.$validate === "function" &&
393
+ node.$validate.name === "Validate_Reference"
394
+ ) {
395
+ const refPath = node.$ref as string;
396
+
397
+ let target: any = this.getSchemaRef(refPath);
398
+ if (typeof target === "undefined") {
399
+ target = this.getSchemaById(refPath);
400
+ }
401
+
402
+ if (typeof target === "boolean") {
403
+ if (target === true) {
404
+ node.$validate = getNamedFunction("Validate_Ref_True", () => {});
405
+ } else {
406
+ const defineError = getDefinedErrorFunctionForKey(
407
+ "$ref",
408
+ node as any,
409
+ this.failFast
410
+ );
411
+
412
+ node.$validate = getNamedFunction(
413
+ "Validate_Ref_False",
414
+ (_data: any) => defineError("Value is not valid")
415
+ );
416
+ }
417
+ continue;
418
+ }
419
+
420
+ if (target && typeof target.$validate === "function") {
421
+ node.$validate = target.$validate;
422
+ }
423
+ }
424
+
425
+ for (const key in node) {
426
+ const value = node[key];
427
+ if (!value) continue;
428
+
429
+ if (Array.isArray(value)) {
430
+ for (let i = 0; i < value.length; i++) {
431
+ const v = value[i];
432
+ if (v && typeof v === "object") {
433
+ stack.push(v);
434
+ }
435
+ }
436
+ } else if (typeof value === "object") {
437
+ stack.push(value);
438
+ }
439
+ }
440
+ }
441
+ }
295
442
  }
@@ -1,8 +1,9 @@
1
- import { isCompiledSchema, isObject } from "../utils";
1
+ import { hasChanged, isCompiledSchema, isObject } from "../utils";
2
2
 
3
3
  import { KeywordFunction } from "../index";
4
4
 
5
5
  export const ArrayKeywords: Record<string, KeywordFunction> = {
6
+ // lib/keywords/array-keywords.ts
6
7
  items(schema, data, defineError) {
7
8
  if (!Array.isArray(data)) {
8
9
  return;
@@ -15,17 +16,19 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
15
16
  if (schemaItems === false && dataLength > 0) {
16
17
  return defineError("Array items are not allowed", { data });
17
18
  }
18
-
19
19
  return;
20
20
  }
21
21
 
22
22
  if (Array.isArray(schemaItems)) {
23
23
  const schemaItemsLength = schemaItems.length;
24
- const itemsLength = Math.min(schemaItemsLength, dataLength);
24
+ const itemsLength =
25
+ schemaItemsLength < dataLength ? schemaItemsLength : dataLength;
26
+
25
27
  for (let i = 0; i < itemsLength; i++) {
26
28
  const schemaItem = schemaItems[i];
29
+
27
30
  if (typeof schemaItem === "boolean") {
28
- if (schemaItem === false && typeof data[i] !== "undefined") {
31
+ if (schemaItem === false && data[i] !== undefined) {
29
32
  return defineError("Array item is not allowed", {
30
33
  item: i,
31
34
  data: data[i]
@@ -34,8 +37,9 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
34
37
  continue;
35
38
  }
36
39
 
37
- if (isCompiledSchema(schemaItem)) {
38
- const error = schemaItem.$validate(data[i]);
40
+ const validate = schemaItem && schemaItem.$validate;
41
+ if (typeof validate === "function") {
42
+ const error = validate(data[i]);
39
43
  if (error) {
40
44
  return defineError("Array item is invalid", {
41
45
  item: i,
@@ -49,29 +53,36 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
49
53
  return;
50
54
  }
51
55
 
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
- }
56
+ const validate = schemaItems && schemaItems.$validate;
57
+ if (typeof validate !== "function") {
58
+ return;
63
59
  }
64
60
 
65
- return;
61
+ for (let i = 0; i < dataLength; i++) {
62
+ const error = validate(data[i]);
63
+ if (error) {
64
+ return defineError("Array item is invalid", {
65
+ item: i,
66
+ cause: error,
67
+ data: data[i]
68
+ });
69
+ }
70
+ }
66
71
  },
67
72
 
68
73
  elements(schema, data, defineError) {
69
- if (!Array.isArray(data) || !isCompiledSchema(schema.elements)) {
74
+ if (!Array.isArray(data)) {
75
+ return;
76
+ }
77
+
78
+ const elementsSchema = schema.elements;
79
+ const validate = elementsSchema && elementsSchema.$validate;
80
+ if (typeof validate !== "function") {
70
81
  return;
71
82
  }
72
83
 
73
84
  for (let i = 0; i < data.length; i++) {
74
- const error = schema.elements.$validate(data[i]);
85
+ const error = validate(data[i]);
75
86
  if (error) {
76
87
  return defineError("Array item is invalid", {
77
88
  item: i,
@@ -80,8 +91,6 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
80
91
  });
81
92
  }
82
93
  }
83
-
84
- return;
85
94
  },
86
95
 
87
96
  minItems(schema, data, defineError) {
@@ -138,34 +147,39 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
138
147
  return;
139
148
  }
140
149
 
141
- const unique = new Set();
142
-
143
- for (const item of data) {
144
- let itemStr;
145
-
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);
150
+ const len = data.length;
151
+ if (len <= 1) {
152
+ return;
153
+ }
154
+
155
+ const primitiveSeen = new Set<any>();
156
+
157
+ for (let i = 0; i < len; i++) {
158
+ const item = data[i];
159
+ const type = typeof item;
160
+
161
+ if (
162
+ item === null ||
163
+ type === "string" ||
164
+ type === "number" ||
165
+ type === "boolean"
166
+ ) {
167
+ if (primitiveSeen.has(item)) {
168
+ return defineError("Array items are not unique", { data: item });
169
+ }
170
+ primitiveSeen.add(item);
171
+ continue;
160
172
  }
161
173
 
162
- if (unique.has(itemStr)) {
163
- return defineError("Array items are not unique", { data: item });
174
+ if (item && typeof item === "object") {
175
+ for (let j = 0; j < i; j++) {
176
+ const prev = data[j];
177
+ if (prev && typeof prev === "object" && !hasChanged(prev, item)) {
178
+ return defineError("Array items are not unique", { data: item });
179
+ }
180
+ }
164
181
  }
165
- unique.add(itemStr);
166
182
  }
167
-
168
- return;
169
183
  },
170
184
 
171
185
  contains(schema, data, defineError) {