-
Notifications
You must be signed in to change notification settings - Fork 640
/
AjvValidator.js
325 lines (266 loc) · 8.49 KB
/
AjvValidator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
'use strict';
const Ajv = require('ajv');
const addFormats = require('ajv-formats');
const { Validator } = require('./Validator');
const { ValidationErrorType } = require('../model/ValidationError');
const { isObject, once, cloneDeep: lodashCloneDeep, omit } = require('../utils/objectUtils');
class AjvValidator extends Validator {
static init(self, conf) {
super.init(self, conf);
self.ajvOptions = Object.assign({}, conf.options, {
allErrors: true,
});
// Create a normal Ajv instance.
self.ajv = new Ajv(
Object.assign(
{
useDefaults: true,
},
self.ajvOptions,
),
);
// Create an instance that doesn't set default values. We need this one
// to validate `patch` objects (objects that have a subset of properties).
self.ajvNoDefaults = new Ajv(
Object.assign({}, self.ajvOptions, {
useDefaults: false,
}),
);
// A cache for the compiled validator functions.
self.cache = new Map();
const setupAjv = (ajv) => {
if (conf.onCreateAjv) {
conf.onCreateAjv(ajv);
}
// Only add Ajv formats if they weren't added in user-space already
if (!ajv.formats['date-time']) {
addFormats(ajv);
}
};
setupAjv(self.ajv);
setupAjv(self.ajvNoDefaults);
}
beforeValidate({ json, model, options, ctx }) {
ctx.jsonSchema = model.constructor.getJsonSchema();
// Objection model's have a `$beforeValidate` hook that is allowed to modify the schema.
// We need to clone the schema in case the function modifies it. We only do this in the
// rare case that the given model has implemented the hook.
if (model.$beforeValidate !== model.$objectionModelClass.prototype.$beforeValidate) {
ctx.jsonSchema = cloneDeep(ctx.jsonSchema);
const ret = model.$beforeValidate(ctx.jsonSchema, json, options);
if (ret !== undefined) {
ctx.jsonSchema = ret;
}
}
}
validate({ json, model, options, ctx }) {
if (!ctx.jsonSchema) {
return json;
}
const modelClass = model.constructor;
const validator = this.getValidator(modelClass, ctx.jsonSchema, !!options.patch);
// We need to clone the input json if we are about to set default values.
if (!options.mutable && !options.patch && setsDefaultValues(ctx.jsonSchema)) {
json = cloneDeep(json);
}
validator.call(model, json);
const error = parseValidationError(validator.errors, modelClass, options, this.ajvOptions);
if (error) {
throw error;
}
return json;
}
getValidator(modelClass, jsonSchema, isPatchObject) {
// Use the Ajv custom serializer if provided.
const createCacheKey = this.ajvOptions.serialize || JSON.stringify;
// Optimization for the common case where jsonSchema is never modified.
// In that case we don't need to call the costly createCacheKey function.
const cacheKey =
jsonSchema === modelClass.getJsonSchema()
? modelClass.uniqueTag()
: createCacheKey(jsonSchema);
let validators = this.cache.get(cacheKey);
let validator = null;
if (!validators) {
validators = {
// Validator created for the schema object without `required` properties
// using the Ajv instance that doesn't set default values.
patchValidator: null,
// Validator created for the unmodified schema.
normalValidator: null,
};
this.cache.set(cacheKey, validators);
}
if (isPatchObject) {
validator = validators.patchValidator;
if (!validator) {
validator = this.compilePatchValidator(jsonSchema);
validators.patchValidator = validator;
}
} else {
validator = validators.normalValidator;
if (!validator) {
validator = this.compileNormalValidator(jsonSchema);
validators.normalValidator = validator;
}
}
return validator;
}
compilePatchValidator(jsonSchema) {
jsonSchema = jsonSchemaWithoutRequired(jsonSchema);
// We need to use the ajv instance that doesn't set the default values.
return this.ajvNoDefaults.compile(jsonSchema);
}
compileNormalValidator(jsonSchema) {
return this.ajv.compile(jsonSchema);
}
}
function parseValidationError(errors, modelClass, options, ajvOptions) {
if (!errors) {
return null;
}
let relationNames = modelClass.getRelationNames();
let errorHash = {};
let numErrors = 0;
for (const error of errors) {
// If additionalProperties = false, relations can pop up as additionalProperty
// errors. Skip those.
if (
error.params &&
error.params.additionalProperty &&
relationNames.includes(error.params.additionalProperty)
) {
continue;
}
let path = error.instancePath.replace(/\//g, '.');
if (error.params) {
if (error.params.missingProperty) {
path += `.${error.params.missingProperty}`;
} else if (error.params.additionalProperty) {
path += `.${error.params.additionalProperty}`;
}
}
const key = `${options.dataPath || ''}${path}`.substring(1);
// More than one error can occur for the same key in Ajv, merge them in the array:
const array = errorHash[key] || (errorHash[key] = []);
// Prepare error object
const errorObj = {
message: error.message,
keyword: error.keyword,
params: error.params,
};
// Add data if verbose enabled
if (ajvOptions.verbose) {
errorObj.data = error.data;
}
// Use unshift instead of push so that the last error ends up at [0],
// preserving previous behavior where only the last error was stored.
array.unshift(errorObj);
++numErrors;
}
if (numErrors === 0) {
return null;
}
return modelClass.createValidationError({
type: ValidationErrorType.ModelValidation,
data: errorHash,
});
}
function cloneDeep(obj) {
if (isObject(obj) && obj.$isObjectionModel) {
return obj.$clone();
} else {
return lodashCloneDeep(obj);
}
}
function setsDefaultValues(jsonSchema) {
return jsonSchema && jsonSchema.properties && hasDefaults(jsonSchema.properties);
}
function hasDefaults(obj) {
if (Array.isArray(obj)) {
return arrayHasDefaults(obj);
} else {
return objectHasDefaults(obj);
}
}
function arrayHasDefaults(arr) {
for (let i = 0, l = arr.length; i < l; ++i) {
const val = arr[i];
if (isObject(val) && hasDefaults(val)) {
return true;
}
}
return false;
}
function objectHasDefaults(obj) {
const keys = Object.keys(obj);
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
if (key === 'default') {
return true;
} else {
const val = obj[key];
if (isObject(val) && hasDefaults(val)) {
return true;
}
}
}
return false;
}
function jsonSchemaWithoutRequired(jsonSchema) {
const subSchemaProps = ['anyOf', 'oneOf', 'allOf', 'not', 'then', 'else', 'properties'];
return Object.assign(
omit(jsonSchema, ['required', ...subSchemaProps]),
...subSchemaProps.map((prop) => subSchemaWithoutRequired(jsonSchema, prop)),
jsonSchema && jsonSchema.definitions && Object.keys(jsonSchema.definitions).length > 0
? {
definitions: Object.assign(
...Object.keys(jsonSchema.definitions).map((prop) => ({
[prop]: jsonSchemaWithoutRequired(jsonSchema.definitions[prop]),
})),
),
}
: {},
jsonSchema.discriminator && jsonSchema.discriminator.propertyName
? { required: [jsonSchema.discriminator.propertyName] }
: {},
);
}
function subSchemaWithoutRequired(jsonSchema, prop) {
if (jsonSchema[prop]) {
if (Array.isArray(jsonSchema[prop])) {
const schemaArray = jsonSchemaArrayWithoutRequired(jsonSchema[prop]);
if (schemaArray.length !== 0) {
return {
[prop]: schemaArray,
};
} else {
return {};
}
} else if (jsonSchema.type === 'object' && prop === 'properties') {
return {
[prop]: Object.fromEntries(
Object.entries(jsonSchema[prop]).map(([key, schema]) => [
key,
jsonSchemaWithoutRequired(schema),
]),
),
};
} else {
return {
[prop]: jsonSchemaWithoutRequired(jsonSchema[prop]),
};
}
} else {
return {};
}
}
function jsonSchemaArrayWithoutRequired(jsonSchemaArray) {
return jsonSchemaArray.map(jsonSchemaWithoutRequired).filter(isNotEmptyObject);
}
function isNotEmptyObject(obj) {
return Object.keys(obj).length !== 0;
}
module.exports = {
AjvValidator,
};