123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- const Validator = require('./Validator');
- const { Type: ValidationErrorType } = require('../model/ValidationError');
- const { isObject, once, cloneDeep: lodashCloneDeep, omit } = require('../utils/objectUtils');
- const getAjv = once(() => {
- try {
- return require('ajv');
- } catch (err) {
- throw new Error('Optional ajv dependency not installed. Please run `npm install ajv --save`');
- }
- });
- class AjvValidator extends Validator {
- constructor(conf) {
- super();
- this.ajvOptions = Object.assign({ errorDataPath: 'property' }, conf.options, {
- allErrors: true
- });
- // Create a normal Ajv instance.
- this.ajv = new getAjv()(
- Object.assign(
- {
- useDefaults: true
- },
- this.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).
- this.ajvNoDefaults = new getAjv()(
- Object.assign({}, this.ajvOptions, {
- useDefaults: false
- })
- );
- // A cache for the compiled validator functions.
- this.cache = new Map();
- conf.onCreateAjv(this.ajv);
- conf.onCreateAjv(this.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);
- 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) {
- if (!errors) {
- return null;
- }
- let relations = modelClass.getRelations();
- let errorHash = {};
- let numErrors = 0;
- for (let i = 0; i < errors.length; ++i) {
- const error = errors[i];
- const dataPath = `${options.dataPath || ''}${error.dataPath}`;
- // If additionalProperties = false, relations can pop up as additionalProperty
- // errors. Skip those.
- if (
- error.params &&
- error.params.additionalProperty &&
- relations[error.params.additionalProperty]
- ) {
- continue;
- }
- // Unknown properties are reported in `['propertyName']` notation,
- // so replace those with dot-notation, see:
- // https://github.com/epoberezkin/ajv/issues/671
- const key = dataPath.replace(/\['([^' ]*)'\]/g, '.$1').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] = []);
- // 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({
- message: error.message,
- keyword: error.keyword,
- params: error.params
- });
- ++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'];
- return Object.assign(
- omit(jsonSchema, ['required', ...subSchemaProps]),
- ...subSchemaProps.map(prop => subSchemaWithoutRequired(jsonSchema, prop))
- );
- }
- 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 {
- 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;
|