1 | 'use strict';
|
2 |
|
3 | const _ = require('lodash');
|
4 | const Utils = require('./utils');
|
5 | const sequelizeError = require('./errors');
|
6 | const Promise = require('./promise');
|
7 | const DataTypes = require('./data-types');
|
8 | const BelongsTo = require('./associations/belongs-to');
|
9 | const validator = require('./utils/validator-extras').validator;
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | class 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 |
|
28 | this.options = _.defaults(options, {
|
29 | skip: [],
|
30 | hooks: true
|
31 | });
|
32 |
|
33 | this.modelInstance = modelInstance;
|
34 |
|
35 | |
36 |
|
37 |
|
38 |
|
39 |
|
40 | this.validator = validator;
|
41 |
|
42 | |
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 | this.errors = [];
|
50 |
|
51 | |
52 |
|
53 |
|
54 |
|
55 | this.inProgress = false;
|
56 | }
|
57 |
|
58 | |
59 |
|
60 |
|
61 |
|
62 |
|
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 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 | validate() {
|
90 | return this.options.hooks ? this._validateAndRunHooks() : this._validate();
|
91 | }
|
92 |
|
93 | |
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
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 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 | _perAttributeValidators() {
|
122 |
|
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 |
|
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 |
|
151 |
|
152 |
|
153 |
|
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 |
|
164 | .catch(() => {})
|
165 | .reflect();
|
166 |
|
167 | validators.push(valprom);
|
168 | });
|
169 |
|
170 | return Promise.all(validators);
|
171 | }
|
172 |
|
173 | |
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 | _singleAttrValidate(value, field, allowNull) {
|
185 |
|
186 | if ((value === null || value === undefined) && !allowNull) {
|
187 |
|
188 | return Promise.resolve();
|
189 | }
|
190 |
|
191 |
|
192 | const validators = [];
|
193 | _.forIn(this.modelInstance.validators[field], (test, validatorType) => {
|
194 |
|
195 | if (validatorType === 'isUrl' || validatorType === 'isURL' || validatorType === 'isEmail') {
|
196 |
|
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 |
|
207 | if (typeof test === 'function') {
|
208 | validators.push(this._invokeCustomValidator(test, validatorType, true, value, field).reflect());
|
209 | return;
|
210 | }
|
211 |
|
212 |
|
213 | if (value === null || value === undefined) {
|
214 | return;
|
215 | }
|
216 |
|
217 | const validatorPromise = this._invokeBuiltinValidator(value, test, validatorType, field);
|
218 |
|
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 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 | _invokeCustomValidator(validator, validatorType, optAttrDefined, optValue, optField) {
|
242 | let validatorFunction = null;
|
243 | let isAsync = false;
|
244 |
|
245 | const validatorArity = validator.length;
|
246 |
|
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 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 | _invokeBuiltinValidator(value, test, validatorType, field) {
|
286 | return Promise.try(() => {
|
287 |
|
288 | const valueString = String(value);
|
289 |
|
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 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
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 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
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',
|
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',
|
361 | field,
|
362 | value,
|
363 | this.modelInstance,
|
364 | 'not_a_string'
|
365 | ));
|
366 | }
|
367 | }
|
368 | }
|
369 |
|
370 |
|
371 | |
372 |
|
373 |
|
374 |
|
375 |
|
376 |
|
377 |
|
378 |
|
379 |
|
380 |
|
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 |
|
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 |
|
403 |
|
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',
|
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 |
|
425 |
|
426 |
|
427 | InstanceValidator.RAW_KEY_NAME = 'original';
|
428 |
|
429 | module.exports = InstanceValidator;
|
430 | module.exports.InstanceValidator = InstanceValidator;
|
431 | module.exports.default = InstanceValidator;
|