UNPKG

92.7 kBJavaScriptView Raw
1'use strict';
2
3/*!
4 * Module dependencies.
5 */
6
7const EventEmitter = require('events').EventEmitter;
8const InternalCache = require('./internal');
9const MongooseError = require('./error');
10const MixedSchema = require('./schema/mixed');
11const ObjectExpectedError = require('./error/objectExpected');
12const ObjectParameterError = require('./error/objectParameter');
13const StrictModeError = require('./error/strict');
14const ValidatorError = require('./schematype').ValidatorError;
15const VirtualType = require('./virtualtype');
16const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths');
17const compile = require('./helpers/document/compile').compile;
18const defineKey = require('./helpers/document/compile').defineKey;
19const flatten = require('./helpers/common').flatten;
20const get = require('./helpers/get');
21const getEmbeddedDiscriminatorPath = require('./helpers/document/getEmbeddedDiscriminatorPath');
22const idGetter = require('./plugins/idGetter');
23const isDefiningProjection = require('./helpers/projection/isDefiningProjection');
24const isExclusive = require('./helpers/projection/isExclusive');
25const inspect = require('util').inspect;
26const internalToObjectOptions = require('./options').internalToObjectOptions;
27const mpath = require('mpath');
28const utils = require('./utils');
29
30const ValidationError = MongooseError.ValidationError;
31const clone = utils.clone;
32const deepEqual = utils.deepEqual;
33const isMongooseObject = utils.isMongooseObject;
34
35const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol;
36const documentArrayParent = require('./helpers/symbols').documentArrayParent;
37const documentSchemaSymbol = require('./helpers/symbols').documentSchemaSymbol;
38const getSymbol = require('./helpers/symbols').getSymbol;
39const populateModelSymbol = require('./helpers/symbols').populateModelSymbol;
40
41let DocumentArray;
42let MongooseArray;
43let Embedded;
44
45const specialProperties = utils.specialProperties;
46
47/**
48 * The core Mongoose document constructor. You should not call this directly,
49 * the Mongoose [Model constructor](./api.html#Model) calls this for you.
50 *
51 * @param {Object} obj the values to set
52 * @param {Object} [fields] optional object containing the fields which were selected in the query returning this document and any populated paths data
53 * @param {Boolean} [skipId] bool, should we auto create an ObjectId _id
54 * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter
55 * @event `init`: Emitted on a document after it has was retreived from the db and fully hydrated by Mongoose.
56 * @event `save`: Emitted when the document is successfully saved
57 * @api private
58 */
59
60function Document(obj, fields, skipId, options) {
61 if (typeof skipId === 'object' && skipId != null) {
62 options = skipId;
63 skipId = options.skipId;
64 }
65 options = options || {};
66
67 this.$__ = new InternalCache;
68 this.$__.emitter = new EventEmitter();
69 this.isNew = 'isNew' in options ? options.isNew : true;
70 this.errors = undefined;
71 this.$__.$options = options || {};
72
73 if (obj != null && typeof obj !== 'object') {
74 throw new ObjectParameterError(obj, 'obj', 'Document');
75 }
76
77 const schema = this.schema;
78
79 if (typeof fields === 'boolean') {
80 this.$__.strictMode = fields;
81 fields = undefined;
82 } else {
83 this.$__.strictMode = schema.options.strict;
84 this.$__.selected = fields;
85 }
86
87 const required = schema.requiredPaths(true);
88 for (let i = 0; i < required.length; ++i) {
89 this.$__.activePaths.require(required[i]);
90 }
91
92 this.$__.emitter.setMaxListeners(0);
93
94 let exclude = null;
95
96 // determine if this doc is a result of a query with
97 // excluded fields
98 if (utils.isPOJO(fields)) {
99 exclude = isExclusive(fields);
100 }
101
102 const hasIncludedChildren = exclude === false && fields ?
103 $__hasIncludedChildren(fields) :
104 {};
105
106 if (this._doc == null) {
107 this.$__buildDoc(obj, fields, skipId, exclude, hasIncludedChildren, false);
108
109 // By default, defaults get applied **before** setting initial values
110 // Re: gh-6155
111 $__applyDefaults(this, fields, skipId, exclude, hasIncludedChildren, true, {
112 isNew: this.isNew
113 });
114 }
115
116 if (obj) {
117 if (obj instanceof Document) {
118 this.isNew = obj.isNew;
119 }
120 // Skip set hooks
121 if (this.$__original_set) {
122 this.$__original_set(obj, undefined, true);
123 } else {
124 this.$set(obj, undefined, true);
125 }
126 }
127
128 // Function defaults get applied **after** setting initial values so they
129 // see the full doc rather than an empty one, unless they opt out.
130 // Re: gh-3781, gh-6155
131 if (options.willInit) {
132 this.once('init', () => {
133 $__applyDefaults(this, fields, skipId, exclude, hasIncludedChildren, false, options.skipDefaults, {
134 isNew: this.isNew
135 });
136 });
137 } else {
138 $__applyDefaults(this, fields, skipId, exclude, hasIncludedChildren, false, options.skipDefaults, {
139 isNew: this.isNew
140 });
141 }
142
143 this.$__._id = this._id;
144 this.$locals = {};
145
146 if (!schema.options.strict && obj) {
147 const _this = this;
148 const keys = Object.keys(this._doc);
149
150 keys.forEach(function(key) {
151 if (!(key in schema.tree)) {
152 defineKey(key, null, _this);
153 }
154 });
155 }
156
157 applyQueue(this);
158}
159
160/*!
161 * Document exposes the NodeJS event emitter API, so you can use
162 * `on`, `once`, etc.
163 */
164utils.each(
165 ['on', 'once', 'emit', 'listeners', 'removeListener', 'setMaxListeners',
166 'removeAllListeners', 'addListener'],
167 function(emitterFn) {
168 Document.prototype[emitterFn] = function() {
169 return this.$__.emitter[emitterFn].apply(this.$__.emitter, arguments);
170 };
171 });
172
173Document.prototype.constructor = Document;
174
175/**
176 * The documents schema.
177 *
178 * @api public
179 * @property schema
180 * @memberOf Document
181 * @instance
182 */
183
184Document.prototype.schema;
185
186/**
187 * Empty object that you can use for storing properties on the document. This
188 * is handy for passing data to middleware without conflicting with Mongoose
189 * internals.
190 *
191 * ####Example:
192 *
193 * schema.pre('save', function() {
194 * // Mongoose will set `isNew` to `false` if `save()` succeeds
195 * this.$locals.wasNew = this.isNew;
196 * });
197 *
198 * schema.post('save', function() {
199 * // Prints true if `isNew` was set before `save()`
200 * console.log(this.$locals.wasNew);
201 * });
202 *
203 * @api public
204 * @property $locals
205 * @memberOf Document
206 * @instance
207 */
208
209Object.defineProperty(Document.prototype, '$locals', {
210 configurable: false,
211 enumerable: false,
212 writable: true
213});
214
215/**
216 * Boolean flag specifying if the document is new.
217 *
218 * @api public
219 * @property isNew
220 * @memberOf Document
221 * @instance
222 */
223
224Document.prototype.isNew;
225
226/**
227 * The string version of this documents _id.
228 *
229 * ####Note:
230 *
231 * This getter exists on all documents by default. The getter can be disabled by setting the `id` [option](/docs/guide.html#id) of its `Schema` to false at construction time.
232 *
233 * new Schema({ name: String }, { id: false });
234 *
235 * @api public
236 * @see Schema options /docs/guide.html#options
237 * @property id
238 * @memberOf Document
239 * @instance
240 */
241
242Document.prototype.id;
243
244/**
245 * Hash containing current validation errors.
246 *
247 * @api public
248 * @property errors
249 * @memberOf Document
250 * @instance
251 */
252
253Document.prototype.errors;
254
255/*!
256 * ignore
257 */
258
259function $__hasIncludedChildren(fields) {
260 const hasIncludedChildren = {};
261 const keys = Object.keys(fields);
262 for (let j = 0; j < keys.length; ++j) {
263 const parts = keys[j].split('.');
264 const c = [];
265 for (let k = 0; k < parts.length; ++k) {
266 c.push(parts[k]);
267 hasIncludedChildren[c.join('.')] = 1;
268 }
269 }
270
271 return hasIncludedChildren;
272}
273
274/*!
275 * ignore
276 */
277
278function $__applyDefaults(doc, fields, skipId, exclude, hasIncludedChildren, isBeforeSetters, pathsToSkip) {
279 const paths = Object.keys(doc.schema.paths);
280 const plen = paths.length;
281
282 for (let i = 0; i < plen; ++i) {
283 let def;
284 let curPath = '';
285 const p = paths[i];
286
287 if (p === '_id' && skipId) {
288 continue;
289 }
290
291 const type = doc.schema.paths[p];
292 const path = p.split('.');
293 const len = path.length;
294 let included = false;
295 let doc_ = doc._doc;
296
297 for (let j = 0; j < len; ++j) {
298 if (doc_ == null) {
299 break;
300 }
301
302 const piece = path[j];
303 curPath += (!curPath.length ? '' : '.') + piece;
304
305 if (exclude === true) {
306 if (curPath in fields) {
307 break;
308 }
309 } else if (exclude === false && fields && !included) {
310 if (curPath in fields) {
311 included = true;
312 } else if (!hasIncludedChildren[curPath]) {
313 break;
314 }
315 }
316
317 if (j === len - 1) {
318 if (doc_[piece] !== void 0) {
319 break;
320 }
321
322 if (typeof type.defaultValue === 'function') {
323 if (!type.defaultValue.$runBeforeSetters && isBeforeSetters) {
324 break;
325 }
326 if (type.defaultValue.$runBeforeSetters && !isBeforeSetters) {
327 break;
328 }
329 } else if (!isBeforeSetters) {
330 // Non-function defaults should always run **before** setters
331 continue;
332 }
333
334 if (pathsToSkip && pathsToSkip[curPath]) {
335 break;
336 }
337
338 if (fields && exclude !== null) {
339 if (exclude === true) {
340 // apply defaults to all non-excluded fields
341 if (p in fields) {
342 continue;
343 }
344
345 def = type.getDefault(doc, false);
346 if (typeof def !== 'undefined') {
347 doc_[piece] = def;
348 doc.$__.activePaths.default(p);
349 }
350 } else if (included) {
351 // selected field
352 def = type.getDefault(doc, false);
353 if (typeof def !== 'undefined') {
354 doc_[piece] = def;
355 doc.$__.activePaths.default(p);
356 }
357 }
358 } else {
359 def = type.getDefault(doc, false);
360 if (typeof def !== 'undefined') {
361 doc_[piece] = def;
362 doc.$__.activePaths.default(p);
363 }
364 }
365 } else {
366 doc_ = doc_[piece];
367 }
368 }
369 }
370}
371
372/**
373 * Builds the default doc structure
374 *
375 * @param {Object} obj
376 * @param {Object} [fields]
377 * @param {Boolean} [skipId]
378 * @api private
379 * @method $__buildDoc
380 * @memberOf Document
381 * @instance
382 */
383
384Document.prototype.$__buildDoc = function(obj, fields, skipId, exclude, hasIncludedChildren) {
385 const doc = {};
386
387 const paths = Object.keys(this.schema.paths).
388 // Don't build up any paths that are underneath a map, we don't know
389 // what the keys will be
390 filter(p => !p.includes('$*'));
391 const plen = paths.length;
392 let ii = 0;
393
394 for (; ii < plen; ++ii) {
395 const p = paths[ii];
396
397 if (p === '_id') {
398 if (skipId) {
399 continue;
400 }
401 if (obj && '_id' in obj) {
402 continue;
403 }
404 }
405
406 const path = p.split('.');
407 const len = path.length;
408 const last = len - 1;
409 let curPath = '';
410 let doc_ = doc;
411 let included = false;
412
413 for (let i = 0; i < len; ++i) {
414 const piece = path[i];
415
416 curPath += (!curPath.length ? '' : '.') + piece;
417
418 // support excluding intermediary levels
419 if (exclude === true) {
420 if (curPath in fields) {
421 break;
422 }
423 } else if (exclude === false && fields && !included) {
424 if (curPath in fields) {
425 included = true;
426 } else if (!hasIncludedChildren[curPath]) {
427 break;
428 }
429 }
430
431 if (i < last) {
432 doc_ = doc_[piece] || (doc_[piece] = {});
433 }
434 }
435 }
436
437 this._doc = doc;
438};
439
440/*!
441 * Converts to POJO when you use the document for querying
442 */
443
444Document.prototype.toBSON = function() {
445 return this.toObject(internalToObjectOptions);
446};
447
448/**
449 * Initializes the document without setters or marking anything modified.
450 *
451 * Called internally after a document is returned from mongodb. Normally,
452 * you do **not** need to call this function on your own.
453 *
454 * This function triggers `init` [middleware](/docs/middleware.html).
455 * Note that `init` hooks are [synchronous](/docs/middleware.html#synchronous).
456 *
457 * @param {Object} doc document returned by mongo
458 * @api public
459 * @memberOf Document
460 * @instance
461 */
462
463Document.prototype.init = function(doc, opts, fn) {
464 if (typeof opts === 'function') {
465 fn = opts;
466 opts = null;
467 }
468
469 this.$__init(doc, opts);
470
471 if (fn) {
472 fn(null, this);
473 }
474
475 return this;
476};
477
478/*!
479 * ignore
480 */
481
482Document.prototype.$__init = function(doc, opts) {
483 this.isNew = false;
484 this.$init = true;
485
486 // handle docs with populated paths
487 // If doc._id is not null or undefined
488 if (doc._id !== null && doc._id !== undefined &&
489 opts && opts.populated && opts.populated.length) {
490 const id = String(doc._id);
491 for (let i = 0; i < opts.populated.length; ++i) {
492 const item = opts.populated[i];
493 if (item.isVirtual) {
494 this.populated(item.path, utils.getValue(item.path, doc), item);
495 } else {
496 this.populated(item.path, item._docs[id], item);
497 }
498 }
499 }
500
501 init(this, doc, this._doc);
502
503 this.emit('init', this);
504 this.constructor.emit('init', this);
505
506 this.$__._id = this._id;
507
508 return this;
509};
510
511/*!
512 * Init helper.
513 *
514 * @param {Object} self document instance
515 * @param {Object} obj raw mongodb doc
516 * @param {Object} doc object we are initializing
517 * @api private
518 */
519
520function init(self, obj, doc, prefix) {
521 prefix = prefix || '';
522
523 const keys = Object.keys(obj);
524 const len = keys.length;
525 let schema;
526 let path;
527 let i;
528 let index = 0;
529
530 while (index < len) {
531 _init(index++);
532 }
533
534 function _init(index) {
535 i = keys[index];
536 path = prefix + i;
537 schema = self.schema.path(path);
538
539 // Should still work if not a model-level discriminator, but should not be
540 // necessary. This is *only* to catch the case where we queried using the
541 // base model and the discriminated model has a projection
542 if (self.schema.$isRootDiscriminator && !self.isSelected(path)) {
543 return;
544 }
545
546 if (!schema && utils.isPOJO(obj[i])) {
547 // assume nested object
548 if (!doc[i]) {
549 doc[i] = {};
550 }
551 init(self, obj[i], doc[i], path + '.');
552 } else if (!schema) {
553 doc[i] = obj[i];
554 } else {
555 if (obj[i] === null) {
556 doc[i] = null;
557 } else if (obj[i] !== undefined) {
558 const intCache = obj[i].$__ || {};
559 const wasPopulated = intCache.wasPopulated || null;
560 if (schema && !wasPopulated) {
561 try {
562 doc[i] = schema.cast(obj[i], self, true);
563 } catch (e) {
564 self.invalidate(e.path, new ValidatorError({
565 path: e.path,
566 message: e.message,
567 type: 'cast',
568 value: e.value
569 }));
570 }
571 } else {
572 doc[i] = obj[i];
573 }
574 }
575 // mark as hydrated
576 if (!self.isModified(path)) {
577 self.$__.activePaths.init(path);
578 }
579 }
580 }
581}
582
583/**
584 * Sends an update command with this document `_id` as the query selector.
585 *
586 * ####Example:
587 *
588 * weirdCar.update({$inc: {wheels:1}}, { w: 1 }, callback);
589 *
590 * ####Valid options:
591 *
592 * - same as in [Model.update](#model_Model.update)
593 *
594 * @see Model.update #model_Model.update
595 * @param {Object} doc
596 * @param {Object} options
597 * @param {Function} callback
598 * @return {Query}
599 * @api public
600 * @memberOf Document
601 * @instance
602 */
603
604Document.prototype.update = function update() {
605 const args = utils.args(arguments);
606 args.unshift({_id: this._id});
607 const query = this.constructor.update.apply(this.constructor, args);
608
609 if (this.$session() != null) {
610 if (!('session' in query.options)) {
611 query.options.session = this.$session();
612 }
613 }
614
615 return query;
616};
617
618/**
619 * Sends an updateOne command with this document `_id` as the query selector.
620 *
621 * ####Example:
622 *
623 * weirdCar.updateOne({$inc: {wheels:1}}, { w: 1 }, callback);
624 *
625 * ####Valid options:
626 *
627 * - same as in [Model.updateOne](#model_Model.updateOne)
628 *
629 * @see Model.updateOne #model_Model.updateOne
630 * @param {Object} doc
631 * @param {Object} options
632 * @param {Function} callback
633 * @return {Query}
634 * @api public
635 * @memberOf Document
636 * @instance
637 */
638
639Document.prototype.updateOne = function updateOne(doc, options, callback) {
640 const query = this.constructor.updateOne({_id: this._id}, doc, options);
641 query._pre(cb => {
642 this.constructor._middleware.execPre('updateOne', this, [], cb);
643 });
644 query._post(cb => {
645 this.constructor._middleware.execPost('updateOne', this, [], {}, cb);
646 });
647
648 if (this.$session() != null) {
649 if (!('session' in query.options)) {
650 query.options.session = this.$session();
651 }
652 }
653
654 if (callback != null) {
655 return query.exec(callback);
656 }
657
658 return query;
659};
660
661/**
662 * Sends a replaceOne command with this document `_id` as the query selector.
663 *
664 * ####Valid options:
665 *
666 * - same as in [Model.replaceOne](#model_Model.replaceOne)
667 *
668 * @see Model.replaceOne #model_Model.replaceOne
669 * @param {Object} doc
670 * @param {Object} options
671 * @param {Function} callback
672 * @return {Query}
673 * @api public
674 * @memberOf Document
675 * @instance
676 */
677
678Document.prototype.replaceOne = function replaceOne() {
679 const args = utils.args(arguments);
680 args.unshift({ _id: this._id });
681 return this.constructor.replaceOne.apply(this.constructor, args);
682};
683
684/**
685 * Getter/setter around the session associated with this document. Used to
686 * automatically set `session` if you `save()` a doc that you got from a
687 * query with an associated session.
688 *
689 * ####Example:
690 *
691 * const session = MyModel.startSession();
692 * const doc = await MyModel.findOne().session(session);
693 * doc.$session() === session; // true
694 * doc.$session(null);
695 * doc.$session() === null; // true
696 *
697 * If this is a top-level document, setting the session propagates to all child
698 * docs.
699 *
700 * @param {ClientSession} [session] overwrite the current session
701 * @return {ClientSession}
702 * @method $session
703 * @api public
704 * @memberOf Document
705 */
706
707Document.prototype.$session = function $session(session) {
708 if (arguments.length === 0) {
709 return this.$__.session;
710 }
711 this.$__.session = session;
712
713 if (!this.ownerDocument) {
714 const subdocs = this.$__getAllSubdocs();
715 for (const child of subdocs) {
716 child.$session(session);
717 }
718 }
719
720 return session;
721};
722
723/**
724 * Overwrite all values in this document with the values of `obj`, except
725 * for immutable properties. Behaves similarly to `set()`, except for it
726 * unsets all properties that aren't in `obj`.
727 *
728 * @param {Object} obj the object to overwrite this document with
729 * @method overwrite
730 * @name overwrite
731 * @memberOf Document
732 * @instance
733 * @api public
734 */
735
736Document.prototype.overwrite = function overwrite(obj) {
737 const keys = Array.from(new Set(Object.keys(this._doc).concat(Object.keys(obj))));
738
739 for (const key of keys) {
740 if (key === '_id') {
741 continue;
742 }
743 // Explicitly skip version key
744 if (this.schema.options.versionKey && key === this.schema.options.versionKey) {
745 continue;
746 }
747 this.$set(key, obj[key]);
748 }
749
750 return this;
751};
752
753/**
754 * Alias for `set()`, used internally to avoid conflicts
755 *
756 * @param {String|Object} path path or object of key/vals to set
757 * @param {Any} val the value to set
758 * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for "on-the-fly" attributes
759 * @param {Object} [options] optionally specify options that modify the behavior of the set
760 * @method $set
761 * @name $set
762 * @memberOf Document
763 * @instance
764 * @api public
765 */
766
767Document.prototype.$set = function $set(path, val, type, options) {
768 if (utils.isPOJO(type)) {
769 options = type;
770 type = undefined;
771 }
772
773 options = options || {};
774 const merge = options.merge;
775 const adhoc = type && type !== true;
776 const constructing = type === true;
777 let adhocs;
778 let keys;
779 let i = 0;
780 let pathtype;
781 let key;
782 let prefix;
783
784 const strict = 'strict' in options
785 ? options.strict
786 : this.$__.strictMode;
787
788 if (adhoc) {
789 adhocs = this.$__.adhocPaths || (this.$__.adhocPaths = {});
790 adhocs[path] = this.schema.interpretAsType(path, type, this.schema.options);
791 }
792
793 if (typeof path !== 'string') {
794 // new Document({ key: val })
795 if (path === null || path === void 0) {
796 const _ = path;
797 path = val;
798 val = _;
799 } else {
800 prefix = val ? val + '.' : '';
801
802 if (path instanceof Document) {
803 if (path.$__isNested) {
804 path = path.toObject();
805 } else {
806 path = path._doc;
807 }
808 }
809
810 keys = Object.keys(path);
811 const len = keys.length;
812
813 if (len === 0 && !this.schema.options.minimize) {
814 if (val) {
815 this.$set(val, {});
816 }
817 return this;
818 }
819
820 while (i < len) {
821 _handleIndex.call(this, i++);
822 }
823
824 return this;
825 }
826 } else {
827 this.$__.$setCalled.add(path);
828 }
829
830 function _handleIndex(i) {
831 key = keys[i];
832 const pathName = prefix + key;
833 pathtype = this.schema.pathType(pathName);
834
835 // On initial set, delete any nested keys if we're going to overwrite
836 // them to ensure we keep the user's key order.
837 if (type === true &&
838 !prefix &&
839 path[key] != null &&
840 pathtype === 'nested' &&
841 this._doc[key] != null &&
842 Object.keys(this._doc[key]).length === 0) {
843 delete this._doc[key];
844 }
845
846 if (typeof path[key] === 'object' &&
847 !utils.isNativeObject(path[key]) &&
848 path[key] != null &&
849 pathtype !== 'virtual' &&
850 pathtype !== 'real' &&
851 !(this.$__path(pathName) instanceof MixedSchema) &&
852 !(this.schema.paths[pathName] &&
853 this.schema.paths[pathName].options &&
854 this.schema.paths[pathName].options.ref)) {
855 this.$__.$setCalled.add(prefix + key);
856 this.$set(path[key], prefix + key, constructing);
857 } else if (strict) {
858 // Don't overwrite defaults with undefined keys (gh-3981)
859 if (constructing && path[key] === void 0 &&
860 this.get(key) !== void 0) {
861 return;
862 }
863
864 if (pathtype === 'adhocOrUndefined') {
865 pathtype = getEmbeddedDiscriminatorPath(this, pathName, { typeOnly: true });
866 }
867
868 if (pathtype === 'real' || pathtype === 'virtual') {
869 // Check for setting single embedded schema to document (gh-3535)
870 let p = path[key];
871 if (this.schema.paths[pathName] &&
872 this.schema.paths[pathName].$isSingleNested &&
873 path[key] instanceof Document) {
874 p = p.toObject({ virtuals: false, transform: false });
875 }
876 this.$set(prefix + key, p, constructing);
877 } else if (pathtype === 'nested' && path[key] instanceof Document) {
878 this.$set(prefix + key,
879 path[key].toObject({transform: false}), constructing);
880 } else if (strict === 'throw') {
881 if (pathtype === 'nested') {
882 throw new ObjectExpectedError(key, path[key]);
883 } else {
884 throw new StrictModeError(key);
885 }
886 }
887 } else if (path[key] !== void 0) {
888 this.$set(prefix + key, path[key], constructing);
889 }
890 }
891
892 let pathType = this.schema.pathType(path);
893 if (pathType === 'adhocOrUndefined') {
894 pathType = getEmbeddedDiscriminatorPath(this, path, { typeOnly: true });
895 }
896
897 // Assume this is a Mongoose document that was copied into a POJO using
898 // `Object.assign()` or `{...doc}`
899 if (utils.isPOJO(val) && val.$__ != null && val._doc != null) {
900 val = val._doc;
901 }
902
903 if (pathType === 'nested' && val) {
904 if (typeof val === 'object' && val != null) {
905 if (!merge) {
906 this.$__setValue(path, null);
907 cleanModifiedSubpaths(this, path);
908 } else {
909 return this.$set(val, path, constructing);
910 }
911
912 const keys = Object.keys(val);
913 this.$__setValue(path, {});
914 for (const key of keys) {
915 this.$set(path + '.' + key, val[key], constructing);
916 }
917 this.markModified(path);
918 cleanModifiedSubpaths(this, path, { skipDocArrays: true });
919 return this;
920 }
921 this.invalidate(path, new MongooseError.CastError('Object', val, path));
922 return this;
923 }
924
925 let schema;
926 const parts = path.split('.');
927
928 // Might need to change path for top-level alias
929 if (typeof this.schema.aliases[parts[0]] == 'string') {
930 parts[0] = this.schema.aliases[parts[0]];
931 }
932
933 if (pathType === 'adhocOrUndefined' && strict) {
934 // check for roots that are Mixed types
935 let mixed;
936
937 for (i = 0; i < parts.length; ++i) {
938 const subpath = parts.slice(0, i + 1).join('.');
939
940 // If path is underneath a virtual, bypass everything and just set it.
941 if (i + 1 < parts.length && this.schema.pathType(subpath) === 'virtual') {
942 mpath.set(path, val, this);
943 return this;
944 }
945
946 schema = this.schema.path(subpath);
947 if (schema == null) {
948 continue;
949 }
950
951 if (schema instanceof MixedSchema) {
952 // allow changes to sub paths of mixed types
953 mixed = true;
954 break;
955 }
956 }
957
958 if (schema == null) {
959 // Check for embedded discriminators
960 schema = getEmbeddedDiscriminatorPath(this, path);
961 }
962
963 if (!mixed && !schema) {
964 if (strict === 'throw') {
965 throw new StrictModeError(path);
966 }
967 return this;
968 }
969 } else if (pathType === 'virtual') {
970 schema = this.schema.virtualpath(path);
971 schema.applySetters(val, this);
972 return this;
973 } else {
974 schema = this.$__path(path);
975 }
976
977 // gh-4578, if setting a deeply nested path that doesn't exist yet, create it
978 let cur = this._doc;
979 let curPath = '';
980 for (i = 0; i < parts.length - 1; ++i) {
981 cur = cur[parts[i]];
982 curPath += (curPath.length > 0 ? '.' : '') + parts[i];
983 if (!cur) {
984 this.$set(curPath, {});
985 // Hack re: gh-5800. If nested field is not selected, it probably exists
986 // so `MongoError: cannot use the part (nested of nested.num) to
987 // traverse the element ({nested: null})` is not likely. If user gets
988 // that error, its their fault for now. We should reconsider disallowing
989 // modifying not selected paths for 6.x
990 if (!this.isSelected(curPath)) {
991 this.unmarkModified(curPath);
992 }
993 cur = this.$__getValue(curPath);
994 }
995 }
996
997 let pathToMark;
998
999 // When using the $set operator the path to the field must already exist.
1000 // Else mongodb throws: "LEFT_SUBFIELD only supports Object"
1001
1002 if (parts.length <= 1) {
1003 pathToMark = path;
1004 } else {
1005 for (i = 0; i < parts.length; ++i) {
1006 const subpath = parts.slice(0, i + 1).join('.');
1007 if (this.get(subpath) === null) {
1008 pathToMark = subpath;
1009 break;
1010 }
1011 }
1012
1013 if (!pathToMark) {
1014 pathToMark = path;
1015 }
1016 }
1017
1018 // if this doc is being constructed we should not trigger getters
1019 const priorVal = (() => {
1020 if (this.$__.$options.priorDoc != null) {
1021 return this.$__.$options.priorDoc.$__getValue(path);
1022 }
1023 if (constructing) {
1024 return void 0;
1025 }
1026 return this.$__getValue(path);
1027 })();
1028
1029 if (!schema) {
1030 this.$__set(pathToMark, path, constructing, parts, schema, val, priorVal);
1031 return this;
1032 }
1033
1034 let shouldSet = true;
1035 try {
1036 // If the user is trying to set a ref path to a document with
1037 // the correct model name, treat it as populated
1038 const refMatches = (() => {
1039 if (schema.options == null) {
1040 return false;
1041 }
1042 if (!(val instanceof Document)) {
1043 return false;
1044 }
1045 const model = val.constructor;
1046
1047 // Check ref
1048 const ref = schema.options.ref;
1049 if (ref != null && (ref === model.modelName || ref === model.baseModelName)) {
1050 return true;
1051 }
1052
1053 // Check refPath
1054 const refPath = schema.options.refPath;
1055 if (refPath == null) {
1056 return false;
1057 }
1058 const modelName = val.get(refPath);
1059 if (modelName === model.modelName || modelName === model.baseModelName) {
1060 return true;
1061 }
1062 return false;
1063 })();
1064
1065 let didPopulate = false;
1066 if (refMatches && val instanceof Document) {
1067 if (this.ownerDocument) {
1068 this.ownerDocument().populated(this.$__fullPath(path),
1069 val._id, { [populateModelSymbol]: val.constructor });
1070 } else {
1071 this.populated(path, val._id, { [populateModelSymbol]: val.constructor });
1072 }
1073 didPopulate = true;
1074 }
1075
1076 let popOpts;
1077 if (schema.options &&
1078 Array.isArray(schema.options[this.schema.options.typeKey]) &&
1079 schema.options[this.schema.options.typeKey].length &&
1080 schema.options[this.schema.options.typeKey][0].ref &&
1081 Array.isArray(val) &&
1082 val.length > 0 &&
1083 val[0] instanceof Document &&
1084 val[0].constructor.modelName &&
1085 (schema.options[this.schema.options.typeKey][0].ref === val[0].constructor.baseModelName || schema.options[this.schema.options.typeKey][0].ref === val[0].constructor.modelName)) {
1086 if (this.ownerDocument) {
1087 popOpts = { [populateModelSymbol]: val[0].constructor };
1088 this.ownerDocument().populated(this.$__fullPath(path),
1089 val.map(function(v) { return v._id; }), popOpts);
1090 } else {
1091 popOpts = { [populateModelSymbol]: val[0].constructor };
1092 this.populated(path, val.map(function(v) { return v._id; }), popOpts);
1093 }
1094 didPopulate = true;
1095 }
1096
1097 // If this path is underneath a single nested schema, we'll call the setter
1098 // later in `$__set()` because we don't take `_doc` when we iterate through
1099 // a single nested doc. That's to make sure we get the correct context.
1100 // Otherwise we would double-call the setter, see gh-7196.
1101 if (this.schema.singleNestedPaths[path] == null) {
1102 val = schema.applySetters(val, this, false, priorVal);
1103 }
1104
1105 if (!didPopulate && this.$__.populated) {
1106 delete this.$__.populated[path];
1107 }
1108
1109 this.$markValid(path);
1110 } catch (e) {
1111 this.invalidate(path,
1112 new MongooseError.CastError(schema.instance, val, path, e));
1113 shouldSet = false;
1114 }
1115
1116 if (shouldSet) {
1117 this.$__set(pathToMark, path, constructing, parts, schema, val, priorVal);
1118 }
1119
1120 if (schema.$isSingleNested && (this.isDirectModified(path) || val == null)) {
1121 cleanModifiedSubpaths(this, path);
1122 }
1123
1124 return this;
1125};
1126
1127/**
1128 * Sets the value of a path, or many paths.
1129 *
1130 * ####Example:
1131 *
1132 * // path, value
1133 * doc.set(path, value)
1134 *
1135 * // object
1136 * doc.set({
1137 * path : value
1138 * , path2 : {
1139 * path : value
1140 * }
1141 * })
1142 *
1143 * // on-the-fly cast to number
1144 * doc.set(path, value, Number)
1145 *
1146 * // on-the-fly cast to string
1147 * doc.set(path, value, String)
1148 *
1149 * // changing strict mode behavior
1150 * doc.set(path, value, { strict: false });
1151 *
1152 * @param {String|Object} path path or object of key/vals to set
1153 * @param {Any} val the value to set
1154 * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for "on-the-fly" attributes
1155 * @param {Object} [options] optionally specify options that modify the behavior of the set
1156 * @api public
1157 * @method set
1158 * @memberOf Document
1159 * @instance
1160 */
1161
1162Document.prototype.set = Document.prototype.$set;
1163
1164/**
1165 * Determine if we should mark this change as modified.
1166 *
1167 * @return {Boolean}
1168 * @api private
1169 * @method $__shouldModify
1170 * @memberOf Document
1171 * @instance
1172 */
1173
1174Document.prototype.$__shouldModify = function(pathToMark, path, constructing, parts, schema, val, priorVal) {
1175 if (this.isNew) {
1176 return true;
1177 }
1178
1179 // Re: the note about gh-7196, `val` is the raw value without casting or
1180 // setters if the full path is under a single nested subdoc because we don't
1181 // want to double run setters. So don't set it as modified. See gh-7264.
1182 if (this.schema.singleNestedPaths[path] != null) {
1183 return false;
1184 }
1185
1186 if (val === void 0 && !this.isSelected(path)) {
1187 // when a path is not selected in a query, its initial
1188 // value will be undefined.
1189 return true;
1190 }
1191
1192 if (val === void 0 && path in this.$__.activePaths.states.default) {
1193 // we're just unsetting the default value which was never saved
1194 return false;
1195 }
1196
1197 // gh-3992: if setting a populated field to a doc, don't mark modified
1198 // if they have the same _id
1199 if (this.populated(path) &&
1200 val instanceof Document &&
1201 deepEqual(val._id, priorVal)) {
1202 return false;
1203 }
1204
1205 if (!deepEqual(val, priorVal || this.get(path))) {
1206 return true;
1207 }
1208
1209 if (!constructing &&
1210 val !== null &&
1211 val !== undefined &&
1212 path in this.$__.activePaths.states.default &&
1213 deepEqual(val, schema.getDefault(this, constructing))) {
1214 // a path with a default was $unset on the server
1215 // and the user is setting it to the same value again
1216 return true;
1217 }
1218 return false;
1219};
1220
1221/**
1222 * Handles the actual setting of the value and marking the path modified if appropriate.
1223 *
1224 * @api private
1225 * @method $__set
1226 * @memberOf Document
1227 * @instance
1228 */
1229
1230Document.prototype.$__set = function(pathToMark, path, constructing, parts, schema, val, priorVal) {
1231 Embedded = Embedded || require('./types/embedded');
1232
1233 const shouldModify = this.$__shouldModify(pathToMark, path, constructing, parts,
1234 schema, val, priorVal);
1235 const _this = this;
1236
1237 if (shouldModify) {
1238 this.markModified(pathToMark);
1239
1240 // handle directly setting arrays (gh-1126)
1241 MongooseArray || (MongooseArray = require('./types/array'));
1242 if (val && val.isMongooseArray) {
1243 val._registerAtomic('$set', val);
1244
1245 // Update embedded document parent references (gh-5189)
1246 if (val.isMongooseDocumentArray) {
1247 val.forEach(function(item) {
1248 item && item.__parentArray && (item.__parentArray = val);
1249 });
1250 }
1251
1252 // Small hack for gh-1638: if we're overwriting the entire array, ignore
1253 // paths that were modified before the array overwrite
1254 this.$__.activePaths.forEach(function(modifiedPath) {
1255 if (modifiedPath.startsWith(path + '.')) {
1256 _this.$__.activePaths.ignore(modifiedPath);
1257 }
1258 });
1259 }
1260 }
1261
1262 let obj = this._doc;
1263 let i = 0;
1264 const l = parts.length;
1265 let cur = '';
1266
1267 for (; i < l; i++) {
1268 const next = i + 1;
1269 const last = next === l;
1270 cur += (cur ? '.' + parts[i] : parts[i]);
1271 if (specialProperties.has(parts[i])) {
1272 return;
1273 }
1274
1275 if (last) {
1276 if (obj instanceof Map) {
1277 obj.set(parts[i], val);
1278 } else {
1279 obj[parts[i]] = val;
1280 }
1281 } else {
1282 if (utils.isPOJO(obj[parts[i]])) {
1283 obj = obj[parts[i]];
1284 } else if (obj[parts[i]] && obj[parts[i]] instanceof Embedded) {
1285 obj = obj[parts[i]];
1286 } else if (obj[parts[i]] && obj[parts[i]].$isSingleNested) {
1287 obj = obj[parts[i]];
1288 } else if (obj[parts[i]] && Array.isArray(obj[parts[i]])) {
1289 obj = obj[parts[i]];
1290 } else {
1291 obj[parts[i]] = obj[parts[i]] || {};
1292 obj = obj[parts[i]];
1293 }
1294 }
1295 }
1296};
1297
1298/**
1299 * Gets a raw value from a path (no getters)
1300 *
1301 * @param {String} path
1302 * @api private
1303 */
1304
1305Document.prototype.$__getValue = function(path) {
1306 return utils.getValue(path, this._doc);
1307};
1308
1309/**
1310 * Sets a raw value for a path (no casting, setters, transformations)
1311 *
1312 * @param {String} path
1313 * @param {Object} value
1314 * @api private
1315 */
1316
1317Document.prototype.$__setValue = function(path, val) {
1318 utils.setValue(path, val, this._doc);
1319 return this;
1320};
1321
1322/**
1323 * Returns the value of a path.
1324 *
1325 * ####Example
1326 *
1327 * // path
1328 * doc.get('age') // 47
1329 *
1330 * // dynamic casting to a string
1331 * doc.get('age', String) // "47"
1332 *
1333 * @param {String} path
1334 * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for on-the-fly attributes
1335 * @param {Object} [options]
1336 * @param {Boolean} [options.virtuals=false] Apply virtuals before getting this path
1337 * @param {Boolean} [options.getters=true] If false, skip applying getters and just get the raw value
1338 * @api public
1339 */
1340
1341Document.prototype.get = function(path, type, options) {
1342 let adhoc;
1343 options = options || {};
1344 if (type) {
1345 adhoc = this.schema.interpretAsType(path, type, this.schema.options);
1346 }
1347
1348 const schema = this.$__path(path) || this.schema.virtualpath(path);
1349 const pieces = path.split('.');
1350 let obj = this._doc;
1351
1352 if (schema instanceof VirtualType) {
1353 if (schema.getters.length === 0) {
1354 return void 0;
1355 }
1356 return schema.applyGetters(null, this);
1357 }
1358
1359 // Might need to change path for top-level alias
1360 if (typeof this.schema.aliases[pieces[0]] == 'string') {
1361 pieces[0] = this.schema.aliases[pieces[0]];
1362 }
1363
1364 for (let i = 0, l = pieces.length; i < l; i++) {
1365 if (obj && obj._doc) {
1366 obj = obj._doc;
1367 }
1368
1369 if (obj == null) {
1370 obj = void 0;
1371 } else if (obj instanceof Map) {
1372 obj = obj.get(pieces[i]);
1373 } else if (i === l - 1) {
1374 obj = utils.getValue(pieces[i], obj);
1375 } else {
1376 obj = obj[pieces[i]];
1377 }
1378 }
1379
1380 if (adhoc) {
1381 obj = adhoc.cast(obj);
1382 }
1383
1384 if (schema != null && options.getters !== false) {
1385 obj = schema.applyGetters(obj, this);
1386 } else if (this.schema.nested[path] && options.virtuals) {
1387 // Might need to apply virtuals if this is a nested path
1388 return applyGetters(this, utils.clone(obj) || {}, 'virtuals', { path: path });
1389 }
1390
1391 return obj;
1392};
1393
1394/*!
1395 * ignore
1396 */
1397
1398Document.prototype[getSymbol] = Document.prototype.get;
1399
1400/**
1401 * Returns the schematype for the given `path`.
1402 *
1403 * @param {String} path
1404 * @api private
1405 * @method $__path
1406 * @memberOf Document
1407 * @instance
1408 */
1409
1410Document.prototype.$__path = function(path) {
1411 const adhocs = this.$__.adhocPaths;
1412 const adhocType = adhocs && adhocs.hasOwnProperty(path) ? adhocs[path] : null;
1413
1414 if (adhocType) {
1415 return adhocType;
1416 }
1417 return this.schema.path(path);
1418};
1419
1420/**
1421 * Marks the path as having pending changes to write to the db.
1422 *
1423 * _Very helpful when using [Mixed](./schematypes.html#mixed) types._
1424 *
1425 * ####Example:
1426 *
1427 * doc.mixed.type = 'changed';
1428 * doc.markModified('mixed.type');
1429 * doc.save() // changes to mixed.type are now persisted
1430 *
1431 * @param {String} path the path to mark modified
1432 * @param {Document} [scope] the scope to run validators with
1433 * @api public
1434 */
1435
1436Document.prototype.markModified = function(path, scope) {
1437 this.$__.activePaths.modify(path);
1438 if (scope != null && !this.ownerDocument) {
1439 this.$__.pathsToScopes[path] = scope;
1440 }
1441};
1442
1443/**
1444 * Clears the modified state on the specified path.
1445 *
1446 * ####Example:
1447 *
1448 * doc.foo = 'bar';
1449 * doc.unmarkModified('foo');
1450 * doc.save(); // changes to foo will not be persisted
1451 *
1452 * @param {String} path the path to unmark modified
1453 * @api public
1454 */
1455
1456Document.prototype.unmarkModified = function(path) {
1457 this.$__.activePaths.init(path);
1458 delete this.$__.pathsToScopes[path];
1459};
1460
1461/**
1462 * Don't run validation on this path or persist changes to this path.
1463 *
1464 * ####Example:
1465 *
1466 * doc.foo = null;
1467 * doc.$ignore('foo');
1468 * doc.save(); // changes to foo will not be persisted and validators won't be run
1469 *
1470 * @memberOf Document
1471 * @instance
1472 * @method $ignore
1473 * @param {String} path the path to ignore
1474 * @api public
1475 */
1476
1477Document.prototype.$ignore = function(path) {
1478 this.$__.activePaths.ignore(path);
1479};
1480
1481/**
1482 * Returns the list of paths that have been directly modified. A direct
1483 * modified path is a path that you explicitly set, whether via `doc.foo = 'bar'`,
1484 * `Object.assign(doc, { foo: 'bar' })`, or `doc.set('foo', 'bar')`.
1485 *
1486 * A path `a` may be in `modifiedPaths()` but not in `directModifiedPaths()`
1487 * because a child of `a` was directly modified.
1488 *
1489 * ####Example
1490 * const schema = new Schema({ foo: String, nested: { bar: String } });
1491 * const Model = mongoose.model('Test', schema);
1492 * await Model.create({ foo: 'original', nested: { bar: 'original' } });
1493 *
1494 * const doc = await Model.findOne();
1495 * doc.nested.bar = 'modified';
1496 * doc.directModifiedPaths(); // ['nested.bar']
1497 * doc.modifiedPaths(); // ['nested', 'nested.bar']
1498 *
1499 * @return {Array}
1500 * @api public
1501 */
1502
1503Document.prototype.directModifiedPaths = function() {
1504 return Object.keys(this.$__.activePaths.states.modify);
1505};
1506
1507/**
1508 * Returns true if the given path is nullish or only contains empty objects.
1509 * Useful for determining whether this subdoc will get stripped out by the
1510 * [minimize option](/docs/guide.html#minimize).
1511 *
1512 * ####Example:
1513 * const schema = new Schema({ nested: { foo: String } });
1514 * const Model = mongoose.model('Test', schema);
1515 * const doc = new Model({});
1516 * doc.$isEmpty('nested'); // true
1517 * doc.nested.$isEmpty(); // true
1518 *
1519 * doc.nested.foo = 'bar';
1520 * doc.$isEmpty('nested'); // false
1521 * doc.nested.$isEmpty(); // false
1522 *
1523 * @memberOf Document
1524 * @instance
1525 * @api public
1526 * @method $isEmpty
1527 * @return {Boolean}
1528 */
1529
1530Document.prototype.$isEmpty = function(path) {
1531 const isEmptyOptions = {
1532 minimize: true,
1533 virtuals: false,
1534 getters: false,
1535 transform: false
1536 };
1537
1538 if (arguments.length > 0) {
1539 const v = this.get(path);
1540 if (v == null) {
1541 return true;
1542 }
1543 if (typeof v !== 'object') {
1544 return false;
1545 }
1546 if (utils.isPOJO(v)) {
1547 return _isEmpty(v);
1548 }
1549 return Object.keys(v.toObject(isEmptyOptions)).length === 0;
1550 }
1551
1552 return Object.keys(this.toObject(isEmptyOptions)).length === 0;
1553};
1554
1555function _isEmpty(v) {
1556 if (v == null) {
1557 return true;
1558 }
1559 if (typeof v !== 'object' || Array.isArray(v)) {
1560 return false;
1561 }
1562 for (const key of Object.keys(v)) {
1563 if (!_isEmpty(v[key])) {
1564 return false;
1565 }
1566 }
1567 return true;
1568}
1569
1570/**
1571 * Returns the list of paths that have been modified.
1572 *
1573 * @param {Object} [options]
1574 * @param {Boolean} [options.includeChildren=false] if true, returns children of modified paths as well. For example, if false, the list of modified paths for `doc.colors = { primary: 'blue' };` will **not** contain `colors.primary`. If true, `modifiedPaths()` will return an array that contains `colors.primary`.
1575 * @return {Array}
1576 * @api public
1577 */
1578
1579Document.prototype.modifiedPaths = function(options) {
1580 options = options || {};
1581 const directModifiedPaths = Object.keys(this.$__.activePaths.states.modify);
1582 const _this = this;
1583 return directModifiedPaths.reduce(function(list, path) {
1584 const parts = path.split('.');
1585 list = list.concat(parts.reduce(function(chains, part, i) {
1586 return chains.concat(parts.slice(0, i).concat(part).join('.'));
1587 }, []).filter(function(chain) {
1588 return (list.indexOf(chain) === -1);
1589 }));
1590
1591 if (!options.includeChildren) {
1592 return list;
1593 }
1594
1595 let cur = _this.get(path);
1596 if (cur != null && typeof cur === 'object') {
1597 if (cur._doc) {
1598 cur = cur._doc;
1599 }
1600 if (Array.isArray(cur)) {
1601 const len = cur.length;
1602 for (let i = 0; i < len; ++i) {
1603 if (list.indexOf(path + '.' + i) === -1) {
1604 list.push(path + '.' + i);
1605 if (cur[i] != null && cur[i].$__) {
1606 const modified = cur[i].modifiedPaths();
1607 for (const childPath of modified) {
1608 list.push(path + '.' + i + '.' + childPath);
1609 }
1610 }
1611 }
1612 }
1613 } else {
1614 Object.keys(cur).
1615 filter(function(key) {
1616 return list.indexOf(path + '.' + key) === -1;
1617 }).
1618 forEach(function(key) {
1619 list.push(path + '.' + key);
1620 });
1621 }
1622 }
1623
1624 return list;
1625 }, []);
1626};
1627
1628/**
1629 * Returns true if this document was modified, else false.
1630 *
1631 * If `path` is given, checks if a path or any full path containing `path` as part of its path chain has been modified.
1632 *
1633 * ####Example
1634 *
1635 * doc.set('documents.0.title', 'changed');
1636 * doc.isModified() // true
1637 * doc.isModified('documents') // true
1638 * doc.isModified('documents.0.title') // true
1639 * doc.isModified('documents otherProp') // true
1640 * doc.isDirectModified('documents') // false
1641 *
1642 * @param {String} [path] optional
1643 * @return {Boolean}
1644 * @api public
1645 */
1646
1647Document.prototype.isModified = function(paths, modifiedPaths) {
1648 if (paths) {
1649 if (!Array.isArray(paths)) {
1650 paths = paths.split(' ');
1651 }
1652 const modified = modifiedPaths || this.modifiedPaths();
1653 const directModifiedPaths = Object.keys(this.$__.activePaths.states.modify);
1654 const isModifiedChild = paths.some(function(path) {
1655 return !!~modified.indexOf(path);
1656 });
1657 return isModifiedChild || paths.some(function(path) {
1658 return directModifiedPaths.some(function(mod) {
1659 return mod === path || path.startsWith(mod + '.');
1660 });
1661 });
1662 }
1663 return this.$__.activePaths.some('modify');
1664};
1665
1666/**
1667 * Checks if a path is set to its default.
1668 *
1669 * ####Example
1670 *
1671 * MyModel = mongoose.model('test', { name: { type: String, default: 'Val '} });
1672 * var m = new MyModel();
1673 * m.$isDefault('name'); // true
1674 *
1675 * @memberOf Document
1676 * @instance
1677 * @method $isDefault
1678 * @param {String} [path]
1679 * @return {Boolean}
1680 * @api public
1681 */
1682
1683Document.prototype.$isDefault = function(path) {
1684 return (path in this.$__.activePaths.states.default);
1685};
1686
1687/**
1688 * Getter/setter, determines whether the document was removed or not.
1689 *
1690 * ####Example:
1691 * product.remove(function (err, product) {
1692 * product.$isDeleted(); // true
1693 * product.remove(); // no-op, doesn't send anything to the db
1694 *
1695 * product.$isDeleted(false);
1696 * product.$isDeleted(); // false
1697 * product.remove(); // will execute a remove against the db
1698 * })
1699 *
1700 * @param {Boolean} [val] optional, overrides whether mongoose thinks the doc is deleted
1701 * @return {Boolean} whether mongoose thinks this doc is deleted.
1702 * @method $isDeleted
1703 * @memberOf Document
1704 * @instance
1705 * @api public
1706 */
1707
1708Document.prototype.$isDeleted = function(val) {
1709 if (arguments.length === 0) {
1710 return !!this.$__.isDeleted;
1711 }
1712
1713 this.$__.isDeleted = !!val;
1714 return this;
1715};
1716
1717/**
1718 * Returns true if `path` was directly set and modified, else false.
1719 *
1720 * ####Example
1721 *
1722 * doc.set('documents.0.title', 'changed');
1723 * doc.isDirectModified('documents.0.title') // true
1724 * doc.isDirectModified('documents') // false
1725 *
1726 * @param {String} path
1727 * @return {Boolean}
1728 * @api public
1729 */
1730
1731Document.prototype.isDirectModified = function(path) {
1732 return (path in this.$__.activePaths.states.modify);
1733};
1734
1735/**
1736 * Checks if `path` was initialized.
1737 *
1738 * @param {String} path
1739 * @return {Boolean}
1740 * @api public
1741 */
1742
1743Document.prototype.isInit = function(path) {
1744 return (path in this.$__.activePaths.states.init);
1745};
1746
1747/**
1748 * Checks if `path` was selected in the source query which initialized this document.
1749 *
1750 * ####Example
1751 *
1752 * Thing.findOne().select('name').exec(function (err, doc) {
1753 * doc.isSelected('name') // true
1754 * doc.isSelected('age') // false
1755 * })
1756 *
1757 * @param {String} path
1758 * @return {Boolean}
1759 * @api public
1760 */
1761
1762Document.prototype.isSelected = function isSelected(path) {
1763 if (this.$__.selected) {
1764 if (path === '_id') {
1765 return this.$__.selected._id !== 0;
1766 }
1767
1768 const paths = Object.keys(this.$__.selected);
1769 let i = paths.length;
1770 let inclusive = null;
1771 let cur;
1772
1773 if (i === 1 && paths[0] === '_id') {
1774 // only _id was selected.
1775 return this.$__.selected._id === 0;
1776 }
1777
1778 while (i--) {
1779 cur = paths[i];
1780 if (cur === '_id') {
1781 continue;
1782 }
1783 if (!isDefiningProjection(this.$__.selected[cur])) {
1784 continue;
1785 }
1786 inclusive = !!this.$__.selected[cur];
1787 break;
1788 }
1789
1790 if (inclusive === null) {
1791 return true;
1792 }
1793
1794 if (path in this.$__.selected) {
1795 return inclusive;
1796 }
1797
1798 i = paths.length;
1799 const pathDot = path + '.';
1800
1801 while (i--) {
1802 cur = paths[i];
1803 if (cur === '_id') {
1804 continue;
1805 }
1806
1807 if (cur.startsWith(pathDot)) {
1808 return inclusive || cur !== pathDot;
1809 }
1810
1811 if (pathDot.startsWith(cur + '.')) {
1812 return inclusive;
1813 }
1814 }
1815
1816 return !inclusive;
1817 }
1818
1819 return true;
1820};
1821
1822/**
1823 * Checks if `path` was explicitly selected. If no projection, always returns
1824 * true.
1825 *
1826 * ####Example
1827 *
1828 * Thing.findOne().select('nested.name').exec(function (err, doc) {
1829 * doc.isDirectSelected('nested.name') // true
1830 * doc.isDirectSelected('nested.otherName') // false
1831 * doc.isDirectSelected('nested') // false
1832 * })
1833 *
1834 * @param {String} path
1835 * @return {Boolean}
1836 * @api public
1837 */
1838
1839Document.prototype.isDirectSelected = function isDirectSelected(path) {
1840 if (this.$__.selected) {
1841 if (path === '_id') {
1842 return this.$__.selected._id !== 0;
1843 }
1844
1845 const paths = Object.keys(this.$__.selected);
1846 let i = paths.length;
1847 let inclusive = null;
1848 let cur;
1849
1850 if (i === 1 && paths[0] === '_id') {
1851 // only _id was selected.
1852 return this.$__.selected._id === 0;
1853 }
1854
1855 while (i--) {
1856 cur = paths[i];
1857 if (cur === '_id') {
1858 continue;
1859 }
1860 if (!isDefiningProjection(this.$__.selected[cur])) {
1861 continue;
1862 }
1863 inclusive = !!this.$__.selected[cur];
1864 break;
1865 }
1866
1867 if (inclusive === null) {
1868 return true;
1869 }
1870
1871 if (path in this.$__.selected) {
1872 return inclusive;
1873 }
1874
1875 return !inclusive;
1876 }
1877
1878 return true;
1879};
1880
1881/**
1882 * Executes registered validation rules for this document.
1883 *
1884 * ####Note:
1885 *
1886 * This method is called `pre` save and if a validation rule is violated, [save](#model_Model-save) is aborted and the error is returned to your `callback`.
1887 *
1888 * ####Example:
1889 *
1890 * doc.validate(function (err) {
1891 * if (err) handleError(err);
1892 * else // validation passed
1893 * });
1894 *
1895 * @param {Object} optional options internal options
1896 * @param {Function} callback optional callback called after validation completes, passing an error if one occurred
1897 * @return {Promise} Promise
1898 * @api public
1899 */
1900
1901Document.prototype.validate = function(options, callback) {
1902 if (typeof options === 'function') {
1903 callback = options;
1904 options = null;
1905 }
1906
1907 return utils.promiseOrCallback(callback, cb => this.$__validate(options, function(error) {
1908 cb(error);
1909 }), this.constructor.events);
1910};
1911
1912/*!
1913 * ignore
1914 */
1915
1916function _evaluateRequiredFunctions(doc) {
1917 Object.keys(doc.$__.activePaths.states.require).forEach(path => {
1918 const p = doc.schema.path(path);
1919
1920 if (p != null && typeof p.originalRequiredValue === 'function') {
1921 doc.$__.cachedRequired[path] = p.originalRequiredValue.call(doc);
1922 }
1923 });
1924}
1925
1926/*!
1927 * ignore
1928 */
1929
1930function _getPathsToValidate(doc) {
1931 let i;
1932 let len;
1933 const skipSchemaValidators = {};
1934
1935 _evaluateRequiredFunctions(doc);
1936
1937 // only validate required fields when necessary
1938 let paths = Object.keys(doc.$__.activePaths.states.require).filter(function(path) {
1939 if (!doc.isSelected(path) && !doc.isModified(path)) {
1940 return false;
1941 }
1942 if (path in doc.$__.cachedRequired) {
1943 return doc.$__.cachedRequired[path];
1944 }
1945 return true;
1946 });
1947
1948 paths = paths.concat(Object.keys(doc.$__.activePaths.states.init));
1949 paths = paths.concat(Object.keys(doc.$__.activePaths.states.modify));
1950 paths = paths.concat(Object.keys(doc.$__.activePaths.states.default));
1951
1952 if (!doc.ownerDocument) {
1953 const subdocs = doc.$__getAllSubdocs();
1954 let subdoc;
1955 len = subdocs.length;
1956 const modifiedPaths = doc.modifiedPaths();
1957 for (i = 0; i < len; ++i) {
1958 subdoc = subdocs[i];
1959 if (doc.isModified(subdoc.$basePath, modifiedPaths) &&
1960 !doc.isDirectModified(subdoc.$basePath) &&
1961 !doc.$isDefault(subdoc.$basePath)) {
1962 // Remove child paths for now, because we'll be validating the whole
1963 // subdoc
1964 paths = paths.filter(function(p) {
1965 return p != null && p.indexOf(subdoc.$basePath + '.') !== 0;
1966 });
1967 paths.push(subdoc.$basePath);
1968 skipSchemaValidators[subdoc.$basePath] = true;
1969 }
1970 }
1971 }
1972
1973 // gh-661: if a whole array is modified, make sure to run validation on all
1974 // the children as well
1975 len = paths.length;
1976 for (i = 0; i < len; ++i) {
1977 const path = paths[i];
1978
1979 const _pathType = doc.schema.path(path);
1980 if (!_pathType ||
1981 !_pathType.$isMongooseArray ||
1982 // To avoid potential performance issues, skip doc arrays whose children
1983 // are not required. `getPositionalPathType()` may be slow, so avoid
1984 // it unless we have a case of #6364
1985 (_pathType.$isMongooseDocumentArray && !get(_pathType, 'schemaOptions.required'))) {
1986 continue;
1987 }
1988
1989 const val = doc.$__getValue(path);
1990 if (val) {
1991 const numElements = val.length;
1992 for (let j = 0; j < numElements; ++j) {
1993 paths.push(path + '.' + j);
1994 }
1995 }
1996 }
1997
1998 const flattenOptions = { skipArrays: true };
1999 len = paths.length;
2000 for (i = 0; i < len; ++i) {
2001 const pathToCheck = paths[i];
2002 if (doc.schema.nested[pathToCheck]) {
2003 let _v = doc.$__getValue(pathToCheck);
2004 if (isMongooseObject(_v)) {
2005 _v = _v.toObject({ transform: false });
2006 }
2007 const flat = flatten(_v, '', flattenOptions);
2008 const _subpaths = Object.keys(flat).map(function(p) {
2009 return pathToCheck + '.' + p;
2010 });
2011 paths = paths.concat(_subpaths);
2012 }
2013 }
2014
2015 len = paths.length;
2016 for (i = 0; i < len; ++i) {
2017 const path = paths[i];
2018 const _pathType = doc.schema.path(path);
2019 if (!_pathType || !_pathType.$isSchemaMap) {
2020 continue;
2021 }
2022
2023 const val = doc.$__getValue(path);
2024 if (val == null) {
2025 continue;
2026 }
2027 for (const key of val.keys()) {
2028 paths.push(path + '.' + key);
2029 }
2030 }
2031
2032 return [paths, skipSchemaValidators];
2033}
2034
2035/*!
2036 * ignore
2037 */
2038
2039Document.prototype.$__validate = function(options, callback) {
2040 if (typeof options === 'function') {
2041 callback = options;
2042 options = null;
2043 }
2044
2045 const hasValidateModifiedOnlyOption = options &&
2046 (typeof options === 'object') &&
2047 ('validateModifiedOnly' in options);
2048
2049 let shouldValidateModifiedOnly;
2050 if (hasValidateModifiedOnlyOption) {
2051 shouldValidateModifiedOnly = !!options.validateModifiedOnly;
2052 } else {
2053 shouldValidateModifiedOnly = this.schema.options.validateModifiedOnly;
2054 }
2055
2056 const _this = this;
2057 const _complete = () => {
2058 const err = this.$__.validationError;
2059 this.$__.validationError = undefined;
2060 this.$__.cachedRequired = {};
2061 this.emit('validate', _this);
2062 this.constructor.emit('validate', _this);
2063 if (err) {
2064 for (const key in err.errors) {
2065 // Make sure cast errors persist
2066 if (!this[documentArrayParent] && err.errors[key] instanceof MongooseError.CastError) {
2067 this.invalidate(key, err.errors[key]);
2068 }
2069 }
2070
2071 return err;
2072 }
2073 };
2074
2075 // only validate required fields when necessary
2076 const pathDetails = _getPathsToValidate(this);
2077 const paths = shouldValidateModifiedOnly ?
2078 pathDetails[0].filter((path) => this.isModified(path)) :
2079 pathDetails[0];
2080 const skipSchemaValidators = pathDetails[1];
2081
2082 if (paths.length === 0) {
2083 return process.nextTick(function() {
2084 const error = _complete();
2085 if (error) {
2086 return _this.schema.s.hooks.execPost('validate:error', _this, [ _this], { error: error }, function(error) {
2087 callback(error);
2088 });
2089 }
2090 callback(null, _this);
2091 });
2092 }
2093
2094 const validated = {};
2095 let total = 0;
2096
2097 const complete = function() {
2098 const error = _complete();
2099 if (error) {
2100 return _this.schema.s.hooks.execPost('validate:error', _this, [ _this], { error: error }, function(error) {
2101 callback(error);
2102 });
2103 }
2104 callback(null, _this);
2105 };
2106
2107 const validatePath = function(path) {
2108 if (path == null || validated[path]) {
2109 return;
2110 }
2111
2112 validated[path] = true;
2113 total++;
2114
2115 process.nextTick(function() {
2116 const p = _this.schema.path(path);
2117
2118 if (!p) {
2119 return --total || complete();
2120 }
2121
2122 // If user marked as invalid or there was a cast error, don't validate
2123 if (!_this.$isValid(path)) {
2124 --total || complete();
2125 return;
2126 }
2127
2128 const val = _this.$__getValue(path);
2129 const scope = path in _this.$__.pathsToScopes ?
2130 _this.$__.pathsToScopes[path] :
2131 _this;
2132
2133 p.doValidate(val, function(err) {
2134 if (err && (!p.$isMongooseDocumentArray || err.$isArrayValidatorError)) {
2135 if (p.$isSingleNested &&
2136 err.name === 'ValidationError' &&
2137 p.schema.options.storeSubdocValidationError === false) {
2138 return --total || complete();
2139 }
2140 _this.invalidate(path, err, undefined, true);
2141 }
2142 --total || complete();
2143 }, scope, { skipSchemaValidators: skipSchemaValidators[path], path: path });
2144 });
2145 };
2146
2147 const numPaths = paths.length;
2148 for (let i = 0; i < numPaths; ++i) {
2149 validatePath(paths[i]);
2150 }
2151};
2152
2153/**
2154 * Executes registered validation rules (skipping asynchronous validators) for this document.
2155 *
2156 * ####Note:
2157 *
2158 * This method is useful if you need synchronous validation.
2159 *
2160 * ####Example:
2161 *
2162 * var err = doc.validateSync();
2163 * if ( err ){
2164 * handleError( err );
2165 * } else {
2166 * // validation passed
2167 * }
2168 *
2169 * @param {Array|string} pathsToValidate only validate the given paths
2170 * @return {ValidationError|undefined} ValidationError if there are errors during validation, or undefined if there is no error.
2171 * @api public
2172 */
2173
2174Document.prototype.validateSync = function(pathsToValidate, options) {
2175 const _this = this;
2176
2177 const hasValidateModifiedOnlyOption = options &&
2178 (typeof options === 'object') &&
2179 ('validateModifiedOnly' in options);
2180
2181 let shouldValidateModifiedOnly;
2182 if (hasValidateModifiedOnlyOption) {
2183 shouldValidateModifiedOnly = !!options.validateModifiedOnly;
2184 } else {
2185 shouldValidateModifiedOnly = this.schema.options.validateModifiedOnly;
2186 }
2187
2188 if (typeof pathsToValidate === 'string') {
2189 pathsToValidate = pathsToValidate.split(' ');
2190 }
2191
2192 // only validate required fields when necessary
2193 const pathDetails = _getPathsToValidate(this);
2194 let paths = shouldValidateModifiedOnly ?
2195 pathDetails[0].filter((path) => this.isModified(path)) :
2196 pathDetails[0];
2197 const skipSchemaValidators = pathDetails[1];
2198
2199 if (pathsToValidate && pathsToValidate.length) {
2200 const tmp = [];
2201 for (let i = 0; i < paths.length; ++i) {
2202 if (pathsToValidate.indexOf(paths[i]) !== -1) {
2203 tmp.push(paths[i]);
2204 }
2205 }
2206 paths = tmp;
2207 }
2208
2209 const validating = {};
2210
2211 paths.forEach(function(path) {
2212 if (validating[path]) {
2213 return;
2214 }
2215
2216 validating[path] = true;
2217
2218 const p = _this.schema.path(path);
2219 if (!p) {
2220 return;
2221 }
2222 if (!_this.$isValid(path)) {
2223 return;
2224 }
2225
2226 const val = _this.$__getValue(path);
2227 const err = p.doValidateSync(val, _this, {
2228 skipSchemaValidators: skipSchemaValidators[path],
2229 path: path
2230 });
2231 if (err && (!p.$isMongooseDocumentArray || err.$isArrayValidatorError)) {
2232 if (p.$isSingleNested &&
2233 err.name === 'ValidationError' &&
2234 p.schema.options.storeSubdocValidationError === false) {
2235 return;
2236 }
2237 _this.invalidate(path, err, undefined, true);
2238 }
2239 });
2240
2241 const err = _this.$__.validationError;
2242 _this.$__.validationError = undefined;
2243 _this.emit('validate', _this);
2244 _this.constructor.emit('validate', _this);
2245
2246 if (err) {
2247 for (const key in err.errors) {
2248 // Make sure cast errors persist
2249 if (err.errors[key] instanceof MongooseError.CastError) {
2250 _this.invalidate(key, err.errors[key]);
2251 }
2252 }
2253 }
2254
2255 return err;
2256};
2257
2258/**
2259 * Marks a path as invalid, causing validation to fail.
2260 *
2261 * The `errorMsg` argument will become the message of the `ValidationError`.
2262 *
2263 * The `value` argument (if passed) will be available through the `ValidationError.value` property.
2264 *
2265 * doc.invalidate('size', 'must be less than 20', 14);
2266
2267 * doc.validate(function (err) {
2268 * console.log(err)
2269 * // prints
2270 * { message: 'Validation failed',
2271 * name: 'ValidationError',
2272 * errors:
2273 * { size:
2274 * { message: 'must be less than 20',
2275 * name: 'ValidatorError',
2276 * path: 'size',
2277 * type: 'user defined',
2278 * value: 14 } } }
2279 * })
2280 *
2281 * @param {String} path the field to invalidate
2282 * @param {String|Error} errorMsg the error which states the reason `path` was invalid
2283 * @param {Object|String|Number|any} value optional invalid value
2284 * @param {String} [kind] optional `kind` property for the error
2285 * @return {ValidationError} the current ValidationError, with all currently invalidated paths
2286 * @api public
2287 */
2288
2289Document.prototype.invalidate = function(path, err, val, kind) {
2290 if (!this.$__.validationError) {
2291 this.$__.validationError = new ValidationError(this);
2292 }
2293
2294 if (this.$__.validationError.errors[path]) {
2295 return;
2296 }
2297
2298 if (!err || typeof err === 'string') {
2299 err = new ValidatorError({
2300 path: path,
2301 message: err,
2302 type: kind || 'user defined',
2303 value: val
2304 });
2305 }
2306
2307 if (this.$__.validationError === err) {
2308 return this.$__.validationError;
2309 }
2310
2311 this.$__.validationError.addError(path, err);
2312 return this.$__.validationError;
2313};
2314
2315/**
2316 * Marks a path as valid, removing existing validation errors.
2317 *
2318 * @param {String} path the field to mark as valid
2319 * @api public
2320 * @memberOf Document
2321 * @instance
2322 * @method $markValid
2323 */
2324
2325Document.prototype.$markValid = function(path) {
2326 if (!this.$__.validationError || !this.$__.validationError.errors[path]) {
2327 return;
2328 }
2329
2330 delete this.$__.validationError.errors[path];
2331 if (Object.keys(this.$__.validationError.errors).length === 0) {
2332 this.$__.validationError = null;
2333 }
2334};
2335
2336/**
2337 * Saves this document.
2338 *
2339 * ####Example:
2340 *
2341 * product.sold = Date.now();
2342 * product.save(function (err, product) {
2343 * if (err) ..
2344 * })
2345 *
2346 * The callback will receive two parameters
2347 *
2348 * 1. `err` if an error occurred
2349 * 2. `product` which is the saved `product`
2350 *
2351 * As an extra measure of flow control, save will return a Promise.
2352 * ####Example:
2353 * product.save().then(function(product) {
2354 * ...
2355 * });
2356 *
2357 * @param {Object} [options] options optional options
2358 * @param {Object} [options.safe] (DEPRECATED) overrides [schema's safe option](http://mongoosejs.com//docs/guide.html#safe)
2359 * @param {Boolean} [options.validateBeforeSave] set to false to save without validating.
2360 * @param {Function} [fn] optional callback
2361 * @method save
2362 * @memberOf Document
2363 * @instance
2364 * @return {Promise|undefined} Returns undefined if used with callback or a Promise otherwise.
2365 * @api public
2366 * @see middleware http://mongoosejs.com/docs/middleware.html
2367 */
2368
2369/**
2370 * Checks if a path is invalid
2371 *
2372 * @param {String} path the field to check
2373 * @method $isValid
2374 * @memberOf Document
2375 * @instance
2376 * @api private
2377 */
2378
2379Document.prototype.$isValid = function(path) {
2380 return !this.$__.validationError || !this.$__.validationError.errors[path];
2381};
2382
2383/**
2384 * Resets the internal modified state of this document.
2385 *
2386 * @api private
2387 * @return {Document}
2388 * @method $__reset
2389 * @memberOf Document
2390 * @instance
2391 */
2392
2393Document.prototype.$__reset = function reset() {
2394 let _this = this;
2395 DocumentArray || (DocumentArray = require('./types/documentarray'));
2396
2397 this.$__.activePaths
2398 .map('init', 'modify', function(i) {
2399 return _this.$__getValue(i);
2400 })
2401 .filter(function(val) {
2402 return val && val instanceof Array && val.isMongooseDocumentArray && val.length;
2403 })
2404 .forEach(function(array) {
2405 let i = array.length;
2406 while (i--) {
2407 const doc = array[i];
2408 if (!doc) {
2409 continue;
2410 }
2411 doc.$__reset();
2412 }
2413
2414 _this.$__.activePaths.init(array.$path());
2415
2416 array[arrayAtomicsSymbol] = {};
2417 });
2418
2419 this.$__.activePaths.
2420 map('init', 'modify', function(i) {
2421 return _this.$__getValue(i);
2422 }).
2423 filter(function(val) {
2424 return val && val.$isSingleNested;
2425 }).
2426 forEach(function(doc) {
2427 doc.$__reset();
2428 _this.$__.activePaths.init(doc.$basePath);
2429 });
2430
2431 // clear atomics
2432 this.$__dirty().forEach(function(dirt) {
2433 const type = dirt.value;
2434
2435 if (type && type[arrayAtomicsSymbol]) {
2436 type[arrayAtomicsSymbol] = {};
2437 }
2438 });
2439
2440 // Clear 'dirty' cache
2441 this.$__.activePaths.clear('modify');
2442 this.$__.activePaths.clear('default');
2443 this.$__.validationError = undefined;
2444 this.errors = undefined;
2445 _this = this;
2446 this.schema.requiredPaths().forEach(function(path) {
2447 _this.$__.activePaths.require(path);
2448 });
2449
2450 return this;
2451};
2452
2453/**
2454 * Returns this documents dirty paths / vals.
2455 *
2456 * @api private
2457 * @method $__dirty
2458 * @memberOf Document
2459 * @instance
2460 */
2461
2462Document.prototype.$__dirty = function() {
2463 const _this = this;
2464
2465 let all = this.$__.activePaths.map('modify', function(path) {
2466 return {
2467 path: path,
2468 value: _this.$__getValue(path),
2469 schema: _this.$__path(path)
2470 };
2471 });
2472
2473 // gh-2558: if we had to set a default and the value is not undefined,
2474 // we have to save as well
2475 all = all.concat(this.$__.activePaths.map('default', function(path) {
2476 if (path === '_id' || _this.$__getValue(path) == null) {
2477 return;
2478 }
2479 return {
2480 path: path,
2481 value: _this.$__getValue(path),
2482 schema: _this.$__path(path)
2483 };
2484 }));
2485
2486 // Sort dirty paths in a flat hierarchy.
2487 all.sort(function(a, b) {
2488 return (a.path < b.path ? -1 : (a.path > b.path ? 1 : 0));
2489 });
2490
2491 // Ignore "foo.a" if "foo" is dirty already.
2492 const minimal = [];
2493 let lastPath;
2494 let top;
2495
2496 all.forEach(function(item) {
2497 if (!item) {
2498 return;
2499 }
2500 if (lastPath == null || item.path.indexOf(lastPath) !== 0) {
2501 lastPath = item.path + '.';
2502 minimal.push(item);
2503 top = item;
2504 } else if (top != null &&
2505 top.value != null &&
2506 top.value[arrayAtomicsSymbol] != null &&
2507 top.value.hasAtomics()) {
2508 // special case for top level MongooseArrays
2509 // the `top` array itself and a sub path of `top` are being modified.
2510 // the only way to honor all of both modifications is through a $set
2511 // of entire array.
2512 top.value[arrayAtomicsSymbol] = {};
2513 top.value[arrayAtomicsSymbol].$set = top.value;
2514 }
2515 });
2516
2517 top = lastPath = null;
2518 return minimal;
2519};
2520
2521/**
2522 * Assigns/compiles `schema` into this documents prototype.
2523 *
2524 * @param {Schema} schema
2525 * @api private
2526 * @method $__setSchema
2527 * @memberOf Document
2528 * @instance
2529 */
2530
2531Document.prototype.$__setSchema = function(schema) {
2532 schema.plugin(idGetter, { deduplicate: true });
2533 compile(schema.tree, this, undefined, schema.options);
2534
2535 // Apply default getters if virtual doesn't have any (gh-6262)
2536 for (const key of Object.keys(schema.virtuals)) {
2537 schema.virtuals[key]._applyDefaultGetters();
2538 }
2539
2540 this.schema = schema;
2541 this[documentSchemaSymbol] = schema;
2542};
2543
2544
2545/**
2546 * Get active path that were changed and are arrays
2547 *
2548 * @api private
2549 * @method $__getArrayPathsToValidate
2550 * @memberOf Document
2551 * @instance
2552 */
2553
2554Document.prototype.$__getArrayPathsToValidate = function() {
2555 DocumentArray || (DocumentArray = require('./types/documentarray'));
2556
2557 // validate all document arrays.
2558 return this.$__.activePaths
2559 .map('init', 'modify', function(i) {
2560 return this.$__getValue(i);
2561 }.bind(this))
2562 .filter(function(val) {
2563 return val && val instanceof Array && val.isMongooseDocumentArray && val.length;
2564 }).reduce(function(seed, array) {
2565 return seed.concat(array);
2566 }, [])
2567 .filter(function(doc) {
2568 return doc;
2569 });
2570};
2571
2572
2573/**
2574 * Get all subdocs (by bfs)
2575 *
2576 * @api private
2577 * @method $__getAllSubdocs
2578 * @memberOf Document
2579 * @instance
2580 */
2581
2582Document.prototype.$__getAllSubdocs = function() {
2583 DocumentArray || (DocumentArray = require('./types/documentarray'));
2584 Embedded = Embedded || require('./types/embedded');
2585
2586 function docReducer(doc, seed, path) {
2587 let val = doc;
2588 if (path) {
2589 if (doc instanceof Document && doc[documentSchemaSymbol].paths[path]) {
2590 val = doc._doc[path];
2591 } else {
2592 val = doc[path];
2593 }
2594 }
2595 if (val instanceof Embedded) {
2596 seed.push(val);
2597 } else if (val instanceof Map) {
2598 seed = Array.from(val.keys()).reduce(function(seed, path) {
2599 return docReducer(val.get(path), seed, null);
2600 }, seed);
2601 } else if (val && val.$isSingleNested) {
2602 seed = Object.keys(val._doc).reduce(function(seed, path) {
2603 return docReducer(val._doc, seed, path);
2604 }, seed);
2605 seed.push(val);
2606 } else if (val && val.isMongooseDocumentArray) {
2607 val.forEach(function _docReduce(doc) {
2608 if (!doc || !doc._doc) {
2609 return;
2610 }
2611 seed = Object.keys(doc._doc).reduce(function(seed, path) {
2612 return docReducer(doc._doc, seed, path);
2613 }, seed);
2614 if (doc instanceof Embedded) {
2615 seed.push(doc);
2616 }
2617 });
2618 } else if (val instanceof Document && val.$__isNested) {
2619 seed = Object.keys(val).reduce(function(seed, path) {
2620 return docReducer(val, seed, path);
2621 }, seed);
2622 }
2623 return seed;
2624 }
2625
2626 const _this = this;
2627 const subDocs = Object.keys(this._doc).reduce(function(seed, path) {
2628 return docReducer(_this, seed, path);
2629 }, []);
2630
2631 return subDocs;
2632};
2633
2634/*!
2635 * Runs queued functions
2636 */
2637
2638function applyQueue(doc) {
2639 const q = doc.schema && doc.schema.callQueue;
2640 if (!q.length) {
2641 return;
2642 }
2643 let pair;
2644
2645 for (let i = 0; i < q.length; ++i) {
2646 pair = q[i];
2647 if (pair[0] !== 'pre' && pair[0] !== 'post' && pair[0] !== 'on') {
2648 doc[pair[0]].apply(doc, pair[1]);
2649 }
2650 }
2651}
2652
2653/*!
2654 * ignore
2655 */
2656
2657Document.prototype.$__handleReject = function handleReject(err) {
2658 // emit on the Model if listening
2659 if (this.listeners('error').length) {
2660 this.emit('error', err);
2661 } else if (this.constructor.listeners && this.constructor.listeners('error').length) {
2662 this.constructor.emit('error', err);
2663 } else if (this.listeners && this.listeners('error').length) {
2664 this.emit('error', err);
2665 }
2666};
2667
2668/**
2669 * Internal helper for toObject() and toJSON() that doesn't manipulate options
2670 *
2671 * @api private
2672 * @method $toObject
2673 * @memberOf Document
2674 * @instance
2675 */
2676
2677Document.prototype.$toObject = function(options, json) {
2678 let defaultOptions = {
2679 transform: true,
2680 flattenDecimals: true
2681 };
2682
2683 const path = json ? 'toJSON' : 'toObject';
2684 const baseOptions = get(this, 'constructor.base.options.' + path, {});
2685 const schemaOptions = get(this, 'schema.options', {});
2686 // merge base default options with Schema's set default options if available.
2687 // `clone` is necessary here because `utils.options` directly modifies the second input.
2688 defaultOptions = utils.options(defaultOptions, clone(baseOptions));
2689 defaultOptions = utils.options(defaultOptions, clone(schemaOptions[path] || {}));
2690
2691 // If options do not exist or is not an object, set it to empty object
2692 options = utils.isPOJO(options) ? clone(options) : {};
2693
2694 if (!('flattenMaps' in options)) {
2695 options.flattenMaps = defaultOptions.flattenMaps;
2696 }
2697
2698 let _minimize;
2699 if (options.minimize != null) {
2700 _minimize = options.minimize;
2701 } else if (defaultOptions.minimize != null) {
2702 _minimize = defaultOptions.minimize;
2703 } else {
2704 _minimize = schemaOptions.minimize;
2705 }
2706
2707 // The original options that will be passed to `clone()`. Important because
2708 // `clone()` will recursively call `$toObject()` on embedded docs, so we
2709 // need the original options the user passed in, plus `_isNested` and
2710 // `_parentOptions` for checking whether we need to depopulate.
2711 const cloneOptions = Object.assign(utils.clone(options), {
2712 _isNested: true,
2713 json: json,
2714 minimize: _minimize
2715 });
2716
2717 const depopulate = options.depopulate ||
2718 get(options, '_parentOptions.depopulate', false);
2719 // _isNested will only be true if this is not the top level document, we
2720 // should never depopulate
2721 if (depopulate && options._isNested && this.$__.wasPopulated) {
2722 // populated paths that we set to a document
2723 return clone(this._id, cloneOptions);
2724 }
2725
2726 // merge default options with input options.
2727 options = utils.options(defaultOptions, options);
2728 options._isNested = true;
2729 options.json = json;
2730 options.minimize = _minimize;
2731
2732 cloneOptions._parentOptions = options;
2733
2734 // remember the root transform function
2735 // to save it from being overwritten by sub-transform functions
2736 const originalTransform = options.transform;
2737
2738 let ret = clone(this._doc, cloneOptions) || {};
2739
2740 if (options.getters) {
2741 applyGetters(this, ret, 'paths', cloneOptions);
2742 // applyGetters for paths will add nested empty objects;
2743 // if minimize is set, we need to remove them.
2744 if (options.minimize) {
2745 ret = minimize(ret) || {};
2746 }
2747 }
2748
2749 if (options.virtuals || options.getters && options.virtuals !== false) {
2750 applyGetters(this, ret, 'virtuals', cloneOptions);
2751 }
2752
2753 if (options.versionKey === false && this.schema.options.versionKey) {
2754 delete ret[this.schema.options.versionKey];
2755 }
2756
2757 let transform = options.transform;
2758
2759 // In the case where a subdocument has its own transform function, we need to
2760 // check and see if the parent has a transform (options.transform) and if the
2761 // child schema has a transform (this.schema.options.toObject) In this case,
2762 // we need to adjust options.transform to be the child schema's transform and
2763 // not the parent schema's
2764 if (transform === true || (schemaOptions.toObject && transform)) {
2765 const opts = options.json ? schemaOptions.toJSON : schemaOptions.toObject;
2766
2767 if (opts) {
2768 transform = (typeof options.transform === 'function' ? options.transform : opts.transform);
2769 }
2770 } else {
2771 options.transform = originalTransform;
2772 }
2773
2774 if (typeof transform === 'function') {
2775 const xformed = transform(this, ret, options);
2776 if (typeof xformed !== 'undefined') {
2777 ret = xformed;
2778 }
2779 }
2780
2781 return ret;
2782};
2783
2784/**
2785 * Converts this document into a plain javascript object, ready for storage in MongoDB.
2786 *
2787 * Buffers are converted to instances of [mongodb.Binary](http://mongodb.github.com/node-mongodb-native/api-bson-generated/binary.html) for proper storage.
2788 *
2789 * ####Options:
2790 *
2791 * - `getters` apply all getters (path and virtual getters), defaults to false
2792 * - `virtuals` apply virtual getters (can override `getters` option), defaults to false
2793 * - `minimize` remove empty objects (defaults to true)
2794 * - `transform` a transform function to apply to the resulting document before returning
2795 * - `depopulate` depopulate any populated paths, replacing them with their original refs (defaults to false)
2796 * - `versionKey` whether to include the version key (defaults to true)
2797 *
2798 * ####Getters/Virtuals
2799 *
2800 * Example of only applying path getters
2801 *
2802 * doc.toObject({ getters: true, virtuals: false })
2803 *
2804 * Example of only applying virtual getters
2805 *
2806 * doc.toObject({ virtuals: true })
2807 *
2808 * Example of applying both path and virtual getters
2809 *
2810 * doc.toObject({ getters: true })
2811 *
2812 * To apply these options to every document of your schema by default, set your [schemas](#schema_Schema) `toObject` option to the same argument.
2813 *
2814 * schema.set('toObject', { virtuals: true })
2815 *
2816 * ####Transform
2817 *
2818 * We may need to perform a transformation of the resulting object based on some criteria, say to remove some sensitive information or return a custom object. In this case we set the optional `transform` function.
2819 *
2820 * Transform functions receive three arguments
2821 *
2822 * function (doc, ret, options) {}
2823 *
2824 * - `doc` The mongoose document which is being converted
2825 * - `ret` The plain object representation which has been converted
2826 * - `options` The options in use (either schema options or the options passed inline)
2827 *
2828 * ####Example
2829 *
2830 * // specify the transform schema option
2831 * if (!schema.options.toObject) schema.options.toObject = {};
2832 * schema.options.toObject.transform = function (doc, ret, options) {
2833 * // remove the _id of every document before returning the result
2834 * delete ret._id;
2835 * return ret;
2836 * }
2837 *
2838 * // without the transformation in the schema
2839 * doc.toObject(); // { _id: 'anId', name: 'Wreck-it Ralph' }
2840 *
2841 * // with the transformation
2842 * doc.toObject(); // { name: 'Wreck-it Ralph' }
2843 *
2844 * With transformations we can do a lot more than remove properties. We can even return completely new customized objects:
2845 *
2846 * if (!schema.options.toObject) schema.options.toObject = {};
2847 * schema.options.toObject.transform = function (doc, ret, options) {
2848 * return { movie: ret.name }
2849 * }
2850 *
2851 * // without the transformation in the schema
2852 * doc.toObject(); // { _id: 'anId', name: 'Wreck-it Ralph' }
2853 *
2854 * // with the transformation
2855 * doc.toObject(); // { movie: 'Wreck-it Ralph' }
2856 *
2857 * _Note: if a transform function returns `undefined`, the return value will be ignored._
2858 *
2859 * Transformations may also be applied inline, overridding any transform set in the options:
2860 *
2861 * function xform (doc, ret, options) {
2862 * return { inline: ret.name, custom: true }
2863 * }
2864 *
2865 * // pass the transform as an inline option
2866 * doc.toObject({ transform: xform }); // { inline: 'Wreck-it Ralph', custom: true }
2867 *
2868 * If you want to skip transformations, use `transform: false`:
2869 *
2870 * if (!schema.options.toObject) schema.options.toObject = {};
2871 * schema.options.toObject.hide = '_id';
2872 * schema.options.toObject.transform = function (doc, ret, options) {
2873 * if (options.hide) {
2874 * options.hide.split(' ').forEach(function (prop) {
2875 * delete ret[prop];
2876 * });
2877 * }
2878 * return ret;
2879 * }
2880 *
2881 * var doc = new Doc({ _id: 'anId', secret: 47, name: 'Wreck-it Ralph' });
2882 * doc.toObject(); // { secret: 47, name: 'Wreck-it Ralph' }
2883 * doc.toObject({ hide: 'secret _id', transform: false });// { _id: 'anId', secret: 47, name: 'Wreck-it Ralph' }
2884 * doc.toObject({ hide: 'secret _id', transform: true }); // { name: 'Wreck-it Ralph' }
2885 *
2886 * Transforms are applied _only to the document and are not applied to sub-documents_.
2887 *
2888 * Transforms, like all of these options, are also available for `toJSON`. See [this guide to `JSON.stringify()`](https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html) to learn why `toJSON()` and `toObject()` are separate functions.
2889 *
2890 * See [schema options](/docs/guide.html#toObject) for some more details.
2891 *
2892 * _During save, no custom options are applied to the document before being sent to the database._
2893 *
2894 * @param {Object} [options]
2895 * @param {Boolean} [options.getters=false] if true, apply all getters, including virtuals
2896 * @param {Boolean} [options.virtuals=false] if true, apply virtuals. Use `{ getters: true, virtuals: false }` to just apply getters, not virtuals
2897 * @param {Boolean} [options.minimize=true] if true, omit any empty objects from the output
2898 * @param {Function|null} [options.transform=null] if set, mongoose will call this function to allow you to transform the returned object
2899 * @param {Boolean} [options.depopulate=false] if true, replace any conventionally populated paths with the original id in the output. Has no affect on virtual populated paths.
2900 * @param {Boolean} [options.versionKey=true] if false, exclude the version key (`__v` by default) from the output
2901 * @param {Boolean} [options.flattenMaps=false] if true, convert Maps to POJOs. Useful if you want to `JSON.stringify()` the result of `toObject()`.
2902 * @return {Object} js object
2903 * @see mongodb.Binary http://mongodb.github.com/node-mongodb-native/api-bson-generated/binary.html
2904 * @api public
2905 * @memberOf Document
2906 * @instance
2907 */
2908
2909Document.prototype.toObject = function(options) {
2910 return this.$toObject(options);
2911};
2912
2913/*!
2914 * Minimizes an object, removing undefined values and empty objects
2915 *
2916 * @param {Object} object to minimize
2917 * @return {Object}
2918 */
2919
2920function minimize(obj) {
2921 const keys = Object.keys(obj);
2922 let i = keys.length;
2923 let hasKeys;
2924 let key;
2925 let val;
2926
2927 while (i--) {
2928 key = keys[i];
2929 val = obj[key];
2930
2931 if (utils.isObject(val) && !Buffer.isBuffer(val)) {
2932 obj[key] = minimize(val);
2933 }
2934
2935 if (undefined === obj[key]) {
2936 delete obj[key];
2937 continue;
2938 }
2939
2940 hasKeys = true;
2941 }
2942
2943 return hasKeys
2944 ? obj
2945 : undefined;
2946}
2947
2948/*!
2949 * Applies virtuals properties to `json`.
2950 *
2951 * @param {Document} self
2952 * @param {Object} json
2953 * @param {String} type either `virtuals` or `paths`
2954 * @return {Object} `json`
2955 */
2956
2957function applyGetters(self, json, type, options) {
2958 const schema = self.schema;
2959 const paths = Object.keys(schema[type]);
2960 let i = paths.length;
2961 const numPaths = i;
2962 let path;
2963 let assignPath;
2964 let cur = self._doc;
2965 let v;
2966
2967 if (!cur) {
2968 return json;
2969 }
2970
2971 if (type === 'virtuals') {
2972 options = options || {};
2973 for (i = 0; i < numPaths; ++i) {
2974 path = paths[i];
2975 // We may be applying virtuals to a nested object, for example if calling
2976 // `doc.nestedProp.toJSON()`. If so, the path we assign to, `assignPath`,
2977 // will be a trailing substring of the `path`.
2978 assignPath = path;
2979 if (options.path != null) {
2980 if (!path.startsWith(options.path + '.')) {
2981 continue;
2982 }
2983 assignPath = path.substr(options.path.length + 1);
2984 }
2985 const parts = assignPath.split('.');
2986 v = clone(self.get(path), options);
2987 if (v === void 0) {
2988 continue;
2989 }
2990 const plen = parts.length;
2991 cur = json;
2992 for (let j = 0; j < plen - 1; ++j) {
2993 cur[parts[j]] = cur[parts[j]] || {};
2994 cur = cur[parts[j]];
2995 }
2996 cur[parts[plen - 1]] = v;
2997 }
2998
2999 return json;
3000 }
3001
3002 while (i--) {
3003 path = paths[i];
3004
3005 const parts = path.split('.');
3006 const plen = parts.length;
3007 const last = plen - 1;
3008 let branch = json;
3009 let part;
3010 cur = self._doc;
3011
3012 if (!self.isSelected(path)) {
3013 continue;
3014 }
3015
3016 for (let ii = 0; ii < plen; ++ii) {
3017 part = parts[ii];
3018 v = cur[part];
3019 if (ii === last) {
3020 const val = self.get(path);
3021 // Ignore single nested docs: getters will run because of `clone()`
3022 // before `applyGetters()` in `$toObject()`. Quirk because single
3023 // nested subdocs are hydrated docs in `_doc` as opposed to POJOs.
3024 if (val != null && val.$__ == null) {
3025 branch[part] = clone(val, options);
3026 }
3027 } else if (v == null) {
3028 if (part in cur) {
3029 branch[part] = v;
3030 }
3031 break;
3032 } else {
3033 branch = branch[part] || (branch[part] = {});
3034 }
3035 cur = v;
3036 }
3037 }
3038
3039 return json;
3040}
3041
3042/**
3043 * The return value of this method is used in calls to JSON.stringify(doc).
3044 *
3045 * This method accepts the same options as [Document#toObject](#document_Document-toObject). To apply the options to every document of your schema by default, set your [schemas](#schema_Schema) `toJSON` option to the same argument.
3046 *
3047 * schema.set('toJSON', { virtuals: true })
3048 *
3049 * See [schema options](/docs/guide.html#toJSON) for details.
3050 *
3051 * @param {Object} options
3052 * @return {Object}
3053 * @see Document#toObject #document_Document-toObject
3054 * @see JSON.stringify() in JavaScript https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html
3055 * @api public
3056 * @memberOf Document
3057 * @instance
3058 */
3059
3060Document.prototype.toJSON = function(options) {
3061 return this.$toObject(options, true);
3062};
3063
3064/**
3065 * Helper for console.log
3066 *
3067 * @api public
3068 * @method inspect
3069 * @memberOf Document
3070 * @instance
3071 */
3072
3073Document.prototype.inspect = function(options) {
3074 const isPOJO = utils.isPOJO(options);
3075 let opts;
3076 if (isPOJO) {
3077 opts = options;
3078 opts.minimize = false;
3079 }
3080 return this.toObject(opts);
3081};
3082
3083if (inspect.custom) {
3084 /*!
3085 * Avoid Node deprecation warning DEP0079
3086 */
3087
3088 Document.prototype[inspect.custom] = Document.prototype.inspect;
3089}
3090
3091/**
3092 * Helper for console.log
3093 *
3094 * @api public
3095 * @method toString
3096 * @memberOf Document
3097 * @instance
3098 */
3099
3100Document.prototype.toString = function() {
3101 return inspect(this.inspect());
3102};
3103
3104/**
3105 * Returns true if the Document stores the same data as doc.
3106 *
3107 * Documents are considered equal when they have matching `_id`s, unless neither
3108 * document has an `_id`, in which case this function falls back to using
3109 * `deepEqual()`.
3110 *
3111 * @param {Document} doc a document to compare
3112 * @return {Boolean}
3113 * @api public
3114 * @memberOf Document
3115 * @instance
3116 */
3117
3118Document.prototype.equals = function(doc) {
3119 if (!doc) {
3120 return false;
3121 }
3122
3123 const tid = this.get('_id');
3124 const docid = doc.get ? doc.get('_id') : doc;
3125 if (!tid && !docid) {
3126 return deepEqual(this, doc);
3127 }
3128 return tid && tid.equals
3129 ? tid.equals(docid)
3130 : tid === docid;
3131};
3132
3133/**
3134 * Populates document references, executing the `callback` when complete.
3135 * If you want to use promises instead, use this function with
3136 * [`execPopulate()`](#document_Document-execPopulate)
3137 *
3138 * ####Example:
3139 *
3140 * doc
3141 * .populate('company')
3142 * .populate({
3143 * path: 'notes',
3144 * match: /airline/,
3145 * select: 'text',
3146 * model: 'modelName'
3147 * options: opts
3148 * }, function (err, user) {
3149 * assert(doc._id === user._id) // the document itself is passed
3150 * })
3151 *
3152 * // summary
3153 * doc.populate(path) // not executed
3154 * doc.populate(options); // not executed
3155 * doc.populate(path, callback) // executed
3156 * doc.populate(options, callback); // executed
3157 * doc.populate(callback); // executed
3158 * doc.populate(options).execPopulate() // executed, returns promise
3159 *
3160 *
3161 * ####NOTE:
3162 *
3163 * Population does not occur unless a `callback` is passed *or* you explicitly
3164 * call `execPopulate()`.
3165 * Passing the same path a second time will overwrite the previous path options.
3166 * See [Model.populate()](#model_Model.populate) for explaination of options.
3167 *
3168 * @see Model.populate #model_Model.populate
3169 * @see Document.execPopulate #document_Document-execPopulate
3170 * @param {String|Object} [path] The path to populate or an options object
3171 * @param {Function} [callback] When passed, population is invoked
3172 * @api public
3173 * @return {Document} this
3174 * @memberOf Document
3175 * @instance
3176 */
3177
3178Document.prototype.populate = function populate() {
3179 if (arguments.length === 0) {
3180 return this;
3181 }
3182
3183 const pop = this.$__.populate || (this.$__.populate = {});
3184 const args = utils.args(arguments);
3185 let fn;
3186
3187 if (typeof args[args.length - 1] === 'function') {
3188 fn = args.pop();
3189 }
3190
3191 // allow `doc.populate(callback)`
3192 if (args.length) {
3193 // use hash to remove duplicate paths
3194 const res = utils.populate.apply(null, args);
3195 for (let i = 0; i < res.length; ++i) {
3196 pop[res[i].path] = res[i];
3197 }
3198 }
3199
3200 if (fn) {
3201 const paths = utils.object.vals(pop);
3202 this.$__.populate = undefined;
3203 let topLevelModel = this.constructor;
3204 if (this.$__isNested) {
3205 topLevelModel = this.$__.scope.constructor;
3206 const nestedPath = this.$__.nestedPath;
3207 paths.forEach(function(populateOptions) {
3208 populateOptions.path = nestedPath + '.' + populateOptions.path;
3209 });
3210 }
3211
3212 // Use `$session()` by default if the document has an associated session
3213 // See gh-6754
3214 if (this.$session() != null) {
3215 const session = this.$session();
3216 paths.forEach(path => {
3217 if (path.options == null) {
3218 path.options = { session: session };
3219 return;
3220 }
3221 if (!('session' in path.options)) {
3222 path.options.session = session;
3223 }
3224 });
3225 }
3226
3227 topLevelModel.populate(this, paths, fn);
3228 }
3229
3230 return this;
3231};
3232
3233/**
3234 * Explicitly executes population and returns a promise. Useful for ES2015
3235 * integration.
3236 *
3237 * ####Example:
3238 *
3239 * var promise = doc.
3240 * populate('company').
3241 * populate({
3242 * path: 'notes',
3243 * match: /airline/,
3244 * select: 'text',
3245 * model: 'modelName'
3246 * options: opts
3247 * }).
3248 * execPopulate();
3249 *
3250 * // summary
3251 * doc.execPopulate().then(resolve, reject);
3252 *
3253 *
3254 * @see Document.populate #document_Document-populate
3255 * @api public
3256 * @param {Function} [callback] optional callback. If specified, a promise will **not** be returned
3257 * @return {Promise} promise that resolves to the document when population is done
3258 * @memberOf Document
3259 * @instance
3260 */
3261
3262Document.prototype.execPopulate = function(callback) {
3263 return utils.promiseOrCallback(callback, cb => {
3264 this.populate(cb);
3265 }, this.constructor.events);
3266};
3267
3268/**
3269 * Gets _id(s) used during population of the given `path`.
3270 *
3271 * ####Example:
3272 *
3273 * Model.findOne().populate('author').exec(function (err, doc) {
3274 * console.log(doc.author.name) // Dr.Seuss
3275 * console.log(doc.populated('author')) // '5144cf8050f071d979c118a7'
3276 * })
3277 *
3278 * If the path was not populated, undefined is returned.
3279 *
3280 * @param {String} path
3281 * @return {Array|ObjectId|Number|Buffer|String|undefined}
3282 * @memberOf Document
3283 * @instance
3284 * @api public
3285 */
3286
3287Document.prototype.populated = function(path, val, options) {
3288 // val and options are internal
3289 if (val === null || val === void 0) {
3290 if (!this.$__.populated) {
3291 return undefined;
3292 }
3293 const v = this.$__.populated[path];
3294 if (v) {
3295 return v.value;
3296 }
3297 return undefined;
3298 }
3299
3300 // internal
3301 if (val === true) {
3302 if (!this.$__.populated) {
3303 return undefined;
3304 }
3305 return this.$__.populated[path];
3306 }
3307
3308 this.$__.populated || (this.$__.populated = {});
3309 this.$__.populated[path] = {value: val, options: options};
3310
3311 // If this was a nested populate, make sure each populated doc knows
3312 // about its populated children (gh-7685)
3313 const pieces = path.split('.');
3314 for (let i = 0; i < pieces.length - 1; ++i) {
3315 const subpath = pieces.slice(0, i + 1).join('.');
3316 const subdoc = this.get(subpath);
3317 if (subdoc != null && subdoc.$__ != null && this.populated(subpath)) {
3318 const rest = pieces.slice(i + 1).join('.');
3319 subdoc.populated(rest, val, options);
3320 // No need to continue because the above recursion should take care of
3321 // marking the rest of the docs as populated
3322 break;
3323 }
3324 }
3325
3326 return val;
3327};
3328
3329/**
3330 * Takes a populated field and returns it to its unpopulated state.
3331 *
3332 * ####Example:
3333 *
3334 * Model.findOne().populate('author').exec(function (err, doc) {
3335 * console.log(doc.author.name); // Dr.Seuss
3336 * console.log(doc.depopulate('author'));
3337 * console.log(doc.author); // '5144cf8050f071d979c118a7'
3338 * })
3339 *
3340 * If the path was not populated, this is a no-op.
3341 *
3342 * @param {String} path
3343 * @return {Document} this
3344 * @see Document.populate #document_Document-populate
3345 * @api public
3346 * @memberOf Document
3347 * @instance
3348 */
3349
3350Document.prototype.depopulate = function(path) {
3351 if (typeof path === 'string') {
3352 path = path.split(' ');
3353 }
3354 let populatedIds;
3355 const virtualKeys = this.$$populatedVirtuals ? Object.keys(this.$$populatedVirtuals) : [];
3356 const populated = get(this, '$__.populated', {});
3357
3358 if (arguments.length === 0) {
3359 // Depopulate all
3360 for (let i = 0; i < virtualKeys.length; i++) {
3361 delete this.$$populatedVirtuals[virtualKeys[i]];
3362 delete this._doc[virtualKeys[i]];
3363 delete populated[virtualKeys[i]];
3364 }
3365
3366 const keys = Object.keys(populated);
3367
3368 for (let i = 0; i < keys.length; i++) {
3369 populatedIds = this.populated(keys[i]);
3370 if (!populatedIds) {
3371 continue;
3372 }
3373 delete populated[keys[i]];
3374 this.$set(keys[i], populatedIds);
3375 }
3376 return this;
3377 }
3378
3379 for (let i = 0; i < path.length; i++) {
3380 populatedIds = this.populated(path[i]);
3381 delete populated[path[i]];
3382
3383 if (virtualKeys.indexOf(path[i]) !== -1) {
3384 delete this.$$populatedVirtuals[path[i]];
3385 delete this._doc[path[i]];
3386 } else if (populatedIds) {
3387 this.$set(path[i], populatedIds);
3388 }
3389 }
3390 return this;
3391};
3392
3393
3394/**
3395 * Returns the full path to this document.
3396 *
3397 * @param {String} [path]
3398 * @return {String}
3399 * @api private
3400 * @method $__fullPath
3401 * @memberOf Document
3402 * @instance
3403 */
3404
3405Document.prototype.$__fullPath = function(path) {
3406 // overridden in SubDocuments
3407 return path || '';
3408};
3409
3410/*!
3411 * Module exports.
3412 */
3413
3414Document.ValidationError = ValidationError;
3415module.exports = exports = Document;