'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} * @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} * @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;