UNPKG

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