UNPKG

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