UNPKG

50.7 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): bool - 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 * @return {Schema} the cloned schema
271 * @api public
272 * @memberOf Schema
273 * @instance
274 */
275
276Schema.prototype.clone = function() {
277 const s = new Schema({}, this._userProvidedOptions);
278 s.base = this.base;
279 s.obj = this.obj;
280 s.options = utils.clone(this.options);
281 s.callQueue = this.callQueue.map(function(f) { return f; });
282 s.methods = utils.clone(this.methods);
283 s.methodOptions = utils.clone(this.methodOptions);
284 s.statics = utils.clone(this.statics);
285 s.query = utils.clone(this.query);
286 s.plugins = Array.prototype.slice.call(this.plugins);
287 s._indexes = utils.clone(this._indexes);
288 s.s.hooks = this.s.hooks.clone();
289 s._originalSchema = this._originalSchema == null ?
290 this._originalSchema :
291 this._originalSchema.clone();
292
293 s.tree = utils.clone(this.tree);
294 s.paths = utils.clone(this.paths);
295 s.nested = utils.clone(this.nested);
296 s.subpaths = utils.clone(this.subpaths);
297 s.childSchemas = this.childSchemas.slice();
298 s.singleNestedPaths = utils.clone(this.singleNestedPaths);
299
300 s.virtuals = utils.clone(this.virtuals);
301 s.$globalPluginsApplied = this.$globalPluginsApplied;
302 s.$isRootDiscriminator = this.$isRootDiscriminator;
303
304 if (this.discriminatorMapping != null) {
305 s.discriminatorMapping = Object.assign({}, this.discriminatorMapping);
306 }
307 if (s.discriminators != null) {
308 s.discriminators = Object.assign({}, this.discriminators);
309 }
310
311 s.aliases = Object.assign({}, this.aliases);
312
313 // Bubble up `init` for backwards compat
314 s.on('init', v => this.emit('init', v));
315
316 return s;
317};
318
319/**
320 * Returns default options for this schema, merged with `options`.
321 *
322 * @param {Object} options
323 * @return {Object}
324 * @api private
325 */
326
327Schema.prototype.defaultOptions = function(options) {
328 if (options && options.safe === false) {
329 options.safe = {w: 0};
330 }
331
332 if (options && options.safe && options.safe.w === 0) {
333 // if you turn off safe writes, then versioning goes off as well
334 options.versionKey = false;
335 }
336
337 this._userProvidedOptions = options == null ? {} : utils.clone(options);
338
339 const baseOptions = get(this, 'base.options', {});
340 options = utils.options({
341 strict: 'strict' in baseOptions ? baseOptions.strict : true,
342 bufferCommands: true,
343 capped: false, // { size, max, autoIndexId }
344 versionKey: '__v',
345 discriminatorKey: '__t',
346 minimize: true,
347 autoIndex: null,
348 shardKey: null,
349 read: null,
350 validateBeforeSave: true,
351 // the following are only applied at construction time
352 noId: false, // deprecated, use { _id: false }
353 _id: true,
354 noVirtualId: false, // deprecated, use { id: false }
355 id: true,
356 typeKey: 'type'
357 }, utils.clone(options));
358
359 if (options.read) {
360 options.read = readPref(options.read);
361 }
362
363 return options;
364};
365
366/**
367 * Adds key path / schema type pairs to this schema.
368 *
369 * ####Example:
370 *
371 * const ToySchema = new Schema();
372 * ToySchema.add({ name: 'string', color: 'string', price: 'number' });
373 *
374 * const TurboManSchema = new Schema();
375 * // You can also `add()` another schema and copy over all paths, virtuals,
376 * // getters, setters, indexes, methods, and statics.
377 * TurboManSchema.add(ToySchema).add({ year: Number });
378 *
379 * @param {Object|Schema} obj plain object with paths to add, or another schema
380 * @param {String} [prefix] path to prefix the newly added paths with
381 * @return {Schema} the Schema instance
382 * @api public
383 */
384
385Schema.prototype.add = function add(obj, prefix) {
386 if (obj instanceof Schema) {
387 merge(this, obj);
388 return;
389 }
390
391 prefix = prefix || '';
392 const keys = Object.keys(obj);
393
394 for (let i = 0; i < keys.length; ++i) {
395 const key = keys[i];
396
397 if (obj[key] == null) {
398 throw new TypeError('Invalid value for schema path `' + prefix + key + '`');
399 }
400
401 if (Array.isArray(obj[key]) && obj[key].length === 1 && obj[key][0] == null) {
402 throw new TypeError('Invalid value for schema Array path `' + prefix + key + '`');
403 }
404
405 if (utils.isObject(obj[key]) &&
406 (!obj[key].constructor || utils.getFunctionName(obj[key].constructor) === 'Object') &&
407 (!obj[key][this.options.typeKey] || (this.options.typeKey === 'type' && obj[key].type.type))) {
408 if (Object.keys(obj[key]).length) {
409 // nested object { last: { name: String }}
410 this.nested[prefix + key] = true;
411 this.add(obj[key], prefix + key + '.');
412 } else {
413 if (prefix) {
414 this.nested[prefix.substr(0, prefix.length - 1)] = true;
415 }
416 this.path(prefix + key, obj[key]); // mixed type
417 }
418 } else {
419 if (prefix) {
420 this.nested[prefix.substr(0, prefix.length - 1)] = true;
421 }
422 this.path(prefix + key, obj[key]);
423 }
424 }
425
426 const addedKeys = Object.keys(obj).
427 map(key => prefix ? prefix + key : key);
428 aliasFields(this, addedKeys);
429 return this;
430};
431
432/**
433 * Reserved document keys.
434 *
435 * Keys in this object are names that are rejected in schema declarations b/c they conflict with mongoose functionality. Using these key name will throw an error.
436 *
437 * on, emit, _events, db, get, set, init, isNew, errors, schema, options, modelName, collection, _pres, _posts, toObject
438 *
439 * _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.
440 *
441 * var schema = new Schema(..);
442 * schema.methods.init = function () {} // potentially breaking
443 */
444
445Schema.reserved = Object.create(null);
446Schema.prototype.reserved = Schema.reserved;
447const reserved = Schema.reserved;
448// Core object
449reserved['prototype'] =
450// EventEmitter
451reserved.emit =
452reserved.on =
453reserved.once =
454reserved.listeners =
455reserved.removeListener =
456// document properties and functions
457reserved.collection =
458reserved.db =
459reserved.errors =
460reserved.init =
461reserved.isModified =
462reserved.isNew =
463reserved.get =
464reserved.modelName =
465reserved.save =
466reserved.schema =
467reserved.toObject =
468reserved.validate =
469reserved.remove =
470reserved.populated =
471// hooks.js
472reserved._pres = reserved._posts = 1;
473
474/*!
475 * Document keys to print warnings for
476 */
477
478const warnings = {};
479warnings.increment = '`increment` should not be used as a schema path name ' +
480 'unless you have disabled versioning.';
481
482/**
483 * Gets/sets schema paths.
484 *
485 * Sets a path (if arity 2)
486 * Gets a path (if arity 1)
487 *
488 * ####Example
489 *
490 * schema.path('name') // returns a SchemaType
491 * schema.path('name', Number) // changes the schemaType of `name` to Number
492 *
493 * @param {String} path
494 * @param {Object} constructor
495 * @api public
496 */
497
498Schema.prototype.path = function(path, obj) {
499 if (obj === undefined) {
500 if (this.paths.hasOwnProperty(path)) {
501 return this.paths[path];
502 }
503 if (this.subpaths.hasOwnProperty(path)) {
504 return this.subpaths[path];
505 }
506 if (this.singleNestedPaths.hasOwnProperty(path)) {
507 return this.singleNestedPaths[path];
508 }
509
510 // Look for maps
511 for (const _path of Object.keys(this.paths)) {
512 if (!_path.includes('.$*')) {
513 continue;
514 }
515 const re = new RegExp('^' + _path.replace(/\.\$\*/g, '.[^.]+') + '$');
516 if (re.test(path)) {
517 return this.paths[_path];
518 }
519 }
520
521 // subpaths?
522 return /\.\d+\.?.*$/.test(path)
523 ? getPositionalPath(this, path)
524 : undefined;
525 }
526
527 // some path names conflict with document methods
528 if (reserved[path]) {
529 throw new Error('`' + path + '` may not be used as a schema pathname');
530 }
531
532 if (warnings[path]) {
533 console.log('WARN: ' + warnings[path]);
534 }
535
536 if (typeof obj === 'object' && 'ref' in obj) {
537 validateRef(obj.ref, path);
538 }
539
540 // update the tree
541 const subpaths = path.split(/\./);
542 const last = subpaths.pop();
543 let branch = this.tree;
544
545 subpaths.forEach(function(sub, i) {
546 if (!branch[sub]) {
547 branch[sub] = {};
548 }
549 if (typeof branch[sub] !== 'object') {
550 const msg = 'Cannot set nested path `' + path + '`. '
551 + 'Parent path `'
552 + subpaths.slice(0, i).concat([sub]).join('.')
553 + '` already set to type ' + branch[sub].name
554 + '.';
555 throw new Error(msg);
556 }
557 branch = branch[sub];
558 });
559
560 branch[last] = utils.clone(obj);
561
562 this.paths[path] = this.interpretAsType(path, obj, this.options);
563 const schemaType = this.paths[path];
564
565 if (schemaType.$isSchemaMap) {
566 // Maps can have arbitrary keys, so `$*` is internal shorthand for "any key"
567 // The '$' is to imply this path should never be stored in MongoDB so we
568 // can easily build a regexp out of this path, and '*' to imply "any key."
569 const mapPath = path + '.$*';
570 this.paths[path + '.$*'] = this.interpretAsType(mapPath,
571 obj.of || { type: {} }, this.options);
572 schemaType.$__schemaType = this.paths[path + '.$*'];
573 }
574
575 if (schemaType.$isSingleNested) {
576 for (const key in schemaType.schema.paths) {
577 this.singleNestedPaths[path + '.' + key] = schemaType.schema.paths[key];
578 }
579 for (const key in schemaType.schema.singleNestedPaths) {
580 this.singleNestedPaths[path + '.' + key] =
581 schemaType.schema.singleNestedPaths[key];
582 }
583
584 Object.defineProperty(schemaType.schema, 'base', {
585 configurable: true,
586 enumerable: false,
587 writable: false,
588 value: this.base
589 });
590
591 schemaType.caster.base = this.base;
592 this.childSchemas.push({
593 schema: schemaType.schema,
594 model: schemaType.caster
595 });
596 } else if (schemaType.$isMongooseDocumentArray) {
597 Object.defineProperty(schemaType.schema, 'base', {
598 configurable: true,
599 enumerable: false,
600 writable: false,
601 value: this.base
602 });
603
604 schemaType.casterConstructor.base = this.base;
605 this.childSchemas.push({
606 schema: schemaType.schema,
607 model: schemaType.casterConstructor
608 });
609 }
610
611 return this;
612};
613
614/**
615 * The Mongoose instance this schema is associated with
616 *
617 * @property base
618 * @api private
619 */
620
621Object.defineProperty(Schema.prototype, 'base', {
622 configurable: true,
623 enumerable: false,
624 writable: true,
625 value: null
626});
627
628/**
629 * Converts type arguments into Mongoose Types.
630 *
631 * @param {String} path
632 * @param {Object} obj constructor
633 * @api private
634 */
635
636Schema.prototype.interpretAsType = function(path, obj, options) {
637 if (obj instanceof SchemaType) {
638 return obj;
639 }
640
641 // If this schema has an associated Mongoose object, use the Mongoose object's
642 // copy of SchemaTypes re: gh-7158 gh-6933
643 const MongooseTypes = this.base != null ? this.base.Schema.Types : Schema.Types;
644
645 if (obj.constructor) {
646 const constructorName = utils.getFunctionName(obj.constructor);
647 if (constructorName !== 'Object') {
648 const oldObj = obj;
649 obj = {};
650 obj[options.typeKey] = oldObj;
651 }
652 }
653
654 // Get the type making sure to allow keys named "type"
655 // and default to mixed if not specified.
656 // { type: { type: String, default: 'freshcut' } }
657 let type = obj[options.typeKey] && (options.typeKey !== 'type' || !obj.type.type)
658 ? obj[options.typeKey]
659 : {};
660 let name;
661
662 if (utils.getFunctionName(type.constructor) === 'Object' || type === 'mixed') {
663 return new MongooseTypes.Mixed(path, obj);
664 }
665
666 if (Array.isArray(type) || Array === type || type === 'array') {
667 // if it was specified through { type } look for `cast`
668 let cast = (Array === type || type === 'array')
669 ? obj.cast
670 : type[0];
671
672 if (cast && cast.instanceOfSchema) {
673 return new MongooseTypes.DocumentArray(path, cast, obj);
674 }
675 if (cast &&
676 cast[options.typeKey] &&
677 cast[options.typeKey].instanceOfSchema) {
678 return new MongooseTypes.DocumentArray(path, cast[options.typeKey], obj, cast);
679 }
680
681 if (Array.isArray(cast)) {
682 return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj);
683 }
684
685 if (typeof cast === 'string') {
686 cast = MongooseTypes[cast.charAt(0).toUpperCase() + cast.substring(1)];
687 } else if (cast && (!cast[options.typeKey] || (options.typeKey === 'type' && cast.type.type))
688 && utils.getFunctionName(cast.constructor) === 'Object') {
689 if (Object.keys(cast).length) {
690 // The `minimize` and `typeKey` options propagate to child schemas
691 // declared inline, like `{ arr: [{ val: { $type: String } }] }`.
692 // See gh-3560
693 const childSchemaOptions = {minimize: options.minimize};
694 if (options.typeKey) {
695 childSchemaOptions.typeKey = options.typeKey;
696 }
697 //propagate 'strict' option to child schema
698 if (options.hasOwnProperty('strict')) {
699 childSchemaOptions.strict = options.strict;
700 }
701 const childSchema = new Schema(cast, childSchemaOptions);
702 childSchema.$implicitlyCreated = true;
703 return new MongooseTypes.DocumentArray(path, childSchema, obj);
704 } else {
705 // Special case: empty object becomes mixed
706 return new MongooseTypes.Array(path, MongooseTypes.Mixed, obj);
707 }
708 }
709
710 if (cast) {
711 type = cast[options.typeKey] && (options.typeKey !== 'type' || !cast.type.type)
712 ? cast[options.typeKey]
713 : cast;
714
715 name = typeof type === 'string'
716 ? type
717 : type.schemaName || utils.getFunctionName(type);
718
719 if (!(name in MongooseTypes)) {
720 throw new TypeError('Invalid schema configuration: ' +
721 `\`${name}\` is not a valid type within the array \`${path}\`.` +
722 'See http://bit.ly/mongoose-schematypes for a list of valid schema types.');
723 }
724 }
725
726 return new MongooseTypes.Array(path, cast || MongooseTypes.Mixed, obj, options);
727 }
728
729 if (type && type.instanceOfSchema) {
730 return new MongooseTypes.Embedded(type, path, obj);
731 }
732
733 if (Buffer.isBuffer(type)) {
734 name = 'Buffer';
735 } else if (typeof type === 'function' || typeof type === 'object') {
736 name = type.schemaName || utils.getFunctionName(type);
737 } else {
738 name = type == null ? '' + type : type.toString();
739 }
740
741 if (name) {
742 name = name.charAt(0).toUpperCase() + name.substring(1);
743 }
744 // Special case re: gh-7049 because the bson `ObjectID` class' capitalization
745 // doesn't line up with Mongoose's.
746 if (name === 'ObjectID') {
747 name = 'ObjectId';
748 }
749
750 if (MongooseTypes[name] == null) {
751 throw new TypeError(`Invalid schema configuration: \`${name}\` is not ` +
752 `a valid type at path \`${path}\`. See ` +
753 'http://bit.ly/mongoose-schematypes for a list of valid schema types.');
754 }
755
756 return new MongooseTypes[name](path, obj);
757};
758
759/**
760 * Iterates the schemas paths similar to Array#forEach.
761 *
762 * The callback is passed the pathname and schemaType as arguments on each iteration.
763 *
764 * @param {Function} fn callback function
765 * @return {Schema} this
766 * @api public
767 */
768
769Schema.prototype.eachPath = function(fn) {
770 const keys = Object.keys(this.paths);
771 const len = keys.length;
772
773 for (let i = 0; i < len; ++i) {
774 fn(keys[i], this.paths[keys[i]]);
775 }
776
777 return this;
778};
779
780/**
781 * Returns an Array of path strings that are required by this schema.
782 *
783 * @api public
784 * @param {Boolean} invalidate refresh the cache
785 * @return {Array}
786 */
787
788Schema.prototype.requiredPaths = function requiredPaths(invalidate) {
789 if (this._requiredpaths && !invalidate) {
790 return this._requiredpaths;
791 }
792
793 const paths = Object.keys(this.paths);
794 let i = paths.length;
795 const ret = [];
796
797 while (i--) {
798 const path = paths[i];
799 if (this.paths[path].isRequired) {
800 ret.push(path);
801 }
802 }
803 this._requiredpaths = ret;
804 return this._requiredpaths;
805};
806
807/**
808 * Returns indexes from fields and schema-level indexes (cached).
809 *
810 * @api private
811 * @return {Array}
812 */
813
814Schema.prototype.indexedPaths = function indexedPaths() {
815 if (this._indexedpaths) {
816 return this._indexedpaths;
817 }
818 this._indexedpaths = this.indexes();
819 return this._indexedpaths;
820};
821
822/**
823 * Returns the pathType of `path` for this schema.
824 *
825 * Given a path, returns whether it is a real, virtual, nested, or ad-hoc/undefined path.
826 *
827 * @param {String} path
828 * @return {String}
829 * @api public
830 */
831
832Schema.prototype.pathType = function(path) {
833 if (path in this.paths) {
834 return 'real';
835 }
836 if (path in this.virtuals) {
837 return 'virtual';
838 }
839 if (path in this.nested) {
840 return 'nested';
841 }
842 if (path in this.subpaths) {
843 return 'real';
844 }
845 if (path in this.singleNestedPaths) {
846 return 'real';
847 }
848
849 // Look for maps
850 for (const _path of Object.keys(this.paths)) {
851 if (!_path.includes('.$*')) {
852 continue;
853 }
854 const re = new RegExp('^' + _path.replace(/\.\$\*/g, '.[^.]+') + '$');
855 if (re.test(path)) {
856 return 'real';
857 }
858 }
859
860 if (/\.\d+\.|\.\d+$/.test(path)) {
861 return getPositionalPathType(this, path);
862 }
863 return 'adhocOrUndefined';
864};
865
866/**
867 * Returns true iff this path is a child of a mixed schema.
868 *
869 * @param {String} path
870 * @return {Boolean}
871 * @api private
872 */
873
874Schema.prototype.hasMixedParent = function(path) {
875 const subpaths = path.split(/\./g);
876 path = '';
877 for (let i = 0; i < subpaths.length; ++i) {
878 path = i > 0 ? path + '.' + subpaths[i] : subpaths[i];
879 if (path in this.paths &&
880 this.paths[path] instanceof MongooseTypes.Mixed) {
881 return true;
882 }
883 }
884
885 return false;
886};
887
888/**
889 * Setup updatedAt and createdAt timestamps to documents if enabled
890 *
891 * @param {Boolean|Object} timestamps timestamps options
892 * @api private
893 */
894Schema.prototype.setupTimestamp = function(timestamps) {
895 const childHasTimestamp = this.childSchemas.find(withTimestamp);
896
897 function withTimestamp(s) {
898 const ts = s.schema.options.timestamps;
899 return !!ts;
900 }
901
902 if (!timestamps && !childHasTimestamp) {
903 return;
904 }
905
906 const createdAt = handleTimestampOption(timestamps, 'createdAt');
907 const updatedAt = handleTimestampOption(timestamps, 'updatedAt');
908 const schemaAdditions = {};
909
910 this.$timestamps = { createdAt: createdAt, updatedAt: updatedAt };
911
912 if (updatedAt && !this.paths[updatedAt]) {
913 schemaAdditions[updatedAt] = Date;
914 }
915
916 if (createdAt && !this.paths[createdAt]) {
917 schemaAdditions[createdAt] = Date;
918 }
919
920 this.add(schemaAdditions);
921
922 this.pre('save', function(next) {
923 if (get(this, '$__.saveOptions.timestamps') === false) {
924 return next();
925 }
926
927 const defaultTimestamp = (this.ownerDocument ? this.ownerDocument() : this).
928 constructor.base.now();
929 const auto_id = this._id && this._id.auto;
930
931 if (createdAt && !this.get(createdAt) && this.isSelected(createdAt)) {
932 this.set(createdAt, auto_id ? this._id.getTimestamp() : defaultTimestamp);
933 }
934
935 if (updatedAt && (this.isNew || this.isModified())) {
936 let ts = defaultTimestamp;
937 if (this.isNew) {
938 if (createdAt != null) {
939 ts = this.get(createdAt);
940 } else if (auto_id) {
941 ts = this._id.getTimestamp();
942 }
943 }
944 this.set(updatedAt, ts);
945 }
946
947 next();
948 });
949
950 this.methods.initializeTimestamps = function() {
951 if (createdAt && !this.get(createdAt)) {
952 this.set(createdAt, new Date());
953 }
954 if (updatedAt && !this.get(updatedAt)) {
955 this.set(updatedAt, new Date());
956 }
957 return this;
958 };
959
960 _setTimestampsOnUpdate[symbols.builtInMiddleware] = true;
961
962 this.pre('findOneAndUpdate', _setTimestampsOnUpdate);
963 this.pre('replaceOne', _setTimestampsOnUpdate);
964 this.pre('update', _setTimestampsOnUpdate);
965 this.pre('updateOne', _setTimestampsOnUpdate);
966 this.pre('updateMany', _setTimestampsOnUpdate);
967
968 function _setTimestampsOnUpdate(next) {
969 const now = this.model.base.now();
970 applyTimestampsToUpdate(now, createdAt, updatedAt, this.getUpdate(),
971 this.options, this.schema);
972 applyTimestampsToChildren(now, this.getUpdate(), this.model.schema);
973 next();
974 }
975};
976
977/*!
978 * ignore
979 */
980
981function getPositionalPathType(self, path) {
982 const subpaths = path.split(/\.(\d+)\.|\.(\d+)$/).filter(Boolean);
983 if (subpaths.length < 2) {
984 return self.paths.hasOwnProperty(subpaths[0]) ? self.paths[subpaths[0]] : null;
985 }
986
987 let val = self.path(subpaths[0]);
988 let isNested = false;
989 if (!val) {
990 return val;
991 }
992
993 const last = subpaths.length - 1;
994
995 for (let i = 1; i < subpaths.length; ++i) {
996 isNested = false;
997 const subpath = subpaths[i];
998
999 if (i === last && val && !/\D/.test(subpath)) {
1000 if (val.$isMongooseDocumentArray) {
1001 const oldVal = val;
1002 val = new SchemaType(subpath, {
1003 required: get(val, 'schemaOptions.required', false)
1004 });
1005 val.cast = function(value, doc, init) {
1006 return oldVal.cast(value, doc, init)[0];
1007 };
1008 val.$isMongooseDocumentArrayElement = true;
1009 val.caster = oldVal.caster;
1010 val.schema = oldVal.schema;
1011 } else if (val instanceof MongooseTypes.Array) {
1012 // StringSchema, NumberSchema, etc
1013 val = val.caster;
1014 } else {
1015 val = undefined;
1016 }
1017 break;
1018 }
1019
1020 // ignore if its just a position segment: path.0.subpath
1021 if (!/\D/.test(subpath)) {
1022 continue;
1023 }
1024
1025 if (!(val && val.schema)) {
1026 val = undefined;
1027 break;
1028 }
1029
1030 const type = val.schema.pathType(subpath);
1031 isNested = (type === 'nested');
1032 val = val.schema.path(subpath);
1033 }
1034
1035 self.subpaths[path] = val;
1036 if (val) {
1037 return 'real';
1038 }
1039 if (isNested) {
1040 return 'nested';
1041 }
1042 return 'adhocOrUndefined';
1043}
1044
1045
1046/*!
1047 * ignore
1048 */
1049
1050function getPositionalPath(self, path) {
1051 getPositionalPathType(self, path);
1052 return self.subpaths[path];
1053}
1054
1055/**
1056 * Adds a method call to the queue.
1057 *
1058 * @param {String} name name of the document method to call later
1059 * @param {Array} args arguments to pass to the method
1060 * @api public
1061 */
1062
1063Schema.prototype.queue = function(name, args) {
1064 this.callQueue.push([name, args]);
1065 return this;
1066};
1067
1068/**
1069 * Defines a pre hook for the document.
1070 *
1071 * ####Example
1072 *
1073 * var toySchema = new Schema({ name: String, created: Date });
1074 *
1075 * toySchema.pre('save', function(next) {
1076 * if (!this.created) this.created = new Date;
1077 * next();
1078 * });
1079 *
1080 * toySchema.pre('validate', function(next) {
1081 * if (this.name !== 'Woody') this.name = 'Woody';
1082 * next();
1083 * });
1084 *
1085 * // Equivalent to calling `pre()` on `find`, `findOne`, `findOneAndUpdate`.
1086 * toySchema.pre(/^find/, function(next) {
1087 * console.log(this.getQuery());
1088 * });
1089 *
1090 * @param {String|RegExp} method or regular expression to match method name
1091 * @param {Object} [options]
1092 * @param {Boolean} [options.document] If `name` is a hook for both document and query middleware, set to `true` to run on document middleware.
1093 * @param {Boolean} [options.query] If `name` is a hook for both document and query middleware, set to `true` to run on query middleware.
1094 * @param {Function} callback
1095 * @see hooks.js https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3
1096 * @api public
1097 */
1098
1099Schema.prototype.pre = function(name) {
1100 if (name instanceof RegExp) {
1101 const remainingArgs = Array.prototype.slice.call(arguments, 1);
1102 for (const fn of hookNames) {
1103 if (name.test(fn)) {
1104 this.pre.apply(this, [fn].concat(remainingArgs));
1105 }
1106 }
1107 return this;
1108 }
1109 this.s.hooks.pre.apply(this.s.hooks, arguments);
1110 return this;
1111};
1112
1113/**
1114 * Defines a post hook for the document
1115 *
1116 * var schema = new Schema(..);
1117 * schema.post('save', function (doc) {
1118 * console.log('this fired after a document was saved');
1119 * });
1120 *
1121 * schema.post('find', function(docs) {
1122 * console.log('this fired after you ran a find query');
1123 * });
1124 *
1125 * schema.post(/Many$/, function(res) {
1126 * console.log('this fired after you ran `updateMany()` or `deleteMany()`);
1127 * });
1128 *
1129 * var Model = mongoose.model('Model', schema);
1130 *
1131 * var m = new Model(..);
1132 * m.save(function(err) {
1133 * console.log('this fires after the `post` hook');
1134 * });
1135 *
1136 * m.find(function(err, docs) {
1137 * console.log('this fires after the post find hook');
1138 * });
1139 *
1140 * @param {String|RegExp} method or regular expression to match method name
1141 * @param {Object} [options]
1142 * @param {Boolean} [options.document] If `name` is a hook for both document and query middleware, set to `true` to run on document middleware.
1143 * @param {Boolean} [options.query] If `name` is a hook for both document and query middleware, set to `true` to run on query middleware.
1144 * @param {Function} fn callback
1145 * @see middleware http://mongoosejs.com/docs/middleware.html
1146 * @see kareem http://npmjs.org/package/kareem
1147 * @api public
1148 */
1149
1150Schema.prototype.post = function(name) {
1151 if (name instanceof RegExp) {
1152 const remainingArgs = Array.prototype.slice.call(arguments, 1);
1153 for (const fn of hookNames) {
1154 if (name.test(fn)) {
1155 this.post.apply(this, [fn].concat(remainingArgs));
1156 }
1157 }
1158 return this;
1159 }
1160 this.s.hooks.post.apply(this.s.hooks, arguments);
1161 return this;
1162};
1163
1164/**
1165 * Registers a plugin for this schema.
1166 *
1167 * @param {Function} plugin callback
1168 * @param {Object} [opts]
1169 * @see plugins
1170 * @api public
1171 */
1172
1173Schema.prototype.plugin = function(fn, opts) {
1174 if (typeof fn !== 'function') {
1175 throw new Error('First param to `schema.plugin()` must be a function, ' +
1176 'got "' + (typeof fn) + '"');
1177 }
1178
1179 if (opts &&
1180 opts.deduplicate) {
1181 for (let i = 0; i < this.plugins.length; ++i) {
1182 if (this.plugins[i].fn === fn) {
1183 return this;
1184 }
1185 }
1186 }
1187 this.plugins.push({ fn: fn, opts: opts });
1188
1189 fn(this, opts);
1190 return this;
1191};
1192
1193/**
1194 * Adds an instance method to documents constructed from Models compiled from this schema.
1195 *
1196 * ####Example
1197 *
1198 * var schema = kittySchema = new Schema(..);
1199 *
1200 * schema.method('meow', function () {
1201 * console.log('meeeeeoooooooooooow');
1202 * })
1203 *
1204 * var Kitty = mongoose.model('Kitty', schema);
1205 *
1206 * var fizz = new Kitty;
1207 * fizz.meow(); // meeeeeooooooooooooow
1208 *
1209 * If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as methods.
1210 *
1211 * schema.method({
1212 * purr: function () {}
1213 * , scratch: function () {}
1214 * });
1215 *
1216 * // later
1217 * fizz.purr();
1218 * fizz.scratch();
1219 *
1220 * 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)
1221 *
1222 * @param {String|Object} method name
1223 * @param {Function} [fn]
1224 * @api public
1225 */
1226
1227Schema.prototype.method = function(name, fn, options) {
1228 if (typeof name !== 'string') {
1229 for (const i in name) {
1230 this.methods[i] = name[i];
1231 this.methodOptions[i] = utils.clone(options);
1232 }
1233 } else {
1234 this.methods[name] = fn;
1235 this.methodOptions[name] = utils.clone(options);
1236 }
1237 return this;
1238};
1239
1240/**
1241 * Adds static "class" methods to Models compiled from this schema.
1242 *
1243 * ####Example
1244 *
1245 * var schema = new Schema(..);
1246 * schema.static('findByName', function (name, callback) {
1247 * return this.find({ name: name }, callback);
1248 * });
1249 *
1250 * var Drink = mongoose.model('Drink', schema);
1251 * Drink.findByName('sanpellegrino', function (err, drinks) {
1252 * //
1253 * });
1254 *
1255 * If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as statics.
1256 *
1257 * @param {String|Object} name
1258 * @param {Function} [fn]
1259 * @api public
1260 */
1261
1262Schema.prototype.static = function(name, fn) {
1263 if (typeof name !== 'string') {
1264 for (const i in name) {
1265 this.statics[i] = name[i];
1266 }
1267 } else {
1268 this.statics[name] = fn;
1269 }
1270 return this;
1271};
1272
1273/**
1274 * Defines an index (most likely compound) for this schema.
1275 *
1276 * ####Example
1277 *
1278 * schema.index({ first: 1, last: -1 })
1279 *
1280 * @param {Object} fields
1281 * @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)
1282 * @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.
1283 * @api public
1284 */
1285
1286Schema.prototype.index = function(fields, options) {
1287 fields || (fields = {});
1288 options || (options = {});
1289
1290 if (options.expires) {
1291 utils.expires(options);
1292 }
1293
1294 this._indexes.push([fields, options]);
1295 return this;
1296};
1297
1298/**
1299 * Sets/gets a schema option.
1300 *
1301 * ####Example
1302 *
1303 * schema.set('strict'); // 'true' by default
1304 * schema.set('strict', false); // Sets 'strict' to false
1305 * schema.set('strict'); // 'false'
1306 *
1307 * @param {String} key option name
1308 * @param {Object} [value] if not passed, the current option value is returned
1309 * @see Schema ./
1310 * @api public
1311 */
1312
1313Schema.prototype.set = function(key, value, _tags) {
1314 if (arguments.length === 1) {
1315 return this.options[key];
1316 }
1317
1318 switch (key) {
1319 case 'read':
1320 this.options[key] = readPref(value, _tags);
1321 this._userProvidedOptions[key] = this.options[key];
1322 break;
1323 case 'safe':
1324 setSafe(this.options, value);
1325 this._userProvidedOptions[key] = this.options[key];
1326 break;
1327 case 'timestamps':
1328 this.setupTimestamp(value);
1329 this.options[key] = value;
1330 this._userProvidedOptions[key] = this.options[key];
1331 break;
1332 default:
1333 this.options[key] = value;
1334 this._userProvidedOptions[key] = this.options[key];
1335 break;
1336 }
1337
1338 return this;
1339};
1340
1341/*!
1342 * ignore
1343 */
1344
1345const safeDeprecationWarning = 'Mongoose: The `safe` option for schemas is ' +
1346 'deprecated. Use the `writeConcern` option instead: ' +
1347 'http://bit.ly/mongoose-write-concern';
1348
1349const setSafe = util.deprecate(function setSafe(options, value) {
1350 options.safe = value === false ?
1351 {w: 0} :
1352 value;
1353}, safeDeprecationWarning);
1354
1355/**
1356 * Gets a schema option.
1357 *
1358 * @param {String} key option name
1359 * @api public
1360 */
1361
1362Schema.prototype.get = function(key) {
1363 return this.options[key];
1364};
1365
1366/**
1367 * The allowed index types
1368 *
1369 * @receiver Schema
1370 * @static indexTypes
1371 * @api public
1372 */
1373
1374const indexTypes = '2d 2dsphere hashed text'.split(' ');
1375
1376Object.defineProperty(Schema, 'indexTypes', {
1377 get: function() {
1378 return indexTypes;
1379 },
1380 set: function() {
1381 throw new Error('Cannot overwrite Schema.indexTypes');
1382 }
1383});
1384
1385/**
1386 * Returns a list of indexes that this schema declares, via `schema.index()`
1387 * or by `index: true` in a path's options.
1388 *
1389 * @api public
1390 */
1391
1392Schema.prototype.indexes = function() {
1393 return getIndexes(this);
1394};
1395
1396/**
1397 * Creates a virtual type with the given name.
1398 *
1399 * @param {String} name
1400 * @param {Object} [options]
1401 * @param {String|Model} [options.ref] model name or model instance. Marks this as a [populate virtual](populate.html#populate-virtuals).
1402 * @param {String|Function} [options.localField] Required for populate virtuals. See [populate virtual docs](populate.html#populate-virtuals) for more information.
1403 * @param {String|Function} [options.foreignField] Required for populate virtuals. See [populate virtual docs](populate.html#populate-virtuals) for more information.
1404 * @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.
1405 * @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()`.
1406 * @return {VirtualType}
1407 */
1408
1409Schema.prototype.virtual = function(name, options) {
1410 if (options && options.ref) {
1411 if (!options.localField) {
1412 throw new Error('Reference virtuals require `localField` option');
1413 }
1414
1415 if (!options.foreignField) {
1416 throw new Error('Reference virtuals require `foreignField` option');
1417 }
1418
1419 this.pre('init', function(obj) {
1420 if (mpath.has(name, obj)) {
1421 const _v = mpath.get(name, obj);
1422 if (!this.$$populatedVirtuals) {
1423 this.$$populatedVirtuals = {};
1424 }
1425
1426 if (options.justOne || options.count) {
1427 this.$$populatedVirtuals[name] = Array.isArray(_v) ?
1428 _v[0] :
1429 _v;
1430 } else {
1431 this.$$populatedVirtuals[name] = Array.isArray(_v) ?
1432 _v :
1433 _v == null ? [] : [_v];
1434 }
1435
1436 mpath.unset(name, obj);
1437 }
1438 });
1439
1440 const virtual = this.virtual(name);
1441 virtual.options = options;
1442 return virtual.
1443 get(function() {
1444 if (!this.$$populatedVirtuals) {
1445 this.$$populatedVirtuals = {};
1446 }
1447 if (name in this.$$populatedVirtuals) {
1448 return this.$$populatedVirtuals[name];
1449 }
1450 return null;
1451 }).
1452 set(function(_v) {
1453 if (!this.$$populatedVirtuals) {
1454 this.$$populatedVirtuals = {};
1455 }
1456
1457 if (options.justOne || options.count) {
1458 this.$$populatedVirtuals[name] = Array.isArray(_v) ?
1459 _v[0] :
1460 _v;
1461
1462 if (typeof this.$$populatedVirtuals[name] !== 'object') {
1463 this.$$populatedVirtuals[name] = options.count ? _v : null;
1464 }
1465 } else {
1466 this.$$populatedVirtuals[name] = Array.isArray(_v) ?
1467 _v :
1468 _v == null ? [] : [_v];
1469
1470 this.$$populatedVirtuals[name] = this.$$populatedVirtuals[name].filter(function(doc) {
1471 return doc && typeof doc === 'object';
1472 });
1473 }
1474 });
1475 }
1476
1477 const virtuals = this.virtuals;
1478 const parts = name.split('.');
1479
1480 if (this.pathType(name) === 'real') {
1481 throw new Error('Virtual path "' + name + '"' +
1482 ' conflicts with a real path in the schema');
1483 }
1484
1485 virtuals[name] = parts.reduce(function(mem, part, i) {
1486 mem[part] || (mem[part] = (i === parts.length - 1)
1487 ? new VirtualType(options, name)
1488 : {});
1489 return mem[part];
1490 }, this.tree);
1491
1492 return virtuals[name];
1493};
1494
1495/**
1496 * Returns the virtual type with the given `name`.
1497 *
1498 * @param {String} name
1499 * @return {VirtualType}
1500 */
1501
1502Schema.prototype.virtualpath = function(name) {
1503 return this.virtuals.hasOwnProperty(name) ? this.virtuals[name] : null;
1504};
1505
1506/**
1507 * Removes the given `path` (or [`paths`]).
1508 *
1509 * @param {String|Array} path
1510 * @return {Schema} the Schema instance
1511 * @api public
1512 */
1513Schema.prototype.remove = function(path) {
1514 if (typeof path === 'string') {
1515 path = [path];
1516 }
1517 if (Array.isArray(path)) {
1518 path.forEach(function(name) {
1519 if (this.path(name)) {
1520 delete this.paths[name];
1521
1522 const pieces = name.split('.');
1523 const last = pieces.pop();
1524 let branch = this.tree;
1525 for (let i = 0; i < pieces.length; ++i) {
1526 branch = branch[pieces[i]];
1527 }
1528 delete branch[last];
1529 }
1530 }, this);
1531 }
1532 return this;
1533};
1534
1535/**
1536 * 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),
1537 * and [instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Class_body_and_method_definitions)
1538 * to schema [virtuals](http://mongoosejs.com/docs/guide.html#virtuals),
1539 * [statics](http://mongoosejs.com/docs/guide.html#statics), and
1540 * [methods](http://mongoosejs.com/docs/guide.html#methods).
1541 *
1542 * ####Example:
1543 *
1544 * ```javascript
1545 * const md5 = require('md5');
1546 * const userSchema = new Schema({ email: String });
1547 * class UserClass {
1548 * // `gravatarImage` becomes a virtual
1549 * get gravatarImage() {
1550 * const hash = md5(this.email.toLowerCase());
1551 * return `https://www.gravatar.com/avatar/${hash}`;
1552 * }
1553 *
1554 * // `getProfileUrl()` becomes a document method
1555 * getProfileUrl() {
1556 * return `https://mysite.com/${this.email}`;
1557 * }
1558 *
1559 * // `findByEmail()` becomes a static
1560 * static findByEmail(email) {
1561 * return this.findOne({ email });
1562 * }
1563 * }
1564 *
1565 * // `schema` will now have a `gravatarImage` virtual, a `getProfileUrl()` method,
1566 * // and a `findByEmail()` static
1567 * userSchema.loadClass(UserClass);
1568 * ```
1569 *
1570 * @param {Function} model
1571 * @param {Boolean} [virtualsOnly] if truthy, only pulls virtuals from the class, not methods or statics
1572 */
1573Schema.prototype.loadClass = function(model, virtualsOnly) {
1574 if (model === Object.prototype ||
1575 model === Function.prototype ||
1576 model.prototype.hasOwnProperty('$isMongooseModelPrototype')) {
1577 return this;
1578 }
1579
1580 this.loadClass(Object.getPrototypeOf(model));
1581
1582 // Add static methods
1583 if (!virtualsOnly) {
1584 Object.getOwnPropertyNames(model).forEach(function(name) {
1585 if (name.match(/^(length|name|prototype)$/)) {
1586 return;
1587 }
1588 const method = Object.getOwnPropertyDescriptor(model, name);
1589 if (typeof method.value === 'function') {
1590 this.static(name, method.value);
1591 }
1592 }, this);
1593 }
1594
1595 // Add methods and virtuals
1596 Object.getOwnPropertyNames(model.prototype).forEach(function(name) {
1597 if (name.match(/^(constructor)$/)) {
1598 return;
1599 }
1600 const method = Object.getOwnPropertyDescriptor(model.prototype, name);
1601 if (!virtualsOnly) {
1602 if (typeof method.value === 'function') {
1603 this.method(name, method.value);
1604 }
1605 }
1606 if (typeof method.get === 'function') {
1607 this.virtual(name).get(method.get);
1608 }
1609 if (typeof method.set === 'function') {
1610 this.virtual(name).set(method.set);
1611 }
1612 }, this);
1613
1614 return this;
1615};
1616
1617/*!
1618 * ignore
1619 */
1620
1621Schema.prototype._getSchema = function(path) {
1622 const _this = this;
1623 const pathschema = _this.path(path);
1624 const resultPath = [];
1625
1626 if (pathschema) {
1627 pathschema.$fullPath = path;
1628 return pathschema;
1629 }
1630
1631 function search(parts, schema) {
1632 let p = parts.length + 1;
1633 let foundschema;
1634 let trypath;
1635
1636 while (p--) {
1637 trypath = parts.slice(0, p).join('.');
1638 foundschema = schema.path(trypath);
1639 if (foundschema) {
1640 resultPath.push(trypath);
1641
1642 if (foundschema.caster) {
1643 // array of Mixed?
1644 if (foundschema.caster instanceof MongooseTypes.Mixed) {
1645 foundschema.caster.$fullPath = resultPath.join('.');
1646 return foundschema.caster;
1647 }
1648
1649 // Now that we found the array, we need to check if there
1650 // are remaining document paths to look up for casting.
1651 // Also we need to handle array.$.path since schema.path
1652 // doesn't work for that.
1653 // If there is no foundschema.schema we are dealing with
1654 // a path like array.$
1655 if (p !== parts.length && foundschema.schema) {
1656 let ret;
1657 if (parts[p] === '$' || isArrayFilter(parts[p])) {
1658 if (p + 1 === parts.length) {
1659 // comments.$
1660 return foundschema;
1661 }
1662 // comments.$.comments.$.title
1663 ret = search(parts.slice(p + 1), foundschema.schema);
1664 if (ret) {
1665 ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
1666 !foundschema.schema.$isSingleNested;
1667 }
1668 return ret;
1669 }
1670 // this is the last path of the selector
1671 ret = search(parts.slice(p), foundschema.schema);
1672 if (ret) {
1673 ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
1674 !foundschema.schema.$isSingleNested;
1675 }
1676 return ret;
1677 }
1678 }
1679
1680 foundschema.$fullPath = resultPath.join('.');
1681
1682 return foundschema;
1683 }
1684 }
1685 }
1686
1687 // look for arrays
1688 const parts = path.split('.');
1689 for (let i = 0; i < parts.length; ++i) {
1690 if (parts[i] === '$' || isArrayFilter(parts[i])) {
1691 // Re: gh-5628, because `schema.path()` doesn't take $ into account.
1692 parts[i] = '0';
1693 }
1694 }
1695 return search(parts, _this);
1696};
1697
1698/*!
1699 * ignore
1700 */
1701
1702Schema.prototype._getPathType = function(path) {
1703 const _this = this;
1704 const pathschema = _this.path(path);
1705
1706 if (pathschema) {
1707 return 'real';
1708 }
1709
1710 function search(parts, schema) {
1711 let p = parts.length + 1,
1712 foundschema,
1713 trypath;
1714
1715 while (p--) {
1716 trypath = parts.slice(0, p).join('.');
1717 foundschema = schema.path(trypath);
1718 if (foundschema) {
1719 if (foundschema.caster) {
1720 // array of Mixed?
1721 if (foundschema.caster instanceof MongooseTypes.Mixed) {
1722 return { schema: foundschema, pathType: 'mixed' };
1723 }
1724
1725 // Now that we found the array, we need to check if there
1726 // are remaining document paths to look up for casting.
1727 // Also we need to handle array.$.path since schema.path
1728 // doesn't work for that.
1729 // If there is no foundschema.schema we are dealing with
1730 // a path like array.$
1731 if (p !== parts.length && foundschema.schema) {
1732 if (parts[p] === '$' || isArrayFilter(parts[p])) {
1733 if (p === parts.length - 1) {
1734 return { schema: foundschema, pathType: 'nested' };
1735 }
1736 // comments.$.comments.$.title
1737 return search(parts.slice(p + 1), foundschema.schema);
1738 }
1739 // this is the last path of the selector
1740 return search(parts.slice(p), foundschema.schema);
1741 }
1742 return {
1743 schema: foundschema,
1744 pathType: foundschema.$isSingleNested ? 'nested' : 'array'
1745 };
1746 }
1747 return { schema: foundschema, pathType: 'real' };
1748 } else if (p === parts.length && schema.nested[trypath]) {
1749 return { schema: schema, pathType: 'nested' };
1750 }
1751 }
1752 return { schema: foundschema || schema, pathType: 'undefined' };
1753 }
1754
1755 // look for arrays
1756 return search(path.split('.'), _this);
1757};
1758
1759/*!
1760 * ignore
1761 */
1762
1763function isArrayFilter(piece) {
1764 return piece.indexOf('$[') === 0 &&
1765 piece.lastIndexOf(']') === piece.length - 1;
1766}
1767
1768/*!
1769 * Module exports.
1770 */
1771
1772module.exports = exports = Schema;
1773
1774// require down here because of reference issues
1775
1776/**
1777 * The various built-in Mongoose Schema Types.
1778 *
1779 * ####Example:
1780 *
1781 * var mongoose = require('mongoose');
1782 * var ObjectId = mongoose.Schema.Types.ObjectId;
1783 *
1784 * ####Types:
1785 *
1786 * - [String](#schema-string-js)
1787 * - [Number](#schema-number-js)
1788 * - [Boolean](#schema-boolean-js) | Bool
1789 * - [Array](#schema-array-js)
1790 * - [Buffer](#schema-buffer-js)
1791 * - [Date](#schema-date-js)
1792 * - [ObjectId](#schema-objectid-js) | Oid
1793 * - [Mixed](#schema-mixed-js)
1794 *
1795 * Using this exposed access to the `Mixed` SchemaType, we can use them in our schema.
1796 *
1797 * var Mixed = mongoose.Schema.Types.Mixed;
1798 * new mongoose.Schema({ _user: Mixed })
1799 *
1800 * @api public
1801 */
1802
1803Schema.Types = MongooseTypes = require('./schema/index');
1804
1805/*!
1806 * ignore
1807 */
1808
1809exports.ObjectId = MongooseTypes.ObjectId;