UNPKG

32.9 kBJavaScriptView Raw
1'use strict';
2
3/*!
4 * Module dependencies.
5 */
6
7const MongooseError = require('./error');
8const $exists = require('./schema/operators/exists');
9const $type = require('./schema/operators/type');
10const get = require('lodash.get');
11const immediate = require('./helpers/immediate');
12const utils = require('./utils');
13
14const CastError = MongooseError.CastError;
15const ValidatorError = MongooseError.ValidatorError;
16
17/**
18 * SchemaType constructor. Do **not** instantiate `SchemaType` directly.
19 * Mongoose converts your schema paths into SchemaTypes automatically.
20 *
21 * ####Example:
22 *
23 * const schema = new Schema({ name: String });
24 * schema.path('name') instanceof SchemaType; // true
25 *
26 * @param {String} path
27 * @param {Object} [options]
28 * @param {String} [instance]
29 * @api public
30 */
31
32function SchemaType(path, options, instance) {
33 this.path = path;
34 this.instance = instance;
35 this.validators = [];
36 this.setters = [];
37 this.getters = [];
38 this.options = options;
39 this._index = null;
40 this.selected;
41
42 for (const prop in options) {
43 if (this[prop] && typeof this[prop] === 'function') {
44 // { unique: true, index: true }
45 if (prop === 'index' && this._index) {
46 continue;
47 }
48
49 const val = options[prop];
50 // Special case so we don't screw up array defaults, see gh-5780
51 if (prop === 'default') {
52 this.default(val);
53 continue;
54 }
55
56 const opts = Array.isArray(val) ? val : [val];
57
58 this[prop].apply(this, opts);
59 }
60 }
61
62 Object.defineProperty(this, '$$context', {
63 enumerable: false,
64 configurable: false,
65 writable: true,
66 value: null
67 });
68}
69
70/**
71 * Sets a default value for this SchemaType.
72 *
73 * ####Example:
74 *
75 * var schema = new Schema({ n: { type: Number, default: 10 })
76 * var M = db.model('M', schema)
77 * var m = new M;
78 * console.log(m.n) // 10
79 *
80 * Defaults can be either `functions` which return the value to use as the default or the literal value itself. Either way, the value will be cast based on its schema type before being set during document creation.
81 *
82 * ####Example:
83 *
84 * // values are cast:
85 * var schema = new Schema({ aNumber: { type: Number, default: 4.815162342 }})
86 * var M = db.model('M', schema)
87 * var m = new M;
88 * console.log(m.aNumber) // 4.815162342
89 *
90 * // default unique objects for Mixed types:
91 * var schema = new Schema({ mixed: Schema.Types.Mixed });
92 * schema.path('mixed').default(function () {
93 * return {};
94 * });
95 *
96 * // if we don't use a function to return object literals for Mixed defaults,
97 * // each document will receive a reference to the same object literal creating
98 * // a "shared" object instance:
99 * var schema = new Schema({ mixed: Schema.Types.Mixed });
100 * schema.path('mixed').default({});
101 * var M = db.model('M', schema);
102 * var m1 = new M;
103 * m1.mixed.added = 1;
104 * console.log(m1.mixed); // { added: 1 }
105 * var m2 = new M;
106 * console.log(m2.mixed); // { added: 1 }
107 *
108 * @param {Function|any} val the default value
109 * @return {defaultValue}
110 * @api public
111 */
112
113SchemaType.prototype.default = function(val) {
114 if (arguments.length === 1) {
115 if (val === void 0) {
116 this.defaultValue = void 0;
117 return void 0;
118 }
119 this.defaultValue = val;
120 return this.defaultValue;
121 } else if (arguments.length > 1) {
122 this.defaultValue = utils.args(arguments);
123 }
124 return this.defaultValue;
125};
126
127/**
128 * Declares the index options for this schematype.
129 *
130 * ####Example:
131 *
132 * var s = new Schema({ name: { type: String, index: true })
133 * var s = new Schema({ loc: { type: [Number], index: 'hashed' })
134 * var s = new Schema({ loc: { type: [Number], index: '2d', sparse: true })
135 * var s = new Schema({ loc: { type: [Number], index: { type: '2dsphere', sparse: true }})
136 * var s = new Schema({ date: { type: Date, index: { unique: true, expires: '1d' }})
137 * Schema.path('my.path').index(true);
138 * Schema.path('my.date').index({ expires: 60 });
139 * Schema.path('my.path').index({ unique: true, sparse: true });
140 *
141 * ####NOTE:
142 *
143 * _Indexes are created [in the background](https://docs.mongodb.com/manual/core/index-creation/#index-creation-background)
144 * by default. If `background` is set to `false`, MongoDB will not execute any
145 * read/write operations you send until the index build.
146 * Specify `background: false` to override Mongoose's default._
147 *
148 * @param {Object|Boolean|String} options
149 * @return {SchemaType} this
150 * @api public
151 */
152
153SchemaType.prototype.index = function(options) {
154 this._index = options;
155 utils.expires(this._index);
156 return this;
157};
158
159/**
160 * Declares an unique index.
161 *
162 * ####Example:
163 *
164 * var s = new Schema({ name: { type: String, unique: true }});
165 * Schema.path('name').index({ unique: true });
166 *
167 * _NOTE: violating the constraint returns an `E11000` error from MongoDB when saving, not a Mongoose validation error._
168 *
169 * @param {Boolean} bool
170 * @return {SchemaType} this
171 * @api public
172 */
173
174SchemaType.prototype.unique = function(bool) {
175 if (this._index === false) {
176 if (!bool) {
177 return;
178 }
179 throw new Error('Path "' + this.path + '" may not have `index` set to ' +
180 'false and `unique` set to true');
181 }
182 if (this._index == null || this._index === true) {
183 this._index = {};
184 } else if (typeof this._index === 'string') {
185 this._index = {type: this._index};
186 }
187
188 this._index.unique = bool;
189 return this;
190};
191
192/**
193 * Declares a full text index.
194 *
195 * ###Example:
196 *
197 * var s = new Schema({name : {type: String, text : true })
198 * Schema.path('name').index({text : true});
199 * @param {Boolean} bool
200 * @return {SchemaType} this
201 * @api public
202 */
203
204SchemaType.prototype.text = function(bool) {
205 if (this._index === null || this._index === undefined ||
206 typeof this._index === 'boolean') {
207 this._index = {};
208 } else if (typeof this._index === 'string') {
209 this._index = {type: this._index};
210 }
211
212 this._index.text = bool;
213 return this;
214};
215
216/**
217 * Declares a sparse index.
218 *
219 * ####Example:
220 *
221 * var s = new Schema({ name: { type: String, sparse: true })
222 * Schema.path('name').index({ sparse: true });
223 *
224 * @param {Boolean} bool
225 * @return {SchemaType} this
226 * @api public
227 */
228
229SchemaType.prototype.sparse = function(bool) {
230 if (this._index === null || this._index === undefined ||
231 typeof this._index === 'boolean') {
232 this._index = {};
233 } else if (typeof this._index === 'string') {
234 this._index = {type: this._index};
235 }
236
237 this._index.sparse = bool;
238 return this;
239};
240
241/**
242 * Adds a setter to this schematype.
243 *
244 * ####Example:
245 *
246 * function capitalize (val) {
247 * if (typeof val !== 'string') val = '';
248 * return val.charAt(0).toUpperCase() + val.substring(1);
249 * }
250 *
251 * // defining within the schema
252 * var s = new Schema({ name: { type: String, set: capitalize }});
253 *
254 * // or with the SchemaType
255 * var s = new Schema({ name: String })
256 * s.path('name').set(capitalize);
257 *
258 * Setters allow you to transform the data before it gets to the raw mongodb
259 * document or query.
260 *
261 * Suppose you are implementing user registration for a website. Users provide
262 * an email and password, which gets saved to mongodb. The email is a string
263 * that you will want to normalize to lower case, in order to avoid one email
264 * having more than one account -- e.g., otherwise, avenue@q.com can be registered for 2 accounts via avenue@q.com and AvEnUe@Q.CoM.
265 *
266 * You can set up email lower case normalization easily via a Mongoose setter.
267 *
268 * function toLower(v) {
269 * return v.toLowerCase();
270 * }
271 *
272 * var UserSchema = new Schema({
273 * email: { type: String, set: toLower }
274 * });
275 *
276 * var User = db.model('User', UserSchema);
277 *
278 * var user = new User({email: 'AVENUE@Q.COM'});
279 * console.log(user.email); // 'avenue@q.com'
280 *
281 * // or
282 * var user = new User();
283 * user.email = 'Avenue@Q.com';
284 * console.log(user.email); // 'avenue@q.com'
285 * User.updateOne({ _id: _id }, { $set: { email: 'AVENUE@Q.COM' } }); // update to 'avenue@q.com'
286 *
287 * As you can see above, setters allow you to transform the data before it
288 * stored in MongoDB, or before executing a query.
289 *
290 * _NOTE: we could have also just used the built-in `lowercase: true` SchemaType option instead of defining our own function._
291 *
292 * new Schema({ email: { type: String, lowercase: true }})
293 *
294 * Setters are also passed a second argument, the schematype on which the setter was defined. This allows for tailored behavior based on options passed in the schema.
295 *
296 * function inspector (val, schematype) {
297 * if (schematype.options.required) {
298 * return schematype.path + ' is required';
299 * } else {
300 * return val;
301 * }
302 * }
303 *
304 * var VirusSchema = new Schema({
305 * name: { type: String, required: true, set: inspector },
306 * taxonomy: { type: String, set: inspector }
307 * })
308 *
309 * var Virus = db.model('Virus', VirusSchema);
310 * var v = new Virus({ name: 'Parvoviridae', taxonomy: 'Parvovirinae' });
311 *
312 * console.log(v.name); // name is required
313 * console.log(v.taxonomy); // Parvovirinae
314 *
315 * You can also use setters to modify other properties on the document. If
316 * you're setting a property `name` on a document, the setter will run with
317 * `this` as the document. Be careful, in mongoose 5 setters will also run
318 * when querying by `name` with `this` as the query.
319 *
320 * ```javascript
321 * const nameSchema = new Schema({ name: String, keywords: [String] });
322 * nameSchema.path('name').set(function(v) {
323 * // Need to check if `this` is a document, because in mongoose 5
324 * // setters will also run on queries, in which case `this` will be a
325 * // mongoose query object.
326 * if (this instanceof Document && v != null) {
327 * this.keywords = v.split(' ');
328 * }
329 * return v;
330 * });
331 * ```
332 *
333 * @param {Function} fn
334 * @return {SchemaType} this
335 * @api public
336 */
337
338SchemaType.prototype.set = function(fn) {
339 if (typeof fn !== 'function') {
340 throw new TypeError('A setter must be a function.');
341 }
342 this.setters.push(fn);
343 return this;
344};
345
346/**
347 * Adds a getter to this schematype.
348 *
349 * ####Example:
350 *
351 * function dob (val) {
352 * if (!val) return val;
353 * return (val.getMonth() + 1) + "/" + val.getDate() + "/" + val.getFullYear();
354 * }
355 *
356 * // defining within the schema
357 * var s = new Schema({ born: { type: Date, get: dob })
358 *
359 * // or by retreiving its SchemaType
360 * var s = new Schema({ born: Date })
361 * s.path('born').get(dob)
362 *
363 * Getters allow you to transform the representation of the data as it travels from the raw mongodb document to the value that you see.
364 *
365 * Suppose you are storing credit card numbers and you want to hide everything except the last 4 digits to the mongoose user. You can do so by defining a getter in the following way:
366 *
367 * function obfuscate (cc) {
368 * return '****-****-****-' + cc.slice(cc.length-4, cc.length);
369 * }
370 *
371 * var AccountSchema = new Schema({
372 * creditCardNumber: { type: String, get: obfuscate }
373 * });
374 *
375 * var Account = db.model('Account', AccountSchema);
376 *
377 * Account.findById(id, function (err, found) {
378 * console.log(found.creditCardNumber); // '****-****-****-1234'
379 * });
380 *
381 * Getters are also passed a second argument, the schematype on which the getter was defined. This allows for tailored behavior based on options passed in the schema.
382 *
383 * function inspector (val, schematype) {
384 * if (schematype.options.required) {
385 * return schematype.path + ' is required';
386 * } else {
387 * return schematype.path + ' is not';
388 * }
389 * }
390 *
391 * var VirusSchema = new Schema({
392 * name: { type: String, required: true, get: inspector },
393 * taxonomy: { type: String, get: inspector }
394 * })
395 *
396 * var Virus = db.model('Virus', VirusSchema);
397 *
398 * Virus.findById(id, function (err, virus) {
399 * console.log(virus.name); // name is required
400 * console.log(virus.taxonomy); // taxonomy is not
401 * })
402 *
403 * @param {Function} fn
404 * @return {SchemaType} this
405 * @api public
406 */
407
408SchemaType.prototype.get = function(fn) {
409 if (typeof fn !== 'function') {
410 throw new TypeError('A getter must be a function.');
411 }
412 this.getters.push(fn);
413 return this;
414};
415
416/**
417 * Adds validator(s) for this document path.
418 *
419 * Validators always receive the value to validate as their first argument and
420 * must return `Boolean`. Returning `false` or throwing an error means
421 * validation failed.
422 *
423 * The error message argument is optional. If not passed, the [default generic error message template](#error_messages_MongooseError-messages) will be used.
424 *
425 * ####Examples:
426 *
427 * // make sure every value is equal to "something"
428 * function validator (val) {
429 * return val == 'something';
430 * }
431 * new Schema({ name: { type: String, validate: validator }});
432 *
433 * // with a custom error message
434 *
435 * var custom = [validator, 'Uh oh, {PATH} does not equal "something".']
436 * new Schema({ name: { type: String, validate: custom }});
437 *
438 * // adding many validators at a time
439 *
440 * var many = [
441 * { validator: validator, msg: 'uh oh' }
442 * , { validator: anotherValidator, msg: 'failed' }
443 * ]
444 * new Schema({ name: { type: String, validate: many }});
445 *
446 * // or utilizing SchemaType methods directly:
447 *
448 * var schema = new Schema({ name: 'string' });
449 * schema.path('name').validate(validator, 'validation of `{PATH}` failed with value `{VALUE}`');
450 *
451 * ####Error message templates:
452 *
453 * From the examples above, you may have noticed that error messages support
454 * basic templating. There are a few other template keywords besides `{PATH}`
455 * and `{VALUE}` too. To find out more, details are available
456 * [here](#error_messages_MongooseError.messages).
457 *
458 * If Mongoose's built-in error message templating isn't enough, Mongoose
459 * supports setting the `message` property to a function.
460 *
461 * schema.path('name').validate({
462 * validator: v => v.length > 5,
463 * // `errors['name']` will be "name must have length > 5, got 'foo'"
464 * message: props => `${props.path} must have length > 5, got '${props.value}'`
465 * });
466 *
467 * To bypass Mongoose's error messages and just copy the error message that
468 * the validator throws, do this:
469 *
470 * schema.path('name').validate({
471 * validator: () => { throw new Error('Oops!'); },
472 * // `errors['name']` will be "Oops!"
473 * message: props => props.reason.message
474 * });
475 *
476 * ####Asynchronous validation:
477 *
478 * Mongoose supports validators that return a promise. A validator that returns
479 * a promise is called an _async validator_. Async validators run in
480 * parallel, and `validate()` will wait until all async validators have settled.
481 *
482 * schema.path('name').validate({
483 * validator: function (value) {
484 * return new Promise(function (resolve, reject) {
485 * resolve(false); // validation failed
486 * });
487 * }
488 * });
489 *
490 * You might use asynchronous validators to retreive other documents from the database to validate against or to meet other I/O bound validation needs.
491 *
492 * Validation occurs `pre('save')` or whenever you manually execute [document#validate](#document_Document-validate).
493 *
494 * If validation fails during `pre('save')` and no callback was passed to receive the error, an `error` event will be emitted on your Models associated db [connection](#connection_Connection), passing the validation error object along.
495 *
496 * var conn = mongoose.createConnection(..);
497 * conn.on('error', handleError);
498 *
499 * var Product = conn.model('Product', yourSchema);
500 * var dvd = new Product(..);
501 * dvd.save(); // emits error on the `conn` above
502 *
503 * If you want to handle these errors at the Model level, add an `error`
504 * listener to your Model as shown below.
505 *
506 * // registering an error listener on the Model lets us handle errors more locally
507 * Product.on('error', handleError);
508 *
509 * @param {RegExp|Function|Object} obj validator
510 * @param {String|Function} [errorMsg] optional error message. If function, should return the error message as a string
511 * @param {String} [type] optional validator type
512 * @return {SchemaType} this
513 * @api public
514 */
515
516SchemaType.prototype.validate = function(obj, message, type) {
517 if (typeof obj === 'function' || obj && utils.getFunctionName(obj.constructor) === 'RegExp') {
518 let properties;
519 if (message instanceof Object && !type) {
520 properties = utils.clone(message);
521 if (!properties.message) {
522 properties.message = properties.msg;
523 }
524 properties.validator = obj;
525 properties.type = properties.type || 'user defined';
526 } else {
527 if (!message) {
528 message = MongooseError.messages.general.default;
529 }
530 if (!type) {
531 type = 'user defined';
532 }
533 properties = {message: message, type: type, validator: obj};
534 }
535 this.validators.push(properties);
536 return this;
537 }
538
539 let i,
540 length,
541 arg;
542
543 for (i = 0, length = arguments.length; i < length; i++) {
544 arg = arguments[i];
545 if (!(arg && utils.getFunctionName(arg.constructor) === 'Object')) {
546 const msg = 'Invalid validator. Received (' + typeof arg + ') '
547 + arg
548 + '. See http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate';
549
550 throw new Error(msg);
551 }
552 this.validate(arg.validator, arg);
553 }
554
555 return this;
556};
557
558/**
559 * Adds a required validator to this SchemaType. The validator gets added
560 * to the front of this SchemaType's validators array using `unshift()`.
561 *
562 * ####Example:
563 *
564 * var s = new Schema({ born: { type: Date, required: true })
565 *
566 * // or with custom error message
567 *
568 * var s = new Schema({ born: { type: Date, required: '{PATH} is required!' })
569 *
570 * // or with a function
571 *
572 * var s = new Schema({
573 * userId: ObjectId,
574 * username: {
575 * type: String,
576 * required: function() { return this.userId != null; }
577 * }
578 * })
579 *
580 * // or with a function and a custom message
581 * var s = new Schema({
582 * userId: ObjectId,
583 * username: {
584 * type: String,
585 * required: [
586 * function() { return this.userId != null; },
587 * 'username is required if id is specified'
588 * ]
589 * }
590 * })
591 *
592 * // or through the path API
593 *
594 * Schema.path('name').required(true);
595 *
596 * // with custom error messaging
597 *
598 * Schema.path('name').required(true, 'grrr :( ');
599 *
600 * // or make a path conditionally required based on a function
601 * var isOver18 = function() { return this.age >= 18; };
602 * Schema.path('voterRegistrationId').required(isOver18);
603 *
604 * The required validator uses the SchemaType's `checkRequired` function to
605 * determine whether a given value satisfies the required validator. By default,
606 * a value satisfies the required validator if `val != null` (that is, if
607 * the value is not null nor undefined). However, most built-in mongoose schema
608 * types override the default `checkRequired` function:
609 *
610 * @param {Boolean|Function|Object} required enable/disable the validator, or function that returns required boolean, or options object
611 * @param {Boolean|Function} [options.isRequired] enable/disable the validator, or function that returns required boolean
612 * @param {Function} [options.ErrorConstructor] custom error constructor. The constructor receives 1 parameter, an object containing the validator properties.
613 * @param {String} [message] optional custom error message
614 * @return {SchemaType} this
615 * @see Customized Error Messages #error_messages_MongooseError-messages
616 * @see SchemaArray#checkRequired #schema_array_SchemaArray.checkRequired
617 * @see SchemaBoolean#checkRequired #schema_boolean_SchemaBoolean-checkRequired
618 * @see SchemaBuffer#checkRequired #schema_buffer_SchemaBuffer.schemaName
619 * @see SchemaNumber#checkRequired #schema_number_SchemaNumber-min
620 * @see SchemaObjectId#checkRequired #schema_objectid_ObjectId-auto
621 * @see SchemaString#checkRequired #schema_string_SchemaString-checkRequired
622 * @api public
623 */
624
625SchemaType.prototype.required = function(required, message) {
626 let customOptions = {};
627 if (typeof required === 'object') {
628 customOptions = required;
629 message = customOptions.message || message;
630 required = required.isRequired;
631 }
632
633 if (required === false) {
634 this.validators = this.validators.filter(function(v) {
635 return v.validator !== this.requiredValidator;
636 }, this);
637
638 this.isRequired = false;
639 delete this.originalRequiredValue;
640 return this;
641 }
642
643 const _this = this;
644 this.isRequired = true;
645
646 this.requiredValidator = function(v) {
647 const cachedRequired = get(this, '$__.cachedRequired');
648
649 // no validation when this path wasn't selected in the query.
650 if (cachedRequired != null && !this.isSelected(_this.path) && !this.isModified(_this.path)) {
651 return true;
652 }
653
654 // `$cachedRequired` gets set in `_evaluateRequiredFunctions()` so we
655 // don't call required functions multiple times in one validate call
656 // See gh-6801
657 if (cachedRequired != null && _this.path in cachedRequired) {
658 const res = cachedRequired[_this.path] ?
659 _this.checkRequired(v, this) :
660 true;
661 delete cachedRequired[_this.path];
662 return res;
663 } else if (typeof required === 'function') {
664 return required.apply(this) ? _this.checkRequired(v, this) : true;
665 }
666
667 return _this.checkRequired(v, this);
668 };
669 this.originalRequiredValue = required;
670
671 if (typeof required === 'string') {
672 message = required;
673 required = undefined;
674 }
675
676 const msg = message || MongooseError.messages.general.required;
677 this.validators.unshift(Object.assign({}, customOptions, {
678 validator: this.requiredValidator,
679 message: msg,
680 type: 'required'
681 }));
682
683 return this;
684};
685
686/**
687 * Gets the default value
688 *
689 * @param {Object} scope the scope which callback are executed
690 * @param {Boolean} init
691 * @api private
692 */
693
694SchemaType.prototype.getDefault = function(scope, init) {
695 let ret = typeof this.defaultValue === 'function'
696 ? this.defaultValue.call(scope)
697 : this.defaultValue;
698
699 if (ret !== null && ret !== undefined) {
700 if (typeof ret === 'object' && (!this.options || !this.options.shared)) {
701 ret = utils.clone(ret);
702 }
703
704 const casted = this.cast(ret, scope, init);
705 if (casted && casted.$isSingleNested) {
706 casted.$parent = scope;
707 }
708 return casted;
709 }
710 return ret;
711};
712
713/*!
714 * Applies setters without casting
715 *
716 * @api private
717 */
718
719SchemaType.prototype._applySetters = function(value, scope, init, priorVal) {
720 let v = value;
721 const setters = this.setters;
722 let len = setters.length;
723 const caster = this.caster;
724
725 while (len--) {
726 v = setters[len].call(scope, v, this);
727 }
728
729 if (Array.isArray(v) && caster && caster.setters) {
730 const newVal = [];
731 for (let i = 0; i < v.length; i++) {
732 newVal.push(caster.applySetters(v[i], scope, init, priorVal));
733 }
734 v = newVal;
735 }
736
737 return v;
738};
739
740/**
741 * Applies setters
742 *
743 * @param {Object} value
744 * @param {Object} scope
745 * @param {Boolean} init
746 * @api private
747 */
748
749SchemaType.prototype.applySetters = function(value, scope, init, priorVal, options) {
750 let v = this._applySetters(value, scope, init, priorVal, options);
751
752 if (v == null) {
753 return v;
754 }
755
756 // do not cast until all setters are applied #665
757 v = this.cast(v, scope, init, priorVal, options);
758
759 return v;
760};
761
762/**
763 * Applies getters to a value
764 *
765 * @param {Object} value
766 * @param {Object} scope
767 * @api private
768 */
769
770SchemaType.prototype.applyGetters = function(value, scope) {
771 let v = value;
772 const getters = this.getters;
773 const len = getters.length;
774
775 if (len === 0) {
776 return v;
777 }
778
779 for (let i = 0; i < len; ++i) {
780 v = getters[i].call(scope, v, this);
781 }
782
783 return v;
784};
785
786/**
787 * Sets default `select()` behavior for this path.
788 *
789 * Set to `true` if this path should always be included in the results, `false` if it should be excluded by default. This setting can be overridden at the query level.
790 *
791 * ####Example:
792 *
793 * T = db.model('T', new Schema({ x: { type: String, select: true }}));
794 * T.find(..); // field x will always be selected ..
795 * // .. unless overridden;
796 * T.find().select('-x').exec(callback);
797 *
798 * @param {Boolean} val
799 * @return {SchemaType} this
800 * @api public
801 */
802
803SchemaType.prototype.select = function select(val) {
804 this.selected = !!val;
805 return this;
806};
807
808/**
809 * Performs a validation of `value` using the validators declared for this SchemaType.
810 *
811 * @param {any} value
812 * @param {Function} callback
813 * @param {Object} scope
814 * @api private
815 */
816
817SchemaType.prototype.doValidate = function(value, fn, scope) {
818 let err = false;
819 const path = this.path;
820 let count = this.validators.length;
821
822 if (!count) {
823 return fn(null);
824 }
825
826 const validate = function(ok, validatorProperties) {
827 if (err) {
828 return;
829 }
830 if (ok === undefined || ok) {
831 if (--count <= 0) {
832 immediate(function() {
833 fn(null);
834 });
835 }
836 } else {
837 const ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError;
838 err = new ErrorConstructor(validatorProperties);
839 err.$isValidatorError = true;
840 immediate(function() {
841 fn(err);
842 });
843 }
844 };
845
846 const _this = this;
847 this.validators.forEach(function(v) {
848 if (err) {
849 return;
850 }
851
852 const validator = v.validator;
853 let ok;
854
855 const validatorProperties = utils.clone(v);
856 validatorProperties.path = path;
857 validatorProperties.value = value;
858
859 if (validator instanceof RegExp) {
860 validate(validator.test(value), validatorProperties);
861 } else if (typeof validator === 'function') {
862 if (value === undefined && validator !== _this.requiredValidator) {
863 validate(true, validatorProperties);
864 return;
865 }
866 if (validatorProperties.isAsync) {
867 asyncValidate(validator, scope, value, validatorProperties, validate);
868 } else {
869 try {
870 ok = validator.call(scope, value, validatorProperties);
871 } catch (error) {
872 ok = false;
873 validatorProperties.reason = error;
874 if (error.message) {
875 validatorProperties.message = error.message;
876 }
877 }
878 if (ok != null && typeof ok.then === 'function') {
879 ok.then(
880 function(ok) { validate(ok, validatorProperties); },
881 function(error) {
882 validatorProperties.reason = error;
883 ok = false;
884 validate(ok, validatorProperties);
885 });
886 } else {
887 validate(ok, validatorProperties);
888 }
889 }
890 }
891 });
892};
893
894/*!
895 * Handle async validators
896 */
897
898function asyncValidate(validator, scope, value, props, cb) {
899 let called = false;
900 const returnVal = validator.call(scope, value, function(ok, customMsg) {
901 if (called) {
902 return;
903 }
904 called = true;
905 if (customMsg) {
906 props.message = customMsg;
907 }
908 cb(ok, props);
909 });
910 if (typeof returnVal === 'boolean') {
911 called = true;
912 cb(returnVal, props);
913 } else if (returnVal && typeof returnVal.then === 'function') {
914 // Promise
915 returnVal.then(
916 function(ok) {
917 if (called) {
918 return;
919 }
920 called = true;
921 cb(ok, props);
922 },
923 function(error) {
924 if (called) {
925 return;
926 }
927 called = true;
928
929 props.reason = error;
930 cb(false, props);
931 });
932 }
933}
934
935/**
936 * Performs a validation of `value` using the validators declared for this SchemaType.
937 *
938 * ####Note:
939 *
940 * This method ignores the asynchronous validators.
941 *
942 * @param {any} value
943 * @param {Object} scope
944 * @return {MongooseError|undefined}
945 * @api private
946 */
947
948SchemaType.prototype.doValidateSync = function(value, scope) {
949 let err = null;
950 const path = this.path;
951 const count = this.validators.length;
952
953 if (!count) {
954 return null;
955 }
956
957 const validate = function(ok, validatorProperties) {
958 if (err) {
959 return;
960 }
961 if (ok !== undefined && !ok) {
962 const ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError;
963 err = new ErrorConstructor(validatorProperties);
964 err.$isValidatorError = true;
965 }
966 };
967
968 let validators = this.validators;
969 if (value === void 0) {
970 if (this.validators.length > 0 && this.validators[0].type === 'required') {
971 validators = [this.validators[0]];
972 } else {
973 return null;
974 }
975 }
976
977 validators.forEach(function(v) {
978 if (err) {
979 return;
980 }
981
982 const validator = v.validator;
983 const validatorProperties = utils.clone(v);
984 validatorProperties.path = path;
985 validatorProperties.value = value;
986 let ok;
987
988 if (validator instanceof RegExp) {
989 validate(validator.test(value), validatorProperties);
990 } else if (typeof validator === 'function') {
991 // if not async validators
992 if (validator.length !== 2 && !validatorProperties.isAsync) {
993 try {
994 ok = validator.call(scope, value);
995 } catch (error) {
996 ok = false;
997 validatorProperties.reason = error;
998 }
999 validate(ok, validatorProperties);
1000 }
1001 }
1002 });
1003
1004 return err;
1005};
1006
1007/**
1008 * Determines if value is a valid Reference.
1009 *
1010 * @param {SchemaType} self
1011 * @param {Object} value
1012 * @param {Document} doc
1013 * @param {Boolean} init
1014 * @return {Boolean}
1015 * @api private
1016 */
1017
1018SchemaType._isRef = function(self, value, doc, init) {
1019 // fast path
1020 let ref = init && self.options && (self.options.ref || self.options.refPath);
1021
1022 if (!ref && doc && doc.$__fullPath) {
1023 // checks for
1024 // - this populated with adhoc model and no ref was set in schema OR
1025 // - setting / pushing values after population
1026 const path = doc.$__fullPath(self.path);
1027 const owner = doc.ownerDocument ? doc.ownerDocument() : doc;
1028 ref = owner.populated(path);
1029 }
1030
1031 if (ref) {
1032 if (value == null) {
1033 return true;
1034 }
1035 if (!Buffer.isBuffer(value) && // buffers are objects too
1036 value._bsontype !== 'Binary' // raw binary value from the db
1037 && utils.isObject(value) // might have deselected _id in population query
1038 ) {
1039 return true;
1040 }
1041 }
1042
1043 return false;
1044};
1045
1046/*!
1047 * ignore
1048 */
1049
1050function handleSingle(val) {
1051 return this.castForQuery(val);
1052}
1053
1054/*!
1055 * ignore
1056 */
1057
1058function handleArray(val) {
1059 const _this = this;
1060 if (!Array.isArray(val)) {
1061 return [this.castForQuery(val)];
1062 }
1063 return val.map(function(m) {
1064 return _this.castForQuery(m);
1065 });
1066}
1067
1068/*!
1069 * Just like handleArray, except also allows `[]` because surprisingly
1070 * `$in: [1, []]` works fine
1071 */
1072
1073function handle$in(val) {
1074 const _this = this;
1075 if (!Array.isArray(val)) {
1076 return [this.castForQuery(val)];
1077 }
1078 return val.map(function(m) {
1079 if (Array.isArray(m) && m.length === 0) {
1080 return m;
1081 }
1082 return _this.castForQuery(m);
1083 });
1084}
1085
1086/*!
1087 * ignore
1088 */
1089
1090SchemaType.prototype.$conditionalHandlers = {
1091 $all: handleArray,
1092 $eq: handleSingle,
1093 $in: handle$in,
1094 $ne: handleSingle,
1095 $nin: handleArray,
1096 $exists: $exists,
1097 $type: $type
1098};
1099
1100/*!
1101 * Wraps `castForQuery` to handle context
1102 */
1103
1104SchemaType.prototype.castForQueryWrapper = function(params) {
1105 this.$$context = params.context;
1106 if ('$conditional' in params) {
1107 return this.castForQuery(params.$conditional, params.val);
1108 }
1109 if (params.$skipQueryCastForUpdate) {
1110 return this._castForQuery(params.val);
1111 }
1112 return this.castForQuery(params.val);
1113};
1114
1115/**
1116 * Cast the given value with the given optional query operator.
1117 *
1118 * @param {String} [$conditional] query operator, like `$eq` or `$in`
1119 * @param {any} val
1120 * @api private
1121 */
1122
1123SchemaType.prototype.castForQuery = function($conditional, val) {
1124 let handler;
1125 if (arguments.length === 2) {
1126 handler = this.$conditionalHandlers[$conditional];
1127 if (!handler) {
1128 throw new Error('Can\'t use ' + $conditional);
1129 }
1130 return handler.call(this, val);
1131 }
1132 val = $conditional;
1133 return this._castForQuery(val);
1134};
1135
1136/*!
1137 * Internal switch for runSetters
1138 *
1139 * @api private
1140 */
1141
1142SchemaType.prototype._castForQuery = function(val) {
1143 return this.applySetters(val, this.$$context);
1144};
1145
1146/**
1147 * Default check for if this path satisfies the `required` validator.
1148 *
1149 * @param {any} val
1150 * @api private
1151 */
1152
1153SchemaType.prototype.checkRequired = function(val) {
1154 return val != null;
1155};
1156
1157/*!
1158 * Module exports.
1159 */
1160
1161module.exports = exports = SchemaType;
1162
1163exports.CastError = CastError;
1164
1165exports.ValidatorError = ValidatorError;