419 lines
13 KiB
JavaScript
419 lines
13 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const _ = require('lodash');
|
||
|
const Utils = require('./utils');
|
||
|
const sequelizeError = require('./errors');
|
||
|
const DataTypes = require('./data-types');
|
||
|
const BelongsTo = require('./associations/belongs-to');
|
||
|
const validator = require('./utils/validator-extras').validator;
|
||
|
const { promisify } = require('util');
|
||
|
|
||
|
/**
|
||
|
* Instance Validator.
|
||
|
*
|
||
|
* @param {Instance} modelInstance The model instance.
|
||
|
* @param {object} options A dictionary with options.
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
class InstanceValidator {
|
||
|
constructor(modelInstance, options) {
|
||
|
options = {
|
||
|
// assign defined and default options
|
||
|
hooks: true,
|
||
|
...options
|
||
|
};
|
||
|
|
||
|
if (options.fields && !options.skip) {
|
||
|
options.skip = _.difference(Object.keys(modelInstance.constructor.rawAttributes), options.fields);
|
||
|
} else {
|
||
|
options.skip = options.skip || [];
|
||
|
}
|
||
|
|
||
|
this.options = options;
|
||
|
|
||
|
this.modelInstance = modelInstance;
|
||
|
|
||
|
/**
|
||
|
* Exposes a reference to validator.js. This allows you to add custom validations using `validator.extend`
|
||
|
*
|
||
|
* @name validator
|
||
|
* @private
|
||
|
*/
|
||
|
this.validator = validator;
|
||
|
|
||
|
/**
|
||
|
* All errors will be stored here from the validations.
|
||
|
*
|
||
|
* @type {Array} Will contain keys that correspond to attributes which will
|
||
|
* be Arrays of Errors.
|
||
|
* @private
|
||
|
*/
|
||
|
this.errors = [];
|
||
|
|
||
|
/**
|
||
|
* @type {boolean} Indicates if validations are in progress
|
||
|
* @private
|
||
|
*/
|
||
|
this.inProgress = false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The main entry point for the Validation module, invoke to start the dance.
|
||
|
*
|
||
|
* @returns {Promise}
|
||
|
* @private
|
||
|
*/
|
||
|
async _validate() {
|
||
|
if (this.inProgress) throw new Error('Validations already in progress.');
|
||
|
|
||
|
this.inProgress = true;
|
||
|
|
||
|
await Promise.all([
|
||
|
this._perAttributeValidators(),
|
||
|
this._customValidators()
|
||
|
]);
|
||
|
|
||
|
if (this.errors.length) {
|
||
|
throw new sequelizeError.ValidationError(null, this.errors);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Invoke the Validation sequence and run validation hooks if defined
|
||
|
* - Before Validation Model Hooks
|
||
|
* - Validation
|
||
|
* - On validation success: After Validation Model Hooks
|
||
|
* - On validation failure: Validation Failed Model Hooks
|
||
|
*
|
||
|
* @returns {Promise}
|
||
|
* @private
|
||
|
*/
|
||
|
async validate() {
|
||
|
return await (this.options.hooks ? this._validateAndRunHooks() : this._validate());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Invoke the Validation sequence and run hooks
|
||
|
* - Before Validation Model Hooks
|
||
|
* - Validation
|
||
|
* - On validation success: After Validation Model Hooks
|
||
|
* - On validation failure: Validation Failed Model Hooks
|
||
|
*
|
||
|
* @returns {Promise}
|
||
|
* @private
|
||
|
*/
|
||
|
async _validateAndRunHooks() {
|
||
|
const runHooks = this.modelInstance.constructor.runHooks.bind(this.modelInstance.constructor);
|
||
|
await runHooks('beforeValidate', this.modelInstance, this.options);
|
||
|
|
||
|
try {
|
||
|
await this._validate();
|
||
|
} catch (error) {
|
||
|
const newError = await runHooks('validationFailed', this.modelInstance, this.options, error);
|
||
|
throw newError || error;
|
||
|
}
|
||
|
|
||
|
await runHooks('afterValidate', this.modelInstance, this.options);
|
||
|
return this.modelInstance;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Will run all the validators defined per attribute (built-in validators and custom validators)
|
||
|
*
|
||
|
* @returns {Promise<Array>}
|
||
|
* @private
|
||
|
*/
|
||
|
async _perAttributeValidators() {
|
||
|
// promisify all attribute invocations
|
||
|
const validators = [];
|
||
|
|
||
|
_.forIn(this.modelInstance.rawAttributes, (rawAttribute, field) => {
|
||
|
if (this.options.skip.includes(field)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const value = this.modelInstance.dataValues[field];
|
||
|
|
||
|
if (value instanceof Utils.SequelizeMethod) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!rawAttribute._autoGenerated && !rawAttribute.autoIncrement) {
|
||
|
// perform validations based on schema
|
||
|
this._validateSchema(rawAttribute, field, value);
|
||
|
}
|
||
|
|
||
|
if (Object.prototype.hasOwnProperty.call(this.modelInstance.validators, field)) {
|
||
|
validators.push(this._singleAttrValidate(value, field, rawAttribute.allowNull));
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return await Promise.all(validators);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Will run all the custom validators defined in the model's options.
|
||
|
*
|
||
|
* @returns {Promise<Array>}
|
||
|
* @private
|
||
|
*/
|
||
|
async _customValidators() {
|
||
|
const validators = [];
|
||
|
_.each(this.modelInstance.constructor.options.validate, (validator, validatorType) => {
|
||
|
if (this.options.skip.includes(validatorType)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const valprom = this._invokeCustomValidator(validator, validatorType)
|
||
|
// errors are handled in settling, stub this
|
||
|
.catch(() => {});
|
||
|
|
||
|
validators.push(valprom);
|
||
|
});
|
||
|
|
||
|
return await Promise.all(validators);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validate a single attribute with all the defined built-in validators and custom validators.
|
||
|
*
|
||
|
* @private
|
||
|
*
|
||
|
* @param {*} value Anything.
|
||
|
* @param {string} field The field name.
|
||
|
* @param {boolean} allowNull Whether or not the schema allows null values
|
||
|
*
|
||
|
* @returns {Promise} A promise, will always resolve, auto populates error on this.error local object.
|
||
|
*/
|
||
|
async _singleAttrValidate(value, field, allowNull) {
|
||
|
// If value is null and allowNull is false, no validators should run (see #9143)
|
||
|
if ((value === null || value === undefined) && !allowNull) {
|
||
|
// The schema validator (_validateSchema) has already generated the validation error. Nothing to do here.
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Promisify each validator
|
||
|
const validators = [];
|
||
|
_.forIn(this.modelInstance.validators[field], (test, validatorType) => {
|
||
|
|
||
|
if (['isUrl', 'isURL', 'isEmail'].includes(validatorType)) {
|
||
|
// Preserve backwards compat. Validator.js now expects the second param to isURL and isEmail to be an object
|
||
|
if (typeof test === 'object' && test !== null && test.msg) {
|
||
|
test = {
|
||
|
msg: test.msg
|
||
|
};
|
||
|
} else if (test === true) {
|
||
|
test = {};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Custom validators should always run, except if value is null and allowNull is false (see #9143)
|
||
|
if (typeof test === 'function') {
|
||
|
validators.push(this._invokeCustomValidator(test, validatorType, true, value, field));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If value is null, built-in validators should not run (only custom validators have to run) (see #9134).
|
||
|
if (value === null || value === undefined) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const validatorPromise = this._invokeBuiltinValidator(value, test, validatorType, field);
|
||
|
// errors are handled in settling, stub this
|
||
|
validatorPromise.catch(() => {});
|
||
|
validators.push(validatorPromise);
|
||
|
});
|
||
|
|
||
|
return Promise
|
||
|
.all(validators.map(validator => validator.catch(rejection => {
|
||
|
const isBuiltIn = !!rejection.validatorName;
|
||
|
this._pushError(isBuiltIn, field, rejection, value, rejection.validatorName, rejection.validatorArgs);
|
||
|
})));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Prepare and invoke a custom validator.
|
||
|
*
|
||
|
* @private
|
||
|
*
|
||
|
* @param {Function} validator The custom validator.
|
||
|
* @param {string} validatorType the custom validator type (name).
|
||
|
* @param {boolean} optAttrDefined Set to true if custom validator was defined from the attribute
|
||
|
* @param {*} optValue value for attribute
|
||
|
* @param {string} optField field for attribute
|
||
|
*
|
||
|
* @returns {Promise} A promise.
|
||
|
*/
|
||
|
async _invokeCustomValidator(validator, validatorType, optAttrDefined, optValue, optField) {
|
||
|
let isAsync = false;
|
||
|
|
||
|
const validatorArity = validator.length;
|
||
|
// check if validator is async and requires a callback
|
||
|
let asyncArity = 1;
|
||
|
let errorKey = validatorType;
|
||
|
let invokeArgs;
|
||
|
if (optAttrDefined) {
|
||
|
asyncArity = 2;
|
||
|
invokeArgs = optValue;
|
||
|
errorKey = optField;
|
||
|
}
|
||
|
if (validatorArity === asyncArity) {
|
||
|
isAsync = true;
|
||
|
}
|
||
|
|
||
|
if (isAsync) {
|
||
|
try {
|
||
|
if (optAttrDefined) {
|
||
|
return await promisify(validator.bind(this.modelInstance, invokeArgs))();
|
||
|
}
|
||
|
return await promisify(validator.bind(this.modelInstance))();
|
||
|
} catch (e) {
|
||
|
return this._pushError(false, errorKey, e, optValue, validatorType);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
return await validator.call(this.modelInstance, invokeArgs);
|
||
|
} catch (e) {
|
||
|
return this._pushError(false, errorKey, e, optValue, validatorType);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Prepare and invoke a build-in validator.
|
||
|
*
|
||
|
* @private
|
||
|
*
|
||
|
* @param {*} value Anything.
|
||
|
* @param {*} test The test case.
|
||
|
* @param {string} validatorType One of known to Sequelize validators.
|
||
|
* @param {string} field The field that is being validated
|
||
|
*
|
||
|
* @returns {object} An object with specific keys to invoke the validator.
|
||
|
*/
|
||
|
async _invokeBuiltinValidator(value, test, validatorType, field) {
|
||
|
// Cast value as string to pass new Validator.js string requirement
|
||
|
const valueString = String(value);
|
||
|
// check if Validator knows that kind of validation test
|
||
|
if (typeof validator[validatorType] !== 'function') {
|
||
|
throw new Error(`Invalid validator function: ${validatorType}`);
|
||
|
}
|
||
|
|
||
|
const validatorArgs = this._extractValidatorArgs(test, validatorType, field);
|
||
|
|
||
|
if (!validator[validatorType](valueString, ...validatorArgs)) {
|
||
|
throw Object.assign(new Error(test.msg || `Validation ${validatorType} on ${field} failed`), { validatorName: validatorType, validatorArgs });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Will extract arguments for the validator.
|
||
|
*
|
||
|
* @param {*} test The test case.
|
||
|
* @param {string} validatorType One of known to Sequelize validators.
|
||
|
* @param {string} field The field that is being validated.
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
_extractValidatorArgs(test, validatorType, field) {
|
||
|
let validatorArgs = test.args || test;
|
||
|
const isLocalizedValidator = typeof validatorArgs !== 'string' && ['isAlpha', 'isAlphanumeric', 'isMobilePhone'].includes(validatorType);
|
||
|
|
||
|
if (!Array.isArray(validatorArgs)) {
|
||
|
if (validatorType === 'isImmutable') {
|
||
|
validatorArgs = [validatorArgs, field, this.modelInstance];
|
||
|
} else if (isLocalizedValidator || validatorType === 'isIP') {
|
||
|
validatorArgs = [];
|
||
|
} else {
|
||
|
validatorArgs = [validatorArgs];
|
||
|
}
|
||
|
} else {
|
||
|
validatorArgs = validatorArgs.slice(0);
|
||
|
}
|
||
|
return validatorArgs;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Will validate a single field against its schema definition (isnull).
|
||
|
*
|
||
|
* @param {object} rawAttribute As defined in the Schema.
|
||
|
* @param {string} field The field name.
|
||
|
* @param {*} value anything.
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
_validateSchema(rawAttribute, field, value) {
|
||
|
if (rawAttribute.allowNull === false && (value === null || value === undefined)) {
|
||
|
const association = Object.values(this.modelInstance.constructor.associations).find(association => association instanceof BelongsTo && association.foreignKey === rawAttribute.fieldName);
|
||
|
if (!association || !this.modelInstance.get(association.associationAccessor)) {
|
||
|
const validators = this.modelInstance.validators[field];
|
||
|
const errMsg = _.get(validators, 'notNull.msg', `${this.modelInstance.constructor.name}.${field} cannot be null`);
|
||
|
|
||
|
this.errors.push(new sequelizeError.ValidationErrorItem(
|
||
|
errMsg,
|
||
|
'notNull Violation', // sequelizeError.ValidationErrorItem.Origins.CORE,
|
||
|
field,
|
||
|
value,
|
||
|
this.modelInstance,
|
||
|
'is_null'
|
||
|
));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (rawAttribute.type instanceof DataTypes.STRING || rawAttribute.type instanceof DataTypes.TEXT || rawAttribute.type instanceof DataTypes.CITEXT) {
|
||
|
if (Array.isArray(value) || _.isObject(value) && !(value instanceof Utils.SequelizeMethod) && !Buffer.isBuffer(value)) {
|
||
|
this.errors.push(new sequelizeError.ValidationErrorItem(
|
||
|
`${field} cannot be an array or an object`,
|
||
|
'string violation', // sequelizeError.ValidationErrorItem.Origins.CORE,
|
||
|
field,
|
||
|
value,
|
||
|
this.modelInstance,
|
||
|
'not_a_string'
|
||
|
));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Signs all errors retaining the original.
|
||
|
*
|
||
|
* @param {boolean} isBuiltin - Determines if error is from builtin validator.
|
||
|
* @param {string} errorKey - name of invalid attribute.
|
||
|
* @param {Error|string} rawError - The original error.
|
||
|
* @param {string|number} value - The data that triggered the error.
|
||
|
* @param {string} fnName - Name of the validator, if any
|
||
|
* @param {Array} fnArgs - Arguments for the validator [function], if any
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
_pushError(isBuiltin, errorKey, rawError, value, fnName, fnArgs) {
|
||
|
const message = rawError.message || rawError || 'Validation error';
|
||
|
const error = new sequelizeError.ValidationErrorItem(
|
||
|
message,
|
||
|
'Validation error', // sequelizeError.ValidationErrorItem.Origins.FUNCTION,
|
||
|
errorKey,
|
||
|
value,
|
||
|
this.modelInstance,
|
||
|
fnName,
|
||
|
isBuiltin ? fnName : undefined,
|
||
|
isBuiltin ? fnArgs : undefined
|
||
|
);
|
||
|
|
||
|
error[InstanceValidator.RAW_KEY_NAME] = rawError;
|
||
|
|
||
|
this.errors.push(error);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* The error key for arguments as passed by custom validators
|
||
|
*
|
||
|
* @type {string}
|
||
|
* @private
|
||
|
*/
|
||
|
InstanceValidator.RAW_KEY_NAME = 'original';
|
||
|
|
||
|
module.exports = InstanceValidator;
|
||
|
module.exports.InstanceValidator = InstanceValidator;
|
||
|
module.exports.default = InstanceValidator;
|