UNPKG

54.5 kBJavaScriptView Raw
1'use strict';
2
3/*!
4 * Module dependencies.
5 */
6
7const EventEmitter = require('events').EventEmitter;
8const Kareem = require('kareem');
9const SchemaType = require('./schematype');
10const VirtualType = require('./virtualtype');
11const applyTimestampsToChildren = require('./helpers/update/applyTimestampsToChildren');
12const applyTimestampsToUpdate = require('./helpers/update/applyTimestampsToUpdate');
13const get = require('./helpers/get');
14const getIndexes = require('./helpers/schema/getIndexes');
15const handleTimestampOption = require('./helpers/schema/handleTimestampOption');
16const merge = require('./helpers/schema/merge');
17const mpath = require('mpath');
18const readPref = require('./driver').get().ReadPreference;
19const symbols = require('./schema/symbols');
20const util = require('util');
21const utils = require('./utils');
22const validateRef = require('./helpers/populate/validateRef');
23
24let MongooseTypes;
25
26const queryHooks = require('./helpers/query/applyQueryMiddleware').
27 middlewareFunctions;
28const documentHooks = require('./helpers/model/applyHooks').middlewareFunctions;
29const hookNames = queryHooks.concat(documentHooks).
30 reduce((s, hook) => s.add(hook), new Set());
31
32let id = 0;
33
34/**
35 * Schema constructor.
36 *
37 * ####Example:
38 *
39 * var child = new Schema({ name: String });
40 * var schema = new Schema({ name: String, age: Number, children: [child] });
41 * var Tree = mongoose.model('Tree', schema);
42 *
43 * // setting schema options
44 * new Schema({ name: String }, { _id: false, autoIndex: false })
45 *
46 * ####Options:
47 *
48 * - [autoIndex](/docs/guide.html#autoIndex): bool - defaults to null (which means use the connection's autoIndex option)
49 * - [autoCreate](/docs/guide.html#autoCreate): bool - defaults to null (which means use the connection's autoCreate option)
50 * - [bufferCommands](/docs/guide.html#bufferCommands): bool - defaults to true
51 * - [capped](/docs/guide.html#capped): bool - defaults to false
52 * - [collection](/docs/guide.html#collection): string - no default
53 * - [id](/docs/guide.html#id): bool - defaults to true
54 * - [_id](/docs/guide.html#_id): bool - defaults to true
55 * - `minimize`: bool - controls [document#toObject](#document_Document-toObject) behavior when called manually - defaults to true
56 * - [read](/docs/guide.html#read): string
57 * - [writeConcern](/docs/guide.html#writeConcern): object - defaults to null, use to override [the MongoDB server's default write concern settings](https://docs.mongodb.com/manual/reference/write-concern/)
58 * - [shardKey](/docs/guide.html#shardKey): object - defaults to `null`
59 * - [strict](/docs/guide.html#strict): bool - defaults to true
60 * - [toJSON](/docs/guide.html#toJSON) - object - no default
61 * - [toObject](/docs/guide.html#toObject) - object - no default
62 * - [typeKey](/docs/guide.html#typeKey) - string - defaults to 'type'
63 * - [useNestedStrict](/docs/guide.html#useNestedStrict) - boolean - defaults to false
64 * - [validateBeforeSave](/docs/guide.html#validateBeforeSave) - bool - defaults to `true`
65 * - [versionKey](/docs/guide.html#versionKey): string - defaults to "__v"
66 * - [collation](/docs/guide.html#collation): object - defaults to null (which means use no collation)
67 * - [selectPopulatedPaths](/docs/guide.html#selectPopulatedPaths): boolean - defaults to `true`
68 *
69 * ####Note:
70 *
71 * _When nesting schemas, (`children` in the example above), always declare the child schema first before passing it into its parent._
72 *
73 * @param {Object|Schema|Array} [definition] Can be one of: object describing schema paths, or schema to copy, or array of objects and schemas
74 * @param {Object} [options]
75 * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter
76 * @event `init`: Emitted after the schema is compiled into a `Model`.
77 * @api public
78 */
79
80function Schema(obj, options) {
81 if (!(this instanceof Schema)) {
82 return new Schema(obj, options);
83 }
84
85 this.obj = obj;
86 this.paths = {};
87 this.aliases = {};
88 this.subpaths = {};
89 this.virtuals = {};
90 this.singleNestedPaths = {};
91 this.nested = {};
92 this.inherits = {};
93 this.callQueue = [];
94 this._indexes = [];
95 this.methods = {};
96 this.methodOptions = {};
97 this.statics = {};
98 this.tree = {};
99 this.query = {};
100 this.childSchemas = [];
101 this.plugins = [];
102 // For internal debugging. Do not use this to try to save a schema in MDB.
103 this.$id = ++id;
104
105 this.s = {
106 hooks: new Kareem()
107 };
108
109 this.options = this.defaultOptions(options);
110
111 // build paths
112 if (Array.isArray(obj)) {
113 for (const definition of obj) {
114 this.add(definition);
115 }
116 } else if (obj) {
117 this.add(obj);
118 }
119
120 // check if _id's value is a subdocument (gh-2276)
121 const _idSubDoc = obj && obj._id && utils.isObject(obj._id);
122
123 // ensure the documents get an auto _id unless disabled
124 const auto_id = !this.paths['_id'] &&
125 (!this.options.noId && this.options._id) && !_idSubDoc;
126
127 if (auto_id) {
128 const _obj = {_id: {auto: true}};
129 _obj._id[this.options.typeKey] = Schema.ObjectId;
130 this.add(_obj);
131 }
132
133 this.setupTimestamp(this.options.timestamps);
134}
135
136/*!
137 * Create virtual properties with alias field
138 */
139function aliasFields(schema, paths) {
140 paths = paths || Object.keys(schema.paths);
141 for (const path of paths) {
142 const options = get(schema.paths[path], 'options');
143 if (options == null) {
144 continue;
145 }
146
147 const prop = schema.paths[path].path;
148 const alias = options.alias;
149
150 if (!alias) {
151 continue;
152 }
153
154 if (typeof alias !== 'string') {
155 throw new Error('Invalid value for alias option on ' + prop + ', got ' + alias);
156 }
157
158 schema.aliases[alias] = prop;
159
160 schema.
161 virtual(alias).
162 get((function(p) {
163 return function() {
164 if (typeof this.get === 'function') {
165 return this.get(p);
166 }
167 return this[p];
168 };
169 })(prop)).
170 set((function(p) {
171 return function(v) {
172 return this.set(p, v);
173 };
174 })(prop));
175 }
176}
177
178/*!
179 * Inherit from EventEmitter.
180 */
181Schema.prototype = Object.create(EventEmitter.prototype);
182Schema.prototype.constructor = Schema;
183Schema.prototype.instanceOfSchema = true;
184
185/*!
186 * ignore
187 */
188
189Object.defineProperty(Schema.prototype, '$schemaType', {
190 configurable: false,
191 enumerable: false,
192 writable: true
193});
194
195/**
196 * Array of child schemas (from document arrays and single nested subdocs)
197 * and their corresponding compiled models. Each element of the array is
198 * an object with 2 properties: `schema` and `model`.
199 *
200 * This property is typically only useful for plugin authors and advanced users.
201 * You do not need to interact with this property at all to use mongoose.
202 *
203 * @api public
204 * @property childSchemas
205 * @memberOf Schema
206 * @instance
207 */
208
209Object.defineProperty(Schema.prototype, 'childSchemas', {
210 configurable: false,
211 enumerable: true,
212 writable: true
213});
214
215/**
216 * The original object passed to the schema constructor
217 *
218 * ####Example:
219 *
220 * var schema = new Schema({ a: String }).add({ b: String });
221 * schema.obj; // { a: String }
222 *
223 * @api public
224 * @property obj
225 * @memberOf Schema
226 * @instance
227 */
228
229Schema.prototype.obj;
230
231/**
232 * Schema as flat paths
233 *
234 * ####Example:
235 * {
236 * '_id' : SchemaType,
237 * , 'nested.key' : SchemaType,
238 * }
239 *
240 * @api private
241 * @property paths
242 * @memberOf Schema
243 * @instance
244 */
245
246Schema.prototype.paths;
247
248/**
249 * Schema as a tree
250 *
251 * ####Example:
252 * {
253 * '_id' : ObjectId
254 * , 'nested' : {
255 * 'key' : String
256 * }
257 * }
258 *
259 * @api private
260 * @property tree
261 * @memberOf Schema
262 * @instance
263 */
264
265Schema.prototype.tree;
266
267/**
268 * Returns a deep copy of the schema
269 *
270 * ####Example:
271 *
272 * const schema = new Schema({ name: String });
273 * const clone = schema.clone();
274 * clone === schema; // false
275 * clone.path('name'); // SchemaString { ... }
276 *
277 * @return {Schema} the cloned schema
278 * @api public
279 * @memberOf Schema
280 * @instance
281 */
282
283Schema.prototype.clone = function() {
284 const s = new Schema({}, this._userProvidedOptions);
285 s.base = this.base;
286 s.obj = this.obj;
287 s.options = utils.clone(this.options);
288 s.callQueue = this.callQueue.map(function(f) { return f; });
289 s.methods = utils.clone(this.methods);
290 s.methodOptions = utils.clone(this.methodOptions);
291 s.statics = utils.clone(this.statics);
292 s.query = utils.clone(this.query);
293 s.plugins = Array.prototype.slice.call(this.plugins);
294 s._indexes = utils.clone(this._indexes);
295 s.s.hooks = this.s.hooks.clone();
296 s._originalSchema = this._originalSchema == null ?
297 this._originalSchema :
298 this._originalSchema.clone();
299
300 s.tree = utils.clone(this.tree);
301 s.paths = utils.clone(this.paths);
302 s.nested = utils.clone(this.nested);
303 s.subpaths = utils.clone(this.subpaths);
304 s.singleNestedPaths = utils.clone(this.singleNestedPaths);
305 s.childSchemas = gatherChildSchemas(s);
306
307 s.virtuals = utils.clone(this.virtuals);
308 s.$globalPluginsApplied = this.$globalPluginsApplied;
309 s.$isRootDiscriminator = this.$isRootDiscriminator;
310 s.$implicitlyCreated = this.$implicitlyCreated;
311
312 if (this.discriminatorMapping != null) {
313 s.discriminatorMapping = Object.assign({}, this.discriminatorMapping);
314 }
315 if (s.discriminators != null) {
316 s.discriminators = Object.assign({}, this.discriminators);
317 }
318
319 s.aliases = Object.assign({}, this.aliases);
320
321 // Bubble up `init` for backwards compat
322 s.on('init', v => this.emit('init', v));
323
324 return s;
325};
326
327/**
328 * Returns default options for this schema, merged with `options`.
329 *
330 * @param {Object} options
331 * @return {Object}
332 * @api private
333 */
334
335Schema.prototype.defaultOptions = function(options) {
336 if (options && options.safe === false) {
337 options.safe = {w: 0};
338 }
339
340 if (options && options.safe && options.safe.w === 0) {
341 // if you turn off safe writes, then versioning goes off as well
342 options.versionKey = false;
343 }
344
345 this._userProvidedOptions = options == null ? {} : utils.clone(options);
346
347 const baseOptions = get(this, 'base.options', {});
348 options = utils.options({
349 strict: 'strict' in baseOptions ? baseOptions.strict : true,
350 bufferCommands: true,
351 capped: false, // { size, max, autoIndexId }
352 versionKey: '__v',
353 discriminatorKey: '__t',
354 minimize: true,
355 autoIndex: null,
356 shardKey: null,
357 read: null,
358 validateBeforeSave: true,
359 // the following are only applied at construction time
360 noId: false, // deprecated, use { _id: false }
361 _id: true,
362 noVirtualId: false, // deprecated, use { id: false }
363 id: true,
364 typeKey: 'type'
365 }, utils.clone(options));
366
367 if (options.read) {
368 options.read = readPref(options.read);
369 }
370
371 return options;
372};
373
374/**
375 * Adds key path / schema type pairs to this schema.
376 *
377 * ####Example:
378 *
379 * const ToySchema = new Schema();
380 * ToySchema.add({ name: 'string', color: 'string', price: 'number' });
381 *
382 * const TurboManSchema = new Schema();
383 * // You can also `add()` another schema and copy over all paths, virtuals,
384 * // getters, setters, indexes, methods, and statics.
385 * TurboManSchema.add(ToySchema).add({ year: Number });
386 *
387 * @param {Object|Schema} obj plain object with paths to add, or another schema
388 * @param {String} [prefix] path to prefix the newly added paths with
389 * @return {Schema} the Schema instance
390 * @api public
391 */
392
393Schema.prototype.add = function add(obj, prefix) {
394 if (obj instanceof Schema) {
395 merge(this, obj);
396 return;
397 }
398
399 // Special case: setting top-level `_id` to false should convert to disabling
400 // the `_id` option. This behavior never worked before 5.4.11 but numerous
401 // codebases use it (see gh-7516, gh-7512).
402 if (obj._id === false && prefix == null) {
403 delete obj._id;
404 this.options._id = false;
405 }
406
407 prefix = prefix || '';
408 const keys = Object.keys(obj);
409
410 for (let i = 0; i < keys.length; ++i) {
411 const key = keys[i];
412
413 if (obj[key] == null) {
414 throw new TypeError('Invalid value for schema path `' + prefix + key + '`');
415 }
416
417 if (Array.isArray(obj[key]) && obj[key].length === 1 && obj[key][0] == null) {
418 throw new TypeError('Invalid value for schema Array path `' + prefix + key + '`');
419 }
420
421 if (utils.isPOJO(obj[key]) &&
422 (!obj[key][this.options.typeKey] || (this.options.typeKey === 'type' && obj[key].type.type))) {
423 if (Object.keys(obj[key]).length) {
424 // nested object { last: { name: String }}
425 this.nested[prefix + key] = true;
426 this.add(obj[key], prefix + key + '.');
427 } else {
428 if (prefix) {
429 this.nested[prefix.substr(0, prefix.length - 1)] = true;
430 }
431 this.path(prefix + key, obj[key]); // mixed type
432 }
433 } else {
434 if (prefix) {
435 this.nested[prefix.substr(0, prefix.length - 1)] = true;
436 }
437 this.path(prefix + key, obj[key]);
438 }
439 }
440
441 const addedKeys = Object.keys(obj).
442 map(key => prefix ? prefix + key : key);
443 aliasFields(this, addedKeys);
444 return this;
445};
446
447/**
448 * Reserved document keys.
449 *
450 * Keys in this object are names that are rejected in schema declarations
451 * because they conflict with Mongoose functionality. If you create a schema
452 * using `new Schema()` with one of these property names, Mongoose will throw
453 * an error.
454 *
455 * - prototype
456 * - emit
457 * - on
458 * - once
459 * - listeners
460 * - removeListener
461 * - collection
462 * - db
463 * - errors
464 * - init
465 * - isModified
466 * - isNew
467 * - get
468 * - modelName
469 * - save
470 * - schema
471 * - toObject
472 * - validate
473 * - remove
474 * - populated
475 * - _pres
476 * - _posts
477 *
478 * _NOTE:_ Use of these terms as method names is permitted, but play at your own risk, as they may be existing mongoose document methods you are stomping on.
479 *
480 * var schema = new Schema(..);
481 * schema.methods.init = function () {} // potentially breaking
482 */
483
484Schema.reserved = Object.create(null);
485Schema.prototype.reserved = Schema.reserved;
486const reserved = Schema.reserved;
487// Core object
488reserved['prototype'] =
489// EventEmitter
490reserved.emit =
491reserved.on =
492reserved.once =
493reserved.listeners =
494reserved.removeListener =
495// document properties and functions
496reserved.collection =
497reserved.db =
498reserved.errors =
499reserved.init =
500reserved.isModified =
501reserved.isNew =
502reserved.get =
503reserved.modelName =
504reserved.save =
505reserved.schema =
506reserved.toObject =
507reserved.validate =
508reserved.remove =
509reserved.populated =
510// hooks.js
511reserved._pres = reserved._posts = 1;
512
513/*!
514 * Document keys to print warnings for
515 */
516
517const warnings = {};
518warnings.increment = '`increment` should not be used as a schema path name ' +
519 'unless you have disabled versioning.';
520
521/**
522 * Gets/sets schema paths.
523 *
524 * Sets a path (if arity 2)
525 * Gets a path (if arity 1)
526 *
527 * ####Example
528 *
529 * schema.path('name') // returns a SchemaType
530 * schema.path('name', Number) // changes the schemaType of `name` to Number
531 *
532 * @param {String} path
533 * @param {Object} constructor
534 * @api public
535 */
536
537Schema.prototype.path = function(path, obj) {
538 if (obj === undefined) {
539 if (this.paths.hasOwnProperty(path)) {
540 return this.paths[path];
541 }
542 if (this.subpaths.hasOwnProperty(path)) {
543 return this.subpaths[path];
544 }
545 if (this.singleNestedPaths.hasOwnProperty(path)) {
546 return this.singleNestedPaths[path];
547 }
548
549 // Look for maps
550 const mapPath = getMapPath(this, path);
551 if (mapPath != null) {
552 return mapPath;
553 }
554
555 // subpaths?
556 return /\.\d+\.?.*$/.test(path)
557 ? getPositionalPath(this, path)
558 : undefined;
559 }
560
561 // some path names conflict with document methods
562 if (reserved[path]) {
563 throw new Error('`' + path + '` may not be used as a schema pathname');
564 }
565
566 if (warnings[path]) {
567 console.log('WARN: ' + warnings[path]);
568 }
569
570 if (typeof obj === 'object' && utils.hasUserDefinedProperty(obj, 'ref')) {
571 validateRef(obj.ref, path);
572 }
573
574 // update the tree
575 const subpaths = path.split(/\./);
576 const last = subpaths.pop();
577 let branch = this.tree;
578
579 subpaths.forEach(function(sub, i) {
580 if (!branch[sub]) {
581 branch[sub] = {};
582 }
583 if (typeof branch[sub] !== 'object') {
584 const msg = 'Cannot set nested path `' + path + '`. '
585 + 'Parent path `'
586 + subpaths.slice(0, i).concat([sub]).join('.')
587 + '` already set to type ' + branch[sub].name
588 + '.';
589 throw new Error(msg);
590 }
591 branch = branch[sub];
592 });
593
594 branch[last] = utils.clone(obj);
595
596 this.paths[path] = this.interpretAsType(path, obj, this.options);
597 const schemaType = this.paths[path];
598
599 if (schemaType.$isSchemaMap) {
600 // Maps can have arbitrary keys, so `$*` is internal shorthand for "any key"
601 // The '$' is to imply this path should never be stored in MongoDB so we
602 // can easily build a regexp out of this path, and '*' to imply "any key."
603 const mapPath = path + '.$*';
604 this.paths[path + '.$*'] = this.interpretAsType(mapPath,
605 obj.of || { type: {} }, this.options);
606 schemaType.$__schemaType = this.paths[path + '.$*'];
607 }
608
609 if (schemaType.$isSingleNested) {
610 for (const key in schemaType.schema.paths) {
611 this.singleNestedPaths[path + '.' + key] = schemaType.schema.paths[key];
612 }
613 for (const key in schemaType.schema.singleNestedPaths) {
614 this.singleNestedPaths[path + '.' + key] =
615 schemaType.schema.singleNestedPaths[key];
616 }
617
618 Object.defineProperty(schemaType.schema, 'base', {
619 configurable: true,
620 enumerable: false,
621 writable: false,
622 value: this.base
623 });
624
625 schemaType.caster.base = this.base;
626 this.childSchemas.push({
627 schema: schemaType.schema,
628 model: schemaType.caster
629 });
630 } else if (schemaType.$isMongooseDocumentArray) {
631 Object.defineProperty(schemaType.schema, 'base', {
632 configurable: true,
633 enumerable: false,
634 writable: false,
635 value: this.base
636 });
637
638 schemaType.casterConstructor.base = this.base;
639 this.childSchemas.push({
640 schema: schemaType.schema,
641 model: schemaType.casterConstructor
642 });
643 }
644
645 return this;
646};
647
648/*!
649 * ignore
650 */
651
652function gatherChildSchemas(schema) {
653 const childSchemas = [];
654
655 for (const path of Object.keys(schema.paths)) {
656 const schematype = schema.paths[path];
657 if (schematype.$isMongooseDocumentArray || schematype.$isSingleNested) {
658 childSchemas.push({ schema: schematype.schema, model: schematype.caster });
659 }
660 }
661
662 return childSchemas;
663}
664
665/*!
666 * ignore
667 */
668
669function getMapPath(schema, path) {
670 for (const _path of Object.keys(schema.paths)) {
671 if (!_path.includes('.$*')) {
672 continue;
673 }
674 const re = new RegExp('^' + _path.replace(/\.\$\*/g, '\\.[^.]+') + '$');
675 if (re.test(path)) {
676 return schema.paths[_path];
677 }
678 }
679
680 return null;
681}
682
683/**
684 * The Mongoose instance this schema is associated with
685 *
686 * @property base
687 * @api private
688 */
689
690Object.defineProperty(Schema.prototype, 'base', {
691 configurable: true,
692 enumerable: false,
693 writable: true,
694 value: null
695});
696
697/**
698 * Converts type arguments into Mongoose Types.
699 *
700 * @param {String} path
701 * @param {Object} obj constructor
702 * @api private
703 */
704
705Schema.prototype.interpretAsType = function(path, obj, options) {
706 if (obj instanceof SchemaType) {
707 return obj;
708 }
709
710 // If this schema has an associated Mongoose object, use the Mongoose object's
711 // copy of SchemaTypes re: gh-7158 gh-6933
712 const MongooseTypes = this.base != null ? this.base.Schema.Types : Schema.Types;
713
714 if (obj.constructor) {
715 const constructorName = utils.getFunctionName(obj.constructor);
716 if (constructorName !== 'Object') {
717 const oldObj = obj;
718 obj = {};
719 obj[options.typeKey] = oldObj;
720 }
721 }
722
723 // Get the type making sure to allow keys named "type"
724 // and default to mixed if not specified.
725 // { type: { type: String, default: 'freshcut' } }
726 let type = obj[options.typeKey] && (options.typeKey !== 'type' || !obj.type.type)
727 ? obj[options.typeKey]
728 : {};
729 let name;
730
731 if (utils.isPOJO(type) || type === 'mixed') {
732 return new MongooseTypes.Mixed(path, obj);
733 }
734
735 if (Array.isArray(type) || Array === type || type === 'array') {
736 // if it was specified through { type } look for `cast`
737 let cast = (Array === type || type === 'array')
738 ? obj.cast
739 : type[0];
740
741 if (cast && cast.instanceOfSchema) {
742 return new MongooseTypes.DocumentArray(path, cast, obj);
743 }
744 if (cast &&
745 cast[options.typeKey] &&
746 cast[options.typeKey].instanceOfSchema) {
747 return new MongooseTypes.DocumentArray(path, cast[options.typeKey], obj, cast);
748 }
749
750 if (Array.isArray(cast)) {
751 return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj);
752 }
753
754 if (typeof cast === 'string') {
755 cast = MongooseTypes[cast.charAt(0).toUpperCase() + cast.substring(1)];
756 } else if (cast && (!cast[options.typeKey] || (options.typeKey === 'type' && cast.type.type))
757 && utils.isPOJO(cast)) {
758 if (Object.keys(cast).length) {
759 // The `minimize` and `typeKey` options propagate to child schemas
760 // declared inline, like `{ arr: [{ val: { $type: String } }] }`.
761 // See gh-3560
762 const childSchemaOptions = {minimize: options.minimize};
763 if (options.typeKey) {
764 childSchemaOptions.typeKey = options.typeKey;
765 }
766 //propagate 'strict' option to child schema
767 if (options.hasOwnProperty('strict')) {
768 childSchemaOptions.strict = options.strict;
769 }
770 const childSchema = new Schema(cast, childSchemaOptions);
771 childSchema.$implicitlyCreated = true;
772 return new MongooseTypes.DocumentArray(path, childSchema, obj);
773 } else {
774 // Special case: empty object becomes mixed
775 return new MongooseTypes.Array(path, MongooseTypes.Mixed, obj);
776 }
777 }
778
779 if (cast) {
780 type = cast[options.typeKey] && (options.typeKey !== 'type' || !cast.type.type)
781 ? cast[options.typeKey]
782 : cast;
783
784 name = typeof type === 'string'
785 ? type
786 : type.schemaName || utils.getFunctionName(type);
787
788 if (!(name in MongooseTypes)) {
789 throw new TypeError('Invalid schema configuration: ' +
790 `\`${name}\` is not a valid type within the array \`${path}\`.` +
791 'See http://bit.ly/mongoose-schematypes for a list of valid schema types.');
792 }
793 }
794
795 return new MongooseTypes.Array(path, cast || MongooseTypes.Mixed, obj, options);
796 }
797
798 if (type && type.instanceOfSchema) {
799 return new MongooseTypes.Embedded(type, path, obj);
800 }
801
802 if (Buffer.isBuffer(type)) {
803 name = 'Buffer';
804 } else if (typeof type === 'function' || typeof type === 'object') {
805 name = type.schemaName || utils.getFunctionName(type);
806 } else {
807 name = type == null ? '' + type : type.toString();
808 }
809
810 if (name) {
811 name = name.charAt(0).toUpperCase() + name.substring(1);
812 }
813 // Special case re: gh-7049 because the bson `ObjectID` class' capitalization
814 // doesn't line up with Mongoose's.
815 if (name === 'ObjectID') {
816 name = 'ObjectId';
817 }
818
819 if (MongooseTypes[name] == null) {
820 throw new TypeError(`Invalid schema configuration: \`${name}\` is not ` +
821 `a valid type at path \`${path}\`. See ` +
822 'http://bit.ly/mongoose-schematypes for a list of valid schema types.');
823 }
824
825 return new MongooseTypes[name](path, obj);
826};
827
828/**
829 * Iterates the schemas paths similar to Array#forEach.
830 *
831 * The callback is passed the pathname and the schemaType instance.
832 *
833 * ####Example:
834 *
835 * const userSchema = new Schema({ name: String, registeredAt: Date });
836 * userSchema.eachPath((pathname, schematype) => {
837 * // Prints twice:
838 * // name SchemaString { ... }
839 * // registeredAt SchemaDate { ... }
840 * console.log(pathname, schematype);
841 * });
842 *
843 * @param {Function} fn callback function
844 * @return {Schema} this
845 * @api public
846 */
847
848Schema.prototype.eachPath = function(fn) {
849 const keys = Object.keys(this.paths);
850 const len = keys.length;
851
852 for (let i = 0; i < len; ++i) {
853 fn(keys[i], this.paths[keys[i]]);
854 }
855
856 return this;
857};
858
859/**
860 * Returns an Array of path strings that are required by this schema.
861 *
862 * ####Example:
863 * const s = new Schema({
864 * name: { type: String, required: true },
865 * age: { type: String, required: true },
866 * notes: String
867 * });
868 * s.requiredPaths(); // [ 'age', 'name' ]
869 *
870 * @api public
871 * @param {Boolean} invalidate refresh the cache
872 * @return {Array}
873 */
874
875Schema.prototype.requiredPaths = function requiredPaths(invalidate) {
876 if (this._requiredpaths && !invalidate) {
877 return this._requiredpaths;
878 }
879
880 const paths = Object.keys(this.paths);
881 let i = paths.length;
882 const ret = [];
883
884 while (i--) {
885 const path = paths[i];
886 if (this.paths[path].isRequired) {
887 ret.push(path);
888 }
889 }
890 this._requiredpaths = ret;
891 return this._requiredpaths;
892};
893
894/**
895 * Returns indexes from fields and schema-level indexes (cached).
896 *
897 * @api private
898 * @return {Array}
899 */
900
901Schema.prototype.indexedPaths = function indexedPaths() {
902 if (this._indexedpaths) {
903 return this._indexedpaths;
904 }
905 this._indexedpaths = this.indexes();
906 return this._indexedpaths;
907};
908
909/**
910 * Returns the pathType of `path` for this schema.
911 *
912 * Given a path, returns whether it is a real, virtual, nested, or ad-hoc/undefined path.
913 *
914 * ####Example:
915 * const s = new Schema({ name: String, nested: { foo: String } });
916 * s.virtual('foo').get(() => 42);
917 * s.pathType('name'); // "real"
918 * s.pathType('nested'); // "nested"
919 * s.pathType('foo'); // "virtual"
920 * s.pathType('fail'); // "adhocOrUndefined"
921 *
922 * @param {String} path
923 * @return {String}
924 * @api public
925 */
926
927Schema.prototype.pathType = function(path) {
928 if (path in this.paths) {
929 return 'real';
930 }
931 if (path in this.virtuals) {
932 return 'virtual';
933 }
934 if (path in this.nested) {
935 return 'nested';
936 }
937 if (path in this.subpaths) {
938 return 'real';
939 }
940 if (path in this.singleNestedPaths) {
941 return 'real';
942 }
943
944 // Look for maps
945 const mapPath = getMapPath(this, path);
946 if (mapPath != null) {
947 return 'real';
948 }
949
950 if (/\.\d+\.|\.\d+$/.test(path)) {
951 return getPositionalPathType(this, path);
952 }
953 return 'adhocOrUndefined';
954};
955
956/**
957 * Returns true iff this path is a child of a mixed schema.
958 *
959 * @param {String} path
960 * @return {Boolean}
961 * @api private
962 */
963
964Schema.prototype.hasMixedParent = function(path) {
965 const subpaths = path.split(/\./g);
966 path = '';
967 for (let i = 0; i < subpaths.length; ++i) {
968 path = i > 0 ? path + '.' + subpaths[i] : subpaths[i];
969 if (path in this.paths &&
970 this.paths[path] instanceof MongooseTypes.Mixed) {
971 return true;
972 }
973 }
974
975 return false;
976};
977
978/**
979 * Setup updatedAt and createdAt timestamps to documents if enabled
980 *
981 * @param {Boolean|Object} timestamps timestamps options
982 * @api private
983 */
984Schema.prototype.setupTimestamp = function(timestamps) {
985 const childHasTimestamp = this.childSchemas.find(withTimestamp);
986
987 function withTimestamp(s) {
988 const ts = s.schema.options.timestamps;
989 return !!ts;
990 }
991
992 if (!timestamps && !childHasTimestamp) {
993 return;
994 }
995
996 const createdAt = handleTimestampOption(timestamps, 'createdAt');
997 const updatedAt = handleTimestampOption(timestamps, 'updatedAt');
998 const schemaAdditions = {};
999
1000 this.$timestamps = { createdAt: createdAt, updatedAt: updatedAt };
1001
1002 if (updatedAt && !this.paths[updatedAt]) {
1003 schemaAdditions[updatedAt] = Date;
1004 }
1005
1006 if (createdAt && !this.paths[createdAt]) {
1007 schemaAdditions[createdAt] = Date;
1008 }
1009
1010 this.add(schemaAdditions);
1011
1012 this.pre('save', function(next) {
1013 if (get(this, '$__.saveOptions.timestamps') === false) {
1014 return next();
1015 }
1016
1017 const defaultTimestamp = (this.ownerDocument ? this.ownerDocument() : this).
1018 constructor.base.now();
1019 const auto_id = this._id && this._id.auto;
1020
1021 if (createdAt && !this.get(createdAt) && this.isSelected(createdAt)) {
1022 this.set(createdAt, auto_id ? this._id.getTimestamp() : defaultTimestamp);
1023 }
1024
1025 if (updatedAt && (this.isNew || this.isModified())) {
1026 let ts = defaultTimestamp;
1027 if (this.isNew) {
1028 if (createdAt != null) {
1029 ts = this.getValue(createdAt);
1030 } else if (auto_id) {
1031 ts = this._id.getTimestamp();
1032 }
1033 }
1034 this.set(updatedAt, ts);
1035 }
1036
1037 next();
1038 });
1039
1040 this.methods.initializeTimestamps = function() {
1041 if (createdAt && !this.get(createdAt)) {
1042 this.set(createdAt, new Date());
1043 }
1044 if (updatedAt && !this.get(updatedAt)) {
1045 this.set(updatedAt, new Date());
1046 }
1047 return this;
1048 };
1049
1050 _setTimestampsOnUpdate[symbols.builtInMiddleware] = true;
1051
1052 const opts = { query: true, model: false };
1053 this.pre('findOneAndUpdate', opts, _setTimestampsOnUpdate);
1054 this.pre('replaceOne', opts, _setTimestampsOnUpdate);
1055 this.pre('update', opts, _setTimestampsOnUpdate);
1056 this.pre('updateOne', opts, _setTimestampsOnUpdate);
1057 this.pre('updateMany', opts, _setTimestampsOnUpdate);
1058
1059 function _setTimestampsOnUpdate(next) {
1060 const now = this.model.base.now();
1061 applyTimestampsToUpdate(now, createdAt, updatedAt, this.getUpdate(),
1062 this.options, this.schema);
1063 applyTimestampsToChildren(now, this.getUpdate(), this.model.schema);
1064 next();
1065 }
1066};
1067
1068/*!
1069 * ignore
1070 */
1071
1072function getPositionalPathType(self, path) {
1073 const subpaths = path.split(/\.(\d+)\.|\.(\d+)$/).filter(Boolean);
1074 if (subpaths.length < 2) {
1075 return self.paths.hasOwnProperty(subpaths[0]) ? self.paths[subpaths[0]] : null;
1076 }
1077
1078 let val = self.path(subpaths[0]);
1079 let isNested = false;
1080 if (!val) {
1081 return val;
1082 }
1083
1084 const last = subpaths.length - 1;
1085
1086 for (let i = 1; i < subpaths.length; ++i) {
1087 isNested = false;
1088 const subpath = subpaths[i];
1089
1090 if (i === last && val && !/\D/.test(subpath)) {
1091 if (val.$isMongooseDocumentArray) {
1092 const oldVal = val;
1093 val = new SchemaType(subpath, {
1094 required: get(val, 'schemaOptions.required', false)
1095 });
1096 val.cast = function(value, doc, init) {
1097 return oldVal.cast(value, doc, init)[0];
1098 };
1099 val.$isMongooseDocumentArrayElement = true;
1100 val.caster = oldVal.caster;
1101 val.schema = oldVal.schema;
1102 } else if (val instanceof MongooseTypes.Array) {
1103 // StringSchema, NumberSchema, etc
1104 val = val.caster;
1105 } else {
1106 val = undefined;
1107 }
1108 break;
1109 }
1110
1111 // ignore if its just a position segment: path.0.subpath
1112 if (!/\D/.test(subpath)) {
1113 continue;
1114 }
1115
1116 if (!(val && val.schema)) {
1117 val = undefined;
1118 break;
1119 }
1120
1121 const type = val.schema.pathType(subpath);
1122 isNested = (type === 'nested');
1123 val = val.schema.path(subpath);
1124 }
1125
1126 self.subpaths[path] = val;
1127 if (val) {
1128 return 'real';
1129 }
1130 if (isNested) {
1131 return 'nested';
1132 }
1133 return 'adhocOrUndefined';
1134}
1135
1136
1137/*!
1138 * ignore
1139 */
1140
1141function getPositionalPath(self, path) {
1142 getPositionalPathType(self, path);
1143 return self.subpaths[path];
1144}
1145
1146/**
1147 * Adds a method call to the queue.
1148 *
1149 * ####Example:
1150 *
1151 * schema.methods.print = function() { console.log(this); };
1152 * schema.queue('print', []); // Print the doc every one is instantiated
1153 *
1154 * const Model = mongoose.model('Test', schema);
1155 * new Model({ name: 'test' }); // Prints '{"_id": ..., "name": "test" }'
1156 *
1157 * @param {String} name name of the document method to call later
1158 * @param {Array} args arguments to pass to the method
1159 * @api public
1160 */
1161
1162Schema.prototype.queue = function(name, args) {
1163 this.callQueue.push([name, args]);
1164 return this;
1165};
1166
1167/**
1168 * Defines a pre hook for the document.
1169 *
1170 * ####Example
1171 *
1172 * var toySchema = new Schema({ name: String, created: Date });
1173 *
1174 * toySchema.pre('save', function(next) {
1175 * if (!this.created) this.created = new Date;
1176 * next();
1177 * });
1178 *
1179 * toySchema.pre('validate', function(next) {
1180 * if (this.name !== 'Woody') this.name = 'Woody';
1181 * next();
1182 * });
1183 *
1184 * // Equivalent to calling `pre()` on `find`, `findOne`, `findOneAndUpdate`.
1185 * toySchema.pre(/^find/, function(next) {
1186 * console.log(this.getQuery());
1187 * });
1188 *
1189 * @param {String|RegExp} The method name or regular expression to match method name
1190 * @param {Object} [options]
1191 * @param {Boolean} [options.document] If `name` is a hook for both document and query middleware, set to `true` to run on document middleware.
1192 * @param {Boolean} [options.query] If `name` is a hook for both document and query middleware, set to `true` to run on query middleware.
1193 * @param {Function} callback
1194 * @see hooks.js https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3
1195 * @api public
1196 */
1197
1198Schema.prototype.pre = function(name) {
1199 if (name instanceof RegExp) {
1200 const remainingArgs = Array.prototype.slice.call(arguments, 1);
1201 for (const fn of hookNames) {
1202 if (name.test(fn)) {
1203 this.pre.apply(this, [fn].concat(remainingArgs));
1204 }
1205 }
1206 return this;
1207 }
1208 this.s.hooks.pre.apply(this.s.hooks, arguments);
1209 return this;
1210};
1211
1212/**
1213 * Defines a post hook for the document
1214 *
1215 * var schema = new Schema(..);
1216 * schema.post('save', function (doc) {
1217 * console.log('this fired after a document was saved');
1218 * });
1219 *
1220 * schema.post('find', function(docs) {
1221 * console.log('this fired after you ran a find query');
1222 * });
1223 *
1224 * schema.post(/Many$/, function(res) {
1225 * console.log('this fired after you ran `updateMany()` or `deleteMany()`);
1226 * });
1227 *
1228 * var Model = mongoose.model('Model', schema);
1229 *
1230 * var m = new Model(..);
1231 * m.save(function(err) {
1232 * console.log('this fires after the `post` hook');
1233 * });
1234 *
1235 * m.find(function(err, docs) {
1236 * console.log('this fires after the post find hook');
1237 * });
1238 *
1239 * @param {String|RegExp} The method name or regular expression to match method name
1240 * @param {Object} [options]
1241 * @param {Boolean} [options.document] If `name` is a hook for both document and query middleware, set to `true` to run on document middleware.
1242 * @param {Boolean} [options.query] If `name` is a hook for both document and query middleware, set to `true` to run on query middleware.
1243 * @param {Function} fn callback
1244 * @see middleware http://mongoosejs.com/docs/middleware.html
1245 * @see kareem http://npmjs.org/package/kareem
1246 * @api public
1247 */
1248
1249Schema.prototype.post = function(name) {
1250 if (name instanceof RegExp) {
1251 const remainingArgs = Array.prototype.slice.call(arguments, 1);
1252 for (const fn of hookNames) {
1253 if (name.test(fn)) {
1254 this.post.apply(this, [fn].concat(remainingArgs));
1255 }
1256 }
1257 return this;
1258 }
1259 this.s.hooks.post.apply(this.s.hooks, arguments);
1260 return this;
1261};
1262
1263/**
1264 * Registers a plugin for this schema.
1265 *
1266 * ####Example:
1267 *
1268 * const s = new Schema({ name: String });
1269 * s.plugin(schema => console.log(schema.path('name').path));
1270 * mongoose.model('Test', schema); // Prints 'name'
1271 *
1272 * @param {Function} plugin callback
1273 * @param {Object} [opts]
1274 * @see plugins
1275 * @api public
1276 */
1277
1278Schema.prototype.plugin = function(fn, opts) {
1279 if (typeof fn !== 'function') {
1280 throw new Error('First param to `schema.plugin()` must be a function, ' +
1281 'got "' + (typeof fn) + '"');
1282 }
1283
1284 if (opts &&
1285 opts.deduplicate) {
1286 for (let i = 0; i < this.plugins.length; ++i) {
1287 if (this.plugins[i].fn === fn) {
1288 return this;
1289 }
1290 }
1291 }
1292 this.plugins.push({ fn: fn, opts: opts });
1293
1294 fn(this, opts);
1295 return this;
1296};
1297
1298/**
1299 * Adds an instance method to documents constructed from Models compiled from this schema.
1300 *
1301 * ####Example
1302 *
1303 * var schema = kittySchema = new Schema(..);
1304 *
1305 * schema.method('meow', function () {
1306 * console.log('meeeeeoooooooooooow');
1307 * })
1308 *
1309 * var Kitty = mongoose.model('Kitty', schema);
1310 *
1311 * var fizz = new Kitty;
1312 * fizz.meow(); // meeeeeooooooooooooow
1313 *
1314 * If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as methods.
1315 *
1316 * schema.method({
1317 * purr: function () {}
1318 * , scratch: function () {}
1319 * });
1320 *
1321 * // later
1322 * fizz.purr();
1323 * fizz.scratch();
1324 *
1325 * NOTE: `Schema.method()` adds instance methods to the `Schema.methods` object. You can also add instance methods directly to the `Schema.methods` object as seen in the [guide](./guide.html#methods)
1326 *
1327 * @param {String|Object} method name
1328 * @param {Function} [fn]
1329 * @api public
1330 */
1331
1332Schema.prototype.method = function(name, fn, options) {
1333 if (typeof name !== 'string') {
1334 for (const i in name) {
1335 this.methods[i] = name[i];
1336 this.methodOptions[i] = utils.clone(options);
1337 }
1338 } else {
1339 this.methods[name] = fn;
1340 this.methodOptions[name] = utils.clone(options);
1341 }
1342 return this;
1343};
1344
1345/**
1346 * Adds static "class" methods to Models compiled from this schema.
1347 *
1348 * ####Example
1349 *
1350 * var schema = new Schema(..);
1351 * schema.static('findByName', function (name, callback) {
1352 * return this.find({ name: name }, callback);
1353 * });
1354 *
1355 * var Drink = mongoose.model('Drink', schema);
1356 * Drink.findByName('sanpellegrino', function (err, drinks) {
1357 * //
1358 * });
1359 *
1360 * If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as statics.
1361 *
1362 * @param {String|Object} name
1363 * @param {Function} [fn]
1364 * @api public
1365 */
1366
1367Schema.prototype.static = function(name, fn) {
1368 if (typeof name !== 'string') {
1369 for (const i in name) {
1370 this.statics[i] = name[i];
1371 }
1372 } else {
1373 this.statics[name] = fn;
1374 }
1375 return this;
1376};
1377
1378/**
1379 * Defines an index (most likely compound) for this schema.
1380 *
1381 * ####Example
1382 *
1383 * schema.index({ first: 1, last: -1 })
1384 *
1385 * @param {Object} fields
1386 * @param {Object} [options] Options to pass to [MongoDB driver's `createIndex()` function](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#createIndex)
1387 * @param {String} [options.expires=null] Mongoose-specific syntactic sugar, uses [ms](https://www.npmjs.com/package/ms) to convert `expires` option into seconds for the `expireAfterSeconds` in the above link.
1388 * @api public
1389 */
1390
1391Schema.prototype.index = function(fields, options) {
1392 fields || (fields = {});
1393 options || (options = {});
1394
1395 if (options.expires) {
1396 utils.expires(options);
1397 }
1398
1399 this._indexes.push([fields, options]);
1400 return this;
1401};
1402
1403/**
1404 * Sets/gets a schema option.
1405 *
1406 * ####Example
1407 *
1408 * schema.set('strict'); // 'true' by default
1409 * schema.set('strict', false); // Sets 'strict' to false
1410 * schema.set('strict'); // 'false'
1411 *
1412 * @param {String} key option name
1413 * @param {Object} [value] if not passed, the current option value is returned
1414 * @see Schema ./
1415 * @api public
1416 */
1417
1418Schema.prototype.set = function(key, value, _tags) {
1419 if (arguments.length === 1) {
1420 return this.options[key];
1421 }
1422
1423 switch (key) {
1424 case 'read':
1425 this.options[key] = readPref(value, _tags);
1426 this._userProvidedOptions[key] = this.options[key];
1427 break;
1428 case 'safe':
1429 setSafe(this.options, value);
1430 this._userProvidedOptions[key] = this.options[key];
1431 break;
1432 case 'timestamps':
1433 this.setupTimestamp(value);
1434 this.options[key] = value;
1435 this._userProvidedOptions[key] = this.options[key];
1436 break;
1437 default:
1438 this.options[key] = value;
1439 this._userProvidedOptions[key] = this.options[key];
1440 break;
1441 }
1442
1443 return this;
1444};
1445
1446/*!
1447 * ignore
1448 */
1449
1450const safeDeprecationWarning = 'Mongoose: The `safe` option for schemas is ' +
1451 'deprecated. Use the `writeConcern` option instead: ' +
1452 'http://bit.ly/mongoose-write-concern';
1453
1454const setSafe = util.deprecate(function setSafe(options, value) {
1455 options.safe = value === false ?
1456 {w: 0} :
1457 value;
1458}, safeDeprecationWarning);
1459
1460/**
1461 * Gets a schema option.
1462 *
1463 * ####Example:
1464 *
1465 * schema.get('strict'); // true
1466 * schema.set('strict', false);
1467 * schema.get('strict'); // false
1468 *
1469 * @param {String} key option name
1470 * @api public
1471 * @return {Any} the option's value
1472 */
1473
1474Schema.prototype.get = function(key) {
1475 return this.options[key];
1476};
1477
1478/**
1479 * The allowed index types
1480 *
1481 * @receiver Schema
1482 * @static indexTypes
1483 * @api public
1484 */
1485
1486const indexTypes = '2d 2dsphere hashed text'.split(' ');
1487
1488Object.defineProperty(Schema, 'indexTypes', {
1489 get: function() {
1490 return indexTypes;
1491 },
1492 set: function() {
1493 throw new Error('Cannot overwrite Schema.indexTypes');
1494 }
1495});
1496
1497/**
1498 * Returns a list of indexes that this schema declares, via `schema.index()`
1499 * or by `index: true` in a path's options.
1500 *
1501 * ####Example:
1502 *
1503 * const userSchema = new Schema({
1504 * email: { type: String, required: true, unique: true },
1505 * registeredAt: { type: Date, index: true }
1506 * });
1507 *
1508 * // [ [ { email: 1 }, { unique: true, background: true } ],
1509 * // [ { registeredAt: 1 }, { background: true } ] ]
1510 * userSchema.indexes();
1511 *
1512 * @api public
1513 * @return {Array} list of indexes defined in the schema
1514 */
1515
1516Schema.prototype.indexes = function() {
1517 return getIndexes(this);
1518};
1519
1520/**
1521 * Creates a virtual type with the given name.
1522 *
1523 * @param {String} name
1524 * @param {Object} [options]
1525 * @param {String|Model} [options.ref] model name or model instance. Marks this as a [populate virtual](populate.html#populate-virtuals).
1526 * @param {String|Function} [options.localField] Required for populate virtuals. See [populate virtual docs](populate.html#populate-virtuals) for more information.
1527 * @param {String|Function} [options.foreignField] Required for populate virtuals. See [populate virtual docs](populate.html#populate-virtuals) for more information.
1528 * @param {Boolean|Function} [options.justOne=false] Only works with populate virtuals. If truthy, will be a single doc or `null`. Otherwise, the populate virtual will be an array.
1529 * @param {Boolean} [options.count=false] Only works with populate virtuals. If truthy, this populate virtual will contain the number of documents rather than the documents themselves when you `populate()`.
1530 * @return {VirtualType}
1531 */
1532
1533Schema.prototype.virtual = function(name, options) {
1534 if (options && options.ref) {
1535 if (!options.localField) {
1536 throw new Error('Reference virtuals require `localField` option');
1537 }
1538
1539 if (!options.foreignField) {
1540 throw new Error('Reference virtuals require `foreignField` option');
1541 }
1542
1543 this.pre('init', function(obj) {
1544 if (mpath.has(name, obj)) {
1545 const _v = mpath.get(name, obj);
1546 if (!this.$$populatedVirtuals) {
1547 this.$$populatedVirtuals = {};
1548 }
1549
1550 if (options.justOne || options.count) {
1551 this.$$populatedVirtuals[name] = Array.isArray(_v) ?
1552 _v[0] :
1553 _v;
1554 } else {
1555 this.$$populatedVirtuals[name] = Array.isArray(_v) ?
1556 _v :
1557 _v == null ? [] : [_v];
1558 }
1559
1560 mpath.unset(name, obj);
1561 }
1562 });
1563
1564 const virtual = this.virtual(name);
1565 virtual.options = options;
1566 return virtual.
1567 get(function() {
1568 if (!this.$$populatedVirtuals) {
1569 this.$$populatedVirtuals = {};
1570 }
1571 if (this.$$populatedVirtuals.hasOwnProperty(name)) {
1572 return this.$$populatedVirtuals[name];
1573 }
1574 return void 0;
1575 }).
1576 set(function(_v) {
1577 if (!this.$$populatedVirtuals) {
1578 this.$$populatedVirtuals = {};
1579 }
1580
1581 if (options.justOne || options.count) {
1582 this.$$populatedVirtuals[name] = Array.isArray(_v) ?
1583 _v[0] :
1584 _v;
1585
1586 if (typeof this.$$populatedVirtuals[name] !== 'object') {
1587 this.$$populatedVirtuals[name] = options.count ? _v : null;
1588 }
1589 } else {
1590 this.$$populatedVirtuals[name] = Array.isArray(_v) ?
1591 _v :
1592 _v == null ? [] : [_v];
1593
1594 this.$$populatedVirtuals[name] = this.$$populatedVirtuals[name].filter(function(doc) {
1595 return doc && typeof doc === 'object';
1596 });
1597 }
1598 });
1599 }
1600
1601 const virtuals = this.virtuals;
1602 const parts = name.split('.');
1603
1604 if (this.pathType(name) === 'real') {
1605 throw new Error('Virtual path "' + name + '"' +
1606 ' conflicts with a real path in the schema');
1607 }
1608
1609 virtuals[name] = parts.reduce(function(mem, part, i) {
1610 mem[part] || (mem[part] = (i === parts.length - 1)
1611 ? new VirtualType(options, name)
1612 : {});
1613 return mem[part];
1614 }, this.tree);
1615
1616 return virtuals[name];
1617};
1618
1619/**
1620 * Returns the virtual type with the given `name`.
1621 *
1622 * @param {String} name
1623 * @return {VirtualType}
1624 */
1625
1626Schema.prototype.virtualpath = function(name) {
1627 return this.virtuals.hasOwnProperty(name) ? this.virtuals[name] : null;
1628};
1629
1630/**
1631 * Removes the given `path` (or [`paths`]).
1632 *
1633 * ####Example:
1634 *
1635 * const schema = new Schema({ name: String, age: Number });
1636 * schema.remove('name');
1637 * schema.path('name'); // Undefined
1638 * schema.path('age'); // SchemaNumber { ... }
1639 *
1640 * @param {String|Array} path
1641 * @return {Schema} the Schema instance
1642 * @api public
1643 */
1644Schema.prototype.remove = function(path) {
1645 if (typeof path === 'string') {
1646 path = [path];
1647 }
1648 if (Array.isArray(path)) {
1649 path.forEach(function(name) {
1650 if (this.path(name) == null && !this.nested[name]) {
1651 return;
1652 }
1653 if (this.nested[name]) {
1654 const allKeys = Object.keys(this.paths).
1655 concat(Object.keys(this.nested));
1656 for (const path of allKeys) {
1657 if (path.startsWith(name + '.')) {
1658 delete this.paths[path];
1659 delete this.nested[path];
1660 _deletePath(this, path);
1661 }
1662 }
1663
1664 delete this.nested[name];
1665 _deletePath(this, name);
1666 return;
1667 }
1668
1669 delete this.paths[name];
1670 _deletePath(this, name);
1671 }, this);
1672 }
1673 return this;
1674};
1675
1676/*!
1677 * ignore
1678 */
1679
1680function _deletePath(schema, name) {
1681 const pieces = name.split('.');
1682 const last = pieces.pop();
1683 let branch = schema.tree;
1684 for (let i = 0; i < pieces.length; ++i) {
1685 branch = branch[pieces[i]];
1686 }
1687 delete branch[last];
1688}
1689
1690/**
1691 * Loads an ES6 class into a schema. Maps [setters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) + [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get), [static methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static),
1692 * and [instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Class_body_and_method_definitions)
1693 * to schema [virtuals](http://mongoosejs.com/docs/guide.html#virtuals),
1694 * [statics](http://mongoosejs.com/docs/guide.html#statics), and
1695 * [methods](http://mongoosejs.com/docs/guide.html#methods).
1696 *
1697 * ####Example:
1698 *
1699 * ```javascript
1700 * const md5 = require('md5');
1701 * const userSchema = new Schema({ email: String });
1702 * class UserClass {
1703 * // `gravatarImage` becomes a virtual
1704 * get gravatarImage() {
1705 * const hash = md5(this.email.toLowerCase());
1706 * return `https://www.gravatar.com/avatar/${hash}`;
1707 * }
1708 *
1709 * // `getProfileUrl()` becomes a document method
1710 * getProfileUrl() {
1711 * return `https://mysite.com/${this.email}`;
1712 * }
1713 *
1714 * // `findByEmail()` becomes a static
1715 * static findByEmail(email) {
1716 * return this.findOne({ email });
1717 * }
1718 * }
1719 *
1720 * // `schema` will now have a `gravatarImage` virtual, a `getProfileUrl()` method,
1721 * // and a `findByEmail()` static
1722 * userSchema.loadClass(UserClass);
1723 * ```
1724 *
1725 * @param {Function} model
1726 * @param {Boolean} [virtualsOnly] if truthy, only pulls virtuals from the class, not methods or statics
1727 */
1728Schema.prototype.loadClass = function(model, virtualsOnly) {
1729 if (model === Object.prototype ||
1730 model === Function.prototype ||
1731 model.prototype.hasOwnProperty('$isMongooseModelPrototype')) {
1732 return this;
1733 }
1734
1735 this.loadClass(Object.getPrototypeOf(model));
1736
1737 // Add static methods
1738 if (!virtualsOnly) {
1739 Object.getOwnPropertyNames(model).forEach(function(name) {
1740 if (name.match(/^(length|name|prototype)$/)) {
1741 return;
1742 }
1743 const method = Object.getOwnPropertyDescriptor(model, name);
1744 if (typeof method.value === 'function') {
1745 this.static(name, method.value);
1746 }
1747 }, this);
1748 }
1749
1750 // Add methods and virtuals
1751 Object.getOwnPropertyNames(model.prototype).forEach(function(name) {
1752 if (name.match(/^(constructor)$/)) {
1753 return;
1754 }
1755 const method = Object.getOwnPropertyDescriptor(model.prototype, name);
1756 if (!virtualsOnly) {
1757 if (typeof method.value === 'function') {
1758 this.method(name, method.value);
1759 }
1760 }
1761 if (typeof method.get === 'function') {
1762 this.virtual(name).get(method.get);
1763 }
1764 if (typeof method.set === 'function') {
1765 this.virtual(name).set(method.set);
1766 }
1767 }, this);
1768
1769 return this;
1770};
1771
1772/*!
1773 * ignore
1774 */
1775
1776Schema.prototype._getSchema = function(path) {
1777 const _this = this;
1778 const pathschema = _this.path(path);
1779 const resultPath = [];
1780
1781 if (pathschema) {
1782 pathschema.$fullPath = path;
1783 return pathschema;
1784 }
1785
1786 function search(parts, schema) {
1787 let p = parts.length + 1;
1788 let foundschema;
1789 let trypath;
1790
1791 while (p--) {
1792 trypath = parts.slice(0, p).join('.');
1793 foundschema = schema.path(trypath);
1794 if (foundschema) {
1795 resultPath.push(trypath);
1796
1797 if (foundschema.caster) {
1798 // array of Mixed?
1799 if (foundschema.caster instanceof MongooseTypes.Mixed) {
1800 foundschema.caster.$fullPath = resultPath.join('.');
1801 return foundschema.caster;
1802 }
1803
1804 // Now that we found the array, we need to check if there
1805 // are remaining document paths to look up for casting.
1806 // Also we need to handle array.$.path since schema.path
1807 // doesn't work for that.
1808 // If there is no foundschema.schema we are dealing with
1809 // a path like array.$
1810 if (p !== parts.length && foundschema.schema) {
1811 let ret;
1812 if (parts[p] === '$' || isArrayFilter(parts[p])) {
1813 if (p + 1 === parts.length) {
1814 // comments.$
1815 return foundschema;
1816 }
1817 // comments.$.comments.$.title
1818 ret = search(parts.slice(p + 1), foundschema.schema);
1819 if (ret) {
1820 ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
1821 !foundschema.schema.$isSingleNested;
1822 }
1823 return ret;
1824 }
1825 // this is the last path of the selector
1826 ret = search(parts.slice(p), foundschema.schema);
1827 if (ret) {
1828 ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
1829 !foundschema.schema.$isSingleNested;
1830 }
1831 return ret;
1832 }
1833 }
1834
1835 foundschema.$fullPath = resultPath.join('.');
1836
1837 return foundschema;
1838 }
1839 }
1840 }
1841
1842 // look for arrays
1843 const parts = path.split('.');
1844 for (let i = 0; i < parts.length; ++i) {
1845 if (parts[i] === '$' || isArrayFilter(parts[i])) {
1846 // Re: gh-5628, because `schema.path()` doesn't take $ into account.
1847 parts[i] = '0';
1848 }
1849 }
1850 return search(parts, _this);
1851};
1852
1853/*!
1854 * ignore
1855 */
1856
1857Schema.prototype._getPathType = function(path) {
1858 const _this = this;
1859 const pathschema = _this.path(path);
1860
1861 if (pathschema) {
1862 return 'real';
1863 }
1864
1865 function search(parts, schema) {
1866 let p = parts.length + 1,
1867 foundschema,
1868 trypath;
1869
1870 while (p--) {
1871 trypath = parts.slice(0, p).join('.');
1872 foundschema = schema.path(trypath);
1873 if (foundschema) {
1874 if (foundschema.caster) {
1875 // array of Mixed?
1876 if (foundschema.caster instanceof MongooseTypes.Mixed) {
1877 return { schema: foundschema, pathType: 'mixed' };
1878 }
1879
1880 // Now that we found the array, we need to check if there
1881 // are remaining document paths to look up for casting.
1882 // Also we need to handle array.$.path since schema.path
1883 // doesn't work for that.
1884 // If there is no foundschema.schema we are dealing with
1885 // a path like array.$
1886 if (p !== parts.length && foundschema.schema) {
1887 if (parts[p] === '$' || isArrayFilter(parts[p])) {
1888 if (p === parts.length - 1) {
1889 return { schema: foundschema, pathType: 'nested' };
1890 }
1891 // comments.$.comments.$.title
1892 return search(parts.slice(p + 1), foundschema.schema);
1893 }
1894 // this is the last path of the selector
1895 return search(parts.slice(p), foundschema.schema);
1896 }
1897 return {
1898 schema: foundschema,
1899 pathType: foundschema.$isSingleNested ? 'nested' : 'array'
1900 };
1901 }
1902 return { schema: foundschema, pathType: 'real' };
1903 } else if (p === parts.length && schema.nested[trypath]) {
1904 return { schema: schema, pathType: 'nested' };
1905 }
1906 }
1907 return { schema: foundschema || schema, pathType: 'undefined' };
1908 }
1909
1910 // look for arrays
1911 return search(path.split('.'), _this);
1912};
1913
1914/*!
1915 * ignore
1916 */
1917
1918function isArrayFilter(piece) {
1919 return piece.indexOf('$[') === 0 &&
1920 piece.lastIndexOf(']') === piece.length - 1;
1921}
1922
1923/*!
1924 * Module exports.
1925 */
1926
1927module.exports = exports = Schema;
1928
1929// require down here because of reference issues
1930
1931/**
1932 * The various built-in Mongoose Schema Types.
1933 *
1934 * ####Example:
1935 *
1936 * var mongoose = require('mongoose');
1937 * var ObjectId = mongoose.Schema.Types.ObjectId;
1938 *
1939 * ####Types:
1940 *
1941 * - [String](#schema-string-js)
1942 * - [Number](#schema-number-js)
1943 * - [Boolean](#schema-boolean-js) | Bool
1944 * - [Array](#schema-array-js)
1945 * - [Buffer](#schema-buffer-js)
1946 * - [Date](#schema-date-js)
1947 * - [ObjectId](#schema-objectid-js) | Oid
1948 * - [Mixed](#schema-mixed-js)
1949 *
1950 * Using this exposed access to the `Mixed` SchemaType, we can use them in our schema.
1951 *
1952 * var Mixed = mongoose.Schema.Types.Mixed;
1953 * new mongoose.Schema({ _user: Mixed })
1954 *
1955 * @api public
1956 */
1957
1958Schema.Types = MongooseTypes = require('./schema/index');
1959
1960/*!
1961 * ignore
1962 */
1963
1964exports.ObjectId = MongooseTypes.ObjectId;