UNPKG

14.4 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const Utils = require('./utils');
5const sequelizeError = require('./errors');
6const Promise = require('./promise');
7const DataTypes = require('./data-types');
8const BelongsTo = require('./associations/belongs-to');
9const validator = require('./utils/validator-extras').validator;
10
11/**
12 * Instance Validator.
13 *
14 * @param {Instance} modelInstance The model instance.
15 * @param {Object} options A dictionary with options.
16 *
17 * @private
18 */
19class InstanceValidator {
20 constructor(modelInstance, options) {
21 options = _.clone(options) || {};
22
23 if (options.fields && !options.skip) {
24 options.skip = _.difference(Object.keys(modelInstance.constructor.rawAttributes), options.fields);
25 }
26
27 // assign defined and default options
28 this.options = _.defaults(options, {
29 skip: [],
30 hooks: true
31 });
32
33 this.modelInstance = modelInstance;
34
35 /**
36 * Exposes a reference to validator.js. This allows you to add custom validations using `validator.extend`
37 * @name validator
38 * @private
39 */
40 this.validator = validator;
41
42 /**
43 * All errors will be stored here from the validations.
44 *
45 * @type {Array} Will contain keys that correspond to attributes which will
46 * be Arrays of Errors.
47 * @private
48 */
49 this.errors = [];
50
51 /**
52 * @type {boolean} Indicates if validations are in progress
53 * @private
54 */
55 this.inProgress = false;
56 }
57
58 /**
59 * The main entry point for the Validation module, invoke to start the dance.
60 *
61 * @returns {Promise}
62 * @private
63 */
64 _validate() {
65 if (this.inProgress) throw new Error('Validations already in progress.');
66
67 this.inProgress = true;
68
69 return Promise.all([
70 this._perAttributeValidators().reflect(),
71 this._customValidators().reflect()
72 ]).then(() => {
73 if (this.errors.length) {
74 throw new sequelizeError.ValidationError(null, this.errors);
75 }
76 });
77 }
78
79 /**
80 * Invoke the Validation sequence and run validation hooks if defined
81 * - Before Validation Model Hooks
82 * - Validation
83 * - On validation success: After Validation Model Hooks
84 * - On validation failure: Validation Failed Model Hooks
85 *
86 * @returns {Promise}
87 * @private
88 */
89 validate() {
90 return this.options.hooks ? this._validateAndRunHooks() : this._validate();
91 }
92
93 /**
94 * Invoke the Validation sequence and run hooks
95 * - Before Validation Model Hooks
96 * - Validation
97 * - On validation success: After Validation Model Hooks
98 * - On validation failure: Validation Failed Model Hooks
99 *
100 * @returns {Promise}
101 * @private
102 */
103 _validateAndRunHooks() {
104 const runHooks = this.modelInstance.constructor.runHooks.bind(this.modelInstance.constructor);
105 return runHooks('beforeValidate', this.modelInstance, this.options)
106 .then(() =>
107 this._validate()
108 .catch(error => runHooks('validationFailed', this.modelInstance, this.options, error)
109 .then(newError => { throw newError || error; }))
110 )
111 .then(() => runHooks('afterValidate', this.modelInstance, this.options))
112 .return(this.modelInstance);
113 }
114
115 /**
116 * Will run all the validators defined per attribute (built-in validators and custom validators)
117 *
118 * @returns {Promise<Array.<Promise.PromiseInspection>>} A promise from .reflect().
119 * @private
120 */
121 _perAttributeValidators() {
122 // promisify all attribute invocations
123 const validators = [];
124
125 _.forIn(this.modelInstance.rawAttributes, (rawAttribute, field) => {
126 if (this.options.skip.includes(field)) {
127 return;
128 }
129
130 const value = this.modelInstance.dataValues[field];
131
132 if (value instanceof Utils.SequelizeMethod) {
133 return;
134 }
135
136 if (!rawAttribute._autoGenerated && !rawAttribute.autoIncrement) {
137 // perform validations based on schema
138 this._validateSchema(rawAttribute, field, value);
139 }
140
141 if (Object.prototype.hasOwnProperty.call(this.modelInstance.validators, field)) {
142 validators.push(this._singleAttrValidate(value, field, rawAttribute.allowNull).reflect());
143 }
144 });
145
146 return Promise.all(validators);
147 }
148
149 /**
150 * Will run all the custom validators defined in the model's options.
151 *
152 * @returns {Promise<Array.<Promise.PromiseInspection>>} A promise from .reflect().
153 * @private
154 */
155 _customValidators() {
156 const validators = [];
157 _.each(this.modelInstance._modelOptions.validate, (validator, validatorType) => {
158 if (this.options.skip.includes(validatorType)) {
159 return;
160 }
161
162 const valprom = this._invokeCustomValidator(validator, validatorType)
163 // errors are handled in settling, stub this
164 .catch(() => {})
165 .reflect();
166
167 validators.push(valprom);
168 });
169
170 return Promise.all(validators);
171 }
172
173 /**
174 * Validate a single attribute with all the defined built-in validators and custom validators.
175 *
176 * @private
177 *
178 * @param {*} value Anything.
179 * @param {string} field The field name.
180 * @param {boolean} allowNull Whether or not the schema allows null values
181 *
182 * @returns {Promise} A promise, will always resolve, auto populates error on this.error local object.
183 */
184 _singleAttrValidate(value, field, allowNull) {
185 // If value is null and allowNull is false, no validators should run (see #9143)
186 if ((value === null || value === undefined) && !allowNull) {
187 // The schema validator (_validateSchema) has already generated the validation error. Nothing to do here.
188 return Promise.resolve();
189 }
190
191 // Promisify each validator
192 const validators = [];
193 _.forIn(this.modelInstance.validators[field], (test, validatorType) => {
194
195 if (validatorType === 'isUrl' || validatorType === 'isURL' || validatorType === 'isEmail') {
196 // Preserve backwards compat. Validator.js now expects the second param to isURL and isEmail to be an object
197 if (typeof test === 'object' && test !== null && test.msg) {
198 test = {
199 msg: test.msg
200 };
201 } else if (test === true) {
202 test = {};
203 }
204 }
205
206 // Custom validators should always run, except if value is null and allowNull is false (see #9143)
207 if (typeof test === 'function') {
208 validators.push(this._invokeCustomValidator(test, validatorType, true, value, field).reflect());
209 return;
210 }
211
212 // If value is null, built-in validators should not run (only custom validators have to run) (see #9134).
213 if (value === null || value === undefined) {
214 return;
215 }
216
217 const validatorPromise = this._invokeBuiltinValidator(value, test, validatorType, field);
218 // errors are handled in settling, stub this
219 validatorPromise.catch(() => {});
220 validators.push(validatorPromise.reflect());
221 });
222
223 return Promise
224 .all(validators)
225 .then(results => this._handleReflectedResult(field, value, results));
226 }
227
228 /**
229 * Prepare and invoke a custom validator.
230 *
231 * @private
232 *
233 * @param {Function} validator The custom validator.
234 * @param {string} validatorType the custom validator type (name).
235 * @param {boolean} optAttrDefined Set to true if custom validator was defined from the attribute
236 * @param {*} optValue value for attribute
237 * @param {string} optField field for attribute
238 *
239 * @returns {Promise} A promise.
240 */
241 _invokeCustomValidator(validator, validatorType, optAttrDefined, optValue, optField) {
242 let validatorFunction = null; // the validation function to call
243 let isAsync = false;
244
245 const validatorArity = validator.length;
246 // check if validator is async and requires a callback
247 let asyncArity = 1;
248 let errorKey = validatorType;
249 let invokeArgs;
250 if (optAttrDefined) {
251 asyncArity = 2;
252 invokeArgs = optValue;
253 errorKey = optField;
254 }
255 if (validatorArity === asyncArity) {
256 isAsync = true;
257 }
258
259 if (isAsync) {
260 if (optAttrDefined) {
261 validatorFunction = Promise.promisify(validator.bind(this.modelInstance, invokeArgs));
262 } else {
263 validatorFunction = Promise.promisify(validator.bind(this.modelInstance));
264 }
265 return validatorFunction()
266 .catch(e => this._pushError(false, errorKey, e, optValue, validatorType));
267 }
268 return Promise
269 .try(() => validator.call(this.modelInstance, invokeArgs))
270 .catch(e => this._pushError(false, errorKey, e, optValue, validatorType));
271 }
272
273 /**
274 * Prepare and invoke a build-in validator.
275 *
276 * @private
277 *
278 * @param {*} value Anything.
279 * @param {*} test The test case.
280 * @param {string} validatorType One of known to Sequelize validators.
281 * @param {string} field The field that is being validated
282 *
283 * @returns {Object} An object with specific keys to invoke the validator.
284 */
285 _invokeBuiltinValidator(value, test, validatorType, field) {
286 return Promise.try(() => {
287 // Cast value as string to pass new Validator.js string requirement
288 const valueString = String(value);
289 // check if Validator knows that kind of validation test
290 if (typeof validator[validatorType] !== 'function') {
291 throw new Error(`Invalid validator function: ${validatorType}`);
292 }
293
294 const validatorArgs = this._extractValidatorArgs(test, validatorType, field);
295
296 if (!validator[validatorType](valueString, ...validatorArgs)) {
297 throw Object.assign(new Error(test.msg || `Validation ${validatorType} on ${field} failed`), { validatorName: validatorType, validatorArgs });
298 }
299 });
300 }
301
302 /**
303 * Will extract arguments for the validator.
304 *
305 * @param {*} test The test case.
306 * @param {string} validatorType One of known to Sequelize validators.
307 * @param {string} field The field that is being validated.
308 *
309 * @private
310 */
311 _extractValidatorArgs(test, validatorType, field) {
312 let validatorArgs = test.args || test;
313 const isLocalizedValidator = typeof validatorArgs !== 'string' && (validatorType === 'isAlpha' || validatorType === 'isAlphanumeric' || validatorType === 'isMobilePhone');
314
315 if (!Array.isArray(validatorArgs)) {
316 if (validatorType === 'isImmutable') {
317 validatorArgs = [validatorArgs, field, this.modelInstance];
318 } else if (isLocalizedValidator || validatorType === 'isIP') {
319 validatorArgs = [];
320 } else {
321 validatorArgs = [validatorArgs];
322 }
323 } else {
324 validatorArgs = validatorArgs.slice(0);
325 }
326 return validatorArgs;
327 }
328
329 /**
330 * Will validate a single field against its schema definition (isnull).
331 *
332 * @param {Object} rawAttribute As defined in the Schema.
333 * @param {string} field The field name.
334 * @param {*} value anything.
335 *
336 * @private
337 */
338 _validateSchema(rawAttribute, field, value) {
339 if (rawAttribute.allowNull === false && (value === null || value === undefined)) {
340 const association = _.values(this.modelInstance.constructor.associations).find(association => association instanceof BelongsTo && association.foreignKey === rawAttribute.fieldName);
341 if (!association || !this.modelInstance.get(association.associationAccessor)) {
342 const validators = this.modelInstance.validators[field];
343 const errMsg = _.get(validators, 'notNull.msg', `${this.modelInstance.constructor.name}.${field} cannot be null`);
344
345 this.errors.push(new sequelizeError.ValidationErrorItem(
346 errMsg,
347 'notNull Violation', // sequelizeError.ValidationErrorItem.Origins.CORE,
348 field,
349 value,
350 this.modelInstance,
351 'is_null'
352 ));
353 }
354 }
355
356 if (rawAttribute.type instanceof DataTypes.STRING || rawAttribute.type instanceof DataTypes.TEXT || rawAttribute.type instanceof DataTypes.CITEXT) {
357 if (Array.isArray(value) || _.isObject(value) && !(value instanceof Utils.SequelizeMethod) && !Buffer.isBuffer(value)) {
358 this.errors.push(new sequelizeError.ValidationErrorItem(
359 `${field} cannot be an array or an object`,
360 'string violation', // sequelizeError.ValidationErrorItem.Origins.CORE,
361 field,
362 value,
363 this.modelInstance,
364 'not_a_string'
365 ));
366 }
367 }
368 }
369
370
371 /**
372 * Handles the returned result of a Promise.reflect.
373 *
374 * If errors are found it populates this.error.
375 *
376 * @param {string} field The attribute name.
377 * @param {string|number} value The data value.
378 * @param {Array<Promise.PromiseInspection>} promiseInspections objects.
379 *
380 * @private
381 */
382 _handleReflectedResult(field, value, promiseInspections) {
383 for (const promiseInspection of promiseInspections) {
384 if (promiseInspection.isRejected()) {
385 const rejection = promiseInspection.error();
386 const isBuiltIn = !!rejection.validatorName;
387
388 this._pushError(isBuiltIn, field, rejection, value, rejection.validatorName, rejection.validatorArgs);
389 }
390 }
391 }
392
393 /**
394 * Signs all errors retaining the original.
395 *
396 * @param {boolean} isBuiltin - Determines if error is from builtin validator.
397 * @param {string} errorKey - name of invalid attribute.
398 * @param {Error|string} rawError - The original error.
399 * @param {string|number} value - The data that triggered the error.
400 * @param {string} fnName - Name of the validator, if any
401 * @param {Array} fnArgs - Arguments for the validator [function], if any
402 *
403 * @private
404 */
405 _pushError(isBuiltin, errorKey, rawError, value, fnName, fnArgs) {
406 const message = rawError.message || rawError || 'Validation error';
407 const error = new sequelizeError.ValidationErrorItem(
408 message,
409 'Validation error', // sequelizeError.ValidationErrorItem.Origins.FUNCTION,
410 errorKey,
411 value,
412 this.modelInstance,
413 fnName,
414 isBuiltin ? fnName : undefined,
415 isBuiltin ? fnArgs : undefined
416 );
417
418 error[InstanceValidator.RAW_KEY_NAME] = rawError;
419
420 this.errors.push(error);
421 }
422}
423/**
424 * @define {string} The error key for arguments as passed by custom validators
425 * @private
426 */
427InstanceValidator.RAW_KEY_NAME = 'original';
428
429module.exports = InstanceValidator;
430module.exports.InstanceValidator = InstanceValidator;
431module.exports.default = InstanceValidator;