UNPKG

28.1 kBJavaScriptView Raw
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'use strict';
7
8const g = require('strong-globalize')();
9const util = require('util');
10const extend = util._extend;
11
12/*!
13 * Module exports
14 */
15exports.ValidationError = ValidationError;
16exports.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 */
30function 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 */
57Validatable.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 */
75Validatable.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 */
110Validatable.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 */
132Validatable.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 */
154Validatable.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 */
169Validatable.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 */
184Validatable.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 */
207Validatable.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 */
239Validatable.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 */
268Validatable.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 */
282Validatable.validatesDateOf = getConfigurator('date');
283
284// implementation of validators
285
286/*!
287 * Presence validator
288 */
289function validatePresence(attr, conf, err, options) {
290 if (blank(this[attr])) {
291 err();
292 }
293}
294
295/*!
296 * Absence validator
297 */
298function validateAbsence(attr, conf, err, options) {
299 if (!blank(this[attr])) {
300 err();
301 }
302}
303
304/*!
305 * Length validator
306 */
307function 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 */
325function 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 */
339function 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 */
350function 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 */
361function 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 */
377function 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
396function 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 */
407function 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 */
452function 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
459const 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
472function 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 */
514Validatable.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
606function cleanErrors(inst) {
607 Object.defineProperty(inst, 'errors', {
608 enumerable: false,
609 configurable: true,
610 value: false,
611 });
612}
613
614function 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
672function 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
691const 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 */
723function 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 */
749function 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
758function 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
788function Errors() {
789 Object.defineProperty(this, 'codes', {
790 enumerable: false,
791 configurable: true,
792 value: {},
793 });
794}
795
796Errors.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
806function 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*/
865function 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
896util.inherits(ValidationError, Error);
897
898const errorHasStackProperty = !!(new Error).stack;
899
900ValidationError.maxPropertyStringLength = 32;
901
902function 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
919function 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
942function 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}