123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- '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 (validatorType === 'isUrl' || validatorType === 'isURL' || validatorType === 'isEmail') {
- // 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' && (validatorType === 'isAlpha' || validatorType === 'isAlphanumeric' || validatorType === 'isMobilePhone');
- 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;
|