1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
2 | // Node module: loopback-datasource-juggler
|
3 | // This file is licensed under the MIT License.
|
4 | // License text available at https://opensource.org/licenses/MIT
|
5 |
|
6 | ;
|
7 |
|
8 | const g = require('strong-globalize')();
|
9 | const util = require('util');
|
10 | const extend = util._extend;
|
11 |
|
12 | /*!
|
13 | * Module exports
|
14 | */
|
15 | exports.ValidationError = ValidationError;
|
16 | exports.Validatable = Validatable;
|
17 |
|
18 | /**
|
19 | * This class provides methods that add validation cababilities to models.
|
20 | * Each of the validations runs when the `obj.isValid()` method is called.
|
21 | *
|
22 | * All of the methods have an options object parameter that has a
|
23 | * `message` property. When there is only a single error message, this property is just a string;
|
24 | * for example: `Post.validatesPresenceOf('title', { message: 'can not be blank' });`
|
25 | *
|
26 | * In more complicated cases it can be a set of messages, for each possible error condition; for example:
|
27 | * `User.validatesLengthOf('password', { min: 6, max: 20, message: {min: 'too short', max: 'too long'}});`
|
28 | * @class Validatable
|
29 | */
|
30 | function Validatable() {
|
31 | }
|
32 |
|
33 | /**
|
34 | * Validate presence of one or more specified properties.
|
35 | *
|
36 | * Requires a model to include a property to be considered valid; fails when validated field is blank.
|
37 | *
|
38 | * For example, validate presence of title
|
39 | * ```
|
40 | * Post.validatesPresenceOf('title');
|
41 | * ```
|
42 | * Validate that model has first, last, and age properties:
|
43 | * ```
|
44 | * User.validatesPresenceOf('first', 'last', 'age');
|
45 | * ```
|
46 | * Example with custom message
|
47 | * ```
|
48 | * Post.validatesPresenceOf('title', {message: 'Cannot be blank'});
|
49 | * ```
|
50 | *
|
51 | * @param {String} propertyName One or more property names.
|
52 | * @options {Object} options Configuration parameters; see below.
|
53 | * @property {String} message Error message to use instead of default.
|
54 | * @property {String} if Validate only if `if` exists.
|
55 | * @property {String} unless Validate only if `unless` exists.
|
56 | */
|
57 | Validatable.validatesPresenceOf = getConfigurator('presence');
|
58 |
|
59 | /**
|
60 | * Validate absence of one or more specified properties.
|
61 | *
|
62 | * A model should not include a property to be considered valid; fails when validated field is not blank.
|
63 | *
|
64 | * For example, validate absence of reserved
|
65 | * ```
|
66 | * Post.validatesAbsenceOf('reserved', { unless: 'special' });
|
67 | * ```
|
68 | *
|
69 | * @param {String} propertyName One or more property names.
|
70 | * @options {Object} options Configuration parameters; see below.
|
71 | * @property {String} message Error message to use instead of default.
|
72 | * @property {String} if Validate only if `if` exists.
|
73 | * @property {String} unless Validate only if `unless` exists.
|
74 | */
|
75 | Validatable.validatesAbsenceOf = getConfigurator('absence');
|
76 |
|
77 | /**
|
78 | * Validate length.
|
79 | *
|
80 | * Require a property length to be within a specified range.
|
81 | *
|
82 | * There are three kinds of validations: min, max, is.
|
83 | *
|
84 | * Default error messages:
|
85 | *
|
86 | * - min: too short
|
87 | * - max: too long
|
88 | * - is: length is wrong
|
89 | *
|
90 | * Example: length validations
|
91 | * ```
|
92 | * User.validatesLengthOf('password', {min: 7});
|
93 | * User.validatesLengthOf('email', {max: 100});
|
94 | * User.validatesLengthOf('state', {is: 2});
|
95 | * User.validatesLengthOf('nick', {min: 3, max: 15});
|
96 | * ```
|
97 | * Example: length validations with custom error messages
|
98 | * ```
|
99 | * User.validatesLengthOf('password', {min: 7, message: {min: 'too weak'}});
|
100 | * User.validatesLengthOf('state', {is: 2, message: {is: 'is not valid state name'}});
|
101 | * ```
|
102 | *
|
103 | * @param {String} propertyName Property name to validate.
|
104 | * @options {Object} options Configuration parameters; see below.
|
105 | * @property {Number} is Value that property must equal to validate.
|
106 | * @property {Number} min Value that property must be less than to be valid.
|
107 | * @property {Number} max Value that property must be less than to be valid.
|
108 | * @property {Object} message Optional object with string properties for custom error message for each validation: is, min, or max.
|
109 | */
|
110 | Validatable.validatesLengthOf = getConfigurator('length');
|
111 |
|
112 | /**
|
113 | * Validate numericality.
|
114 | *
|
115 | * Requires a value for property to be either an integer or number.
|
116 | *
|
117 | * Example
|
118 | * ```
|
119 | * User.validatesNumericalityOf('age', { message: { number: 'is not a number' }});
|
120 | * User.validatesNumericalityOf('age', {int: true, message: { int: 'is not an integer' }});
|
121 | * ```
|
122 | *
|
123 | * @param {String} propertyName Property name to validate.
|
124 | * @options {Object} options Configuration parameters; see below.
|
125 | * @property {Boolean} int If true, then property must be an integer to be valid.
|
126 | * @property {Boolean} allowBlank Allow property to be blank.
|
127 | * @property {Boolean} allowNull Allow property to be null.
|
128 | * @property {Object} message Optional object with string properties for 'int' for integer validation. Default error messages:
|
129 | * - number: is not a number
|
130 | * - int: is not an integer
|
131 | */
|
132 | Validatable.validatesNumericalityOf = getConfigurator('numericality');
|
133 |
|
134 | /**
|
135 | * Validate inclusion in set.
|
136 | *
|
137 | * Require a value for property to be in the specified array.
|
138 | *
|
139 | * Example:
|
140 | * ```
|
141 | * User.validatesInclusionOf('gender', {in: ['male', 'female']});
|
142 | * User.validatesInclusionOf('role', {
|
143 | * in: ['admin', 'moderator', 'user'], message: 'is not allowed'
|
144 | * });
|
145 | * ```
|
146 | *
|
147 | * @param {String} propertyName Property name to validate.
|
148 | * @options {Object} options Configuration parameters; see below.
|
149 | * @property {Array} in Property must match one of the values in the array to be valid.
|
150 | * @property {String} message Optional error message if property is not valid.
|
151 | * Default error message: "is not included in the list".
|
152 | * @property {Boolean} allowNull Whether null values are allowed.
|
153 | */
|
154 | Validatable.validatesInclusionOf = getConfigurator('inclusion');
|
155 |
|
156 | /**
|
157 | * Validate exclusion in a set.
|
158 | *
|
159 | * Require a property value not be in the specified array.
|
160 | *
|
161 | * Example: `Company.validatesExclusionOf('domain', {in: ['www', 'admin']});`
|
162 | *
|
163 | * @param {String} propertyName Property name to validate.
|
164 | * @options {Object} options Configuration parameters; see below.
|
165 | * @property {Array} in Property must not match any of the values in the array to be valid.
|
166 | * @property {String} message Optional error message if property is not valid. Default error message: "is reserved".
|
167 | * @property {Boolean} allowNull Whether null values are allowed.
|
168 | */
|
169 | Validatable.validatesExclusionOf = getConfigurator('exclusion');
|
170 |
|
171 | /**
|
172 | * Validate format.
|
173 | *
|
174 | * Require a model to include a property that matches the given format.
|
175 | *
|
176 | * Example: `User.validatesFormatOf('name', {with: /\w+/});`
|
177 | *
|
178 | * @param {String} propertyName Property name to validate.
|
179 | * @options {Object} options Configuration parameters; see below.
|
180 | * @property {RegExp} with Regular expression to validate format.
|
181 | * @property {String} message Optional error message if property is not valid. Default error message: " is invalid".
|
182 | * @property {Boolean} allowNull Whether null values are allowed.
|
183 | */
|
184 | Validatable.validatesFormatOf = getConfigurator('format');
|
185 |
|
186 | /**
|
187 | * Validate using custom validation function.
|
188 | *
|
189 | * Example:
|
190 | *```javascript
|
191 | * User.validate('name', customValidator, {message: 'Bad name'});
|
192 | * function customValidator(err) {
|
193 | * if (this.name === 'bad') err();
|
194 | * });
|
195 | * var user = new User({name: 'Peter'});
|
196 | * user.isValid(); // true
|
197 | * user.name = 'bad';
|
198 | * user.isValid(); // false
|
199 | * ```
|
200 | *
|
201 | * @param {String} propertyName Property name to validate.
|
202 | * @param {Function} validatorFn Custom validation function.
|
203 | * @options {Object} options Configuration parameters; see below.
|
204 | * @property {String} message Optional error message if property is not valid. Default error message: " is invalid".
|
205 | * @property {Boolean} allowNull Whether null values are allowed.
|
206 | */
|
207 | Validatable.validate = getConfigurator('custom');
|
208 |
|
209 | /**
|
210 | * Validate using custom asynchronous validation function.
|
211 | *
|
212 | * Example:
|
213 | *```js
|
214 | * User.validateAsync('name', customValidator, {message: 'Bad name'});
|
215 | * function customValidator(err, done) {
|
216 | * process.nextTick(function () {
|
217 | * if (this.name === 'bad') err();
|
218 | * done();
|
219 | * });
|
220 | * });
|
221 | * var user = new User({name: 'Peter'});
|
222 | * user.isValid(); // false (because async validation setup)
|
223 | * user.isValid(function (isValid) {
|
224 | * isValid; // true
|
225 | * })
|
226 | * user.name = 'bad';
|
227 | * user.isValid(); // false
|
228 | * user.isValid(function (isValid) {
|
229 | * isValid; // false
|
230 | * })
|
231 | * ```
|
232 | *
|
233 | * @param {String} propertyName Property name to validate.
|
234 | * @param {Function} validatorFn Custom validation function.
|
235 | * @options {Object} options Configuration parameters; see below.
|
236 | * @property {String} message Optional error message if property is not valid. Default error message: " is invalid".
|
237 | * @property {Boolean} allowNull Whether null values are allowed.
|
238 | */
|
239 | Validatable.validateAsync = getConfigurator('custom', {async: true});
|
240 |
|
241 | /**
|
242 | * Validate uniqueness of the value for a property in the collection of models.
|
243 | *
|
244 | * Not available for all connectors. Currently supported with these connectors:
|
245 | * - In Memory
|
246 | * - Oracle
|
247 | * - MongoDB
|
248 | *
|
249 | * ```
|
250 | * // The login must be unique across all User instances.
|
251 | * User.validatesUniquenessOf('login');
|
252 | *
|
253 | * // Assuming SiteUser.belongsTo(Site)
|
254 | * // The login must be unique within each Site.
|
255 | * SiteUser.validateUniquenessOf('login', { scopedTo: ['siteId'] });
|
256 | * ```
|
257 | *
|
258 | * @param {String} propertyName Property name to validate.
|
259 | * @options {Object} options Configuration parameters; see below.
|
260 | * @property {RegExp} with Regular expression to validate format.
|
261 | * @property {Array.<String>} scopedTo List of properties defining the scope.
|
262 | * @property {String} message Optional error message if property is not valid. Default error message: "is not unique".
|
263 | * @property {Boolean} allowNull Whether null values are allowed.
|
264 | * @property {String} ignoreCase Make the validation case insensitive.
|
265 | * @property {String} if Validate only if `if` exists.
|
266 | * @property {String} unless Validate only if `unless` exists.
|
267 | */
|
268 | Validatable.validatesUniquenessOf = getConfigurator('uniqueness', {async: true});
|
269 |
|
270 | /**
|
271 | * Validate if a value for a property is a Date.
|
272 | *
|
273 | * Example
|
274 | * ```
|
275 | * User.validatesDateOf('today', {message: 'today is not a date!'});
|
276 | * ```
|
277 | *
|
278 | * @param {String} propertyName Property name to validate.
|
279 | * @options {Object} options Configuration parameters; see below.
|
280 | * @property {String} message Error message to use instead of default.
|
281 | */
|
282 | Validatable.validatesDateOf = getConfigurator('date');
|
283 |
|
284 | // implementation of validators
|
285 |
|
286 | /*!
|
287 | * Presence validator
|
288 | */
|
289 | function validatePresence(attr, conf, err, options) {
|
290 | if (blank(this[attr])) {
|
291 | err();
|
292 | }
|
293 | }
|
294 |
|
295 | /*!
|
296 | * Absence validator
|
297 | */
|
298 | function validateAbsence(attr, conf, err, options) {
|
299 | if (!blank(this[attr])) {
|
300 | err();
|
301 | }
|
302 | }
|
303 |
|
304 | /*!
|
305 | * Length validator
|
306 | */
|
307 | function validateLength(attr, conf, err, options) {
|
308 | if (nullCheck.call(this, attr, conf, err)) return;
|
309 |
|
310 | const len = this[attr].length;
|
311 | if (conf.min && len < conf.min) {
|
312 | err('min');
|
313 | }
|
314 | if (conf.max && len > conf.max) {
|
315 | err('max');
|
316 | }
|
317 | if (conf.is && len !== conf.is) {
|
318 | err('is');
|
319 | }
|
320 | }
|
321 |
|
322 | /*!
|
323 | * Numericality validator
|
324 | */
|
325 | function validateNumericality(attr, conf, err, options) {
|
326 | if (nullCheck.call(this, attr, conf, err)) return;
|
327 |
|
328 | if (typeof this[attr] !== 'number' || isNaN(this[attr])) {
|
329 | return err('number');
|
330 | }
|
331 | if (conf.int && this[attr] !== Math.round(this[attr])) {
|
332 | return err('int');
|
333 | }
|
334 | }
|
335 |
|
336 | /*!
|
337 | * Inclusion validator
|
338 | */
|
339 | function validateInclusion(attr, conf, err, options) {
|
340 | if (nullCheck.call(this, attr, conf, err)) return;
|
341 |
|
342 | if (!~conf.in.indexOf(this[attr])) {
|
343 | err();
|
344 | }
|
345 | }
|
346 |
|
347 | /*!
|
348 | * Exclusion validator
|
349 | */
|
350 | function validateExclusion(attr, conf, err, options) {
|
351 | if (nullCheck.call(this, attr, conf, err)) return;
|
352 |
|
353 | if (~conf.in.indexOf(this[attr])) {
|
354 | err();
|
355 | }
|
356 | }
|
357 |
|
358 | /*!
|
359 | * Format validator
|
360 | */
|
361 | function validateFormat(attr, conf, err, options) {
|
362 | if (nullCheck.call(this, attr, conf, err)) return;
|
363 |
|
364 | if (typeof this[attr] === 'string' || typeof this[attr] === 'number') {
|
365 | const regex = new RegExp(conf['with']);
|
366 | if (!regex.test(this[attr])) {
|
367 | err();
|
368 | }
|
369 | } else {
|
370 | err();
|
371 | }
|
372 | }
|
373 |
|
374 | /*!
|
375 | * Custom validator
|
376 | */
|
377 | function validateCustom(attr, conf, err, options, done) {
|
378 | if (typeof options === 'function') {
|
379 | done = options;
|
380 | options = {};
|
381 | }
|
382 | if (!done) {
|
383 | // called from a sync validator, stick options on end
|
384 | conf.customValidator.call(this, err, options);
|
385 | } else {
|
386 | if (conf.customValidator.length === 3) {
|
387 | // if they declared the validator with 3 args, they are expecting options
|
388 | conf.customValidator.call(this, err, options, done);
|
389 | } else {
|
390 | // otherwise just pass the expected two (no context)
|
391 | conf.customValidator.call(this, err, done);
|
392 | }
|
393 | }
|
394 | }
|
395 |
|
396 | function escapeStringRegexp(str) {
|
397 | if (typeof str !== 'string') {
|
398 | throw new TypeError('Expected a string');
|
399 | }
|
400 | const matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g;
|
401 | return str.replace(matchOperatorsRe, '\\$&');
|
402 | }
|
403 |
|
404 | /*!
|
405 | * Uniqueness validator
|
406 | */
|
407 | function validateUniqueness(attr, conf, err, options, done) {
|
408 | if (typeof options === 'function') {
|
409 | done = options;
|
410 | options = {};
|
411 | }
|
412 | if (blank(this[attr])) {
|
413 | return process.nextTick(done);
|
414 | }
|
415 | const cond = {where: {}};
|
416 |
|
417 | if (conf && conf.ignoreCase) {
|
418 | cond.where[attr] = new RegExp('^' + escapeStringRegexp(this[attr]) + '$', 'i');
|
419 | } else {
|
420 | cond.where[attr] = this[attr];
|
421 | }
|
422 |
|
423 | if (conf && conf.scopedTo) {
|
424 | conf.scopedTo.forEach(function(k) {
|
425 | const val = this[k];
|
426 | if (val !== undefined)
|
427 | cond.where[k] = this[k];
|
428 | }, this);
|
429 | }
|
430 |
|
431 | const idName = this.constructor.definition.idName();
|
432 | const isNewRecord = this.isNewRecord();
|
433 | this.constructor.find(cond, options, function(error, found) {
|
434 | if (error) {
|
435 | err(error);
|
436 | } else if (found.length > 1) {
|
437 | err();
|
438 | } else if (found.length === 1 && idName === attr && isNewRecord) {
|
439 | err();
|
440 | } else if (found.length === 1 && (
|
441 | !this.id || !found[0].id || found[0].id.toString() != this.id.toString()
|
442 | )) {
|
443 | err();
|
444 | }
|
445 | done();
|
446 | }.bind(this));
|
447 | }
|
448 |
|
449 | /*!
|
450 | * Date validator
|
451 | */
|
452 | function validateDate(attr, conf, err) {
|
453 | if (this[attr] === null || this[attr] === undefined) return;
|
454 |
|
455 | const date = new Date(this[attr]);
|
456 | if (isNaN(date.getTime())) return err();
|
457 | }
|
458 |
|
459 | const validators = {
|
460 | presence: validatePresence,
|
461 | absence: validateAbsence,
|
462 | length: validateLength,
|
463 | numericality: validateNumericality,
|
464 | inclusion: validateInclusion,
|
465 | exclusion: validateExclusion,
|
466 | format: validateFormat,
|
467 | custom: validateCustom,
|
468 | uniqueness: validateUniqueness,
|
469 | date: validateDate,
|
470 | };
|
471 |
|
472 | function getConfigurator(name, opts) {
|
473 | return function() {
|
474 | const args = Array.prototype.slice.call(arguments);
|
475 | args[1] = args[1] || {};
|
476 | configure(this, name, args, opts);
|
477 | };
|
478 | }
|
479 |
|
480 | /**
|
481 | * This method performs validation and triggers validation hooks.
|
482 | * Before validation the `obj.errors` collection is cleaned.
|
483 | * Each validation can add errors to `obj.errors` collection.
|
484 | * If collection is not blank, validation failed.
|
485 | *
|
486 | * NOTE: This method can be called as synchronous only when no asynchronous validation is
|
487 | * configured. It's strongly recommended to run all validations as asyncronous.
|
488 | *
|
489 | * Example: ExpressJS controller - render user if valid, show flash otherwise
|
490 | * ```javascript
|
491 | * user.isValid(function (valid) {
|
492 | * if (valid) res.render({user: user});
|
493 | * else res.flash('error', 'User is not valid'), console.log(user.errors), res.redirect('/users');
|
494 | * });
|
495 | * ```
|
496 | * Another example:
|
497 | * ```javascript
|
498 | * user.isValid(function (valid) {
|
499 | * if (!valid) {
|
500 | * console.log(user.errors);
|
501 | * // => hash of errors
|
502 | * // => {
|
503 | * // => username: [errmessage, errmessage, ...],
|
504 | * // => email: ...
|
505 | * // => }
|
506 | * }
|
507 | * });
|
508 | * ```
|
509 | * @callback {Function} callback Called with (valid).
|
510 | * @param {Object} data Data to be validated.
|
511 | * @param {Object} options Options to be specified upon validation.
|
512 | * @returns {Boolean} True if no asynchronous validation is configured and all properties pass validation.
|
513 | */
|
514 | Validatable.prototype.isValid = function(callback, data, options) {
|
515 | options = options || {};
|
516 | let valid = true, wait = 0, async = false;
|
517 | const inst = this;
|
518 | const validations = this.constructor.validations;
|
519 |
|
520 | const reportDiscardedProperties = this.__strict &&
|
521 | this.__unknownProperties && this.__unknownProperties.length;
|
522 |
|
523 | // exit with success when no errors
|
524 | if (typeof validations !== 'object' && !reportDiscardedProperties) {
|
525 | cleanErrors(this);
|
526 | if (callback) {
|
527 | this.trigger('validate', function(validationsDone) {
|
528 | validationsDone.call(inst, function() {
|
529 | callback(valid);
|
530 | });
|
531 | }, data, callback);
|
532 | }
|
533 | return valid;
|
534 | }
|
535 |
|
536 | Object.defineProperty(this, 'errors', {
|
537 | enumerable: false,
|
538 | configurable: true,
|
539 | value: new Errors,
|
540 | });
|
541 |
|
542 | this.trigger('validate', function(validationsDone) {
|
543 | const inst = this;
|
544 | let asyncFail = false;
|
545 |
|
546 | const attrs = Object.keys(validations || {});
|
547 |
|
548 | attrs.forEach(function(attr) {
|
549 | const attrValidations = validations[attr] || [];
|
550 | attrValidations.forEach(function(v) {
|
551 | if (v.options && v.options.async) {
|
552 | async = true;
|
553 | wait += 1;
|
554 | process.nextTick(function() {
|
555 | validationFailed(inst, attr, v, options, done);
|
556 | });
|
557 | } else {
|
558 | if (validationFailed(inst, attr, v, options)) {
|
559 | valid = false;
|
560 | }
|
561 | }
|
562 | });
|
563 | });
|
564 |
|
565 | if (reportDiscardedProperties) {
|
566 | for (const ix in inst.__unknownProperties) {
|
567 | const key = inst.__unknownProperties[ix];
|
568 | const code = 'unknown-property';
|
569 | const msg = defaultMessages[code];
|
570 | inst.errors.add(key, msg, code);
|
571 | valid = false;
|
572 | }
|
573 | }
|
574 |
|
575 | if (!async) {
|
576 | validationsDone.call(inst, function() {
|
577 | if (valid) cleanErrors(inst);
|
578 | if (callback) {
|
579 | callback(valid);
|
580 | }
|
581 | });
|
582 | }
|
583 |
|
584 | function done(fail) {
|
585 | asyncFail = asyncFail || fail;
|
586 | if (--wait === 0) {
|
587 | validationsDone.call(inst, function() {
|
588 | if (valid && !asyncFail) cleanErrors(inst);
|
589 | if (callback) {
|
590 | callback(valid && !asyncFail);
|
591 | }
|
592 | });
|
593 | }
|
594 | }
|
595 | }, data, callback);
|
596 |
|
597 | if (async) {
|
598 | // in case of async validation we should return undefined here,
|
599 | // because not all validations are finished yet
|
600 | return;
|
601 | } else {
|
602 | return valid;
|
603 | }
|
604 | };
|
605 |
|
606 | function cleanErrors(inst) {
|
607 | Object.defineProperty(inst, 'errors', {
|
608 | enumerable: false,
|
609 | configurable: true,
|
610 | value: false,
|
611 | });
|
612 | }
|
613 |
|
614 | function validationFailed(inst, attr, conf, options, cb) {
|
615 | const opts = conf.options || {};
|
616 |
|
617 | if (typeof options === 'function') {
|
618 | cb = options;
|
619 | options = {};
|
620 | }
|
621 |
|
622 | if (typeof attr !== 'string') return false;
|
623 |
|
624 | // here we should check skip validation conditions (if, unless)
|
625 | // that can be specified in conf
|
626 | if (skipValidation(inst, conf, 'if') ||
|
627 | skipValidation(inst, conf, 'unless')) {
|
628 | if (cb) cb(false);
|
629 | return false;
|
630 | }
|
631 |
|
632 | let fail = false;
|
633 | const validator = validators[conf.validation];
|
634 | const validatorArguments = [];
|
635 | validatorArguments.push(attr);
|
636 | validatorArguments.push(conf);
|
637 | validatorArguments.push(function onerror(kind) {
|
638 | let message, code = conf.code || conf.validation;
|
639 | if (conf.message) {
|
640 | message = conf.message;
|
641 | }
|
642 | if (!message && defaultMessages[conf.validation]) {
|
643 | message = defaultMessages[conf.validation];
|
644 | }
|
645 | if (!message) {
|
646 | message = 'is invalid';
|
647 | }
|
648 | if (kind) {
|
649 | code += '.' + kind;
|
650 | if (message[kind]) {
|
651 | // get deeper
|
652 | message = message[kind];
|
653 | } else if (defaultMessages.common[kind]) {
|
654 | message = defaultMessages.common[kind];
|
655 | } else {
|
656 | message = 'is invalid';
|
657 | }
|
658 | }
|
659 | if (kind !== false) inst.errors.add(attr, message, code);
|
660 | fail = true;
|
661 | });
|
662 | validatorArguments.push(options);
|
663 | if (cb) {
|
664 | validatorArguments.push(function() {
|
665 | cb(fail);
|
666 | });
|
667 | }
|
668 | validator.apply(inst, validatorArguments);
|
669 | return fail;
|
670 | }
|
671 |
|
672 | function skipValidation(inst, conf, kind) {
|
673 | let doValidate = true;
|
674 | if (typeof conf[kind] === 'function') {
|
675 | doValidate = conf[kind].call(inst);
|
676 | if (kind === 'unless') doValidate = !doValidate;
|
677 | } else if (typeof conf[kind] === 'string') {
|
678 | if (typeof inst[conf[kind]] === 'function') {
|
679 | doValidate = inst[conf[kind]].call(inst);
|
680 | if (kind === 'unless') doValidate = !doValidate;
|
681 | } else if (inst.__data.hasOwnProperty(conf[kind])) {
|
682 | doValidate = inst[conf[kind]];
|
683 | if (kind === 'unless') doValidate = !doValidate;
|
684 | } else {
|
685 | doValidate = kind === 'if';
|
686 | }
|
687 | }
|
688 | return !doValidate;
|
689 | }
|
690 |
|
691 | const defaultMessages = {
|
692 | presence: 'can\'t be blank',
|
693 | absence: 'can\'t be set',
|
694 | 'unknown-property': 'is not defined in the model',
|
695 | length: {
|
696 | min: 'too short',
|
697 | max: 'too long',
|
698 | is: 'length is wrong',
|
699 | },
|
700 | common: {
|
701 | blank: 'is blank',
|
702 | 'null': 'is null',
|
703 | },
|
704 | numericality: {
|
705 | 'int': 'is not an integer',
|
706 | 'number': 'is not a number',
|
707 | },
|
708 | inclusion: 'is not included in the list',
|
709 | exclusion: 'is reserved',
|
710 | uniqueness: 'is not unique',
|
711 | date: 'is not a valid date',
|
712 | };
|
713 |
|
714 | /**
|
715 | * Checks if attribute is undefined or null. Calls err function with 'blank' or 'null'.
|
716 | * See defaultMessages. You can affect this behaviour with conf.allowBlank and conf.allowNull.
|
717 | * @private
|
718 | * @param {String} attr Property name of attribute
|
719 | * @param {Object} conf conf object for validator
|
720 | * @param {Function} err
|
721 | * @return {Boolean} returns true if attribute is null or blank
|
722 | */
|
723 | function nullCheck(attr, conf, err) {
|
724 | // First determine if attribute is defined
|
725 | if (typeof this[attr] === 'undefined' || this[attr] === '') {
|
726 | if (!conf.allowBlank) {
|
727 | err('blank');
|
728 | }
|
729 | return true;
|
730 | } else {
|
731 | // Now check if attribute is null
|
732 | if (this[attr] === null) {
|
733 | if (!conf.allowNull) {
|
734 | err('null');
|
735 | }
|
736 | return true;
|
737 | }
|
738 | }
|
739 | return false;
|
740 | }
|
741 |
|
742 | /*!
|
743 | * Return true when v is undefined, blank array, null or empty string
|
744 | * otherwise returns false
|
745 | *
|
746 | * @param {Mix} v
|
747 | * Returns true if `v` is blank.
|
748 | */
|
749 | function blank(v) {
|
750 | if (typeof v === 'undefined') return true;
|
751 | if (v instanceof Array && v.length === 0) return true;
|
752 | if (v === null) return true;
|
753 | if (typeof v === 'number' && isNaN(v)) return true;
|
754 | if (typeof v == 'string' && v === '') return true;
|
755 | return false;
|
756 | }
|
757 |
|
758 | function configure(cls, validation, args, opts) {
|
759 | if (!cls.validations) {
|
760 | Object.defineProperty(cls, 'validations', {
|
761 | writable: true,
|
762 | configurable: true,
|
763 | enumerable: false,
|
764 | value: {},
|
765 | });
|
766 | }
|
767 | args = [].slice.call(args);
|
768 | let conf;
|
769 | if (typeof args[args.length - 1] === 'object') {
|
770 | conf = args.pop();
|
771 | } else {
|
772 | conf = {};
|
773 | }
|
774 | if (validation === 'custom' && typeof args[args.length - 1] === 'function') {
|
775 | conf.customValidator = args.pop();
|
776 | }
|
777 | conf.validation = validation;
|
778 | args.forEach(function(attr) {
|
779 | if (typeof attr === 'string') {
|
780 | const validation = extend({}, conf);
|
781 | validation.options = opts || {};
|
782 | cls.validations[attr] = cls.validations[attr] || [];
|
783 | cls.validations[attr].push(validation);
|
784 | }
|
785 | });
|
786 | }
|
787 |
|
788 | function Errors() {
|
789 | Object.defineProperty(this, 'codes', {
|
790 | enumerable: false,
|
791 | configurable: true,
|
792 | value: {},
|
793 | });
|
794 | }
|
795 |
|
796 | Errors.prototype.add = function(field, message, code) {
|
797 | code = code || 'invalid';
|
798 | if (!this[field]) {
|
799 | this[field] = [];
|
800 | this.codes[field] = [];
|
801 | }
|
802 | this[field].push(message);
|
803 | this.codes[field].push(code);
|
804 | };
|
805 |
|
806 | function ErrorCodes(messages) {
|
807 | const c = this;
|
808 | Object.keys(messages).forEach(function(field) {
|
809 | c[field] = messages[field].codes;
|
810 | });
|
811 | }
|
812 |
|
813 | /**
|
814 | * ValidationError is raised when the application attempts to save an invalid model instance.
|
815 | * Example:
|
816 | * ```
|
817 | * {
|
818 | * "name": "ValidationError",
|
819 | * "status": 422,
|
820 | * "message": "The Model instance is not valid. \
|
821 | * See `details` property of the error object for more info.",
|
822 | * "statusCode": 422,
|
823 | * "details": {
|
824 | * "context": "user",
|
825 | * "codes": {
|
826 | * "password": [
|
827 | * "presence"
|
828 | * ],
|
829 | * "email": [
|
830 | * "uniqueness"
|
831 | * ]
|
832 | * },
|
833 | * "messages": {
|
834 | * "password": [
|
835 | * "can't be blank"
|
836 | * ],
|
837 | * "email": [
|
838 | * "Email already exists"
|
839 | * ]
|
840 | * }
|
841 | * },
|
842 | * }
|
843 | * ```
|
844 | * You might run into situations where you need to raise a validation error yourself, for example in a "before" hook or a
|
845 | * custom model method.
|
846 | * ```
|
847 | * MyModel.prototype.preflight = function(changes, callback) {
|
848 | * // Update properties, do not save to db
|
849 | * for (var key in changes) {
|
850 | * model[key] = changes[key];
|
851 | * }
|
852 | *
|
853 | * if (model.isValid()) {
|
854 | * return callback(null, { success: true });
|
855 | * }
|
856 | *
|
857 | * // This line shows how to create a ValidationError
|
858 | * var err = new MyModel.ValidationError(model);
|
859 | * callback(err);
|
860 | * }
|
861 | * ```
|
862 | *
|
863 | * @private
|
864 | */
|
865 | function ValidationError(obj) {
|
866 | if (!(this instanceof ValidationError)) return new ValidationError(obj);
|
867 |
|
868 | this.name = 'ValidationError';
|
869 |
|
870 | const context = obj && obj.constructor && obj.constructor.modelName;
|
871 | this.message = g.f(
|
872 | 'The %s instance is not valid. Details: %s.',
|
873 | context ? '`' + context + '`' : 'model',
|
874 | formatErrors(obj.errors, obj.toJSON()) || '(unknown)',
|
875 | );
|
876 |
|
877 | this.statusCode = 422;
|
878 |
|
879 | this.details = {
|
880 | context: context,
|
881 | codes: obj.errors && obj.errors.codes,
|
882 | messages: obj.errors,
|
883 | };
|
884 |
|
885 | if (Error.captureStackTrace) {
|
886 | // V8 (Chrome, Opera, Node)
|
887 | Error.captureStackTrace(this, this.constructor);
|
888 | } else if (errorHasStackProperty) {
|
889 | // Firefox
|
890 | this.stack = (new Error).stack;
|
891 | }
|
892 | // Safari and PhantomJS initializes `error.stack` on throw
|
893 | // Internet Explorer does not support `error.stack`
|
894 | }
|
895 |
|
896 | util.inherits(ValidationError, Error);
|
897 |
|
898 | const errorHasStackProperty = !!(new Error).stack;
|
899 |
|
900 | ValidationError.maxPropertyStringLength = 32;
|
901 |
|
902 | function formatErrors(errors, propertyValues) {
|
903 | const DELIM = '; ';
|
904 | errors = errors || {};
|
905 | return Object.getOwnPropertyNames(errors)
|
906 | .filter(function(propertyName) {
|
907 | return Array.isArray(errors[propertyName]);
|
908 | })
|
909 | .map(function(propertyName) {
|
910 | const messages = errors[propertyName];
|
911 | const propertyValue = propertyValues[propertyName];
|
912 | return messages.map(function(msg) {
|
913 | return formatPropertyError(propertyName, propertyValue, msg);
|
914 | }).join(DELIM);
|
915 | })
|
916 | .join(DELIM);
|
917 | }
|
918 |
|
919 | function formatPropertyError(propertyName, propertyValue, errorMessage) {
|
920 | let formattedValue;
|
921 | const valueType = typeof propertyValue;
|
922 | if (valueType === 'string') {
|
923 | formattedValue = JSON.stringify(truncatePropertyString(propertyValue));
|
924 | } else if (propertyValue instanceof Date) {
|
925 | formattedValue = isNaN(propertyValue.getTime()) ? propertyValue.toString() : propertyValue.toISOString();
|
926 | } else if (valueType === 'object') {
|
927 | // objects and arrays
|
928 | formattedValue = util.inspect(propertyValue, {
|
929 | showHidden: false,
|
930 | color: false,
|
931 | // show top-level object properties only
|
932 | depth: Array.isArray(propertyValue) ? 1 : 0,
|
933 | });
|
934 | formattedValue = truncatePropertyString(formattedValue);
|
935 | } else {
|
936 | formattedValue = truncatePropertyString('' + propertyValue);
|
937 | }
|
938 | return '`' + propertyName + '` ' + errorMessage +
|
939 | ' (value: ' + formattedValue + ')';
|
940 | }
|
941 |
|
942 | function truncatePropertyString(value) {
|
943 | let len = ValidationError.maxPropertyStringLength;
|
944 | if (value.length <= len) return value;
|
945 |
|
946 | // preserve few last characters like `}` or `]`, but no more than 3
|
947 | // this way the last `} ]` in the array of objects is included in the message
|
948 | let tail;
|
949 | const m = value.match(/([ \t})\]]+)$/);
|
950 | if (m) {
|
951 | tail = m[1].slice(-3);
|
952 | len -= tail.length;
|
953 | } else {
|
954 | tail = value.slice(-3);
|
955 | len -= 3;
|
956 | }
|
957 |
|
958 | return value.slice(0, len - 4) + '...' + tail;
|
959 | }
|