schema-shield 0.0.5 → 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,17 +1,22 @@
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";
11
13
  import { Types } from "./types";
12
14
  import { keywords } from "./keywords";
13
15
 
14
- export type Result = void | ValidationError;
16
+ export { ValidationError } from "./utils";
17
+ export { deepClone } from "./utils";
18
+
19
+ export type Result = void | ValidationError | true;
15
20
 
16
21
  export interface KeywordFunction {
17
22
  (
@@ -40,22 +45,37 @@ export interface CompiledSchema {
40
45
  }
41
46
 
42
47
  export interface Validator {
43
- (data: any): { data: any; error: ValidationError | null; valid: boolean };
48
+ (data: any): {
49
+ data: any;
50
+ error: ValidationError | null | true;
51
+ valid: boolean;
52
+ };
44
53
  compiledSchema: CompiledSchema;
45
54
  }
46
55
 
56
+ interface ValidatorItem {
57
+ fn: KeywordFunction;
58
+ defineError: DefineErrorFunction;
59
+ }
60
+
47
61
  export class SchemaShield {
48
62
  private types: Record<string, TypeFunction | false> = {};
49
63
  private formats: Record<string, FormatFunction | false> = {};
50
64
  private keywords: Record<string, KeywordFunction | false> = {};
51
65
  private immutable = false;
66
+ private rootSchema: CompiledSchema | null = null;
67
+ private idRegistry: Map<string, CompiledSchema> = new Map();
68
+ private failFast: boolean = true;
52
69
 
53
70
  constructor({
54
- immutable = false
71
+ immutable = false,
72
+ failFast = true
55
73
  }: {
56
74
  immutable?: boolean;
75
+ failFast?: boolean;
57
76
  } = {}) {
58
77
  this.immutable = immutable;
78
+ this.failFast = failFast;
59
79
 
60
80
  for (const [type, validator] of Object.entries(Types)) {
61
81
  if (validator) {
@@ -107,146 +127,189 @@ export class SchemaShield {
107
127
  return this.keywords[keyword];
108
128
  }
109
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
+
110
141
  compile(schema: any): Validator {
142
+ this.idRegistry.clear();
111
143
  const compiledSchema = this.compileSchema(schema);
144
+ this.rootSchema = compiledSchema;
145
+ this.linkReferences(compiledSchema);
146
+
112
147
  if (!compiledSchema.$validate) {
113
148
  if (this.isSchemaLike(schema) === false) {
114
149
  throw new ValidationError("Invalid schema");
115
150
  }
116
151
 
117
152
  compiledSchema.$validate = getNamedFunction<ValidateFunction>(
118
- "any",
153
+ "Validate_Any",
119
154
  () => {}
120
155
  );
121
156
  }
122
157
 
123
158
  const validate: Validator = (data: any) => {
159
+ this.rootSchema = compiledSchema;
160
+
124
161
  const clonedData = this.immutable ? deepClone(data) : data;
125
- const error = compiledSchema.$validate(clonedData);
162
+ const res = compiledSchema.$validate!(clonedData);
126
163
 
127
- return {
128
- data: clonedData,
129
- error: error ? error : null,
130
- valid: !error
131
- };
164
+ if (res) {
165
+ return { data: clonedData, error: res, valid: false };
166
+ }
167
+
168
+ return { data: clonedData, error: null, valid: true };
132
169
  };
133
170
 
134
171
  validate.compiledSchema = compiledSchema;
135
-
136
172
  return validate;
137
173
  }
138
174
 
139
175
  private compileSchema(schema: Partial<CompiledSchema> | any): CompiledSchema {
140
176
  if (!isObject(schema)) {
141
177
  if (schema === true) {
142
- schema = {
143
- anyOf: [{}]
144
- };
178
+ schema = { anyOf: [{}] }; // Always valid
145
179
  } else if (schema === false) {
146
- schema = {
147
- oneOf: []
148
- };
180
+ schema = { oneOf: [] }; // Always invalid
149
181
  } else {
150
- schema = {
151
- oneOf: [schema]
152
- };
182
+ schema = { oneOf: [schema] };
153
183
  }
154
184
  }
155
185
 
156
186
  const compiledSchema: CompiledSchema = deepClone(schema) as CompiledSchema;
157
- const defineTypeError = getDefinedErrorFunctionForKey("type", schema);
158
- const typeValidations: TypeFunction[] = [];
159
187
 
160
- 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[] = [];
161
217
 
162
218
  if ("type" in schema) {
219
+ const defineTypeError = getDefinedErrorFunctionForKey(
220
+ "type",
221
+ schema,
222
+ this.failFast
223
+ );
163
224
  const types = Array.isArray(schema.type)
164
225
  ? schema.type
165
- : 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[] = [];
166
230
 
167
231
  for (const type of types) {
168
232
  const validator = this.getType(type);
169
233
  if (validator) {
170
- typeValidations.push(validator);
171
- methodName += (methodName ? "_OR_" : "") + validator.name;
234
+ typeFunctions.push(validator);
235
+ typeNames.push(validator.name);
172
236
  }
173
237
  }
174
238
 
175
- const typeValidationsLength = typeValidations.length;
176
-
177
- if (typeValidationsLength === 0) {
178
- 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 });
179
245
  }
180
246
 
181
- if (typeValidationsLength === 1) {
182
- const typeValidation = typeValidations[0];
183
- compiledSchema.$validate = getNamedFunction<ValidateFunction>(
184
- methodName,
185
- (data) => {
186
- if (!typeValidation(data)) {
187
- return defineTypeError("Invalid type", { data });
188
- }
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 });
189
256
  }
190
- );
191
- } else if (typeValidationsLength > 1) {
192
- compiledSchema.$validate = getNamedFunction<ValidateFunction>(
193
- methodName,
194
- (data) => {
195
- for (let i = 0; i < typeValidationsLength; i++) {
196
- if (typeValidations[i](data)) {
197
- return;
198
- }
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;
199
264
  }
200
- return defineTypeError("Invalid type", { data });
201
265
  }
202
- );
266
+ return defineTypeError("Invalid type", { data });
267
+ };
203
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);
204
278
  }
205
279
 
206
- for (const key of Object.keys(schema)) {
207
- if (key === "type") {
208
- compiledSchema.type = schema.type;
209
- continue;
210
- }
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;
211
297
 
212
- const keywordValidator = this.getKeyword(key);
213
- if (keywordValidator) {
214
- const defineError = getDefinedErrorFunctionForKey(key, schema[key]);
215
- if (compiledSchema.$validate) {
216
- const prevValidator = compiledSchema.$validate;
217
- methodName += `_AND_${keywordValidator.name}`;
218
- compiledSchema.$validate = getNamedFunction<ValidateFunction>(
219
- methodName,
220
- (data) => {
221
- const error = prevValidator(data);
222
- if (error) {
223
- return error;
224
- }
225
- return (keywordValidator as KeywordFunction)(
226
- compiledSchema,
227
- data,
228
- defineError,
229
- this
230
- );
231
- }
232
- );
233
- } else {
234
- methodName = keywordValidator.name;
235
- compiledSchema.$validate = getNamedFunction<ValidateFunction>(
236
- methodName,
237
- (data) =>
238
- (keywordValidator as KeywordFunction)(
239
- compiledSchema,
240
- data,
241
- defineError,
242
- this
243
- )
244
- );
245
- }
298
+ validators.push({
299
+ fn: keywordFn as KeywordFunction,
300
+ defineError
301
+ });
302
+
303
+ activeNames.push(fnName);
246
304
  }
305
+ }
247
306
 
307
+ const literalKeywords = ["enum", "const", "default", "examples"];
308
+ for (const key of keyOrder) {
309
+ if (literalKeywords.includes(key)) {
310
+ continue;
311
+ }
248
312
  if (isObject(schema[key])) {
249
- // If the key is properties go through each property and try to compile it as a schema
250
313
  if (key === "properties") {
251
314
  for (const subKey of Object.keys(schema[key])) {
252
315
  compiledSchema[key][subKey] = this.compileSchema(
@@ -260,15 +323,42 @@ export class SchemaShield {
260
323
  }
261
324
 
262
325
  if (Array.isArray(schema[key])) {
263
- compiledSchema[key] = schema[key].map((subSchema, index) =>
264
- this.isSchemaLike(subSchema)
265
- ? this.compileSchema(subSchema)
266
- : subSchema
267
- );
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
+ }
268
331
  continue;
269
332
  }
333
+ }
270
334
 
271
- 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
+ );
272
362
  }
273
363
 
274
364
  return compiledSchema as CompiledSchema;
@@ -288,4 +378,65 @@ export class SchemaShield {
288
378
  }
289
379
  return false;
290
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
+ }
291
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) {