UNPKG

162 kBJavaScriptView Raw
1'use strict';
2
3/*!
4 * Module dependencies.
5 */
6
7const DivergentArrayError = require('./error/divergentArray');
8const EventEmitter = require('events').EventEmitter;
9const InternalCache = require('./internal');
10const MongooseBuffer = require('./types/buffer');
11const MongooseError = require('./error/index');
12const MixedSchema = require('./schema/mixed');
13const ModifiedPathsSnapshot = require('./modifiedPathsSnapshot');
14const ObjectExpectedError = require('./error/objectExpected');
15const ObjectParameterError = require('./error/objectParameter');
16const ParallelValidateError = require('./error/parallelValidate');
17const Schema = require('./schema');
18const StrictModeError = require('./error/strict');
19const ValidationError = require('./error/validation');
20const ValidatorError = require('./error/validator');
21const $__hasIncludedChildren = require('./helpers/projection/hasIncludedChildren');
22const applyDefaults = require('./helpers/document/applyDefaults');
23const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths');
24const clone = require('./helpers/clone');
25const compile = require('./helpers/document/compile').compile;
26const defineKey = require('./helpers/document/compile').defineKey;
27const firstKey = require('./helpers/firstKey');
28const flatten = require('./helpers/common').flatten;
29const getEmbeddedDiscriminatorPath = require('./helpers/document/getEmbeddedDiscriminatorPath');
30const getKeysInSchemaOrder = require('./helpers/schema/getKeysInSchemaOrder');
31const getSubdocumentStrictValue = require('./helpers/schema/getSubdocumentStrictValue');
32const handleSpreadDoc = require('./helpers/document/handleSpreadDoc');
33const immediate = require('./helpers/immediate');
34const isBsonType = require('./helpers/isBsonType');
35const isDefiningProjection = require('./helpers/projection/isDefiningProjection');
36const isExclusive = require('./helpers/projection/isExclusive');
37const isPathExcluded = require('./helpers/projection/isPathExcluded');
38const inspect = require('util').inspect;
39const internalToObjectOptions = require('./options').internalToObjectOptions;
40const markArraySubdocsPopulated = require('./helpers/populate/markArraySubdocsPopulated');
41const minimize = require('./helpers/minimize');
42const mpath = require('mpath');
43const parentPaths = require('./helpers/path/parentPaths');
44const queryhelpers = require('./queryHelpers');
45const utils = require('./utils');
46const isPromise = require('./helpers/isPromise');
47
48const deepEqual = utils.deepEqual;
49const isMongooseObject = utils.isMongooseObject;
50
51const arrayAtomicsBackupSymbol = require('./helpers/symbols').arrayAtomicsBackupSymbol;
52const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol;
53const documentArrayParent = require('./helpers/symbols').documentArrayParent;
54const documentIsModified = require('./helpers/symbols').documentIsModified;
55const documentModifiedPaths = require('./helpers/symbols').documentModifiedPaths;
56const documentSchemaSymbol = require('./helpers/symbols').documentSchemaSymbol;
57const getSymbol = require('./helpers/symbols').getSymbol;
58const populateModelSymbol = require('./helpers/symbols').populateModelSymbol;
59const scopeSymbol = require('./helpers/symbols').scopeSymbol;
60const schemaMixedSymbol = require('./schema/symbols').schemaMixedSymbol;
61const getDeepestSubdocumentForPath = require('./helpers/document/getDeepestSubdocumentForPath');
62const sessionNewDocuments = require('./helpers/symbols').sessionNewDocuments;
63
64let DocumentArray;
65let MongooseArray;
66let Embedded;
67
68const specialProperties = utils.specialProperties;
69
70const VERSION_WHERE = 1;
71const VERSION_INC = 2;
72const VERSION_ALL = VERSION_WHERE | VERSION_INC;
73
74/**
75 * The core Mongoose document constructor. You should not call this directly,
76 * the Mongoose [Model constructor](./api/model.html#Model) calls this for you.
77 *
78 * @param {Object} obj the values to set
79 * @param {Object} [fields] optional object containing the fields which were selected in the query returning this document and any populated paths data
80 * @param {Object} [options] various configuration options for the document
81 * @param {Boolean} [options.defaults=true] if `false`, skip applying default values to this document.
82 * @inherits NodeJS EventEmitter https://nodejs.org/api/events.html#class-eventemitter
83 * @event `init`: Emitted on a document after it has been retrieved from the db and fully hydrated by Mongoose.
84 * @event `save`: Emitted when the document is successfully saved
85 * @api private
86 */
87
88function Document(obj, fields, skipId, options) {
89 if (typeof skipId === 'object' && skipId != null) {
90 options = skipId;
91 skipId = options.skipId;
92 }
93 options = Object.assign({}, options);
94
95 // Support `browserDocument.js` syntax
96 if (this.$__schema == null) {
97 const _schema = utils.isObject(fields) && !fields.instanceOfSchema ?
98 new Schema(fields) :
99 fields;
100 this.$__setSchema(_schema);
101 fields = skipId;
102 skipId = options;
103 options = arguments[4] || {};
104 }
105
106 this.$__ = new InternalCache();
107
108 // Avoid setting `isNew` to `true`, because it is `true` by default
109 if (options.isNew != null && options.isNew !== true) {
110 this.$isNew = options.isNew;
111 }
112
113 if (options.priorDoc != null) {
114 this.$__.priorDoc = options.priorDoc;
115 }
116
117 if (skipId) {
118 this.$__.skipId = skipId;
119 }
120
121 if (obj != null && typeof obj !== 'object') {
122 throw new ObjectParameterError(obj, 'obj', 'Document');
123 }
124
125 let defaults = true;
126 if (options.defaults !== undefined) {
127 this.$__.defaults = options.defaults;
128 defaults = options.defaults;
129 }
130
131 const schema = this.$__schema;
132
133 if (typeof fields === 'boolean' || fields === 'throw') {
134 if (fields !== true) {
135 this.$__.strictMode = fields;
136 }
137 fields = undefined;
138 } else if (schema.options.strict !== true) {
139 this.$__.strictMode = schema.options.strict;
140 }
141
142 const requiredPaths = schema.requiredPaths(true);
143 for (const path of requiredPaths) {
144 this.$__.activePaths.require(path);
145 }
146
147 let exclude = null;
148
149 // determine if this doc is a result of a query with
150 // excluded fields
151 if (utils.isPOJO(fields) && Object.keys(fields).length > 0) {
152 exclude = isExclusive(fields);
153 this.$__.selected = fields;
154 this.$__.exclude = exclude;
155 }
156
157 const hasIncludedChildren = exclude === false && fields ?
158 $__hasIncludedChildren(fields) :
159 null;
160
161 if (this._doc == null) {
162 this.$__buildDoc(obj, fields, skipId, exclude, hasIncludedChildren, false);
163
164 // By default, defaults get applied **before** setting initial values
165 // Re: gh-6155
166 if (defaults) {
167 applyDefaults(this, fields, exclude, hasIncludedChildren, true, null, {
168 skipParentChangeTracking: true
169 });
170 }
171 }
172 if (obj) {
173 // Skip set hooks
174 if (this.$__original_set) {
175 this.$__original_set(obj, undefined, true, options);
176 } else {
177 this.$set(obj, undefined, true, options);
178 }
179
180 if (obj instanceof Document) {
181 this.$isNew = obj.$isNew;
182 }
183 }
184
185 // Function defaults get applied **after** setting initial values so they
186 // see the full doc rather than an empty one, unless they opt out.
187 // Re: gh-3781, gh-6155
188 if (options.willInit && defaults) {
189 if (options.skipDefaults) {
190 this.$__.skipDefaults = options.skipDefaults;
191 }
192 } else if (defaults) {
193 applyDefaults(this, fields, exclude, hasIncludedChildren, false, options.skipDefaults);
194 }
195
196 if (!this.$__.strictMode && obj) {
197 const _this = this;
198 const keys = Object.keys(this._doc);
199
200 keys.forEach(function(key) {
201 // Avoid methods, virtuals, existing fields, and `$` keys. The latter is to avoid overwriting
202 // Mongoose internals.
203 if (!(key in schema.tree) && !(key in schema.methods) && !(key in schema.virtuals) && !key.startsWith('$')) {
204 defineKey({ prop: key, subprops: null, prototype: _this });
205 }
206 });
207 }
208
209 applyQueue(this);
210}
211
212Document.prototype.$isMongooseDocumentPrototype = true;
213
214/**
215 * Boolean flag specifying if the document is new. If you create a document
216 * using `new`, this document will be considered "new". `$isNew` is how
217 * Mongoose determines whether `save()` should use `insertOne()` to create
218 * a new document or `updateOne()` to update an existing document.
219 *
220 * #### Example:
221 *
222 * const user = new User({ name: 'John Smith' });
223 * user.$isNew; // true
224 *
225 * await user.save(); // Sends an `insertOne` to MongoDB
226 *
227 * On the other hand, if you load an existing document from the database
228 * using `findOne()` or another [query operation](https://mongoosejs.com/docs/queries.html),
229 * `$isNew` will be false.
230 *
231 * #### Example:
232 *
233 * const user = await User.findOne({ name: 'John Smith' });
234 * user.$isNew; // false
235 *
236 * Mongoose sets `$isNew` to `false` immediately after `save()` succeeds.
237 * That means Mongoose sets `$isNew` to false **before** `post('save')` hooks run.
238 * In `post('save')` hooks, `$isNew` will be `false` if `save()` succeeded.
239 *
240 * #### Example:
241 *
242 * userSchema.post('save', function() {
243 * this.$isNew; // false
244 * });
245 * await User.create({ name: 'John Smith' });
246 *
247 * For subdocuments, `$isNew` is true if either the parent has `$isNew` set,
248 * or if you create a new subdocument.
249 *
250 * #### Example:
251 *
252 * // Assume `Group` has a document array `users`
253 * const group = await Group.findOne();
254 * group.users[0].$isNew; // false
255 *
256 * group.users.push({ name: 'John Smith' });
257 * group.users[1].$isNew; // true
258 *
259 * @api public
260 * @property $isNew
261 * @memberOf Document
262 * @instance
263 */
264
265Object.defineProperty(Document.prototype, 'isNew', {
266 get: function() {
267 return this.$isNew;
268 },
269 set: function(value) {
270 this.$isNew = value;
271 }
272});
273
274/**
275 * Hash containing current validation errors.
276 *
277 * @api public
278 * @property errors
279 * @memberOf Document
280 * @instance
281 */
282
283Object.defineProperty(Document.prototype, 'errors', {
284 get: function() {
285 return this.$errors;
286 },
287 set: function(value) {
288 this.$errors = value;
289 }
290});
291
292/*!
293 * ignore
294 */
295
296Document.prototype.$isNew = true;
297
298/*!
299 * Document exposes the NodeJS event emitter API, so you can use
300 * `on`, `once`, etc.
301 */
302utils.each(
303 ['on', 'once', 'emit', 'listeners', 'removeListener', 'setMaxListeners',
304 'removeAllListeners', 'addListener'],
305 function(emitterFn) {
306 Document.prototype[emitterFn] = function() {
307 // Delay creating emitter until necessary because emitters take up a lot of memory,
308 // especially for subdocuments.
309 if (!this.$__.emitter) {
310 if (emitterFn === 'emit') {
311 return;
312 }
313 this.$__.emitter = new EventEmitter();
314 this.$__.emitter.setMaxListeners(0);
315 }
316 return this.$__.emitter[emitterFn].apply(this.$__.emitter, arguments);
317 };
318 Document.prototype[`$${emitterFn}`] = Document.prototype[emitterFn];
319 });
320
321Document.prototype.constructor = Document;
322
323for (const i in EventEmitter.prototype) {
324 Document[i] = EventEmitter.prototype[i];
325}
326
327/**
328 * The document's internal schema.
329 *
330 * @api private
331 * @property schema
332 * @memberOf Document
333 * @instance
334 */
335
336Document.prototype.$__schema;
337
338/**
339 * The document's schema.
340 *
341 * @api public
342 * @property schema
343 * @memberOf Document
344 * @instance
345 */
346
347Document.prototype.schema;
348
349/**
350 * Empty object that you can use for storing properties on the document. This
351 * is handy for passing data to middleware without conflicting with Mongoose
352 * internals.
353 *
354 * #### Example:
355 *
356 * schema.pre('save', function() {
357 * // Mongoose will set `isNew` to `false` if `save()` succeeds
358 * this.$locals.wasNew = this.isNew;
359 * });
360 *
361 * schema.post('save', function() {
362 * // Prints true if `isNew` was set before `save()`
363 * console.log(this.$locals.wasNew);
364 * });
365 *
366 * @api public
367 * @property $locals
368 * @memberOf Document
369 * @instance
370 */
371
372Object.defineProperty(Document.prototype, '$locals', {
373 configurable: false,
374 enumerable: false,
375 get: function() {
376 if (this.$__.locals == null) {
377 this.$__.locals = {};
378 }
379 return this.$__.locals;
380 },
381 set: function(v) {
382 this.$__.locals = v;
383 }
384});
385
386/**
387 * Legacy alias for `$isNew`.
388 *
389 * @api public
390 * @property isNew
391 * @memberOf Document
392 * @see $isNew https://mongoosejs.com/docs/api/document.html#Document.prototype.$isNew
393 * @instance
394 */
395
396Document.prototype.isNew;
397
398/**
399 * Set this property to add additional query filters when Mongoose saves this document and `isNew` is false.
400 *
401 * #### Example:
402 *
403 * // Make sure `save()` never updates a soft deleted document.
404 * schema.pre('save', function() {
405 * this.$where = { isDeleted: false };
406 * });
407 *
408 * @api public
409 * @property $where
410 * @memberOf Document
411 * @instance
412 */
413
414Object.defineProperty(Document.prototype, '$where', {
415 configurable: false,
416 enumerable: false,
417 writable: true
418});
419
420/**
421 * The string version of this documents _id.
422 *
423 * #### Note:
424 *
425 * This getter exists on all documents by default. The getter can be disabled by setting the `id` [option](https://mongoosejs.com/docs/guide.html#id) of its `Schema` to false at construction time.
426 *
427 * new Schema({ name: String }, { id: false });
428 *
429 * @api public
430 * @see Schema options https://mongoosejs.com/docs/guide.html#options
431 * @property id
432 * @memberOf Document
433 * @instance
434 */
435
436Document.prototype.id;
437
438/**
439 * Hash containing current validation $errors.
440 *
441 * @api public
442 * @property $errors
443 * @memberOf Document
444 * @instance
445 */
446
447Document.prototype.$errors;
448
449/**
450 * A string containing the current operation that Mongoose is executing
451 * on this document. May be `null`, `'save'`, `'validate'`, or `'remove'`.
452 *
453 * #### Example:
454 *
455 * const doc = new Model({ name: 'test' });
456 * doc.$op; // null
457 *
458 * const promise = doc.save();
459 * doc.$op; // 'save'
460 *
461 * await promise;
462 * doc.$op; // null
463 *
464 * @api public
465 * @property $op
466 * @memberOf Document
467 * @instance
468 */
469
470Object.defineProperty(Document.prototype, '$op', {
471 get: function() {
472 return this.$__.op || null;
473 },
474 set: function(value) {
475 this.$__.op = value;
476 }
477});
478
479/*!
480 * ignore
481 */
482
483function $applyDefaultsToNested(val, path, doc) {
484 if (val == null) {
485 return;
486 }
487
488 const paths = Object.keys(doc.$__schema.paths);
489 const plen = paths.length;
490
491 const pathPieces = path.indexOf('.') === -1 ? [path] : path.split('.');
492
493 for (let i = 0; i < plen; ++i) {
494 let curPath = '';
495 const p = paths[i];
496
497 if (!p.startsWith(path + '.')) {
498 continue;
499 }
500
501 const type = doc.$__schema.paths[p];
502 const pieces = type.splitPath().slice(pathPieces.length);
503 const len = pieces.length;
504
505 if (type.defaultValue === void 0) {
506 continue;
507 }
508
509 let cur = val;
510
511 for (let j = 0; j < len; ++j) {
512 if (cur == null) {
513 break;
514 }
515
516 const piece = pieces[j];
517
518 if (j === len - 1) {
519 if (cur[piece] !== void 0) {
520 break;
521 }
522
523 try {
524 const def = type.getDefault(doc, false);
525 if (def !== void 0) {
526 cur[piece] = def;
527 }
528 } catch (err) {
529 doc.invalidate(path + '.' + curPath, err);
530 break;
531 }
532
533 break;
534 }
535
536 curPath += (!curPath.length ? '' : '.') + piece;
537
538 cur[piece] = cur[piece] || {};
539 cur = cur[piece];
540 }
541 }
542}
543
544/**
545 * Builds the default doc structure
546 *
547 * @param {Object} obj
548 * @param {Object} [fields]
549 * @param {Boolean} [skipId]
550 * @param {Boolean} [exclude]
551 * @param {Object} [hasIncludedChildren]
552 * @api private
553 * @method $__buildDoc
554 * @memberOf Document
555 * @instance
556 */
557
558Document.prototype.$__buildDoc = function(obj, fields, skipId, exclude, hasIncludedChildren) {
559 const doc = {};
560
561 const paths = Object.keys(this.$__schema.paths).
562 // Don't build up any paths that are underneath a map, we don't know
563 // what the keys will be
564 filter(p => !p.includes('$*'));
565 const plen = paths.length;
566 let ii = 0;
567
568 for (; ii < plen; ++ii) {
569 const p = paths[ii];
570
571 if (p === '_id') {
572 if (skipId) {
573 continue;
574 }
575 if (obj && '_id' in obj) {
576 continue;
577 }
578 }
579
580 const path = this.$__schema.paths[p].splitPath();
581 const len = path.length;
582 const last = len - 1;
583 let curPath = '';
584 let doc_ = doc;
585 let included = false;
586
587 for (let i = 0; i < len; ++i) {
588 const piece = path[i];
589
590 if (!curPath.length) {
591 curPath = piece;
592 } else {
593 curPath += '.' + piece;
594 }
595
596 // support excluding intermediary levels
597 if (exclude === true) {
598 if (curPath in fields) {
599 break;
600 }
601 } else if (exclude === false && fields && !included) {
602 if (curPath in fields) {
603 included = true;
604 } else if (!hasIncludedChildren[curPath]) {
605 break;
606 }
607 }
608
609 if (i < last) {
610 doc_ = doc_[piece] || (doc_[piece] = {});
611 }
612 }
613 }
614
615 this._doc = doc;
616};
617
618/*!
619 * Converts to POJO when you use the document for querying
620 */
621
622Document.prototype.toBSON = function() {
623 return this.toObject(internalToObjectOptions);
624};
625
626/**
627 * Initializes the document without setters or marking anything modified.
628 *
629 * Called internally after a document is returned from mongodb. Normally,
630 * you do **not** need to call this function on your own.
631 *
632 * This function triggers `init` [middleware](https://mongoosejs.com/docs/middleware.html).
633 * Note that `init` hooks are [synchronous](https://mongoosejs.com/docs/middleware.html#synchronous).
634 *
635 * @param {Object} doc document returned by mongo
636 * @param {Object} [opts]
637 * @param {Function} [fn]
638 * @api public
639 * @memberOf Document
640 * @instance
641 */
642
643Document.prototype.init = function(doc, opts, fn) {
644 if (typeof opts === 'function') {
645 fn = opts;
646 opts = null;
647 }
648
649 this.$__init(doc, opts);
650
651 if (fn) {
652 fn(null, this);
653 }
654
655 return this;
656};
657
658/**
659 * Alias for [`.init`](https://mongoosejs.com/docs/api/document.html#Document.prototype.init())
660 *
661 * @api public
662 */
663
664Document.prototype.$init = function() {
665 return this.constructor.prototype.init.apply(this, arguments);
666};
667
668/**
669 * Internal "init" function
670 *
671 * @param {Document} doc
672 * @param {Object} [opts]
673 * @returns {Document} this
674 * @api private
675 */
676
677Document.prototype.$__init = function(doc, opts) {
678 this.$isNew = false;
679 opts = opts || {};
680
681 // handle docs with populated paths
682 // If doc._id is not null or undefined
683 if (doc._id != null && opts.populated && opts.populated.length) {
684 const id = String(doc._id);
685 for (const item of opts.populated) {
686 if (item.isVirtual) {
687 this.$populated(item.path, utils.getValue(item.path, doc), item);
688 } else {
689 this.$populated(item.path, item._docs[id], item);
690 }
691
692 if (item._childDocs == null) {
693 continue;
694 }
695 for (const child of item._childDocs) {
696 if (child == null || child.$__ == null) {
697 continue;
698 }
699 child.$__.parent = this;
700 }
701 item._childDocs = [];
702 }
703 }
704
705 init(this, doc, this._doc, opts);
706
707 markArraySubdocsPopulated(this, opts.populated);
708 this.$emit('init', this);
709 this.constructor.emit('init', this);
710
711 const hasIncludedChildren = this.$__.exclude === false && this.$__.selected ?
712 $__hasIncludedChildren(this.$__.selected) :
713 null;
714
715 applyDefaults(this, this.$__.selected, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults);
716 return this;
717};
718
719/**
720 * Init helper.
721 *
722 * @param {Object} self document instance
723 * @param {Object} obj raw mongodb doc
724 * @param {Object} doc object we are initializing
725 * @param {Object} [opts] Optional Options
726 * @param {Boolean} [opts.setters] Call `applySetters` instead of `cast`
727 * @param {String} [prefix] Prefix to add to each path
728 * @api private
729 */
730
731function init(self, obj, doc, opts, prefix) {
732 prefix = prefix || '';
733
734 if (obj.$__ != null) {
735 obj = obj._doc;
736 }
737 const keys = Object.keys(obj);
738 const len = keys.length;
739 let schemaType;
740 let path;
741 let i;
742 let index = 0;
743 const strict = self.$__.strictMode;
744 const docSchema = self.$__schema;
745
746 while (index < len) {
747 _init(index++);
748 }
749
750 function _init(index) {
751 i = keys[index];
752 // avoid prototype pollution
753 if (i === '__proto__' || i === 'constructor') {
754 return;
755 }
756 path = prefix ? prefix + i : i;
757 schemaType = docSchema.path(path);
758 // Should still work if not a model-level discriminator, but should not be
759 // necessary. This is *only* to catch the case where we queried using the
760 // base model and the discriminated model has a projection
761 if (docSchema.$isRootDiscriminator && !self.$__isSelected(path)) {
762 return;
763 }
764
765 const value = obj[i];
766 if (!schemaType && utils.isPOJO(value)) {
767 // assume nested object
768 if (!doc[i]) {
769 doc[i] = {};
770 if (!strict && !(i in docSchema.tree) && !(i in docSchema.methods) && !(i in docSchema.virtuals)) {
771 self[i] = doc[i];
772 }
773 }
774 init(self, value, doc[i], opts, path + '.');
775 } else if (!schemaType) {
776 doc[i] = value;
777 if (!strict && !prefix) {
778 self[i] = value;
779 }
780 } else {
781 // Retain order when overwriting defaults
782 if (doc.hasOwnProperty(i) && value !== void 0 && !opts.hydratedPopulatedDocs) {
783 delete doc[i];
784 }
785 if (value === null) {
786 doc[i] = schemaType._castNullish(null);
787 } else if (value !== undefined) {
788 const wasPopulated = value.$__ == null ? null : value.$__.wasPopulated;
789
790 if (schemaType && !wasPopulated && !opts.hydratedPopulatedDocs) {
791 try {
792 if (opts && opts.setters) {
793 // Call applySetters with `init = false` because otherwise setters are a noop
794 const overrideInit = false;
795 doc[i] = schemaType.applySetters(value, self, overrideInit);
796 } else {
797 doc[i] = schemaType.cast(value, self, true);
798 }
799 } catch (e) {
800 self.invalidate(e.path, new ValidatorError({
801 path: e.path,
802 message: e.message,
803 type: 'cast',
804 value: e.value,
805 reason: e
806 }));
807 }
808 } else {
809 doc[i] = value;
810 }
811 }
812 // mark as hydrated
813 if (!self.$isModified(path)) {
814 self.$__.activePaths.init(path);
815 }
816 }
817 }
818}
819
820/**
821 * Sends an updateOne command with this document `_id` as the query selector.
822 *
823 * #### Example:
824 *
825 * weirdCar.updateOne({$inc: {wheels:1}}, { w: 1 }, callback);
826 *
827 * #### Valid options:
828 *
829 * - same as in [Model.updateOne](https://mongoosejs.com/docs/api/model.html#Model.updateOne)
830 *
831 * @see Model.updateOne https://mongoosejs.com/docs/api/model.html#Model.updateOne
832 * @param {Object} doc
833 * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
834 * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean()) and the [Mongoose lean tutorial](https://mongoosejs.com/docs/tutorials/lean.html).
835 * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
836 * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
837 * @param {Function} [callback]
838 * @return {Query}
839 * @api public
840 * @memberOf Document
841 * @instance
842 */
843
844Document.prototype.updateOne = function updateOne(doc, options, callback) {
845 const query = this.constructor.updateOne({ _id: this._id }, doc, options);
846 const self = this;
847 query.pre(function queryPreUpdateOne(cb) {
848 self.constructor._middleware.execPre('updateOne', self, [self], cb);
849 });
850 query.post(function queryPostUpdateOne(cb) {
851 self.constructor._middleware.execPost('updateOne', self, [self], {}, cb);
852 });
853
854 if (this.$session() != null) {
855 if (!('session' in query.options)) {
856 query.options.session = this.$session();
857 }
858 }
859
860 if (callback != null) {
861 return query.exec(callback);
862 }
863
864 return query;
865};
866
867/**
868 * Sends a replaceOne command with this document `_id` as the query selector.
869 *
870 * #### Valid options:
871 *
872 * - same as in [Model.replaceOne](https://mongoosejs.com/docs/api/model.html#Model.replaceOne())
873 *
874 * @see Model.replaceOne https://mongoosejs.com/docs/api/model.html#Model.replaceOne()
875 * @param {Object} doc
876 * @param {Object} [options]
877 * @param {Function} [callback]
878 * @return {Query}
879 * @api public
880 * @memberOf Document
881 * @instance
882 */
883
884Document.prototype.replaceOne = function replaceOne() {
885 const args = [...arguments];
886 args.unshift({ _id: this._id });
887 return this.constructor.replaceOne.apply(this.constructor, args);
888};
889
890/**
891 * Getter/setter around the session associated with this document. Used to
892 * automatically set `session` if you `save()` a doc that you got from a
893 * query with an associated session.
894 *
895 * #### Example:
896 *
897 * const session = MyModel.startSession();
898 * const doc = await MyModel.findOne().session(session);
899 * doc.$session() === session; // true
900 * doc.$session(null);
901 * doc.$session() === null; // true
902 *
903 * If this is a top-level document, setting the session propagates to all child
904 * docs.
905 *
906 * @param {ClientSession} [session] overwrite the current session
907 * @return {ClientSession}
908 * @method $session
909 * @api public
910 * @memberOf Document
911 */
912
913Document.prototype.$session = function $session(session) {
914 if (arguments.length === 0) {
915 if (this.$__.session != null && this.$__.session.hasEnded) {
916 this.$__.session = null;
917 return null;
918 }
919 return this.$__.session;
920 }
921
922 if (session != null && session.hasEnded) {
923 throw new MongooseError('Cannot set a document\'s session to a session that has ended. Make sure you haven\'t ' +
924 'called `endSession()` on the session you are passing to `$session()`.');
925 }
926
927 if (session == null && this.$__.session == null) {
928 return;
929 }
930
931 this.$__.session = session;
932
933 if (!this.$isSubdocument) {
934 const subdocs = this.$getAllSubdocs();
935 for (const child of subdocs) {
936 child.$session(session);
937 }
938 }
939
940 return session;
941};
942
943/**
944 * Getter/setter around whether this document will apply timestamps by
945 * default when using `save()` and `bulkSave()`.
946 *
947 * #### Example:
948 *
949 * const TestModel = mongoose.model('Test', new Schema({ name: String }, { timestamps: true }));
950 * const doc = new TestModel({ name: 'John Smith' });
951 *
952 * doc.$timestamps(); // true
953 *
954 * doc.$timestamps(false);
955 * await doc.save(); // Does **not** apply timestamps
956 *
957 * @param {Boolean} [value] overwrite the current session
958 * @return {Document|boolean|undefined} When used as a getter (no argument), a boolean will be returned indicating the timestamps option state or if unset "undefined" will be used, otherwise will return "this"
959 * @method $timestamps
960 * @api public
961 * @memberOf Document
962 */
963
964Document.prototype.$timestamps = function $timestamps(value) {
965 if (arguments.length === 0) {
966 if (this.$__.timestamps != null) {
967 return this.$__.timestamps;
968 }
969
970 if (this.$__schema) {
971 return this.$__schema.options.timestamps;
972 }
973
974 return undefined;
975 }
976
977 const currentValue = this.$timestamps();
978 if (value !== currentValue) {
979 this.$__.timestamps = value;
980 }
981
982 return this;
983};
984
985/**
986 * Overwrite all values in this document with the values of `obj`, except
987 * for immutable properties. Behaves similarly to `set()`, except for it
988 * unsets all properties that aren't in `obj`.
989 *
990 * @param {Object} obj the object to overwrite this document with
991 * @method overwrite
992 * @memberOf Document
993 * @instance
994 * @api public
995 * @return {Document} this
996 */
997
998Document.prototype.overwrite = function overwrite(obj) {
999 const keys = Array.from(new Set(Object.keys(this._doc).concat(Object.keys(obj))));
1000
1001 for (const key of keys) {
1002 if (key === '_id') {
1003 continue;
1004 }
1005 // Explicitly skip version key
1006 if (this.$__schema.options.versionKey && key === this.$__schema.options.versionKey) {
1007 continue;
1008 }
1009 if (this.$__schema.options.discriminatorKey && key === this.$__schema.options.discriminatorKey) {
1010 continue;
1011 }
1012 this.$set(key, obj[key]);
1013 }
1014
1015 return this;
1016};
1017
1018/**
1019 * Alias for `set()`, used internally to avoid conflicts
1020 *
1021 * @param {String|Object} path path or object of key/vals to set
1022 * @param {Any} val the value to set
1023 * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for "on-the-fly" attributes
1024 * @param {Object} [options] optionally specify options that modify the behavior of the set
1025 * @param {Boolean} [options.merge=false] if true, setting a [nested path](https://mongoosejs.com/docs/subdocs.html#subdocuments-versus-nested-paths) will merge existing values rather than overwrite the whole object. So `doc.set('nested', { a: 1, b: 2 })` becomes `doc.set('nested.a', 1); doc.set('nested.b', 2);`
1026 * @return {Document} this
1027 * @method $set
1028 * @memberOf Document
1029 * @instance
1030 * @api public
1031 */
1032
1033Document.prototype.$set = function $set(path, val, type, options) {
1034 if (utils.isPOJO(type)) {
1035 options = type;
1036 type = undefined;
1037 }
1038
1039 const merge = options && options.merge;
1040 const adhoc = type && type !== true;
1041 const constructing = type === true;
1042 let adhocs;
1043 let keys;
1044 let i = 0;
1045 let pathtype;
1046 let key;
1047 let prefix;
1048
1049 const userSpecifiedStrict = options && 'strict' in options;
1050 let strict = userSpecifiedStrict
1051 ? options.strict
1052 : this.$__.strictMode;
1053
1054 if (adhoc) {
1055 adhocs = this.$__.adhocPaths || (this.$__.adhocPaths = {});
1056 adhocs[path] = this.$__schema.interpretAsType(path, type, this.$__schema.options);
1057 }
1058
1059 if (path == null) {
1060 [path, val] = [val, path];
1061 } else if (typeof path !== 'string') {
1062 // new Document({ key: val })
1063 if (path instanceof Document) {
1064 if (path.$__isNested) {
1065 path = path.toObject();
1066 } else {
1067 // This ternary is to support gh-7898 (copying virtuals if same schema)
1068 // while not breaking gh-10819, which for some reason breaks if we use toObject()
1069 path = path.$__schema === this.$__schema
1070 ? applyVirtuals(path, { ...path._doc })
1071 : path._doc;
1072 }
1073 }
1074 if (path == null) {
1075 [path, val] = [val, path];
1076 }
1077
1078 prefix = val ? val + '.' : '';
1079 keys = getKeysInSchemaOrder(this.$__schema, path);
1080
1081 const len = keys.length;
1082
1083 // `_skipMinimizeTopLevel` is because we may have deleted the top-level
1084 // nested key to ensure key order.
1085 const _skipMinimizeTopLevel = options && options._skipMinimizeTopLevel || false;
1086 if (len === 0 && _skipMinimizeTopLevel) {
1087 delete options._skipMinimizeTopLevel;
1088 if (val) {
1089 this.$set(val, {});
1090 }
1091 return this;
1092 }
1093
1094 options = Object.assign({}, options, { _skipMinimizeTopLevel: false });
1095
1096 for (let i = 0; i < len; ++i) {
1097 key = keys[i];
1098 const pathName = prefix ? prefix + key : key;
1099 pathtype = this.$__schema.pathType(pathName);
1100 const valForKey = path[key];
1101
1102 // On initial set, delete any nested keys if we're going to overwrite
1103 // them to ensure we keep the user's key order.
1104 if (type === true &&
1105 !prefix &&
1106 valForKey != null &&
1107 pathtype === 'nested' &&
1108 this._doc[key] != null) {
1109 delete this._doc[key];
1110 }
1111
1112 if (utils.isNonBuiltinObject(valForKey) && pathtype === 'nested') {
1113 this.$set(pathName, valForKey, constructing, Object.assign({}, options, { _skipMarkModified: true }));
1114 $applyDefaultsToNested(this.$get(pathName), pathName, this);
1115 continue;
1116 } else if (strict) {
1117 // Don't overwrite defaults with undefined keys (gh-3981) (gh-9039)
1118 if (constructing && valForKey === void 0 &&
1119 this.$get(pathName) !== void 0) {
1120 continue;
1121 }
1122
1123 if (pathtype === 'adhocOrUndefined') {
1124 pathtype = getEmbeddedDiscriminatorPath(this, pathName, { typeOnly: true });
1125 }
1126
1127 if (pathtype === 'real' || pathtype === 'virtual') {
1128 this.$set(pathName, valForKey, constructing, options);
1129 } else if (pathtype === 'nested' && valForKey instanceof Document) {
1130 this.$set(pathName,
1131 valForKey.toObject({ transform: false }), constructing, options);
1132 } else if (strict === 'throw') {
1133 if (pathtype === 'nested') {
1134 throw new ObjectExpectedError(key, valForKey);
1135 } else {
1136 throw new StrictModeError(key);
1137 }
1138 } else if (pathtype === 'nested' && valForKey == null) {
1139 this.$set(pathName, valForKey, constructing, options);
1140 }
1141 } else if (valForKey !== void 0) {
1142 this.$set(pathName, valForKey, constructing, options);
1143 }
1144 }
1145
1146 // Ensure all properties are in correct order
1147 const orderedDoc = {};
1148 const orderedKeys = Object.keys(this.$__schema.tree);
1149 for (let i = 0, len = orderedKeys.length; i < len; ++i) {
1150 (key = orderedKeys[i]) &&
1151 (this._doc.hasOwnProperty(key)) &&
1152 (orderedDoc[key] = undefined);
1153 }
1154 this._doc = Object.assign(orderedDoc, this._doc);
1155
1156 return this;
1157 }
1158
1159 let pathType = this.$__schema.pathType(path);
1160 let parts = null;
1161 if (pathType === 'adhocOrUndefined') {
1162 parts = path.indexOf('.') === -1 ? [path] : path.split('.');
1163 pathType = getEmbeddedDiscriminatorPath(this, parts, { typeOnly: true });
1164 }
1165 if (pathType === 'adhocOrUndefined' && !userSpecifiedStrict) {
1166 // May be path underneath non-strict schema
1167 if (parts == null) {
1168 parts = path.indexOf('.') === -1 ? [path] : path.split('.');
1169 }
1170 const subdocStrict = getSubdocumentStrictValue(this.$__schema, parts);
1171 if (subdocStrict !== undefined) {
1172 strict = subdocStrict;
1173 }
1174 }
1175
1176 // Assume this is a Mongoose document that was copied into a POJO using
1177 // `Object.assign()` or `{...doc}`
1178 val = handleSpreadDoc(val, true);
1179
1180 // if this doc is being constructed we should not trigger getters
1181 const priorVal = (() => {
1182 if (this.$__.priorDoc != null) {
1183 return this.$__.priorDoc.$__getValue(path);
1184 }
1185 if (constructing) {
1186 return void 0;
1187 }
1188 return this.$__getValue(path);
1189 })();
1190
1191 if (pathType === 'nested' && val) {
1192 if (typeof val === 'object' && val != null) {
1193 if (val.$__ != null) {
1194 val = val.toObject(internalToObjectOptions);
1195 }
1196 if (val == null) {
1197 this.invalidate(path, new MongooseError.CastError('Object', val, path));
1198 return this;
1199 }
1200 const wasModified = this.$isModified(path);
1201 const hasInitialVal = this.$__.savedState != null && this.$__.savedState.hasOwnProperty(path);
1202 if (this.$__.savedState != null && !this.$isNew && !this.$__.savedState.hasOwnProperty(path)) {
1203 const initialVal = this.$__getValue(path);
1204 this.$__.savedState[path] = initialVal;
1205
1206 const keys = Object.keys(initialVal || {});
1207 for (const key of keys) {
1208 this.$__.savedState[path + '.' + key] = initialVal[key];
1209 }
1210 }
1211
1212 if (!merge) {
1213 this.$__setValue(path, null);
1214 cleanModifiedSubpaths(this, path);
1215 } else {
1216 return this.$set(val, path, constructing);
1217 }
1218
1219 const keys = getKeysInSchemaOrder(this.$__schema, val, path);
1220
1221 this.$__setValue(path, {});
1222 for (const key of keys) {
1223 this.$set(path + '.' + key, val[key], constructing, { ...options, _skipMarkModified: true });
1224 }
1225 if (priorVal != null &&
1226 (!wasModified || hasInitialVal) &&
1227 utils.deepEqual(hasInitialVal ? this.$__.savedState[path] : priorVal, val)) {
1228 this.unmarkModified(path);
1229 } else {
1230 this.markModified(path);
1231 }
1232 return this;
1233 }
1234 this.invalidate(path, new MongooseError.CastError('Object', val, path));
1235 return this;
1236 }
1237
1238 let schema;
1239 if (parts == null) {
1240 parts = path.indexOf('.') === -1 ? [path] : path.split('.');
1241 }
1242
1243 // Might need to change path for top-level alias
1244 if (typeof this.$__schema.aliases[parts[0]] === 'string') {
1245 parts[0] = this.$__schema.aliases[parts[0]];
1246 }
1247
1248 if (pathType === 'adhocOrUndefined' && strict) {
1249 // check for roots that are Mixed types
1250 let mixed;
1251
1252 for (i = 0; i < parts.length; ++i) {
1253 const subpath = parts.slice(0, i + 1).join('.');
1254
1255 // If path is underneath a virtual, bypass everything and just set it.
1256 if (i + 1 < parts.length && this.$__schema.pathType(subpath) === 'virtual') {
1257 mpath.set(path, val, this);
1258 return this;
1259 }
1260
1261 schema = this.$__schema.path(subpath);
1262 if (schema == null) {
1263 continue;
1264 }
1265
1266 if (schema instanceof MixedSchema) {
1267 // allow changes to sub paths of mixed types
1268 mixed = true;
1269 break;
1270 } else if (schema.$isSchemaMap && schema.$__schemaType instanceof MixedSchema && i < parts.length - 1) {
1271 // Map of mixed and not the last element in the path resolves to mixed
1272 mixed = true;
1273 schema = schema.$__schemaType;
1274 break;
1275 }
1276 }
1277
1278 if (schema == null) {
1279 // Check for embedded discriminators
1280 schema = getEmbeddedDiscriminatorPath(this, path);
1281 }
1282
1283 if (!mixed && !schema) {
1284 if (strict === 'throw') {
1285 throw new StrictModeError(path);
1286 }
1287 return this;
1288 }
1289 } else if (pathType === 'virtual') {
1290 schema = this.$__schema.virtualpath(path);
1291 schema.applySetters(val, this);
1292 return this;
1293 } else {
1294 schema = this.$__path(path);
1295 }
1296
1297 // gh-4578, if setting a deeply nested path that doesn't exist yet, create it
1298 let cur = this._doc;
1299 let curPath = '';
1300 for (i = 0; i < parts.length - 1; ++i) {
1301 cur = cur[parts[i]];
1302 curPath += (curPath.length !== 0 ? '.' : '') + parts[i];
1303 if (!cur) {
1304 this.$set(curPath, {});
1305 // Hack re: gh-5800. If nested field is not selected, it probably exists
1306 // so `MongoServerError: cannot use the part (nested of nested.num) to
1307 // traverse the element ({nested: null})` is not likely. If user gets
1308 // that error, its their fault for now. We should reconsider disallowing
1309 // modifying not selected paths for 6.x
1310 if (!this.$__isSelected(curPath)) {
1311 this.unmarkModified(curPath);
1312 }
1313 cur = this.$__getValue(curPath);
1314 }
1315 }
1316
1317 let pathToMark;
1318
1319 // When using the $set operator the path to the field must already exist.
1320 // Else mongodb throws: "LEFT_SUBFIELD only supports Object"
1321
1322 if (parts.length <= 1) {
1323 pathToMark = path;
1324 } else {
1325 const len = parts.length;
1326 for (i = 0; i < len; ++i) {
1327 const subpath = parts.slice(0, i + 1).join('.');
1328 if (this.$get(subpath, null, { getters: false }) === null) {
1329 pathToMark = subpath;
1330 break;
1331 }
1332 }
1333
1334 if (!pathToMark) {
1335 pathToMark = path;
1336 }
1337 }
1338
1339 if (!schema) {
1340 this.$__set(pathToMark, path, options, constructing, parts, schema, val, priorVal);
1341
1342 if (pathType === 'nested' && val == null) {
1343 cleanModifiedSubpaths(this, path);
1344 }
1345 return this;
1346 }
1347
1348 // If overwriting a subdocument path, make sure to clear out
1349 // any errors _before_ setting, so new errors that happen
1350 // get persisted. Re: #9080
1351 if (schema.$isSingleNested || schema.$isMongooseArray) {
1352 _markValidSubpaths(this, path);
1353 }
1354
1355 if (val != null && merge && schema.$isSingleNested) {
1356 if (val instanceof Document) {
1357 val = val.toObject({ virtuals: false, transform: false });
1358 }
1359 const keys = Object.keys(val);
1360 for (const key of keys) {
1361 this.$set(path + '.' + key, val[key], constructing, options);
1362 }
1363
1364 return this;
1365 }
1366
1367 let shouldSet = true;
1368 try {
1369 // If the user is trying to set a ref path to a document with
1370 // the correct model name, treat it as populated
1371 const refMatches = (() => {
1372 if (schema.options == null) {
1373 return false;
1374 }
1375 if (!(val instanceof Document)) {
1376 return false;
1377 }
1378 const model = val.constructor;
1379
1380 // Check ref
1381 const ref = schema.options.ref;
1382 if (ref != null && (ref === model.modelName || ref === model.baseModelName)) {
1383 return true;
1384 }
1385
1386 // Check refPath
1387 const refPath = schema.options.refPath;
1388 if (refPath == null) {
1389 return false;
1390 }
1391 const modelName = val.get(refPath);
1392 return modelName === model.modelName || modelName === model.baseModelName;
1393 })();
1394
1395 let didPopulate = false;
1396 if (refMatches && val instanceof Document && (!val.$__.wasPopulated || utils.deepEqual(val.$__.wasPopulated.value, val._doc._id))) {
1397 const unpopulatedValue = (schema && schema.$isSingleNested) ? schema.cast(val, this) : val._doc._id;
1398 this.$populated(path, unpopulatedValue, { [populateModelSymbol]: val.constructor });
1399 val.$__.wasPopulated = { value: unpopulatedValue };
1400 didPopulate = true;
1401 }
1402
1403 let popOpts;
1404 const typeKey = this.$__schema.options.typeKey;
1405 if (schema.options &&
1406 Array.isArray(schema.options[typeKey]) &&
1407 schema.options[typeKey].length &&
1408 schema.options[typeKey][0] &&
1409 schema.options[typeKey][0].ref &&
1410 _isManuallyPopulatedArray(val, schema.options[typeKey][0].ref)) {
1411 popOpts = { [populateModelSymbol]: val[0].constructor };
1412 this.$populated(path, val.map(function(v) { return v._doc._id; }), popOpts);
1413
1414 for (const doc of val) {
1415 doc.$__.wasPopulated = { value: doc._doc._id };
1416 }
1417 didPopulate = true;
1418 }
1419
1420 if (!refMatches || !schema.$isSingleNested || !val.$__) {
1421 // If this path is underneath a single nested schema, we'll call the setter
1422 // later in `$__set()` because we don't take `_doc` when we iterate through
1423 // a single nested doc. That's to make sure we get the correct context.
1424 // Otherwise we would double-call the setter, see gh-7196.
1425 let setterContext = this;
1426 if (this.$__schema.singleNestedPaths[path] != null && parts.length > 1) {
1427 setterContext = getDeepestSubdocumentForPath(this, parts, this.schema);
1428 }
1429 if (options != null && options.overwriteImmutable) {
1430 val = schema.applySetters(val, setterContext, false, priorVal, { overwriteImmutable: true });
1431 } else {
1432 val = schema.applySetters(val, setterContext, false, priorVal);
1433 }
1434 }
1435
1436 if (Array.isArray(val) &&
1437 !Array.isArray(schema) &&
1438 schema.$isMongooseDocumentArray &&
1439 val.length !== 0 &&
1440 val[0] != null &&
1441 val[0].$__ != null &&
1442 val[0].$__.populated != null) {
1443 const populatedPaths = Object.keys(val[0].$__.populated);
1444 for (const populatedPath of populatedPaths) {
1445 this.$populated(path + '.' + populatedPath,
1446 val.map(v => v.$populated(populatedPath)),
1447 val[0].$__.populated[populatedPath].options);
1448 }
1449 didPopulate = true;
1450 }
1451
1452 if (!didPopulate && this.$__.populated) {
1453 // If this array partially contains populated documents, convert them
1454 // all to ObjectIds re: #8443
1455 if (Array.isArray(val) && this.$__.populated[path]) {
1456 for (let i = 0; i < val.length; ++i) {
1457 if (val[i] instanceof Document) {
1458 val.set(i, val[i]._doc._id, true);
1459 }
1460 }
1461 }
1462 delete this.$__.populated[path];
1463 }
1464
1465 if (val != null && schema.$isSingleNested) {
1466 _checkImmutableSubpaths(val, schema, priorVal);
1467 }
1468
1469 this.$markValid(path);
1470 } catch (e) {
1471 if (e instanceof MongooseError.StrictModeError && e.isImmutableError) {
1472 this.invalidate(path, e);
1473 } else if (e instanceof MongooseError.CastError) {
1474 this.invalidate(e.path, e);
1475 if (e.$originalErrorPath) {
1476 this.invalidate(path,
1477 new MongooseError.CastError(schema.instance, val, path, e.$originalErrorPath));
1478 }
1479 } else {
1480 this.invalidate(path,
1481 new MongooseError.CastError(schema.instance, val, path, e));
1482 }
1483 shouldSet = false;
1484 }
1485
1486 if (shouldSet) {
1487 let savedState = null;
1488 let savedStatePath = null;
1489 if (!constructing) {
1490 const doc = this.$isSubdocument ? this.ownerDocument() : this;
1491 savedState = doc.$__.savedState;
1492 savedStatePath = this.$isSubdocument ? this.$__.fullPath + '.' + path : path;
1493 doc.$__saveInitialState(savedStatePath);
1494 }
1495
1496 this.$__set(pathToMark, path, options, constructing, parts, schema, val, priorVal);
1497
1498 const isInTransaction = !!this.$__.session?.transaction;
1499 const isModifiedWithinTransaction = this.$__.session &&
1500 this.$__.session[sessionNewDocuments] &&
1501 this.$__.session[sessionNewDocuments].has(this) &&
1502 this.$__.session[sessionNewDocuments].get(this).modifiedPaths &&
1503 !this.$__.session[sessionNewDocuments].get(this).modifiedPaths.has(savedStatePath);
1504 if (savedState != null &&
1505 savedState.hasOwnProperty(savedStatePath) &&
1506 (!isInTransaction || isModifiedWithinTransaction) &&
1507 utils.deepEqual(val, savedState[savedStatePath])) {
1508 this.unmarkModified(path);
1509 }
1510 }
1511
1512 if (schema.$isSingleNested && (this.isDirectModified(path) || val == null)) {
1513 cleanModifiedSubpaths(this, path);
1514 }
1515
1516 return this;
1517};
1518
1519/*!
1520 * ignore
1521 */
1522
1523function _isManuallyPopulatedArray(val, ref) {
1524 if (!Array.isArray(val)) {
1525 return false;
1526 }
1527 if (val.length === 0) {
1528 return false;
1529 }
1530
1531 for (const el of val) {
1532 if (!(el instanceof Document)) {
1533 return false;
1534 }
1535 const modelName = el.constructor.modelName;
1536 if (modelName == null) {
1537 return false;
1538 }
1539 if (el.constructor.modelName != ref && el.constructor.baseModelName != ref) {
1540 return false;
1541 }
1542 }
1543
1544 return true;
1545}
1546
1547/**
1548 * Sets the value of a path, or many paths.
1549 * Alias for [`.$set`](https://mongoosejs.com/docs/api/document.html#Document.prototype.$set()).
1550 *
1551 * #### Example:
1552 *
1553 * // path, value
1554 * doc.set(path, value)
1555 *
1556 * // object
1557 * doc.set({
1558 * path : value
1559 * , path2 : {
1560 * path : value
1561 * }
1562 * })
1563 *
1564 * // on-the-fly cast to number
1565 * doc.set(path, value, Number)
1566 *
1567 * // on-the-fly cast to string
1568 * doc.set(path, value, String)
1569 *
1570 * // changing strict mode behavior
1571 * doc.set(path, value, { strict: false });
1572 *
1573 * @param {String|Object} path path or object of key/vals to set
1574 * @param {Any} val the value to set
1575 * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for "on-the-fly" attributes
1576 * @param {Object} [options] optionally specify options that modify the behavior of the set
1577 * @return {Document} this
1578 * @api public
1579 * @method set
1580 * @memberOf Document
1581 * @instance
1582 */
1583
1584Document.prototype.set = Document.prototype.$set;
1585
1586/**
1587 * Determine if we should mark this change as modified.
1588 *
1589 * @param {never} pathToMark UNUSED
1590 * @param {String|Symbol} path
1591 * @param {Object} options
1592 * @param {Any} constructing
1593 * @param {never} parts UNUSED
1594 * @param {Schema} schema
1595 * @param {Any} val
1596 * @param {Any} priorVal
1597 * @return {Boolean}
1598 * @api private
1599 * @method $__shouldModify
1600 * @memberOf Document
1601 * @instance
1602 */
1603
1604Document.prototype.$__shouldModify = function(pathToMark, path, options, constructing, parts, schema, val, priorVal) {
1605 if (options && options._skipMarkModified) {
1606 return false;
1607 }
1608 if (this.$isNew) {
1609 return true;
1610 }
1611 // Is path already modified? If so, always modify. We may unmark modified later.
1612 if (path in this.$__.activePaths.getStatePaths('modify')) {
1613 return true;
1614 }
1615
1616 if (val === void 0 && !this.$__isSelected(path)) {
1617 // when a path is not selected in a query, its initial
1618 // value will be undefined.
1619 return true;
1620 }
1621
1622 if (val === void 0 && path in this.$__.activePaths.getStatePaths('default')) {
1623 // we're just unsetting the default value which was never saved
1624 return false;
1625 }
1626
1627 // gh-3992: if setting a populated field to a doc, don't mark modified
1628 // if they have the same _id
1629 if (this.$populated(path) &&
1630 val instanceof Document &&
1631 deepEqual(val._doc._id, priorVal)) {
1632 return false;
1633 }
1634
1635 if (!deepEqual(val, priorVal !== undefined ? priorVal : utils.getValue(path, this))) {
1636 return true;
1637 }
1638
1639 if (!constructing &&
1640 val !== null &&
1641 val !== undefined &&
1642 path in this.$__.activePaths.getStatePaths('default') &&
1643 deepEqual(val, schema.getDefault(this, constructing))) {
1644 // a path with a default was $unset on the server
1645 // and the user is setting it to the same value again
1646 return true;
1647 }
1648 return false;
1649};
1650
1651/**
1652 * Handles the actual setting of the value and marking the path modified if appropriate.
1653 *
1654 * @param {String} pathToMark
1655 * @param {String|Symbol} path
1656 * @param {Object} options
1657 * @param {Any} constructing
1658 * @param {Array} parts
1659 * @param {Schema} schema
1660 * @param {Any} val
1661 * @param {Any} priorVal
1662 * @api private
1663 * @method $__set
1664 * @memberOf Document
1665 * @instance
1666 */
1667
1668Document.prototype.$__set = function(pathToMark, path, options, constructing, parts, schema, val, priorVal) {
1669 Embedded = Embedded || require('./types/arraySubdocument');
1670
1671 const shouldModify = this.$__shouldModify(pathToMark, path, options, constructing, parts,
1672 schema, val, priorVal);
1673
1674 if (shouldModify) {
1675 if (this.$__.primitiveAtomics && this.$__.primitiveAtomics[path]) {
1676 delete this.$__.primitiveAtomics[path];
1677 if (Object.keys(this.$__.primitiveAtomics).length === 0) {
1678 delete this.$__.primitiveAtomics;
1679 }
1680 }
1681 this.markModified(pathToMark);
1682
1683 // handle directly setting arrays (gh-1126)
1684 MongooseArray || (MongooseArray = require('./types/array'));
1685 if (val && utils.isMongooseArray(val)) {
1686 val._registerAtomic('$set', val);
1687
1688 // Update embedded document parent references (gh-5189)
1689 if (utils.isMongooseDocumentArray(val)) {
1690 val.forEach(function(item) {
1691 item && item.__parentArray && (item.__parentArray = val);
1692 });
1693 }
1694 }
1695 } else if (Array.isArray(val) && Array.isArray(priorVal) && utils.isMongooseArray(val) && utils.isMongooseArray(priorVal)) {
1696 val[arrayAtomicsSymbol] = priorVal[arrayAtomicsSymbol];
1697 val[arrayAtomicsBackupSymbol] = priorVal[arrayAtomicsBackupSymbol];
1698 if (utils.isMongooseDocumentArray(val)) {
1699 val.forEach(doc => {
1700 if (doc != null) {
1701 doc.$isNew = false;
1702 }
1703 });
1704 }
1705 }
1706
1707 let obj = this._doc;
1708 let i = 0;
1709 const l = parts.length;
1710 let cur = '';
1711
1712 for (; i < l; i++) {
1713 const next = i + 1;
1714 const last = next === l;
1715 cur += (cur ? '.' + parts[i] : parts[i]);
1716 if (specialProperties.has(parts[i])) {
1717 return;
1718 }
1719
1720 if (last) {
1721 if (obj instanceof Map) {
1722 obj.set(parts[i], val);
1723 } else if (obj.$isSingleNested) {
1724 if (!(parts[i] in obj)) {
1725 obj[parts[i]] = val;
1726 obj._doc[parts[i]] = val;
1727 } else {
1728 obj._doc[parts[i]] = val;
1729 }
1730 if (shouldModify) {
1731 obj.markModified(parts[i]);
1732 }
1733 } else {
1734 obj[parts[i]] = val;
1735 }
1736 } else {
1737 const isMap = obj instanceof Map;
1738 let value = isMap ? obj.get(parts[i]) : obj[parts[i]];
1739 if (utils.isPOJO(value)) {
1740 obj = value;
1741 } else if (value && value instanceof Embedded) {
1742 obj = value;
1743 } else if (value && !Array.isArray(value) && value.$isSingleNested) {
1744 obj = value;
1745 } else if (value && Array.isArray(value)) {
1746 obj = value;
1747 } else if (value == null) {
1748 value = {};
1749 if (isMap) {
1750 obj.set(parts[i], value);
1751 } else {
1752 obj[parts[i]] = value;
1753 }
1754 obj = value;
1755 } else {
1756 obj = value;
1757 }
1758 }
1759 }
1760};
1761
1762/**
1763 * Gets a raw value from a path (no getters)
1764 *
1765 * @param {String} path
1766 * @return {Any} Returns the value from the given `path`.
1767 * @api private
1768 */
1769
1770Document.prototype.$__getValue = function(path) {
1771 return utils.getValue(path, this._doc);
1772};
1773
1774/**
1775 * Increments the numeric value at `path` by the given `val`.
1776 * When you call `save()` on this document, Mongoose will send a
1777 * [`$inc`](https://www.mongodb.com/docs/manual/reference/operator/update/inc/)
1778 * as opposed to a `$set`.
1779 *
1780 * #### Example:
1781 *
1782 * const schema = new Schema({ counter: Number });
1783 * const Test = db.model('Test', schema);
1784 *
1785 * const doc = await Test.create({ counter: 0 });
1786 * doc.$inc('counter', 2);
1787 * await doc.save(); // Sends a `{ $inc: { counter: 2 } }` to MongoDB
1788 * doc.counter; // 2
1789 *
1790 * doc.counter += 2;
1791 * await doc.save(); // Sends a `{ $set: { counter: 2 } }` to MongoDB
1792 *
1793 * @param {String|Array} path path or paths to update
1794 * @param {Number} val increment `path` by this value
1795 * @return {Document} this
1796 */
1797
1798Document.prototype.$inc = function $inc(path, val) {
1799 if (val == null) {
1800 val = 1;
1801 }
1802
1803 if (Array.isArray(path)) {
1804 path.forEach((p) => this.$inc(p, val));
1805 return this;
1806 }
1807
1808 const schemaType = this.$__path(path);
1809 if (schemaType == null) {
1810 if (this.$__.strictMode === 'throw') {
1811 throw new StrictModeError(path);
1812 } else if (this.$__.strictMode === true) {
1813 return this;
1814 }
1815 } else if (schemaType.instance !== 'Number') {
1816 this.invalidate(path, new MongooseError.CastError(schemaType.instance, val, path));
1817 return this;
1818 }
1819
1820 const currentValue = this.$__getValue(path) || 0;
1821 let shouldSet = false;
1822 let valToSet = null;
1823 let valToInc = val;
1824
1825 try {
1826 val = schemaType.cast(val);
1827 valToSet = schemaType.applySetters(currentValue + val, this);
1828 valToInc = valToSet - currentValue;
1829 shouldSet = true;
1830 } catch (err) {
1831 this.invalidate(path, new MongooseError.CastError('number', val, path, err));
1832 }
1833
1834 if (shouldSet) {
1835 this.$__.primitiveAtomics = this.$__.primitiveAtomics || {};
1836 if (this.$__.primitiveAtomics[path] == null) {
1837 this.$__.primitiveAtomics[path] = { $inc: valToInc };
1838 } else {
1839 this.$__.primitiveAtomics[path].$inc += valToInc;
1840 }
1841 this.markModified(path);
1842 this.$__setValue(path, valToSet);
1843 }
1844
1845 return this;
1846};
1847
1848/**
1849 * Sets a raw value for a path (no casting, setters, transformations)
1850 *
1851 * @param {String} path
1852 * @param {Object} value
1853 * @return {Document} this
1854 * @api private
1855 */
1856
1857Document.prototype.$__setValue = function(path, val) {
1858 utils.setValue(path, val, this._doc);
1859 return this;
1860};
1861
1862/**
1863 * Returns the value of a path.
1864 *
1865 * #### Example:
1866 *
1867 * // path
1868 * doc.get('age') // 47
1869 *
1870 * // dynamic casting to a string
1871 * doc.get('age', String) // "47"
1872 *
1873 * @param {String} path
1874 * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for on-the-fly attributes
1875 * @param {Object} [options]
1876 * @param {Boolean} [options.virtuals=false] Apply virtuals before getting this path
1877 * @param {Boolean} [options.getters=true] If false, skip applying getters and just get the raw value
1878 * @return {Any}
1879 * @api public
1880 */
1881
1882Document.prototype.get = function(path, type, options) {
1883 let adhoc;
1884 if (options == null) {
1885 options = {};
1886 }
1887 if (type) {
1888 adhoc = this.$__schema.interpretAsType(path, type, this.$__schema.options);
1889 }
1890 const noDottedPath = options.noDottedPath;
1891
1892 // Fast path if we know we're just accessing top-level path on the document:
1893 // just get the schema path, avoid `$__path()` because that does string manipulation
1894 let schema = noDottedPath ? this.$__schema.paths[path] : this.$__path(path);
1895 if (schema == null) {
1896 schema = this.$__schema.virtualpath(path);
1897
1898 if (schema != null) {
1899 return schema.applyGetters(void 0, this);
1900 }
1901 }
1902
1903 if (noDottedPath) {
1904 let obj = this._doc[path];
1905 if (adhoc) {
1906 obj = adhoc.cast(obj);
1907 }
1908 if (schema != null && options.getters !== false) {
1909 return schema.applyGetters(obj, this);
1910 }
1911 return obj;
1912 }
1913
1914 if (schema != null && schema.instance === 'Mixed') {
1915 const virtual = this.$__schema.virtualpath(path);
1916 if (virtual != null) {
1917 schema = virtual;
1918 }
1919 }
1920
1921 const hasDot = path.indexOf('.') !== -1;
1922 let obj = this._doc;
1923
1924 const pieces = hasDot ? path.split('.') : [path];
1925 // Might need to change path for top-level alias
1926 if (typeof this.$__schema.aliases[pieces[0]] === 'string') {
1927 pieces[0] = this.$__schema.aliases[pieces[0]];
1928 }
1929
1930 for (let i = 0, l = pieces.length; i < l; i++) {
1931 if (obj && obj._doc) {
1932 obj = obj._doc;
1933 }
1934
1935 if (obj == null) {
1936 obj = void 0;
1937 } else if (obj instanceof Map) {
1938 obj = obj.get(pieces[i], { getters: false });
1939 } else if (i === l - 1) {
1940 obj = utils.getValue(pieces[i], obj);
1941 } else {
1942 obj = obj[pieces[i]];
1943 }
1944 }
1945
1946 if (adhoc) {
1947 obj = adhoc.cast(obj);
1948 }
1949
1950 if (schema != null && options.getters !== false) {
1951 obj = schema.applyGetters(obj, this);
1952 } else if (this.$__schema.nested[path] && options.virtuals) {
1953 // Might need to apply virtuals if this is a nested path
1954 return applyVirtuals(this, clone(obj) || {}, { path: path });
1955 }
1956
1957 return obj;
1958};
1959
1960/*!
1961 * ignore
1962 */
1963
1964Document.prototype[getSymbol] = Document.prototype.get;
1965Document.prototype.$get = Document.prototype.get;
1966
1967/**
1968 * Returns the schematype for the given `path`.
1969 *
1970 * @param {String} path
1971 * @return {SchemaPath}
1972 * @api private
1973 * @method $__path
1974 * @memberOf Document
1975 * @instance
1976 */
1977
1978Document.prototype.$__path = function(path) {
1979 const adhocs = this.$__.adhocPaths;
1980 const adhocType = adhocs && adhocs.hasOwnProperty(path) ? adhocs[path] : null;
1981
1982 if (adhocType) {
1983 return adhocType;
1984 }
1985 return this.$__schema.path(path);
1986};
1987
1988/**
1989 * Marks the path as having pending changes to write to the db.
1990 *
1991 * _Very helpful when using [Mixed](https://mongoosejs.com/docs/schematypes.html#mixed) types._
1992 *
1993 * #### Example:
1994 *
1995 * doc.mixed.type = 'changed';
1996 * doc.markModified('mixed.type');
1997 * doc.save() // changes to mixed.type are now persisted
1998 *
1999 * @param {String} path the path to mark modified
2000 * @param {Document} [scope] the scope to run validators with
2001 * @api public
2002 */
2003
2004Document.prototype.markModified = function(path, scope) {
2005 this.$__saveInitialState(path);
2006
2007 this.$__.activePaths.modify(path);
2008 if (scope != null && !this.$isSubdocument) {
2009 this.$__.pathsToScopes = this.$__pathsToScopes || {};
2010 this.$__.pathsToScopes[path] = scope;
2011 }
2012};
2013
2014/*!
2015 * ignore
2016 */
2017
2018Document.prototype.$__saveInitialState = function $__saveInitialState(path) {
2019 const savedState = this.$__.savedState;
2020 const savedStatePath = path;
2021 if (savedState != null) {
2022 const firstDot = savedStatePath.indexOf('.');
2023 const topLevelPath = firstDot === -1 ? savedStatePath : savedStatePath.slice(0, firstDot);
2024 if (!savedState.hasOwnProperty(topLevelPath)) {
2025 savedState[topLevelPath] = clone(this.$__getValue(topLevelPath));
2026 }
2027 }
2028};
2029
2030/**
2031 * Clears the modified state on the specified path.
2032 *
2033 * #### Example:
2034 *
2035 * doc.foo = 'bar';
2036 * doc.unmarkModified('foo');
2037 * doc.save(); // changes to foo will not be persisted
2038 *
2039 * @param {String} path the path to unmark modified
2040 * @api public
2041 */
2042
2043Document.prototype.unmarkModified = function(path) {
2044 this.$__.activePaths.init(path);
2045 if (this.$__.pathsToScopes != null) {
2046 delete this.$__.pathsToScopes[path];
2047 }
2048};
2049
2050/**
2051 * Don't run validation on this path or persist changes to this path.
2052 *
2053 * #### Example:
2054 *
2055 * doc.foo = null;
2056 * doc.$ignore('foo');
2057 * doc.save(); // changes to foo will not be persisted and validators won't be run
2058 *
2059 * @memberOf Document
2060 * @instance
2061 * @method $ignore
2062 * @param {String} path the path to ignore
2063 * @api public
2064 */
2065
2066Document.prototype.$ignore = function(path) {
2067 this.$__.activePaths.ignore(path);
2068};
2069
2070/**
2071 * Returns the list of paths that have been directly modified. A direct
2072 * modified path is a path that you explicitly set, whether via `doc.foo = 'bar'`,
2073 * `Object.assign(doc, { foo: 'bar' })`, or `doc.set('foo', 'bar')`.
2074 *
2075 * A path `a` may be in `modifiedPaths()` but not in `directModifiedPaths()`
2076 * because a child of `a` was directly modified.
2077 *
2078 * #### Example:
2079 *
2080 * const schema = new Schema({ foo: String, nested: { bar: String } });
2081 * const Model = mongoose.model('Test', schema);
2082 * await Model.create({ foo: 'original', nested: { bar: 'original' } });
2083 *
2084 * const doc = await Model.findOne();
2085 * doc.nested.bar = 'modified';
2086 * doc.directModifiedPaths(); // ['nested.bar']
2087 * doc.modifiedPaths(); // ['nested', 'nested.bar']
2088 *
2089 * @return {String[]}
2090 * @api public
2091 */
2092
2093Document.prototype.directModifiedPaths = function() {
2094 return Object.keys(this.$__.activePaths.getStatePaths('modify'));
2095};
2096
2097/**
2098 * Returns true if the given path is nullish or only contains empty objects.
2099 * Useful for determining whether this subdoc will get stripped out by the
2100 * [minimize option](https://mongoosejs.com/docs/guide.html#minimize).
2101 *
2102 * #### Example:
2103 *
2104 * const schema = new Schema({ nested: { foo: String } });
2105 * const Model = mongoose.model('Test', schema);
2106 * const doc = new Model({});
2107 * doc.$isEmpty('nested'); // true
2108 * doc.nested.$isEmpty(); // true
2109 *
2110 * doc.nested.foo = 'bar';
2111 * doc.$isEmpty('nested'); // false
2112 * doc.nested.$isEmpty(); // false
2113 *
2114 * @param {String} [path]
2115 * @memberOf Document
2116 * @instance
2117 * @api public
2118 * @method $isEmpty
2119 * @return {Boolean}
2120 */
2121
2122Document.prototype.$isEmpty = function(path) {
2123 const isEmptyOptions = {
2124 minimize: true,
2125 virtuals: false,
2126 getters: false,
2127 transform: false
2128 };
2129
2130 if (arguments.length !== 0) {
2131 const v = this.$get(path);
2132 if (v == null) {
2133 return true;
2134 }
2135 if (typeof v !== 'object') {
2136 return false;
2137 }
2138 if (utils.isPOJO(v)) {
2139 return _isEmpty(v);
2140 }
2141 return Object.keys(v.toObject(isEmptyOptions)).length === 0;
2142 }
2143
2144 return Object.keys(this.toObject(isEmptyOptions)).length === 0;
2145};
2146
2147/*!
2148 * ignore
2149 */
2150
2151function _isEmpty(v) {
2152 if (v == null) {
2153 return true;
2154 }
2155 if (typeof v !== 'object' || Array.isArray(v)) {
2156 return false;
2157 }
2158 for (const key of Object.keys(v)) {
2159 if (!_isEmpty(v[key])) {
2160 return false;
2161 }
2162 }
2163 return true;
2164}
2165
2166/**
2167 * Returns the list of paths that have been modified.
2168 *
2169 * @param {Object} [options]
2170 * @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`.
2171 * @return {String[]}
2172 * @api public
2173 */
2174
2175Document.prototype.modifiedPaths = function(options) {
2176 options = options || {};
2177
2178 const directModifiedPaths = Object.keys(this.$__.activePaths.getStatePaths('modify'));
2179 const result = new Set();
2180
2181 let i = 0;
2182 let j = 0;
2183 const len = directModifiedPaths.length;
2184
2185 for (i = 0; i < len; ++i) {
2186 const path = directModifiedPaths[i];
2187 const parts = parentPaths(path);
2188 const pLen = parts.length;
2189
2190 for (j = 0; j < pLen; ++j) {
2191 result.add(parts[j]);
2192 }
2193
2194 if (!options.includeChildren) {
2195 continue;
2196 }
2197
2198 let ii = 0;
2199 let cur = this.$get(path);
2200 if (typeof cur === 'object' && cur !== null) {
2201 if (cur._doc) {
2202 cur = cur._doc;
2203 }
2204 const len = cur.length;
2205 if (Array.isArray(cur)) {
2206 for (ii = 0; ii < len; ++ii) {
2207 const subPath = path + '.' + ii;
2208 if (!result.has(subPath)) {
2209 result.add(subPath);
2210 if (cur[ii] != null && cur[ii].$__) {
2211 const modified = cur[ii].modifiedPaths();
2212 let iii = 0;
2213 const iiiLen = modified.length;
2214 for (iii = 0; iii < iiiLen; ++iii) {
2215 result.add(subPath + '.' + modified[iii]);
2216 }
2217 }
2218 }
2219 }
2220 } else {
2221 const keys = Object.keys(cur);
2222 let ii = 0;
2223 const len = keys.length;
2224 for (ii = 0; ii < len; ++ii) {
2225 result.add(path + '.' + keys[ii]);
2226 }
2227 }
2228 }
2229 }
2230 return Array.from(result);
2231};
2232
2233Document.prototype[documentModifiedPaths] = Document.prototype.modifiedPaths;
2234
2235/**
2236 * Returns true if any of the given paths is modified, else false. If no arguments, returns `true` if any path
2237 * in this document is modified.
2238 *
2239 * If `path` is given, checks if a path or any full path containing `path` as part of its path chain has been modified.
2240 *
2241 * #### Example:
2242 *
2243 * doc.set('documents.0.title', 'changed');
2244 * doc.isModified() // true
2245 * doc.isModified('documents') // true
2246 * doc.isModified('documents.0.title') // true
2247 * doc.isModified('documents otherProp') // true
2248 * doc.isDirectModified('documents') // false
2249 *
2250 * @param {String} [path] optional
2251 * @param {Object} [options]
2252 * @param {Boolean} [options.ignoreAtomics=false] If true, doesn't return true if path is underneath an array that was modified with atomic operations like `push()`
2253 * @return {Boolean}
2254 * @api public
2255 */
2256
2257Document.prototype.isModified = function(paths, options, modifiedPaths) {
2258 if (paths) {
2259 const ignoreAtomics = options && options.ignoreAtomics;
2260 const directModifiedPathsObj = this.$__.activePaths.states.modify;
2261 if (directModifiedPathsObj == null) {
2262 return false;
2263 }
2264
2265 if (typeof paths === 'string') {
2266 paths = paths.indexOf(' ') === -1 ? [paths] : paths.split(' ');
2267 }
2268
2269 for (const path of paths) {
2270 if (directModifiedPathsObj[path] != null) {
2271 return true;
2272 }
2273 }
2274
2275 const modified = modifiedPaths || this[documentModifiedPaths]();
2276 const isModifiedChild = paths.some(function(path) {
2277 return !!~modified.indexOf(path);
2278 });
2279
2280 let directModifiedPaths = Object.keys(directModifiedPathsObj);
2281 if (ignoreAtomics) {
2282 directModifiedPaths = directModifiedPaths.filter(path => {
2283 const value = this.$__getValue(path);
2284 if (value != null && value[arrayAtomicsSymbol] != null && value[arrayAtomicsSymbol].$set === undefined) {
2285 return false;
2286 }
2287 return true;
2288 });
2289 }
2290 return isModifiedChild || paths.some(function(path) {
2291 return directModifiedPaths.some(function(mod) {
2292 return mod === path || path.startsWith(mod + '.');
2293 });
2294 });
2295 }
2296
2297 return this.$__.activePaths.some('modify');
2298};
2299
2300/**
2301 * Alias of [`.isModified`](https://mongoosejs.com/docs/api/document.html#Document.prototype.isModified())
2302 *
2303 * @method $isModified
2304 * @memberOf Document
2305 * @api public
2306 */
2307
2308Document.prototype.$isModified = Document.prototype.isModified;
2309
2310Document.prototype[documentIsModified] = Document.prototype.isModified;
2311
2312/**
2313 * Checks if a path is set to its default.
2314 *
2315 * #### Example:
2316 *
2317 * MyModel = mongoose.model('test', { name: { type: String, default: 'Val '} });
2318 * const m = new MyModel();
2319 * m.$isDefault('name'); // true
2320 *
2321 * @memberOf Document
2322 * @instance
2323 * @method $isDefault
2324 * @param {String} [path]
2325 * @return {Boolean}
2326 * @api public
2327 */
2328
2329Document.prototype.$isDefault = function(path) {
2330 if (path == null) {
2331 return this.$__.activePaths.some('default');
2332 }
2333
2334 if (typeof path === 'string' && path.indexOf(' ') === -1) {
2335 return this.$__.activePaths.getStatePaths('default').hasOwnProperty(path);
2336 }
2337
2338 let paths = path;
2339 if (!Array.isArray(paths)) {
2340 paths = paths.split(' ');
2341 }
2342
2343 return paths.some(path => this.$__.activePaths.getStatePaths('default').hasOwnProperty(path));
2344};
2345
2346/**
2347 * Getter/setter, determines whether the document was removed or not.
2348 *
2349 * #### Example:
2350 *
2351 * const product = await product.remove();
2352 * product.$isDeleted(); // true
2353 * product.remove(); // no-op, doesn't send anything to the db
2354 *
2355 * product.$isDeleted(false);
2356 * product.$isDeleted(); // false
2357 * product.remove(); // will execute a remove against the db
2358 *
2359 *
2360 * @param {Boolean} [val] optional, overrides whether mongoose thinks the doc is deleted
2361 * @return {Boolean|Document} whether mongoose thinks this doc is deleted.
2362 * @method $isDeleted
2363 * @memberOf Document
2364 * @instance
2365 * @api public
2366 */
2367
2368Document.prototype.$isDeleted = function(val) {
2369 if (arguments.length === 0) {
2370 return !!this.$__.isDeleted;
2371 }
2372
2373 this.$__.isDeleted = !!val;
2374 return this;
2375};
2376
2377/**
2378 * Returns true if `path` was directly set and modified, else false.
2379 *
2380 * #### Example:
2381 *
2382 * doc.set('documents.0.title', 'changed');
2383 * doc.isDirectModified('documents.0.title') // true
2384 * doc.isDirectModified('documents') // false
2385 *
2386 * @param {String|String[]} [path]
2387 * @return {Boolean}
2388 * @api public
2389 */
2390
2391Document.prototype.isDirectModified = function(path) {
2392 if (path == null) {
2393 return this.$__.activePaths.some('modify');
2394 }
2395
2396 if (typeof path === 'string' && path.indexOf(' ') === -1) {
2397 const res = this.$__.activePaths.getStatePaths('modify').hasOwnProperty(path);
2398 if (res || path.indexOf('.') === -1) {
2399 return res;
2400 }
2401
2402 const pieces = path.split('.');
2403 for (let i = 0; i < pieces.length - 1; ++i) {
2404 const subpath = pieces.slice(0, i + 1).join('.');
2405 const subdoc = this.$get(subpath);
2406 if (subdoc != null && subdoc.$__ != null && subdoc.isDirectModified(pieces.slice(i + 1).join('.'))) {
2407 return true;
2408 }
2409 }
2410
2411 return false;
2412 }
2413
2414 let paths = path;
2415 if (typeof paths === 'string') {
2416 paths = paths.split(' ');
2417 }
2418
2419 return paths.some(path => this.isDirectModified(path));
2420};
2421
2422/**
2423 * Checks if `path` is in the `init` state, that is, it was set by `Document#init()` and not modified since.
2424 *
2425 * @param {String} [path]
2426 * @return {Boolean}
2427 * @api public
2428 */
2429
2430Document.prototype.isInit = function(path) {
2431 if (path == null) {
2432 return this.$__.activePaths.some('init');
2433 }
2434
2435 if (typeof path === 'string' && path.indexOf(' ') === -1) {
2436 return this.$__.activePaths.getStatePaths('init').hasOwnProperty(path);
2437 }
2438
2439 let paths = path;
2440 if (!Array.isArray(paths)) {
2441 paths = paths.split(' ');
2442 }
2443
2444 return paths.some(path => this.$__.activePaths.getStatePaths('init').hasOwnProperty(path));
2445};
2446
2447/**
2448 * Checks if `path` was selected in the source query which initialized this document.
2449 *
2450 * #### Example:
2451 *
2452 * const doc = await Thing.findOne().select('name');
2453 * doc.isSelected('name') // true
2454 * doc.isSelected('age') // false
2455 *
2456 * @param {String|String[]} path
2457 * @return {Boolean}
2458 * @api public
2459 */
2460
2461Document.prototype.isSelected = function isSelected(path) {
2462 if (this.$__.selected == null) {
2463 return true;
2464 }
2465 if (!path) {
2466 return false;
2467 }
2468 if (path === '_id') {
2469 return this.$__.selected._id !== 0;
2470 }
2471
2472 if (path.indexOf(' ') !== -1) {
2473 path = path.split(' ');
2474 }
2475 if (Array.isArray(path)) {
2476 return path.some(p => this.$__isSelected(p));
2477 }
2478
2479 const paths = Object.keys(this.$__.selected);
2480 let inclusive = null;
2481
2482 if (paths.length === 1 && paths[0] === '_id') {
2483 // only _id was selected.
2484 return this.$__.selected._id === 0;
2485 }
2486
2487 for (const cur of paths) {
2488 if (cur === '_id') {
2489 continue;
2490 }
2491 if (!isDefiningProjection(this.$__.selected[cur])) {
2492 continue;
2493 }
2494 inclusive = !!this.$__.selected[cur];
2495 break;
2496 }
2497
2498 if (inclusive === null) {
2499 return true;
2500 }
2501
2502 if (path in this.$__.selected) {
2503 return inclusive;
2504 }
2505
2506 const pathDot = path + '.';
2507
2508 for (const cur of paths) {
2509 if (cur === '_id') {
2510 continue;
2511 }
2512
2513 if (cur.startsWith(pathDot)) {
2514 return inclusive || cur !== pathDot;
2515 }
2516
2517 if (pathDot.startsWith(cur + '.')) {
2518 return inclusive;
2519 }
2520 }
2521 return !inclusive;
2522};
2523
2524Document.prototype.$__isSelected = Document.prototype.isSelected;
2525
2526/**
2527 * Checks if `path` was explicitly selected. If no projection, always returns
2528 * true.
2529 *
2530 * #### Example:
2531 *
2532 * Thing.findOne().select('nested.name').exec(function (err, doc) {
2533 * doc.isDirectSelected('nested.name') // true
2534 * doc.isDirectSelected('nested.otherName') // false
2535 * doc.isDirectSelected('nested') // false
2536 * })
2537 *
2538 * @param {String} path
2539 * @return {Boolean}
2540 * @api public
2541 */
2542
2543Document.prototype.isDirectSelected = function isDirectSelected(path) {
2544 if (this.$__.selected == null) {
2545 return true;
2546 }
2547
2548 if (path === '_id') {
2549 return this.$__.selected._id !== 0;
2550 }
2551
2552 if (path.indexOf(' ') !== -1) {
2553 path = path.split(' ');
2554 }
2555 if (Array.isArray(path)) {
2556 return path.some(p => this.isDirectSelected(p));
2557 }
2558
2559 const paths = Object.keys(this.$__.selected);
2560 let inclusive = null;
2561
2562 if (paths.length === 1 && paths[0] === '_id') {
2563 // only _id was selected.
2564 return this.$__.selected._id === 0;
2565 }
2566
2567 for (const cur of paths) {
2568 if (cur === '_id') {
2569 continue;
2570 }
2571 if (!isDefiningProjection(this.$__.selected[cur])) {
2572 continue;
2573 }
2574 inclusive = !!this.$__.selected[cur];
2575 break;
2576 }
2577
2578 if (inclusive === null) {
2579 return true;
2580 }
2581
2582 if (this.$__.selected.hasOwnProperty(path)) {
2583 return inclusive;
2584 }
2585
2586 return !inclusive;
2587};
2588
2589/**
2590 * Executes registered validation rules for this document.
2591 *
2592 * #### Note:
2593 *
2594 * This method is called `pre` save and if a validation rule is violated, [save](https://mongoosejs.com/docs/api/model.html#Model.prototype.save()) is aborted and the error is thrown.
2595 *
2596 * #### Example:
2597 *
2598 * await doc.validate({ validateModifiedOnly: false, pathsToSkip: ['name', 'email']});
2599 *
2600 * @param {Array|String} [pathsToValidate] list of paths to validate. If set, Mongoose will validate only the modified paths that are in the given list.
2601 * @param {Object} [options] internal options
2602 * @param {Boolean} [options.validateModifiedOnly=false] if `true` mongoose validates only modified paths.
2603 * @param {Array|string} [options.pathsToSkip] list of paths to skip. If set, Mongoose will validate every modified path that is not in this list.
2604 * @return {Promise} Returns a Promise.
2605 * @api public
2606 */
2607
2608Document.prototype.validate = async function validate(pathsToValidate, options) {
2609 if (typeof pathsToValidate === 'function' || typeof options === 'function' || typeof arguments[2] === 'function') {
2610 throw new MongooseError('Document.prototype.validate() no longer accepts a callback');
2611 }
2612 let parallelValidate;
2613 this.$op = 'validate';
2614
2615 if (arguments.length === 1) {
2616 if (typeof arguments[0] === 'object' && !Array.isArray(arguments[0])) {
2617 options = arguments[0];
2618 pathsToValidate = null;
2619 }
2620 }
2621 if (options && typeof options.pathsToSkip === 'string') {
2622 const isOnePathOnly = options.pathsToSkip.indexOf(' ') === -1;
2623 options.pathsToSkip = isOnePathOnly ? [options.pathsToSkip] : options.pathsToSkip.split(' ');
2624 }
2625 const _skipParallelValidateCheck = options && options._skipParallelValidateCheck;
2626
2627 if (this.$isSubdocument != null) {
2628 // Skip parallel validate check for subdocuments
2629 } else if (this.$__.validating && !_skipParallelValidateCheck) {
2630 parallelValidate = new ParallelValidateError(this, {
2631 parentStack: options && options.parentStack,
2632 conflictStack: this.$__.validating.stack
2633 });
2634 } else if (!_skipParallelValidateCheck) {
2635 this.$__.validating = new ParallelValidateError(this, { parentStack: options && options.parentStack });
2636 }
2637
2638 if (parallelValidate != null) {
2639 throw parallelValidate;
2640 }
2641
2642 return new Promise((resolve, reject) => {
2643 this.$__validate(pathsToValidate, options, (error) => {
2644 this.$op = null;
2645 this.$__.validating = null;
2646 if (error != null) {
2647 return reject(error);
2648 }
2649 resolve();
2650 });
2651 });
2652};
2653
2654/**
2655 * Alias of [`.validate`](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate())
2656 *
2657 * @method $validate
2658 * @memberOf Document
2659 * @api public
2660 */
2661
2662Document.prototype.$validate = Document.prototype.validate;
2663
2664/*!
2665 * ignore
2666 */
2667
2668function _evaluateRequiredFunctions(doc) {
2669 const requiredFields = Object.keys(doc.$__.activePaths.getStatePaths('require'));
2670 let i = 0;
2671 const len = requiredFields.length;
2672 for (i = 0; i < len; ++i) {
2673 const path = requiredFields[i];
2674
2675 const p = doc.$__schema.path(path);
2676
2677 if (p != null && typeof p.originalRequiredValue === 'function') {
2678 doc.$__.cachedRequired = doc.$__.cachedRequired || {};
2679 try {
2680 doc.$__.cachedRequired[path] = p.originalRequiredValue.call(doc, doc);
2681 } catch (err) {
2682 doc.invalidate(path, err);
2683 }
2684 }
2685 }
2686}
2687
2688/*!
2689 * ignore
2690 */
2691
2692function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) {
2693 const doValidateOptions = {};
2694
2695 _evaluateRequiredFunctions(doc);
2696 // only validate required fields when necessary
2697 let paths = new Set(Object.keys(doc.$__.activePaths.getStatePaths('require')).filter(function(path) {
2698 if (!doc.$__isSelected(path) && !doc.$isModified(path)) {
2699 return false;
2700 }
2701 if (doc.$__.cachedRequired != null && path in doc.$__.cachedRequired) {
2702 return doc.$__.cachedRequired[path];
2703 }
2704 return true;
2705 }));
2706
2707 Object.keys(doc.$__.activePaths.getStatePaths('init')).forEach(addToPaths);
2708 Object.keys(doc.$__.activePaths.getStatePaths('modify')).forEach(addToPaths);
2709 Object.keys(doc.$__.activePaths.getStatePaths('default')).forEach(addToPaths);
2710 function addToPaths(p) { paths.add(p); }
2711
2712 const subdocs = doc.$getAllSubdocs();
2713 const modifiedPaths = doc.modifiedPaths();
2714 for (const subdoc of subdocs) {
2715 if (subdoc.$basePath) {
2716 const fullPathToSubdoc = subdoc.$isSingleNested ? subdoc.$__pathRelativeToParent() : subdoc.$__fullPathWithIndexes();
2717
2718 // Remove child paths for now, because we'll be validating the whole
2719 // subdoc.
2720 // The following is a faster take on looping through every path in `paths`
2721 // and checking if the path starts with `fullPathToSubdoc` re: gh-13191
2722 for (const modifiedPath of subdoc.modifiedPaths()) {
2723 paths.delete(fullPathToSubdoc + '.' + modifiedPath);
2724 }
2725
2726 if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) &&
2727 !doc.isDirectModified(fullPathToSubdoc) &&
2728 !doc.$isDefault(fullPathToSubdoc)) {
2729 paths.add(fullPathToSubdoc);
2730
2731 if (doc.$__.pathsToScopes == null) {
2732 doc.$__.pathsToScopes = {};
2733 }
2734 doc.$__.pathsToScopes[fullPathToSubdoc] = subdoc.$isDocumentArrayElement ?
2735 subdoc.__parentArray :
2736 subdoc.$parent();
2737
2738 doValidateOptions[fullPathToSubdoc] = { skipSchemaValidators: true };
2739 if (subdoc.$isDocumentArrayElement && subdoc.__index != null) {
2740 doValidateOptions[fullPathToSubdoc].index = subdoc.__index;
2741 }
2742 }
2743 }
2744 }
2745
2746 for (const path of paths) {
2747 const _pathType = doc.$__schema.path(path);
2748 if (!_pathType) {
2749 continue;
2750 }
2751
2752 if (_pathType.$isMongooseDocumentArray) {
2753 for (const p of paths) {
2754 if (p == null || p.startsWith(_pathType.path + '.')) {
2755 paths.delete(p);
2756 }
2757 }
2758 }
2759
2760 // Optimization: if primitive path with no validators, or array of primitives
2761 // with no validators, skip validating this path entirely.
2762 if (!_pathType.caster && _pathType.validators.length === 0 && !_pathType.$parentSchemaDocArray) {
2763 paths.delete(path);
2764 } else if (_pathType.$isMongooseArray &&
2765 !_pathType.$isMongooseDocumentArray && // Skip document arrays...
2766 !_pathType.$embeddedSchemaType.$isMongooseArray && // and arrays of arrays
2767 _pathType.validators.length === 0 && // and arrays with top-level validators
2768 _pathType.$embeddedSchemaType.validators.length === 0) {
2769 paths.delete(path);
2770 }
2771 }
2772
2773
2774 if (Array.isArray(pathsToValidate)) {
2775 paths = _handlePathsToValidate(paths, pathsToValidate);
2776 } else if (Array.isArray(pathsToSkip)) {
2777 paths = _handlePathsToSkip(paths, pathsToSkip);
2778 }
2779
2780 // from here on we're not removing items from paths
2781
2782 // gh-661: if a whole array is modified, make sure to run validation on all
2783 // the children as well
2784 _addArrayPathsToValidate(doc, paths);
2785
2786 const flattenOptions = { skipArrays: true };
2787 for (const pathToCheck of paths) {
2788 if (doc.$__schema.nested[pathToCheck]) {
2789 let _v = doc.$__getValue(pathToCheck);
2790 if (isMongooseObject(_v)) {
2791 _v = _v.toObject({ transform: false });
2792 }
2793 const flat = flatten(_v, pathToCheck, flattenOptions, doc.$__schema);
2794 // Single nested paths (paths embedded under single nested subdocs) will
2795 // be validated on their own when we call `validate()` on the subdoc itself.
2796 // Re: gh-8468
2797 Object.keys(flat).filter(path => !doc.$__schema.singleNestedPaths.hasOwnProperty(path)).forEach(addToPaths);
2798 }
2799 }
2800
2801 for (const path of paths) {
2802 const _pathType = doc.$__schema.path(path);
2803
2804 if (!_pathType) {
2805 continue;
2806 }
2807
2808 // If underneath a document array, may need to re-validate the parent
2809 // array re: gh-6818. Do this _after_ adding subpaths, because
2810 // we don't want to add every array subpath.
2811 if (_pathType.$parentSchemaDocArray && typeof _pathType.$parentSchemaDocArray.path === 'string') {
2812 paths.add(_pathType.$parentSchemaDocArray.path);
2813 }
2814
2815 if (!_pathType.$isSchemaMap) {
2816 continue;
2817 }
2818
2819 const val = doc.$__getValue(path);
2820 if (val == null) {
2821 continue;
2822 }
2823 for (const key of val.keys()) {
2824 paths.add(path + '.' + key);
2825 }
2826 }
2827
2828 paths = Array.from(paths);
2829 return [paths, doValidateOptions];
2830}
2831
2832function _addArrayPathsToValidate(doc, paths) {
2833 for (const path of paths) {
2834 const _pathType = doc.$__schema.path(path);
2835 if (!_pathType) {
2836 continue;
2837 }
2838
2839 if (!_pathType.$isMongooseArray ||
2840 // To avoid potential performance issues, skip doc arrays whose children
2841 // are not required. `getPositionalPathType()` may be slow, so avoid
2842 // it unless we have a case of #6364
2843 (!Array.isArray(_pathType) &&
2844 _pathType.$isMongooseDocumentArray &&
2845 !(_pathType && _pathType.schemaOptions && _pathType.schemaOptions.required))) {
2846 continue;
2847 }
2848
2849 // gh-11380: optimization. If the array isn't a document array and there's no validators
2850 // on the array type, there's no need to run validation on the individual array elements.
2851 if (_pathType.$isMongooseArray &&
2852 !_pathType.$isMongooseDocumentArray && // Skip document arrays...
2853 !_pathType.$embeddedSchemaType.$isMongooseArray && // and arrays of arrays
2854 _pathType.$embeddedSchemaType.validators.length === 0) {
2855 continue;
2856 }
2857
2858 const val = doc.$__getValue(path);
2859 _pushNestedArrayPaths(val, paths, path);
2860 }
2861}
2862
2863function _pushNestedArrayPaths(val, paths, path) {
2864 if (val != null) {
2865 const numElements = val.length;
2866 for (let j = 0; j < numElements; ++j) {
2867 if (Array.isArray(val[j])) {
2868 _pushNestedArrayPaths(val[j], paths, path + '.' + j);
2869 } else {
2870 paths.add(path + '.' + j);
2871 }
2872 }
2873 }
2874}
2875
2876/*!
2877 * ignore
2878 */
2879
2880Document.prototype.$__validate = function(pathsToValidate, options, callback) {
2881 if (this.$__.saveOptions && this.$__.saveOptions.pathsToSave && !pathsToValidate) {
2882 pathsToValidate = [...this.$__.saveOptions.pathsToSave];
2883 } else if (typeof pathsToValidate === 'function') {
2884 callback = pathsToValidate;
2885 options = null;
2886 pathsToValidate = null;
2887 } else if (typeof options === 'function') {
2888 callback = options;
2889 options = null;
2890 }
2891
2892 const hasValidateModifiedOnlyOption = options &&
2893 (typeof options === 'object') &&
2894 ('validateModifiedOnly' in options);
2895
2896 const pathsToSkip = (options && options.pathsToSkip) || null;
2897
2898 let shouldValidateModifiedOnly;
2899 if (hasValidateModifiedOnlyOption) {
2900 shouldValidateModifiedOnly = !!options.validateModifiedOnly;
2901 } else {
2902 shouldValidateModifiedOnly = this.$__schema.options.validateModifiedOnly;
2903 }
2904
2905 const validateAllPaths = options && options.validateAllPaths;
2906 if (validateAllPaths) {
2907 if (pathsToSkip) {
2908 throw new TypeError('Cannot set both `validateAllPaths` and `pathsToSkip`');
2909 }
2910 if (pathsToValidate) {
2911 throw new TypeError('Cannot set both `validateAllPaths` and `pathsToValidate`');
2912 }
2913 if (hasValidateModifiedOnlyOption && shouldValidateModifiedOnly) {
2914 throw new TypeError('Cannot set both `validateAllPaths` and `validateModifiedOnly`');
2915 }
2916 }
2917
2918 const _this = this;
2919 const _complete = () => {
2920 let validationError = this.$__.validationError;
2921 this.$__.validationError = null;
2922 this.$__.validating = null;
2923
2924 if (shouldValidateModifiedOnly && validationError != null) {
2925 // Remove any validation errors that aren't from modified paths
2926 const errors = Object.keys(validationError.errors);
2927 for (const errPath of errors) {
2928 if (!this.$isModified(errPath)) {
2929 delete validationError.errors[errPath];
2930 }
2931 }
2932 if (Object.keys(validationError.errors).length === 0) {
2933 validationError = void 0;
2934 }
2935 }
2936
2937 this.$__.cachedRequired = {};
2938 this.$emit('validate', _this);
2939 this.constructor.emit('validate', _this);
2940
2941 if (validationError) {
2942 for (const key in validationError.errors) {
2943 // Make sure cast errors persist
2944 if (!this[documentArrayParent] &&
2945 validationError.errors[key] instanceof MongooseError.CastError) {
2946 this.invalidate(key, validationError.errors[key]);
2947 }
2948 }
2949
2950 return validationError;
2951 }
2952 };
2953
2954 // only validate required fields when necessary
2955 let paths;
2956 let doValidateOptionsByPath;
2957 if (validateAllPaths) {
2958 paths = new Set(Object.keys(this.$__schema.paths));
2959 // gh-661: if a whole array is modified, make sure to run validation on all
2960 // the children as well
2961 for (const path of paths) {
2962 const schemaType = this.$__schema.path(path);
2963 if (!schemaType || !schemaType.$isMongooseArray) {
2964 continue;
2965 }
2966 const val = this.$__getValue(path);
2967 if (!val) {
2968 continue;
2969 }
2970 _pushNestedArrayPaths(val, paths, path);
2971 }
2972 paths = [...paths];
2973 doValidateOptionsByPath = {};
2974 } else {
2975 const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
2976 paths = shouldValidateModifiedOnly ?
2977 pathDetails[0].filter((path) => this.$isModified(path)) :
2978 pathDetails[0];
2979 doValidateOptionsByPath = pathDetails[1];
2980 }
2981
2982 if (typeof pathsToValidate === 'string') {
2983 pathsToValidate = pathsToValidate.split(' ');
2984 }
2985
2986 if (paths.length === 0) {
2987 return immediate(function() {
2988 const error = _complete();
2989 if (error) {
2990 return _this.$__schema.s.hooks.execPost('validate:error', _this, [_this], { error: error }, function(error) {
2991 callback(error);
2992 });
2993 }
2994 callback(null, _this);
2995 });
2996 }
2997
2998 const validated = {};
2999 let total = 0;
3000
3001 let pathsToSave = this.$__.saveOptions?.pathsToSave;
3002 if (Array.isArray(pathsToSave)) {
3003 pathsToSave = new Set(pathsToSave);
3004 for (const path of paths) {
3005 if (!pathsToSave.has(path)) {
3006 continue;
3007 }
3008 validatePath(path);
3009 }
3010 } else {
3011 for (const path of paths) {
3012 validatePath(path);
3013 }
3014 }
3015
3016 function validatePath(path) {
3017 if (path == null || validated[path]) {
3018 return;
3019 }
3020
3021 validated[path] = true;
3022 total++;
3023
3024 immediate(function() {
3025 const schemaType = _this.$__schema.path(path);
3026
3027 if (!schemaType) {
3028 return --total || complete();
3029 }
3030
3031 // If user marked as invalid or there was a cast error, don't validate
3032 if (!_this.$isValid(path)) {
3033 --total || complete();
3034 return;
3035 }
3036
3037 // If setting a path under a mixed path, avoid using the mixed path validator (gh-10141)
3038 if (schemaType[schemaMixedSymbol] != null && path !== schemaType.path) {
3039 return --total || complete();
3040 }
3041
3042 let val = _this.$__getValue(path);
3043
3044 // If you `populate()` and get back a null value, required validators
3045 // shouldn't fail (gh-8018). We should always fall back to the populated
3046 // value.
3047 let pop;
3048 if ((pop = _this.$populated(path))) {
3049 val = pop;
3050 } else if (val != null && val.$__ != null && val.$__.wasPopulated) {
3051 // Array paths, like `somearray.1`, do not show up as populated with `$populated()`,
3052 // so in that case pull out the document's id
3053 val = val._id;
3054 }
3055 const scope = _this.$__.pathsToScopes != null && path in _this.$__.pathsToScopes ?
3056 _this.$__.pathsToScopes[path] :
3057 _this;
3058
3059 const doValidateOptions = {
3060 ...doValidateOptionsByPath[path],
3061 path: path,
3062 validateAllPaths
3063 };
3064
3065 schemaType.doValidate(val, function(err) {
3066 if (err) {
3067 const isSubdoc = schemaType.$isSingleNested ||
3068 schemaType.$isArraySubdocument ||
3069 schemaType.$isMongooseDocumentArray;
3070 if (isSubdoc && err instanceof ValidationError) {
3071 return --total || complete();
3072 }
3073 _this.invalidate(path, err, undefined, true);
3074 }
3075 --total || complete();
3076 }, scope, doValidateOptions);
3077 });
3078 }
3079
3080 function complete() {
3081 const error = _complete();
3082 if (error) {
3083 return _this.$__schema.s.hooks.execPost('validate:error', _this, [_this], { error: error }, function(error) {
3084 callback(error);
3085 });
3086 }
3087 callback(null, _this);
3088 }
3089
3090};
3091
3092/*!
3093 * ignore
3094 */
3095
3096function _handlePathsToValidate(paths, pathsToValidate) {
3097 const _pathsToValidate = new Set(pathsToValidate);
3098 const parentPaths = new Map([]);
3099 for (const path of pathsToValidate) {
3100 if (path.indexOf('.') === -1) {
3101 continue;
3102 }
3103 const pieces = path.split('.');
3104 let cur = pieces[0];
3105 for (let i = 1; i < pieces.length; ++i) {
3106 // Since we skip subpaths under single nested subdocs to
3107 // avoid double validation, we need to add back the
3108 // single nested subpath if the user asked for it (gh-8626)
3109 parentPaths.set(cur, path);
3110 cur = cur + '.' + pieces[i];
3111 }
3112 }
3113
3114 const ret = new Set();
3115 for (const path of paths) {
3116 if (_pathsToValidate.has(path)) {
3117 ret.add(path);
3118 } else if (parentPaths.has(path)) {
3119 ret.add(parentPaths.get(path));
3120 }
3121 }
3122 return ret;
3123}
3124
3125/*!
3126 * ignore
3127 */
3128
3129function _handlePathsToSkip(paths, pathsToSkip) {
3130 pathsToSkip = new Set(pathsToSkip);
3131 paths = Array.from(paths).filter(p => !pathsToSkip.has(p));
3132 return new Set(paths);
3133}
3134
3135/**
3136 * Executes registered validation rules (skipping asynchronous validators) for this document.
3137 *
3138 * #### Note:
3139 *
3140 * This method is useful if you need synchronous validation.
3141 *
3142 * #### Example:
3143 *
3144 * const err = doc.validateSync();
3145 * if (err) {
3146 * handleError(err);
3147 * } else {
3148 * // validation passed
3149 * }
3150 *
3151 * @param {Array|string} [pathsToValidate] only validate the given paths
3152 * @param {Object} [options] options for validation
3153 * @param {Boolean} [options.validateModifiedOnly=false] If `true`, Mongoose will only validate modified paths, as opposed to modified paths and `required` paths.
3154 * @param {Array|string} [options.pathsToSkip] list of paths to skip. If set, Mongoose will validate every modified path that is not in this list.
3155 * @return {ValidationError|undefined} ValidationError if there are errors during validation, or undefined if there is no error.
3156 * @api public
3157 */
3158
3159Document.prototype.validateSync = function(pathsToValidate, options) {
3160 const _this = this;
3161
3162 if (arguments.length === 1 && typeof arguments[0] === 'object' && !Array.isArray(arguments[0])) {
3163 options = arguments[0];
3164 pathsToValidate = null;
3165 }
3166
3167 const hasValidateModifiedOnlyOption = options &&
3168 (typeof options === 'object') &&
3169 ('validateModifiedOnly' in options);
3170
3171 let shouldValidateModifiedOnly;
3172 if (hasValidateModifiedOnlyOption) {
3173 shouldValidateModifiedOnly = !!options.validateModifiedOnly;
3174 } else {
3175 shouldValidateModifiedOnly = this.$__schema.options.validateModifiedOnly;
3176 }
3177
3178 let pathsToSkip = options && options.pathsToSkip;
3179
3180 const validateAllPaths = options && options.validateAllPaths;
3181 if (validateAllPaths) {
3182 if (pathsToSkip) {
3183 throw new TypeError('Cannot set both `validateAllPaths` and `pathsToSkip`');
3184 }
3185 if (pathsToValidate) {
3186 throw new TypeError('Cannot set both `validateAllPaths` and `pathsToValidate`');
3187 }
3188 }
3189
3190 if (typeof pathsToValidate === 'string') {
3191 const isOnePathOnly = pathsToValidate.indexOf(' ') === -1;
3192 pathsToValidate = isOnePathOnly ? [pathsToValidate] : pathsToValidate.split(' ');
3193 } else if (typeof pathsToSkip === 'string' && pathsToSkip.indexOf(' ') !== -1) {
3194 pathsToSkip = pathsToSkip.split(' ');
3195 }
3196
3197 // only validate required fields when necessary
3198 let paths;
3199 let skipSchemaValidators;
3200 if (validateAllPaths) {
3201 paths = new Set(Object.keys(this.$__schema.paths));
3202 // gh-661: if a whole array is modified, make sure to run validation on all
3203 // the children as well
3204 for (const path of paths) {
3205 const schemaType = this.$__schema.path(path);
3206 if (!schemaType || !schemaType.$isMongooseArray) {
3207 continue;
3208 }
3209 const val = this.$__getValue(path);
3210 if (!val) {
3211 continue;
3212 }
3213 _pushNestedArrayPaths(val, paths, path);
3214 }
3215 paths = [...paths];
3216 skipSchemaValidators = {};
3217 } else {
3218 const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
3219 paths = shouldValidateModifiedOnly ?
3220 pathDetails[0].filter((path) => this.$isModified(path)) :
3221 pathDetails[0];
3222 skipSchemaValidators = pathDetails[1];
3223 }
3224
3225 const validating = {};
3226
3227 for (let i = 0, len = paths.length; i < len; ++i) {
3228 const path = paths[i];
3229
3230 if (validating[path]) {
3231 continue;
3232 }
3233
3234 validating[path] = true;
3235
3236 const p = _this.$__schema.path(path);
3237 if (!p) {
3238 continue;
3239 }
3240 if (!_this.$isValid(path)) {
3241 continue;
3242 }
3243
3244 const val = _this.$__getValue(path);
3245 const err = p.doValidateSync(val, _this, {
3246 skipSchemaValidators: skipSchemaValidators[path],
3247 path: path,
3248 validateModifiedOnly: shouldValidateModifiedOnly,
3249 validateAllPaths
3250 });
3251 if (err) {
3252 const isSubdoc = p.$isSingleNested ||
3253 p.$isArraySubdocument ||
3254 p.$isMongooseDocumentArray;
3255 if (isSubdoc && err instanceof ValidationError) {
3256 continue;
3257 }
3258 _this.invalidate(path, err, undefined, true);
3259 }
3260 }
3261
3262 const err = _this.$__.validationError;
3263 _this.$__.validationError = undefined;
3264 _this.$emit('validate', _this);
3265 _this.constructor.emit('validate', _this);
3266
3267 if (err) {
3268 for (const key in err.errors) {
3269 // Make sure cast errors persist
3270 if (err.errors[key] instanceof MongooseError.CastError) {
3271 _this.invalidate(key, err.errors[key]);
3272 }
3273 }
3274 }
3275
3276 return err;
3277};
3278
3279/**
3280 * Marks a path as invalid, causing validation to fail.
3281 *
3282 * The `errorMsg` argument will become the message of the `ValidationError`.
3283 *
3284 * The `value` argument (if passed) will be available through the `ValidationError.value` property.
3285 *
3286 * doc.invalidate('size', 'must be less than 20', 14);
3287 *
3288 * doc.validate(function (err) {
3289 * console.log(err)
3290 * // prints
3291 * { message: 'Validation failed',
3292 * name: 'ValidationError',
3293 * errors:
3294 * { size:
3295 * { message: 'must be less than 20',
3296 * name: 'ValidatorError',
3297 * path: 'size',
3298 * type: 'user defined',
3299 * value: 14 } } }
3300 * })
3301 *
3302 * @param {String} path the field to invalidate. For array elements, use the `array.i.field` syntax, where `i` is the 0-based index in the array.
3303 * @param {String|Error} err the error which states the reason `path` was invalid
3304 * @param {Object|String|Number|any} val optional invalid value
3305 * @param {String} [kind] optional `kind` property for the error
3306 * @return {ValidationError} the current ValidationError, with all currently invalidated paths
3307 * @api public
3308 */
3309
3310Document.prototype.invalidate = function(path, err, val, kind) {
3311 if (!this.$__.validationError) {
3312 this.$__.validationError = new ValidationError(this);
3313 }
3314
3315 if (this.$__.validationError.errors[path]) {
3316 return;
3317 }
3318
3319 if (!err || typeof err === 'string') {
3320 err = new ValidatorError({
3321 path: path,
3322 message: err,
3323 type: kind || 'user defined',
3324 value: val
3325 });
3326 }
3327
3328 if (this.$__.validationError === err) {
3329 return this.$__.validationError;
3330 }
3331
3332 this.$__.validationError.addError(path, err);
3333 return this.$__.validationError;
3334};
3335
3336/**
3337 * Marks a path as valid, removing existing validation errors.
3338 *
3339 * @param {String} path the field to mark as valid
3340 * @api public
3341 * @memberOf Document
3342 * @instance
3343 * @method $markValid
3344 */
3345
3346Document.prototype.$markValid = function(path) {
3347 if (!this.$__.validationError || !this.$__.validationError.errors[path]) {
3348 return;
3349 }
3350
3351 delete this.$__.validationError.errors[path];
3352 if (Object.keys(this.$__.validationError.errors).length === 0) {
3353 this.$__.validationError = null;
3354 }
3355};
3356
3357/*!
3358 * ignore
3359 */
3360
3361function _markValidSubpaths(doc, path) {
3362 if (!doc.$__.validationError) {
3363 return;
3364 }
3365
3366 const keys = Object.keys(doc.$__.validationError.errors);
3367 for (const key of keys) {
3368 if (key.startsWith(path + '.')) {
3369 delete doc.$__.validationError.errors[key];
3370 }
3371 }
3372 if (Object.keys(doc.$__.validationError.errors).length === 0) {
3373 doc.$__.validationError = null;
3374 }
3375}
3376
3377/*!
3378 * ignore
3379 */
3380
3381function _checkImmutableSubpaths(subdoc, schematype, priorVal) {
3382 const schema = schematype.schema;
3383 if (schema == null) {
3384 return;
3385 }
3386
3387 for (const key of Object.keys(schema.paths)) {
3388 const path = schema.paths[key];
3389 if (path.$immutableSetter == null) {
3390 continue;
3391 }
3392 const oldVal = priorVal == null ? void 0 : priorVal.$__getValue(key);
3393 // Calling immutableSetter with `oldVal` even though it expects `newVal`
3394 // is intentional. That's because `$immutableSetter` compares its param
3395 // to the current value.
3396 path.$immutableSetter.call(subdoc, oldVal);
3397 }
3398}
3399
3400/**
3401 * Saves this document by inserting a new document into the database if [document.isNew](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew()) is `true`,
3402 * or sends an [updateOne](https://mongoosejs.com/docs/api/document.html#Document.prototype.updateOne()) operation **only** with the modifications to the database, it does not replace the whole document in the latter case.
3403 *
3404 * #### Example:
3405 *
3406 * product.sold = Date.now();
3407 * product = await product.save();
3408 *
3409 * If save is successful, the returned promise will fulfill with the document
3410 * saved.
3411 *
3412 * #### Example:
3413 *
3414 * const newProduct = await product.save();
3415 * newProduct === product; // true
3416 *
3417 * @param {Object} [options] options optional options
3418 * @param {Session} [options.session=null] the [session](https://www.mongodb.com/docs/manual/reference/server-sessions/) associated with this save operation. If not specified, defaults to the [document's associated session](https://mongoosejs.com/docs/api/document.html#Document.prototype.$session()).
3419 * @param {Object} [options.safe] (DEPRECATED) overrides [schema's safe option](https://mongoosejs.com/docs/guide.html#safe). Use the `w` option instead.
3420 * @param {Boolean} [options.validateBeforeSave] set to false to save without validating.
3421 * @param {Boolean} [options.validateModifiedOnly=false] If `true`, Mongoose will only validate modified paths, as opposed to modified paths and `required` paths.
3422 * @param {Number|String} [options.w] set the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#w-option). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern)
3423 * @param {Boolean} [options.j] set to true for MongoDB to wait until this `save()` has been [journaled before resolving the returned promise](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern)
3424 * @param {Number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern).
3425 * @param {Boolean} [options.checkKeys=true] the MongoDB driver prevents you from saving keys that start with '$' or contain '.' by default. Set this option to `false` to skip that check. See [restrictions on field names](https://www.mongodb.com/docs/manual/reference/limits/#Restrictions-on-Field-Names)
3426 * @param {Boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
3427 * @param {Function} [fn] optional callback
3428 * @method save
3429 * @memberOf Document
3430 * @instance
3431 * @throws {DocumentNotFoundError} if this [save updates an existing document](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew()) but the document doesn't exist in the database. For example, you will get this error if the document is [deleted between when you retrieved the document and when you saved it](documents.html#updating).
3432 * @return {Promise|undefined} Returns undefined if used with callback or a Promise otherwise.
3433 * @api public
3434 * @see middleware https://mongoosejs.com/docs/middleware.html
3435 */
3436
3437/**
3438 * Checks if a path is invalid
3439 *
3440 * @param {String|String[]} [path] the field to check. If unset will always return "false"
3441 * @method $isValid
3442 * @memberOf Document
3443 * @instance
3444 * @api private
3445 */
3446
3447Document.prototype.$isValid = function(path) {
3448 if (this.$__.validationError == null || Object.keys(this.$__.validationError.errors).length === 0) {
3449 return true;
3450 }
3451 if (path == null) {
3452 return false;
3453 }
3454
3455 if (path.indexOf(' ') !== -1) {
3456 path = path.split(' ');
3457 }
3458 if (Array.isArray(path)) {
3459 return path.some(p => this.$__.validationError.errors[p] == null);
3460 }
3461
3462 return this.$__.validationError.errors[path] == null;
3463};
3464
3465/**
3466 * Resets the internal modified state of this document.
3467 *
3468 * @api private
3469 * @return {Document} this
3470 * @method $__reset
3471 * @memberOf Document
3472 * @instance
3473 */
3474
3475Document.prototype.$__reset = function reset() {
3476 let _this = this;
3477
3478 // Skip for subdocuments
3479 const subdocs = !this.$isSubdocument ? this.$getAllSubdocs() : null;
3480 if (subdocs && subdocs.length > 0) {
3481 const resetArrays = new Set();
3482 for (const subdoc of subdocs) {
3483 const fullPathWithIndexes = subdoc.$__fullPathWithIndexes();
3484 subdoc.$__reset();
3485 if (this.isModified(fullPathWithIndexes) || isParentInit(fullPathWithIndexes)) {
3486 if (subdoc.$isDocumentArrayElement) {
3487 resetArrays.add(subdoc.parentArray());
3488 } else {
3489 const parent = subdoc.$parent();
3490 if (parent === this) {
3491 this.$__.activePaths.clearPath(subdoc.$basePath);
3492 } else if (parent != null && parent.$isSubdocument) {
3493 // If map path underneath subdocument, may end up with a case where
3494 // map path is modified but parent still needs to be reset. See gh-10295
3495 parent.$__reset();
3496 }
3497 }
3498 }
3499 }
3500
3501 for (const array of resetArrays) {
3502 this.$__.activePaths.clearPath(array.$path());
3503 array[arrayAtomicsBackupSymbol] = array[arrayAtomicsSymbol];
3504 array[arrayAtomicsSymbol] = {};
3505 }
3506 }
3507
3508 function isParentInit(path) {
3509 path = path.indexOf('.') === -1 ? [path] : path.split('.');
3510 let cur = '';
3511 for (let i = 0; i < path.length; ++i) {
3512 cur += (cur.length ? '.' : '') + path[i];
3513 if (_this.$__.activePaths[cur] === 'init') {
3514 return true;
3515 }
3516 }
3517
3518 return false;
3519 }
3520
3521 // clear atomics
3522 this.$__dirty().forEach(function(dirt) {
3523 const type = dirt.value;
3524
3525 if (type && type[arrayAtomicsSymbol]) {
3526 type[arrayAtomicsBackupSymbol] = type[arrayAtomicsSymbol];
3527 type[arrayAtomicsSymbol] = {};
3528 }
3529 });
3530
3531 this.$__.backup = {};
3532 this.$__.backup.activePaths = {
3533 modify: Object.assign({}, this.$__.activePaths.getStatePaths('modify')),
3534 default: Object.assign({}, this.$__.activePaths.getStatePaths('default'))
3535 };
3536 this.$__.backup.validationError = this.$__.validationError;
3537 this.$__.backup.errors = this.$errors;
3538
3539 // Clear 'dirty' cache
3540 this.$__.activePaths.clear('modify');
3541 this.$__.activePaths.clear('default');
3542 this.$__.validationError = undefined;
3543 this.$errors = undefined;
3544 _this = this;
3545 this.$__schema.requiredPaths().forEach(function(path) {
3546 _this.$__.activePaths.require(path);
3547 });
3548
3549 return this;
3550};
3551
3552/*!
3553 * ignore
3554 */
3555
3556Document.prototype.$__undoReset = function $__undoReset() {
3557 if (this.$__.backup == null || this.$__.backup.activePaths == null) {
3558 return;
3559 }
3560
3561 this.$__.activePaths.states.modify = this.$__.backup.activePaths.modify;
3562 this.$__.activePaths.states.default = this.$__.backup.activePaths.default;
3563
3564 this.$__.validationError = this.$__.backup.validationError;
3565 this.$errors = this.$__.backup.errors;
3566
3567 for (const dirt of this.$__dirty()) {
3568 const type = dirt.value;
3569
3570 if (type && type[arrayAtomicsSymbol] && type[arrayAtomicsBackupSymbol]) {
3571 type[arrayAtomicsSymbol] = type[arrayAtomicsBackupSymbol];
3572 }
3573 }
3574
3575 for (const subdoc of this.$getAllSubdocs()) {
3576 subdoc.$__undoReset();
3577 }
3578};
3579
3580/**
3581 * Returns this documents dirty paths / vals.
3582 *
3583 * @return {Array}
3584 * @api private
3585 * @method $__dirty
3586 * @memberOf Document
3587 * @instance
3588 */
3589
3590Document.prototype.$__dirty = function() {
3591 const _this = this;
3592 let all = this.$__.activePaths.map('modify', function(path) {
3593 return {
3594 path: path,
3595 value: _this.$__getValue(path),
3596 schema: _this.$__path(path)
3597 };
3598 });
3599
3600 // gh-2558: if we had to set a default and the value is not undefined,
3601 // we have to save as well
3602 all = all.concat(this.$__.activePaths.map('default', function(path) {
3603 if (path === '_id' || _this.$__getValue(path) == null) {
3604 return;
3605 }
3606 return {
3607 path: path,
3608 value: _this.$__getValue(path),
3609 schema: _this.$__path(path)
3610 };
3611 }));
3612
3613 const allPaths = new Map(all.filter((el) => el != null).map((el) => [el.path, el.value]));
3614 // Ignore "foo.a" if "foo" is dirty already.
3615 const minimal = [];
3616
3617 all.forEach(function(item) {
3618 if (!item) {
3619 return;
3620 }
3621
3622 let top = null;
3623
3624 const array = parentPaths(item.path);
3625 for (let i = 0; i < array.length - 1; i++) {
3626 if (allPaths.has(array[i])) {
3627 top = allPaths.get(array[i]);
3628 break;
3629 }
3630 }
3631 if (top == null) {
3632 minimal.push(item);
3633 } else if (top != null &&
3634 top[arrayAtomicsSymbol] != null &&
3635 top.hasAtomics()) {
3636 // special case for top level MongooseArrays
3637 // the `top` array itself and a sub path of `top` are being set.
3638 // the only way to honor all of both modifications is through a $set
3639 // of entire array.
3640 top[arrayAtomicsSymbol] = {};
3641 top[arrayAtomicsSymbol].$set = top;
3642 }
3643 });
3644 return minimal;
3645};
3646
3647/**
3648 * Assigns/compiles `schema` into this documents prototype.
3649 *
3650 * @param {Schema} schema
3651 * @api private
3652 * @method $__setSchema
3653 * @memberOf Document
3654 * @instance
3655 */
3656
3657Document.prototype.$__setSchema = function(schema) {
3658 compile(schema.tree, this, undefined, schema.options);
3659
3660 // Apply default getters if virtual doesn't have any (gh-6262)
3661 for (const key of Object.keys(schema.virtuals)) {
3662 schema.virtuals[key]._applyDefaultGetters();
3663 }
3664 if (schema.path('schema') == null) {
3665 this.schema = schema;
3666 }
3667 this.$__schema = schema;
3668 this[documentSchemaSymbol] = schema;
3669};
3670
3671
3672/**
3673 * Get active path that were changed and are arrays
3674 *
3675 * @return {Array}
3676 * @api private
3677 * @method $__getArrayPathsToValidate
3678 * @memberOf Document
3679 * @instance
3680 */
3681
3682Document.prototype.$__getArrayPathsToValidate = function() {
3683 DocumentArray || (DocumentArray = require('./types/documentArray'));
3684
3685 // validate all document arrays.
3686 return this.$__.activePaths
3687 .map('init', 'modify', function(i) {
3688 return this.$__getValue(i);
3689 }.bind(this))
3690 .filter(function(val) {
3691 return val && Array.isArray(val) && utils.isMongooseDocumentArray(val) && val.length;
3692 }).reduce(function(seed, array) {
3693 return seed.concat(array);
3694 }, [])
3695 .filter(function(doc) {
3696 return doc;
3697 });
3698};
3699
3700
3701/**
3702 * Get all subdocs (by bfs)
3703 *
3704 * @return {Array}
3705 * @api public
3706 * @method $getAllSubdocs
3707 * @memberOf Document
3708 * @instance
3709 */
3710
3711Document.prototype.$getAllSubdocs = function() {
3712 DocumentArray || (DocumentArray = require('./types/documentArray'));
3713 Embedded = Embedded || require('./types/arraySubdocument');
3714
3715 function docReducer(doc, seed, path) {
3716 let val = doc;
3717 let isNested = false;
3718 if (path) {
3719 if (doc instanceof Document && doc[documentSchemaSymbol].paths[path]) {
3720 val = doc._doc[path];
3721 } else if (doc instanceof Document && doc[documentSchemaSymbol].nested[path]) {
3722 val = doc._doc[path];
3723 isNested = true;
3724 } else {
3725 val = doc[path];
3726 }
3727 }
3728 if (val instanceof Embedded) {
3729 seed.push(val);
3730 } else if (val instanceof Map) {
3731 seed = Array.from(val.keys()).reduce(function(seed, path) {
3732 return docReducer(val.get(path), seed, null);
3733 }, seed);
3734 } else if (val && !Array.isArray(val) && val.$isSingleNested) {
3735 seed = Object.keys(val._doc).reduce(function(seed, path) {
3736 return docReducer(val, seed, path);
3737 }, seed);
3738 seed.push(val);
3739 } else if (val && utils.isMongooseDocumentArray(val)) {
3740 val.forEach(function _docReduce(doc) {
3741 if (!doc || !doc._doc) {
3742 return;
3743 }
3744 seed = Object.keys(doc._doc).reduce(function(seed, path) {
3745 return docReducer(doc._doc, seed, path);
3746 }, seed);
3747 if (doc instanceof Embedded) {
3748 seed.push(doc);
3749 }
3750 });
3751 } else if (isNested && val != null) {
3752 for (const path of Object.keys(val)) {
3753 docReducer(val, seed, path);
3754 }
3755 }
3756 return seed;
3757 }
3758
3759 const subDocs = [];
3760 for (const path of Object.keys(this._doc)) {
3761 docReducer(this, subDocs, path);
3762 }
3763
3764 return subDocs;
3765};
3766
3767/*!
3768 * Runs queued functions
3769 */
3770
3771function applyQueue(doc) {
3772 const q = doc.$__schema && doc.$__schema.callQueue;
3773 if (!q.length) {
3774 return;
3775 }
3776
3777 for (const pair of q) {
3778 if (pair[0] !== 'pre' && pair[0] !== 'post' && pair[0] !== 'on') {
3779 doc[pair[0]].apply(doc, pair[1]);
3780 }
3781 }
3782}
3783
3784/*!
3785 * ignore
3786 */
3787
3788Document.prototype.$__handleReject = function handleReject(err) {
3789 // emit on the Model if listening
3790 if (this.$listeners('error').length) {
3791 this.$emit('error', err);
3792 } else if (this.constructor.listeners && this.constructor.listeners('error').length) {
3793 this.constructor.emit('error', err);
3794 }
3795};
3796
3797/**
3798 * Internal common logic for toObject() and toJSON()
3799 *
3800 * @return {Object}
3801 * @api private
3802 * @method $toObject
3803 * @memberOf Document
3804 * @instance
3805 */
3806
3807Document.prototype.$toObject = function(options, json) {
3808 const defaultOptions = this.$__schema._defaultToObjectOptions(json);
3809
3810 const hasOnlyPrimitiveValues = this.$__hasOnlyPrimitiveValues();
3811
3812 // If options do not exist or is not an object, set it to empty object
3813 options = utils.isPOJO(options) ? { ...options } : {};
3814 options._calledWithOptions = options._calledWithOptions || { ...options };
3815
3816 let _minimize;
3817 if (options._calledWithOptions.minimize != null) {
3818 _minimize = options.minimize;
3819 } else if (defaultOptions != null && defaultOptions.minimize != null) {
3820 _minimize = defaultOptions.minimize;
3821 } else {
3822 _minimize = this.$__schema.options.minimize;
3823 }
3824
3825 options.minimize = _minimize;
3826 if (!hasOnlyPrimitiveValues) {
3827 options._seen = options._seen || new Map();
3828 }
3829
3830 const depopulate = options._calledWithOptions.depopulate
3831 ?? defaultOptions?.depopulate
3832 ?? options.depopulate
3833 ?? false;
3834 // _isNested will only be true if this is not the top level document, we
3835 // should never depopulate the top-level document
3836 if (depopulate && options._isNested && this.$__.wasPopulated) {
3837 return clone(this.$__.wasPopulated.value || this._doc._id, options);
3838 }
3839 if (depopulate) {
3840 options.depopulate = true;
3841 }
3842
3843 // merge default options with input options.
3844 if (defaultOptions != null) {
3845 for (const key of Object.keys(defaultOptions)) {
3846 if (options[key] == null) {
3847 options[key] = defaultOptions[key];
3848 }
3849 }
3850 }
3851 options._isNested = true;
3852 options.json = json;
3853 options.minimize = _minimize;
3854
3855 const parentOptions = options._parentOptions;
3856 // Parent options should only bubble down for subdocuments, not populated docs
3857 options._parentOptions = this.$isSubdocument ? options : null;
3858
3859 options._skipSingleNestedGetters = false;
3860 // remember the root transform function
3861 // to save it from being overwritten by sub-transform functions
3862 // const originalTransform = options.transform;
3863
3864 let ret;
3865 if (hasOnlyPrimitiveValues && !options.flattenObjectIds) {
3866 // Fast path: if we don't have any nested objects or arrays, we only need a
3867 // shallow clone.
3868 ret = this.$__toObjectShallow();
3869 } else {
3870 ret = clone(this._doc, options) || {};
3871 }
3872
3873 options._skipSingleNestedGetters = true;
3874 const getters = options._calledWithOptions.getters
3875 ?? options.getters
3876 ?? defaultOptions.getters
3877 ?? false;
3878 if (getters) {
3879 applyGetters(this, ret, options);
3880
3881 if (options.minimize) {
3882 ret = minimize(ret) || {};
3883 }
3884 }
3885
3886 const virtuals = options._calledWithOptions.virtuals
3887 ?? defaultOptions.virtuals
3888 ?? parentOptions?.virtuals
3889 ?? undefined;
3890
3891 if (virtuals || (getters && virtuals !== false)) {
3892 applyVirtuals(this, ret, options, options);
3893 }
3894
3895 if (options.versionKey === false && this.$__schema.options.versionKey) {
3896 delete ret[this.$__schema.options.versionKey];
3897 }
3898
3899 const transform = options._calledWithOptions.transform ?? true;
3900 let transformFunction = undefined;
3901 if (transform === true) {
3902 transformFunction = defaultOptions.transform;
3903 } else if (typeof transform === 'function') {
3904 transformFunction = transform;
3905 }
3906
3907 // In the case where a subdocument has its own transform function, we need to
3908 // check and see if the parent has a transform (options.transform) and if the
3909 // child schema has a transform (this.schema.options.toObject) In this case,
3910 // we need to adjust options.transform to be the child schema's transform and
3911 // not the parent schema's
3912 if (transform) {
3913 applySchemaTypeTransforms(this, ret);
3914 }
3915
3916 if (options.useProjection) {
3917 omitDeselectedFields(this, ret);
3918 }
3919
3920 if (typeof transformFunction === 'function') {
3921 const xformed = transformFunction(this, ret, options);
3922 if (typeof xformed !== 'undefined') {
3923 ret = xformed;
3924 }
3925 }
3926
3927 return ret;
3928};
3929
3930/*!
3931 * Internal shallow clone alternative to `$toObject()`: much faster, no options processing
3932 */
3933
3934Document.prototype.$__toObjectShallow = function $__toObjectShallow() {
3935 const ret = {};
3936 if (this._doc != null) {
3937 for (const key of Object.keys(this._doc)) {
3938 const value = this._doc[key];
3939 if (value instanceof Date) {
3940 ret[key] = new Date(value);
3941 } else if (value !== undefined) {
3942 ret[key] = value;
3943 }
3944 }
3945 }
3946
3947 return ret;
3948};
3949
3950/**
3951 * Converts this document into a plain-old JavaScript object ([POJO](https://masteringjs.io/tutorials/fundamentals/pojo)).
3952 *
3953 * Buffers are converted to instances of [mongodb.Binary](https://mongodb.github.io/node-mongodb-native/4.9/classes/Binary.html) for proper storage.
3954 *
3955 * #### Getters/Virtuals
3956 *
3957 * Example of only applying path getters
3958 *
3959 * doc.toObject({ getters: true, virtuals: false })
3960 *
3961 * Example of only applying virtual getters
3962 *
3963 * doc.toObject({ virtuals: true })
3964 *
3965 * Example of applying both path and virtual getters
3966 *
3967 * doc.toObject({ getters: true })
3968 *
3969 * To apply these options to every document of your schema by default, set your [schemas](https://mongoosejs.com/docs/api/schema.html#Schema()) `toObject` option to the same argument.
3970 *
3971 * schema.set('toObject', { virtuals: true })
3972 *
3973 * #### Transform:
3974 *
3975 * 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.
3976 *
3977 * Transform functions receive three arguments
3978 *
3979 * function (doc, ret, options) {}
3980 *
3981 * - `doc` The mongoose document which is being converted
3982 * - `ret` The plain object representation which has been converted
3983 * - `options` The options in use (either schema options or the options passed inline)
3984 *
3985 * #### Example:
3986 *
3987 * // specify the transform schema option
3988 * if (!schema.options.toObject) schema.options.toObject = {};
3989 * schema.options.toObject.transform = function (doc, ret, options) {
3990 * // remove the _id of every document before returning the result
3991 * delete ret._id;
3992 * return ret;
3993 * }
3994 *
3995 * // without the transformation in the schema
3996 * doc.toObject(); // { _id: 'anId', name: 'Wreck-it Ralph' }
3997 *
3998 * // with the transformation
3999 * doc.toObject(); // { name: 'Wreck-it Ralph' }
4000 *
4001 * With transformations we can do a lot more than remove properties. We can even return completely new customized objects:
4002 *
4003 * if (!schema.options.toObject) schema.options.toObject = {};
4004 * schema.options.toObject.transform = function (doc, ret, options) {
4005 * return { movie: ret.name }
4006 * }
4007 *
4008 * // without the transformation in the schema
4009 * doc.toObject(); // { _id: 'anId', name: 'Wreck-it Ralph' }
4010 *
4011 * // with the transformation
4012 * doc.toObject(); // { movie: 'Wreck-it Ralph' }
4013 *
4014 * _Note: if a transform function returns `undefined`, the return value will be ignored._
4015 *
4016 * Transformations may also be applied inline, overridding any transform set in the schema options.
4017 * Any transform function specified in `toObject` options also propagates to any subdocuments.
4018 *
4019 * function deleteId(doc, ret, options) {
4020 * delete ret._id;
4021 * return ret;
4022 * }
4023 *
4024 * const schema = mongoose.Schema({ name: String, docArr: [{ name: String }] });
4025 * const TestModel = mongoose.model('Test', schema);
4026 *
4027 * const doc = new TestModel({ name: 'test', docArr: [{ name: 'test' }] });
4028 *
4029 * // pass the transform as an inline option. Deletes `_id` property
4030 * // from both the top-level document and the subdocument.
4031 * const obj = doc.toObject({ transform: deleteId });
4032 * obj._id; // undefined
4033 * obj.docArr[0]._id; // undefined
4034 *
4035 * If you want to skip transformations, use `transform: false`:
4036 *
4037 * schema.options.toObject.hide = '_id';
4038 * schema.options.toObject.transform = function (doc, ret, options) {
4039 * if (options.hide) {
4040 * options.hide.split(' ').forEach(function (prop) {
4041 * delete ret[prop];
4042 * });
4043 * }
4044 * return ret;
4045 * }
4046 *
4047 * const doc = new Doc({ _id: 'anId', secret: 47, name: 'Wreck-it Ralph' });
4048 * doc.toObject(); // { secret: 47, name: 'Wreck-it Ralph' }
4049 * doc.toObject({ hide: 'secret _id', transform: false });// { _id: 'anId', secret: 47, name: 'Wreck-it Ralph' }
4050 * doc.toObject({ hide: 'secret _id', transform: true }); // { name: 'Wreck-it Ralph' }
4051 *
4052 * If you pass a transform in `toObject()` options, Mongoose will apply the transform
4053 * to [subdocuments](https://mongoosejs.com/docs/subdocs.html) in addition to the top-level document.
4054 * Similarly, `transform: false` skips transforms for all subdocuments.
4055 * Note that this behavior is different for transforms defined in the schema:
4056 * if you define a transform in `schema.options.toObject.transform`, that transform
4057 * will **not** apply to subdocuments.
4058 *
4059 * const memberSchema = new Schema({ name: String, email: String });
4060 * const groupSchema = new Schema({ members: [memberSchema], name: String, email });
4061 * const Group = mongoose.model('Group', groupSchema);
4062 *
4063 * const doc = new Group({
4064 * name: 'Engineering',
4065 * email: 'dev@mongoosejs.io',
4066 * members: [{ name: 'Val', email: 'val@mongoosejs.io' }]
4067 * });
4068 *
4069 * // Removes `email` from both top-level document **and** array elements
4070 * // { name: 'Engineering', members: [{ name: 'Val' }] }
4071 * doc.toObject({ transform: (doc, ret) => { delete ret.email; return ret; } });
4072 *
4073 * 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.
4074 *
4075 * See [schema options](https://mongoosejs.com/docs/guide.html#toObject) for some more details.
4076 *
4077 * _During save, no custom options are applied to the document before being sent to the database._
4078 *
4079 * @param {Object} [options]
4080 * @param {Boolean} [options.getters=false] if true, apply all getters, including virtuals
4081 * @param {Boolean|Object} [options.virtuals=false] if true, apply virtuals, including aliases. Use `{ getters: true, virtuals: false }` to just apply getters, not virtuals. An object of the form `{ pathsToSkip: ['someVirtual'] }` may also be used to omit specific virtuals.
4082 * @param {Boolean} [options.aliases=true] if `options.virtuals = true`, you can set `options.aliases = false` to skip applying aliases. This option is a no-op if `options.virtuals = false`.
4083 * @param {Boolean} [options.minimize=true] if true, omit any empty objects from the output
4084 * @param {Function|null} [options.transform=null] if set, mongoose will call this function to allow you to transform the returned object
4085 * @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.
4086 * @param {Boolean} [options.versionKey=true] if false, exclude the version key (`__v` by default) from the output
4087 * @param {Boolean} [options.flattenMaps=false] if true, convert Maps to POJOs. Useful if you want to `JSON.stringify()` the result of `toObject()`.
4088 * @param {Boolean} [options.flattenObjectIds=false] if true, convert any ObjectIds in the result to 24 character hex strings.
4089 * @param {Boolean} [options.useProjection=false] - If true, omits fields that are excluded in this document's projection. Unless you specified a projection, this will omit any field that has `select: false` in the schema.
4090 * @return {Object} js object (not a POJO)
4091 * @see mongodb.Binary https://mongodb.github.io/node-mongodb-native/4.9/classes/Binary.html
4092 * @api public
4093 * @memberOf Document
4094 * @instance
4095 */
4096
4097Document.prototype.toObject = function(options) {
4098 return this.$toObject(options);
4099};
4100
4101/*!
4102 * Applies virtuals properties to `json`.
4103 */
4104
4105function applyVirtuals(self, json, options, toObjectOptions) {
4106 const schema = self.$__schema;
4107 const virtuals = schema.virtuals;
4108 const paths = Object.keys(virtuals);
4109 let i = paths.length;
4110 const numPaths = i;
4111 let path;
4112 let assignPath;
4113 let cur = self._doc;
4114 let v;
4115 const aliases = typeof (toObjectOptions && toObjectOptions.aliases) === 'boolean'
4116 ? toObjectOptions.aliases
4117 : true;
4118
4119 options = options || {};
4120 let virtualsToApply = null;
4121 if (Array.isArray(options.virtuals)) {
4122 virtualsToApply = new Set(options.virtuals);
4123 } else if (options.virtuals && options.virtuals.pathsToSkip) {
4124 virtualsToApply = new Set(paths);
4125 for (let i = 0; i < options.virtuals.pathsToSkip.length; i++) {
4126 if (virtualsToApply.has(options.virtuals.pathsToSkip[i])) {
4127 virtualsToApply.delete(options.virtuals.pathsToSkip[i]);
4128 }
4129 }
4130 }
4131
4132 if (!cur) {
4133 return json;
4134 }
4135
4136 for (i = 0; i < numPaths; ++i) {
4137 path = paths[i];
4138
4139 if (virtualsToApply != null && !virtualsToApply.has(path)) {
4140 continue;
4141 }
4142
4143 // Allow skipping aliases with `toObject({ virtuals: true, aliases: false })`
4144 if (!aliases && schema.aliases.hasOwnProperty(path)) {
4145 continue;
4146 }
4147
4148 // We may be applying virtuals to a nested object, for example if calling
4149 // `doc.nestedProp.toJSON()`. If so, the path we assign to, `assignPath`,
4150 // will be a trailing substring of the `path`.
4151 assignPath = path;
4152 if (options.path != null) {
4153 if (!path.startsWith(options.path + '.')) {
4154 continue;
4155 }
4156 assignPath = path.substring(options.path.length + 1);
4157 }
4158 if (assignPath.indexOf('.') === -1 && assignPath === path) {
4159 v = virtuals[path].applyGetters(void 0, self);
4160 if (v === void 0) {
4161 continue;
4162 }
4163 v = clone(v, options);
4164 json[assignPath] = v;
4165 continue;
4166 }
4167 const parts = assignPath.split('.');
4168 v = clone(self.get(path), options);
4169 if (v === void 0) {
4170 continue;
4171 }
4172 const plen = parts.length;
4173 cur = json;
4174 for (let j = 0; j < plen - 1; ++j) {
4175 cur[parts[j]] = cur[parts[j]] || {};
4176 cur = cur[parts[j]];
4177 }
4178 cur[parts[plen - 1]] = v;
4179 }
4180
4181 return json;
4182}
4183
4184
4185/**
4186 * Applies virtuals properties to `json`.
4187 *
4188 * @param {Document} self
4189 * @param {Object} json
4190 * @param {Object} [options]
4191 * @return {Object} `json`
4192 * @api private
4193 */
4194
4195function applyGetters(self, json, options) {
4196 const schema = self.$__schema;
4197 const paths = Object.keys(schema.paths);
4198 let i = paths.length;
4199 let path;
4200 let cur = self._doc;
4201 let v;
4202
4203 if (!cur) {
4204 return json;
4205 }
4206
4207 while (i--) {
4208 path = paths[i];
4209
4210 const parts = path.split('.');
4211
4212 const plen = parts.length;
4213 const last = plen - 1;
4214 let branch = json;
4215 let part;
4216 cur = self._doc;
4217
4218 if (!self.$__isSelected(path)) {
4219 continue;
4220 }
4221
4222 for (let ii = 0; ii < plen; ++ii) {
4223 part = parts[ii];
4224 v = cur[part];
4225 // If we've reached a non-object part of the branch, continuing would
4226 // cause "Cannot create property 'foo' on string 'bar'" error.
4227 // Necessary for mongoose-intl plugin re: gh-14446
4228 if (branch != null && typeof branch !== 'object') {
4229 break;
4230 } else if (ii === last) {
4231 const val = self.$get(path);
4232 branch[part] = clone(val, options);
4233 if (Array.isArray(branch[part]) && schema.paths[path].$embeddedSchemaType) {
4234 for (let i = 0; i < branch[part].length; ++i) {
4235 branch[part][i] = schema.paths[path].$embeddedSchemaType.applyGetters(
4236 branch[part][i],
4237 self
4238 );
4239 }
4240 }
4241 } else if (v == null) {
4242 if (part in cur) {
4243 branch[part] = v;
4244 }
4245 break;
4246 } else {
4247 branch = branch[part] || (branch[part] = {});
4248 }
4249 cur = v;
4250 }
4251 }
4252
4253 return json;
4254}
4255
4256/**
4257 * Applies schema type transforms to `json`.
4258 *
4259 * @param {Document} self
4260 * @param {Object} json
4261 * @return {Object} `json`
4262 * @api private
4263 */
4264
4265function applySchemaTypeTransforms(self, json) {
4266 const schema = self.$__schema;
4267 const paths = Object.keys(schema.paths || {});
4268 const cur = self._doc;
4269
4270 if (!cur) {
4271 return json;
4272 }
4273
4274 for (const path of paths) {
4275 const schematype = schema.paths[path];
4276 if (typeof schematype.options.transform === 'function') {
4277 const val = self.$get(path);
4278 if (val === undefined) {
4279 continue;
4280 }
4281 const transformedValue = schematype.options.transform.call(self, val);
4282 throwErrorIfPromise(path, transformedValue);
4283 utils.setValue(path, transformedValue, json);
4284 } else if (schematype.$embeddedSchemaType != null &&
4285 typeof schematype.$embeddedSchemaType.options.transform === 'function') {
4286 const val = self.$get(path);
4287 if (val === undefined) {
4288 continue;
4289 }
4290 const vals = [].concat(val);
4291 const transform = schematype.$embeddedSchemaType.options.transform;
4292 for (let i = 0; i < vals.length; ++i) {
4293 const transformedValue = transform.call(self, vals[i]);
4294 vals[i] = transformedValue;
4295 throwErrorIfPromise(path, transformedValue);
4296 }
4297
4298 json[path] = vals;
4299 }
4300 }
4301
4302 return json;
4303}
4304
4305function throwErrorIfPromise(path, transformedValue) {
4306 if (isPromise(transformedValue)) {
4307 throw new Error('`transform` function must be synchronous, but the transform on path `' + path + '` returned a promise.');
4308 }
4309}
4310
4311/*!
4312 * ignore
4313 */
4314
4315function omitDeselectedFields(self, json) {
4316 const schema = self.$__schema;
4317 const paths = Object.keys(schema.paths || {});
4318 const cur = self._doc;
4319
4320 if (!cur) {
4321 return json;
4322 }
4323
4324 let selected = self.$__.selected;
4325 if (selected === void 0) {
4326 selected = {};
4327 queryhelpers.applyPaths(selected, schema);
4328 }
4329 if (selected == null || Object.keys(selected).length === 0) {
4330 return json;
4331 }
4332
4333 for (const path of paths) {
4334 if (selected[path] != null && !selected[path]) {
4335 delete json[path];
4336 }
4337 }
4338
4339 return json;
4340}
4341
4342/**
4343 * The return value of this method is used in calls to [`JSON.stringify(doc)`](https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript#the-tojson-function).
4344 *
4345 * This method accepts the same options as [Document#toObject](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject()). To apply the options to every document of your schema by default, set your [schemas](https://mongoosejs.com/docs/api/schema.html#Schema()) `toJSON` option to the same argument.
4346 *
4347 * schema.set('toJSON', { virtuals: true });
4348 *
4349 * There is one difference between `toJSON()` and `toObject()` options.
4350 * When you call `toJSON()`, the [`flattenMaps` option](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject()) defaults to `true`, because `JSON.stringify()` doesn't convert maps to objects by default.
4351 * When you call `toObject()`, the `flattenMaps` option is `false` by default.
4352 *
4353 * See [schema options](https://mongoosejs.com/docs/guide.html#toJSON) for more information on setting `toJSON` option defaults.
4354 *
4355 * @param {Object} options
4356 * @param {Boolean} [options.flattenMaps=true] if true, convert Maps to [POJOs](https://masteringjs.io/tutorials/fundamentals/pojo). Useful if you want to `JSON.stringify()` the result.
4357 * @param {Boolean} [options.flattenObjectIds=false] if true, convert any ObjectIds in the result to 24 character hex strings.
4358 * @return {Object}
4359 * @see Document#toObject https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject()
4360 * @see JSON.stringify() in JavaScript https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html
4361 * @api public
4362 * @memberOf Document
4363 * @instance
4364 */
4365
4366Document.prototype.toJSON = function(options) {
4367 return this.$toObject(options, true);
4368};
4369
4370/*!
4371 * ignore
4372 */
4373
4374Document.prototype.ownerDocument = function() {
4375 return this;
4376};
4377
4378
4379/**
4380 * If this document is a subdocument or populated document, returns the document's
4381 * parent. Returns the original document if there is no parent.
4382 *
4383 * @return {Document}
4384 * @api public
4385 * @method parent
4386 * @memberOf Document
4387 * @instance
4388 */
4389
4390Document.prototype.parent = function() {
4391 if (this.$isSubdocument || this.$__.wasPopulated) {
4392 return this.$__.parent;
4393 }
4394 return this;
4395};
4396
4397/**
4398 * Alias for [`parent()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.parent()). If this document is a subdocument or populated
4399 * document, returns the document's parent. Returns `undefined` otherwise.
4400 *
4401 * @return {Document}
4402 * @api public
4403 * @method $parent
4404 * @memberOf Document
4405 * @instance
4406 */
4407
4408Document.prototype.$parent = Document.prototype.parent;
4409
4410/**
4411 * Helper for console.log
4412 *
4413 * @return {String}
4414 * @api public
4415 * @method inspect
4416 * @memberOf Document
4417 * @instance
4418 */
4419
4420Document.prototype.inspect = function(options) {
4421 const isPOJO = utils.isPOJO(options);
4422 let opts;
4423 if (isPOJO) {
4424 opts = options;
4425 opts.minimize = false;
4426 }
4427
4428 const ret = arguments.length > 0 ? this.toObject(opts) : this.toObject();
4429
4430 if (ret == null) {
4431 // If `toObject()` returns null, `this` is still an object, so if `inspect()`
4432 // prints out null this can cause some serious confusion. See gh-7942.
4433 return 'MongooseDocument { ' + ret + ' }';
4434 }
4435
4436 return ret;
4437};
4438
4439if (inspect.custom) {
4440 // Avoid Node deprecation warning DEP0079
4441 Document.prototype[inspect.custom] = Document.prototype.inspect;
4442}
4443
4444/**
4445 * Helper for console.log
4446 *
4447 * @return {String}
4448 * @api public
4449 * @method toString
4450 * @memberOf Document
4451 * @instance
4452 */
4453
4454Document.prototype.toString = function() {
4455 const ret = this.inspect();
4456 if (typeof ret === 'string') {
4457 return ret;
4458 }
4459 return inspect(ret);
4460};
4461
4462/**
4463 * Returns true if this document is equal to another document.
4464 *
4465 * Documents are considered equal when they have matching `_id`s, unless neither
4466 * document has an `_id`, in which case this function falls back to using
4467 * `deepEqual()`.
4468 *
4469 * @param {Document} [doc] a document to compare. If falsy, will always return "false".
4470 * @return {Boolean}
4471 * @api public
4472 * @memberOf Document
4473 * @instance
4474 */
4475
4476Document.prototype.equals = function(doc) {
4477 if (!doc) {
4478 return false;
4479 }
4480
4481 const tid = this.$__getValue('_id');
4482 const docid = doc.$__ != null ? doc.$__getValue('_id') : doc;
4483 if (!tid && !docid) {
4484 return deepEqual(this, doc);
4485 }
4486 return tid && tid.equals
4487 ? tid.equals(docid)
4488 : tid === docid;
4489};
4490
4491/**
4492 * Populates paths on an existing document.
4493 *
4494 * #### Example:
4495 *
4496 * // Given a document, `populate()` lets you pull in referenced docs
4497 * await doc.populate([
4498 * 'stories',
4499 * { path: 'fans', sort: { name: -1 } }
4500 * ]);
4501 * doc.populated('stories'); // Array of ObjectIds
4502 * doc.stories[0].title; // 'Casino Royale'
4503 * doc.populated('fans'); // Array of ObjectIds
4504 *
4505 * // If the referenced doc has been deleted, `populate()` will
4506 * // remove that entry from the array.
4507 * await Story.delete({ title: 'Casino Royale' });
4508 * await doc.populate('stories'); // Empty array
4509 *
4510 * // You can also pass additional query options to `populate()`,
4511 * // like projections:
4512 * await doc.populate('fans', '-email');
4513 * doc.fans[0].email // undefined because of 2nd param `select`
4514 *
4515 * @param {String|Object|Array} path either the path to populate or an object specifying all parameters, or either an array of those
4516 * @param {Object|String} [select] Field selection for the population query
4517 * @param {Model} [model] The model you wish to use for population. If not specified, populate will look up the model by the name in the Schema's `ref` field.
4518 * @param {Object} [match] Conditions for the population query
4519 * @param {Object} [options] Options for the population query (sort, etc)
4520 * @param {String} [options.path=null] The path to populate.
4521 * @param {string|PopulateOptions} [options.populate=null] Recursively populate paths in the populated documents. See [deep populate docs](https://mongoosejs.com/docs/populate.html#deep-populate).
4522 * @param {boolean} [options.retainNullValues=false] by default, Mongoose removes null and undefined values from populated arrays. Use this option to make `populate()` retain `null` and `undefined` array entries.
4523 * @param {boolean} [options.getters=false] if true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](https://mongoosejs.com/docs/schematypes.html#schematype-options).
4524 * @param {boolean} [options.clone=false] When you do `BlogPost.find().populate('author')`, blog posts with the same author will share 1 copy of an `author` doc. Enable this option to make Mongoose clone populated docs before assigning them.
4525 * @param {Object|Function} [options.match=null] Add an additional filter to the populate query. Can be a filter object containing [MongoDB query syntax](https://www.mongodb.com/docs/manual/tutorial/query-documents/), or a function that returns a filter object.
4526 * @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
4527 * @param {Object} [options.options=null] Additional options like `limit` and `lean`.
4528 * @param {Function} [callback] Callback
4529 * @see population https://mongoosejs.com/docs/populate.html
4530 * @see Query#select https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
4531 * @see Model.populate https://mongoosejs.com/docs/api/model.html#Model.populate()
4532 * @memberOf Document
4533 * @instance
4534 * @return {Promise|null} Returns a Promise if no `callback` is given.
4535 * @api public
4536 */
4537
4538Document.prototype.populate = async function populate() {
4539 const pop = {};
4540 const args = [...arguments];
4541 if (typeof args[args.length - 1] === 'function') {
4542 throw new MongooseError('Document.prototype.populate() no longer accepts a callback');
4543 }
4544
4545 if (args.length !== 0) {
4546 // use hash to remove duplicate paths
4547 const res = utils.populate.apply(null, args);
4548 for (const populateOptions of res) {
4549 pop[populateOptions.path] = populateOptions;
4550 }
4551 }
4552
4553 const paths = utils.object.vals(pop);
4554 let topLevelModel = this.constructor;
4555 if (this.$__isNested) {
4556 topLevelModel = this.$__[scopeSymbol].constructor;
4557 const nestedPath = this.$__.nestedPath;
4558 paths.forEach(function(populateOptions) {
4559 populateOptions.path = nestedPath + '.' + populateOptions.path;
4560 });
4561 }
4562
4563 // Use `$session()` by default if the document has an associated session
4564 // See gh-6754
4565 if (this.$session() != null) {
4566 const session = this.$session();
4567 paths.forEach(path => {
4568 if (path.options == null) {
4569 path.options = { session: session };
4570 return;
4571 }
4572 if (!('session' in path.options)) {
4573 path.options.session = session;
4574 }
4575 });
4576 }
4577
4578 paths.forEach(p => {
4579 p._localModel = topLevelModel;
4580 });
4581
4582 return topLevelModel.populate(this, paths);
4583};
4584
4585/**
4586 * Gets all populated documents associated with this document.
4587 *
4588 * @api public
4589 * @return {Document[]} array of populated documents. Empty array if there are no populated documents associated with this document.
4590 * @memberOf Document
4591 * @method $getPopulatedDocs
4592 * @instance
4593 */
4594
4595Document.prototype.$getPopulatedDocs = function $getPopulatedDocs() {
4596 let keys = [];
4597 if (this.$__.populated != null) {
4598 keys = keys.concat(Object.keys(this.$__.populated));
4599 }
4600 let result = [];
4601 for (const key of keys) {
4602 const value = this.$get(key);
4603 if (Array.isArray(value)) {
4604 result = result.concat(value);
4605 } else if (value instanceof Document) {
4606 result.push(value);
4607 }
4608 }
4609 return result;
4610};
4611
4612/**
4613 * Gets _id(s) used during population of the given `path`.
4614 *
4615 * #### Example:
4616 *
4617 * const doc = await Model.findOne().populate('author');
4618 *
4619 * console.log(doc.author.name); // Dr.Seuss
4620 * console.log(doc.populated('author')); // '5144cf8050f071d979c118a7'
4621 *
4622 * If the path was not populated, returns `undefined`.
4623 *
4624 * @param {String} path
4625 * @param {Any} [val]
4626 * @param {Object} [options]
4627 * @return {Array|ObjectId|Number|Buffer|String|undefined}
4628 * @memberOf Document
4629 * @instance
4630 * @api public
4631 */
4632
4633Document.prototype.populated = function(path, val, options) {
4634 // val and options are internal
4635 if (val == null || val === true) {
4636 if (!this.$__.populated) {
4637 return undefined;
4638 }
4639 if (typeof path !== 'string') {
4640 return undefined;
4641 }
4642
4643 // Map paths can be populated with either `path.$*` or just `path`
4644 const _path = path.endsWith('.$*') ? path.replace(/\.\$\*$/, '') : path;
4645
4646 const v = this.$__.populated[_path];
4647 if (v) {
4648 return val === true ? v : v.value;
4649 }
4650 return undefined;
4651 }
4652
4653 this.$__.populated || (this.$__.populated = {});
4654 this.$__.populated[path] = { value: val, options: options };
4655
4656 // If this was a nested populate, make sure each populated doc knows
4657 // about its populated children (gh-7685)
4658 const pieces = path.split('.');
4659 for (let i = 0; i < pieces.length - 1; ++i) {
4660 const subpath = pieces.slice(0, i + 1).join('.');
4661 const subdoc = this.$get(subpath);
4662 if (subdoc != null && subdoc.$__ != null && this.$populated(subpath)) {
4663 const rest = pieces.slice(i + 1).join('.');
4664 subdoc.$populated(rest, val, options);
4665 // No need to continue because the above recursion should take care of
4666 // marking the rest of the docs as populated
4667 break;
4668 }
4669 }
4670
4671 return val;
4672};
4673
4674/**
4675 * Alias of [`.populated`](https://mongoosejs.com/docs/api/document.html#Document.prototype.populated()).
4676 *
4677 * @method $populated
4678 * @memberOf Document
4679 * @api public
4680 */
4681
4682Document.prototype.$populated = Document.prototype.populated;
4683
4684/**
4685 * Throws an error if a given path is not populated
4686 *
4687 * #### Example:
4688 *
4689 * const doc = await Model.findOne().populate('author');
4690 *
4691 * doc.$assertPopulated('author'); // does not throw
4692 * doc.$assertPopulated('other path'); // throws an error
4693 *
4694 * // Manually populate and assert in one call. The following does
4695 * // `doc.$set({ likes })` before asserting.
4696 * doc.$assertPopulated('likes', { likes });
4697 *
4698 *
4699 * @param {String|String[]} path path or array of paths to check. `$assertPopulated` throws if any of the given paths is not populated.
4700 * @param {Object} [values] optional values to `$set()`. Convenient if you want to manually populate a path and assert that the path was populated in 1 call.
4701 * @return {Document} this
4702 * @memberOf Document
4703 * @method $assertPopulated
4704 * @instance
4705 * @api public
4706 */
4707
4708Document.prototype.$assertPopulated = function $assertPopulated(path, values) {
4709 if (Array.isArray(path)) {
4710 path.forEach(p => this.$assertPopulated(p, values));
4711 return this;
4712 }
4713
4714 if (arguments.length > 1) {
4715 this.$set(values);
4716 }
4717
4718 if (!this.$populated(path)) {
4719 throw new MongooseError(`Expected path "${path}" to be populated`);
4720 }
4721
4722 return this;
4723};
4724
4725/**
4726 * Takes a populated field and returns it to its unpopulated state.
4727 *
4728 * #### Example:
4729 *
4730 * Model.findOne().populate('author').exec(function (err, doc) {
4731 * console.log(doc.author.name); // Dr.Seuss
4732 * console.log(doc.depopulate('author'));
4733 * console.log(doc.author); // '5144cf8050f071d979c118a7'
4734 * })
4735 *
4736 * If the path was not provided, then all populated fields are returned to their unpopulated state.
4737 *
4738 * @param {String|String[]} [path] Specific Path to depopulate. If unset, will depopulate all paths on the Document. Or multiple space-delimited paths.
4739 * @return {Document} this
4740 * @see Document.populate https://mongoosejs.com/docs/api/document.html#Document.prototype.populate()
4741 * @api public
4742 * @memberOf Document
4743 * @instance
4744 */
4745
4746Document.prototype.depopulate = function(path) {
4747 if (typeof path === 'string') {
4748 path = path.indexOf(' ') === -1 ? [path] : path.split(' ');
4749 }
4750
4751 let populatedIds;
4752 const virtualKeys = this.$$populatedVirtuals ? Object.keys(this.$$populatedVirtuals) : [];
4753 const populated = this.$__ && this.$__.populated || {};
4754
4755 if (arguments.length === 0) {
4756 // Depopulate all
4757 for (const virtualKey of virtualKeys) {
4758 delete this.$$populatedVirtuals[virtualKey];
4759 delete this._doc[virtualKey];
4760 delete populated[virtualKey];
4761 }
4762
4763 const keys = Object.keys(populated);
4764
4765 for (const key of keys) {
4766 populatedIds = this.$populated(key);
4767 if (!populatedIds) {
4768 continue;
4769 }
4770 delete populated[key];
4771 if (Array.isArray(populatedIds)) {
4772 const arr = utils.getValue(key, this._doc);
4773 if (arr.isMongooseArray) {
4774 const rawArray = arr.__array;
4775 for (let i = 0; i < rawArray.length; ++i) {
4776 const subdoc = rawArray[i];
4777 if (subdoc == null) {
4778 continue;
4779 }
4780 rawArray[i] = subdoc instanceof Document ? subdoc._doc._id : subdoc._id;
4781 }
4782 } else {
4783 utils.setValue(key, populatedIds, this._doc);
4784 }
4785 } else {
4786 utils.setValue(key, populatedIds, this._doc);
4787 }
4788 }
4789 return this;
4790 }
4791
4792 for (const singlePath of path) {
4793 populatedIds = this.$populated(singlePath);
4794 delete populated[singlePath];
4795
4796 if (virtualKeys.indexOf(singlePath) !== -1) {
4797 delete this.$$populatedVirtuals[singlePath];
4798 delete this._doc[singlePath];
4799 } else if (populatedIds) {
4800 if (Array.isArray(populatedIds)) {
4801 const arr = utils.getValue(singlePath, this._doc);
4802 if (arr.isMongooseArray) {
4803 const rawArray = arr.__array;
4804 for (let i = 0; i < rawArray.length; ++i) {
4805 const subdoc = rawArray[i];
4806 if (subdoc == null) {
4807 continue;
4808 }
4809 rawArray[i] = subdoc instanceof Document ? subdoc._doc._id : subdoc._id;
4810 }
4811 } else {
4812 utils.setValue(singlePath, populatedIds, this._doc);
4813 }
4814 } else {
4815 utils.setValue(singlePath, populatedIds, this._doc);
4816 }
4817 }
4818 }
4819 return this;
4820};
4821
4822
4823/**
4824 * Returns the full path to this document.
4825 *
4826 * @param {String} [path]
4827 * @return {String}
4828 * @api private
4829 * @method $__fullPath
4830 * @memberOf Document
4831 * @instance
4832 */
4833
4834Document.prototype.$__fullPath = function(path) {
4835 // overridden in SubDocuments
4836 return path || '';
4837};
4838
4839/**
4840 * Returns the changes that happened to the document
4841 * in the format that will be sent to MongoDB.
4842 *
4843 * #### Example:
4844 *
4845 * const userSchema = new Schema({
4846 * name: String,
4847 * age: Number,
4848 * country: String
4849 * });
4850 * const User = mongoose.model('User', userSchema);
4851 * const user = await User.create({
4852 * name: 'Hafez',
4853 * age: 25,
4854 * country: 'Egypt'
4855 * });
4856 *
4857 * // returns an empty object, no changes happened yet
4858 * user.getChanges(); // { }
4859 *
4860 * user.country = undefined;
4861 * user.age = 26;
4862 *
4863 * user.getChanges(); // { $set: { age: 26 }, { $unset: { country: 1 } } }
4864 *
4865 * await user.save();
4866 *
4867 * user.getChanges(); // { }
4868 *
4869 * Modifying the object that `getChanges()` returns does not affect the document's
4870 * change tracking state. Even if you `delete user.getChanges().$set`, Mongoose
4871 * will still send a `$set` to the server.
4872 *
4873 * @return {Object}
4874 * @api public
4875 * @method getChanges
4876 * @memberOf Document
4877 * @instance
4878 */
4879
4880Document.prototype.getChanges = function() {
4881 const delta = this.$__delta();
4882 const changes = delta ? delta[1] : {};
4883 return changes;
4884};
4885
4886/**
4887 * Produces a special query document of the modified properties used in updates.
4888 *
4889 * @api private
4890 * @method $__delta
4891 * @memberOf Document
4892 * @instance
4893 */
4894
4895Document.prototype.$__delta = function $__delta() {
4896 const dirty = this.$__dirty();
4897 const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
4898 if (optimisticConcurrency) {
4899 if (Array.isArray(optimisticConcurrency)) {
4900 const optCon = new Set(optimisticConcurrency);
4901 const modPaths = this.modifiedPaths();
4902 if (modPaths.find(path => optCon.has(path))) {
4903 this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
4904 }
4905 } else {
4906 this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
4907 }
4908 }
4909
4910 if (!dirty.length && VERSION_ALL !== this.$__.version) {
4911 return;
4912 }
4913 const where = {};
4914 const delta = {};
4915 const len = dirty.length;
4916 const divergent = [];
4917 let d = 0;
4918
4919 where._id = this._doc._id;
4920 // If `_id` is an object, need to depopulate, but also need to be careful
4921 // because `_id` can technically be null (see gh-6406)
4922 if ((where && where._id && where._id.$__ || null) != null) {
4923 where._id = where._id.toObject({ transform: false, depopulate: true });
4924 }
4925 for (; d < len; ++d) {
4926 const data = dirty[d];
4927 let value = data.value;
4928 const match = checkDivergentArray(this, data.path, value);
4929 if (match) {
4930 divergent.push(match);
4931 continue;
4932 }
4933
4934 const pop = this.$populated(data.path, true);
4935 if (!pop && this.$__.selected) {
4936 // If any array was selected using an $elemMatch projection, we alter the path and where clause
4937 // NOTE: MongoDB only supports projected $elemMatch on top level array.
4938 const pathSplit = data.path.split('.');
4939 const top = pathSplit[0];
4940 if (this.$__.selected[top] && this.$__.selected[top].$elemMatch) {
4941 // If the selected array entry was modified
4942 if (pathSplit.length > 1 && pathSplit[1] == 0 && typeof where[top] === 'undefined') {
4943 where[top] = this.$__.selected[top];
4944 pathSplit[1] = '$';
4945 data.path = pathSplit.join('.');
4946 }
4947 // if the selected array was modified in any other way throw an error
4948 else {
4949 divergent.push(data.path);
4950 continue;
4951 }
4952 }
4953 }
4954
4955 // If this path is set to default, and either this path or one of
4956 // its parents is excluded, don't treat this path as dirty.
4957 if (this.$isDefault(data.path) && this.$__.selected) {
4958 if (data.path.indexOf('.') === -1 && isPathExcluded(this.$__.selected, data.path)) {
4959 continue;
4960 }
4961
4962 const pathsToCheck = parentPaths(data.path);
4963 if (pathsToCheck.find(path => isPathExcluded(this.$__.isSelected, path))) {
4964 continue;
4965 }
4966 }
4967
4968 if (divergent.length) continue;
4969 if (value === undefined) {
4970 operand(this, where, delta, data, 1, '$unset');
4971 } else if (value === null) {
4972 operand(this, where, delta, data, null);
4973 } else if (utils.isMongooseArray(value) && value.$path() && value[arrayAtomicsSymbol]) {
4974 // arrays and other custom types (support plugins etc)
4975 handleAtomics(this, where, delta, data, value);
4976 } else if (value[MongooseBuffer.pathSymbol] && Buffer.isBuffer(value)) {
4977 // MongooseBuffer
4978 value = value.toObject();
4979 operand(this, where, delta, data, value);
4980 } else {
4981 if (this.$__.primitiveAtomics && this.$__.primitiveAtomics[data.path] != null) {
4982 const val = this.$__.primitiveAtomics[data.path];
4983 const op = firstKey(val);
4984 operand(this, where, delta, data, val[op], op);
4985 } else {
4986 value = clone(value, {
4987 depopulate: true,
4988 transform: false,
4989 virtuals: false,
4990 getters: false,
4991 omitUndefined: true,
4992 _isNested: true
4993 });
4994 operand(this, where, delta, data, value);
4995 }
4996 }
4997 }
4998
4999 if (divergent.length) {
5000 return new DivergentArrayError(divergent);
5001 }
5002
5003 if (this.$__.version) {
5004 this.$__version(where, delta);
5005 }
5006
5007 if (Object.keys(delta).length === 0) {
5008 return [where, null];
5009 }
5010
5011 return [where, delta];
5012};
5013
5014/**
5015 * Determine if array was populated with some form of filter and is now
5016 * being updated in a manner which could overwrite data unintentionally.
5017 *
5018 * @see https://github.com/Automattic/mongoose/issues/1334
5019 * @param {Document} doc
5020 * @param {String} path
5021 * @param {Any} array
5022 * @return {String|undefined}
5023 * @api private
5024 */
5025
5026function checkDivergentArray(doc, path, array) {
5027 // see if we populated this path
5028 const pop = doc.$populated(path, true);
5029
5030 if (!pop && doc.$__.selected) {
5031 // If any array was selected using an $elemMatch projection, we deny the update.
5032 // NOTE: MongoDB only supports projected $elemMatch on top level array.
5033 const top = path.split('.')[0];
5034 if (doc.$__.selected[top + '.$']) {
5035 return top;
5036 }
5037 }
5038
5039 if (!(pop && utils.isMongooseArray(array))) return;
5040
5041 // If the array was populated using options that prevented all
5042 // documents from being returned (match, skip, limit) or they
5043 // deselected the _id field, $pop and $set of the array are
5044 // not safe operations. If _id was deselected, we do not know
5045 // how to remove elements. $pop will pop off the _id from the end
5046 // of the array in the db which is not guaranteed to be the
5047 // same as the last element we have here. $set of the entire array
5048 // would be similarly destructive as we never received all
5049 // elements of the array and potentially would overwrite data.
5050 const check = pop.options.match ||
5051 pop.options.options && utils.object.hasOwnProperty(pop.options.options, 'limit') || // 0 is not permitted
5052 pop.options.options && pop.options.options.skip || // 0 is permitted
5053 pop.options.select && // deselected _id?
5054 (pop.options.select._id === 0 ||
5055 /\s?-_id\s?/.test(pop.options.select));
5056
5057 if (check) {
5058 const atomics = array[arrayAtomicsSymbol];
5059 if (Object.keys(atomics).length === 0 || atomics.$set || atomics.$pop) {
5060 return path;
5061 }
5062 }
5063}
5064
5065/**
5066 * Apply the operation to the delta (update) clause as
5067 * well as track versioning for our where clause.
5068 *
5069 * @param {Document} self
5070 * @param {Object} where Unused
5071 * @param {Object} delta
5072 * @param {Object} data
5073 * @param {Mixed} val
5074 * @param {String} [op]
5075 * @api private
5076 */
5077
5078function operand(self, where, delta, data, val, op) {
5079 // delta
5080 op || (op = '$set');
5081 if (!delta[op]) delta[op] = {};
5082 delta[op][data.path] = val;
5083 // disabled versioning?
5084 if (self.$__schema.options.versionKey === false) return;
5085
5086 // path excluded from versioning?
5087 if (shouldSkipVersioning(self, data.path)) return;
5088
5089 // already marked for versioning?
5090 if (VERSION_ALL === (VERSION_ALL & self.$__.version)) return;
5091
5092 if (self.$__schema.options.optimisticConcurrency) {
5093 return;
5094 }
5095
5096 switch (op) {
5097 case '$set':
5098 case '$unset':
5099 case '$pop':
5100 case '$pull':
5101 case '$pullAll':
5102 case '$push':
5103 case '$addToSet':
5104 case '$inc':
5105 break;
5106 default:
5107 // nothing to do
5108 return;
5109 }
5110
5111 // ensure updates sent with positional notation are
5112 // editing the correct array element.
5113 // only increment the version if an array position changes.
5114 // modifying elements of an array is ok if position does not change.
5115 if (op === '$push' || op === '$addToSet' || op === '$pullAll' || op === '$pull') {
5116 if (/\.\d+\.|\.\d+$/.test(data.path)) {
5117 self.$__.version = VERSION_ALL;
5118 } else {
5119 self.$__.version = VERSION_INC;
5120 }
5121 } else if (/^\$p/.test(op)) {
5122 // potentially changing array positions
5123 self.$__.version = VERSION_ALL;
5124 } else if (Array.isArray(val)) {
5125 // $set an array
5126 self.$__.version = VERSION_ALL;
5127 } else if (/\.\d+\.|\.\d+$/.test(data.path)) {
5128 // now handling $set, $unset
5129 // subpath of array
5130 self.$__.version = VERSION_WHERE;
5131 }
5132}
5133
5134/**
5135 * Compiles an update and where clause for a `val` with _atomics.
5136 *
5137 * @param {Document} self
5138 * @param {Object} where
5139 * @param {Object} delta
5140 * @param {Object} data
5141 * @param {Array} value
5142 * @api private
5143 */
5144
5145function handleAtomics(self, where, delta, data, value) {
5146 if (delta.$set && delta.$set[data.path]) {
5147 // $set has precedence over other atomics
5148 return;
5149 }
5150
5151 if (typeof value.$__getAtomics === 'function') {
5152 value.$__getAtomics().forEach(function(atomic) {
5153 const op = atomic[0];
5154 const val = atomic[1];
5155 operand(self, where, delta, data, val, op);
5156 });
5157 return;
5158 }
5159
5160 // legacy support for plugins
5161
5162 const atomics = value[arrayAtomicsSymbol];
5163 const ops = Object.keys(atomics);
5164 let i = ops.length;
5165 let val;
5166 let op;
5167
5168 if (i === 0) {
5169 // $set
5170
5171 if (utils.isMongooseObject(value)) {
5172 value = value.toObject({ depopulate: 1, _isNested: true });
5173 } else if (value.valueOf) {
5174 value = value.valueOf();
5175 }
5176
5177 return operand(self, where, delta, data, value);
5178 }
5179
5180 function iter(mem) {
5181 return utils.isMongooseObject(mem)
5182 ? mem.toObject({ depopulate: 1, _isNested: true })
5183 : mem;
5184 }
5185
5186 while (i--) {
5187 op = ops[i];
5188 val = atomics[op];
5189
5190 if (utils.isMongooseObject(val)) {
5191 val = val.toObject({ depopulate: true, transform: false, _isNested: true });
5192 } else if (Array.isArray(val)) {
5193 val = val.map(iter);
5194 } else if (val.valueOf) {
5195 val = val.valueOf();
5196 }
5197
5198 if (op === '$addToSet') {
5199 val = { $each: val };
5200 }
5201
5202 operand(self, where, delta, data, val, op);
5203 }
5204}
5205
5206/**
5207 * Determines whether versioning should be skipped for the given path
5208 *
5209 * @param {Document} self
5210 * @param {String} path
5211 * @return {Boolean} true if versioning should be skipped for the given path
5212 * @api private
5213 */
5214function shouldSkipVersioning(self, path) {
5215 const skipVersioning = self.$__schema.options.skipVersioning;
5216 if (!skipVersioning) return false;
5217
5218 // Remove any array indexes from the path
5219 path = path.replace(/\.\d+\./, '.');
5220
5221 return skipVersioning[path];
5222}
5223
5224/**
5225 * Returns a copy of this document with a deep clone of `_doc` and `$__`.
5226 *
5227 * @return {Document} a copy of this document
5228 * @api public
5229 * @method $clone
5230 * @memberOf Document
5231 * @instance
5232 */
5233
5234Document.prototype.$clone = function() {
5235 const Model = this.constructor;
5236 const clonedDoc = new Model();
5237 clonedDoc.$isNew = this.$isNew;
5238 if (this._doc) {
5239 clonedDoc._doc = clone(this._doc, { retainDocuments: true });
5240 }
5241 if (this.$__) {
5242 const Cache = this.$__.constructor;
5243 const clonedCache = new Cache();
5244 for (const key of Object.getOwnPropertyNames(this.$__)) {
5245 if (key === 'activePaths') {
5246 continue;
5247 }
5248 clonedCache[key] = clone(this.$__[key]);
5249 }
5250 Object.assign(clonedCache.activePaths, clone({ ...this.$__.activePaths }));
5251 clonedDoc.$__ = clonedCache;
5252 }
5253 return clonedDoc;
5254};
5255
5256/**
5257 * Creates a snapshot of this document's internal change tracking state. You can later
5258 * reset this document's change tracking state using `$restoreModifiedPathsSnapshot()`.
5259 *
5260 * #### Example:
5261 *
5262 * const doc = await TestModel.findOne();
5263 * const snapshot = doc.$createModifiedPathsSnapshot();
5264 *
5265 * @return {ModifiedPathsSnapshot} a copy of this document's internal change tracking state
5266 * @api public
5267 * @method $createModifiedPathsSnapshot
5268 * @memberOf Document
5269 * @instance
5270 */
5271
5272Document.prototype.$createModifiedPathsSnapshot = function $createModifiedPathsSnapshot() {
5273 const subdocSnapshot = new WeakMap();
5274 if (!this.$isSubdocument) {
5275 const subdocs = this.$getAllSubdocs();
5276 for (const child of subdocs) {
5277 subdocSnapshot.set(child, child.$__.activePaths.clone());
5278 }
5279 }
5280
5281 return new ModifiedPathsSnapshot(
5282 subdocSnapshot,
5283 this.$__.activePaths.clone(),
5284 this.$__.version
5285 );
5286};
5287
5288/**
5289 * Restore this document's change tracking state to the given snapshot.
5290 * Note that `$restoreModifiedPathsSnapshot()` does **not** modify the document's
5291 * properties, just resets the change tracking state.
5292 *
5293 * This method is especially useful when writing [custom transaction wrappers](https://github.com/Automattic/mongoose/issues/14268#issuecomment-2100505554) that need to restore change tracking when aborting a transaction.
5294 *
5295 * #### Example:
5296 *
5297 * const doc = await TestModel.findOne();
5298 * const snapshot = doc.$createModifiedPathsSnapshot();
5299 *
5300 * doc.name = 'test';
5301 * doc.$restoreModifiedPathsSnapshot(snapshot);
5302 * doc.$isModified('name'); // false because `name` was not modified when snapshot was taken
5303 * doc.name; // 'test', `$restoreModifiedPathsSnapshot()` does **not** modify the document's data, only change tracking
5304 *
5305 * @param {ModifiedPathsSnapshot} snapshot of the document's internal change tracking state snapshot to restore
5306 * @api public
5307 * @method $restoreModifiedPathsSnapshot
5308 * @return {Document} this
5309 * @memberOf Document
5310 * @instance
5311 */
5312
5313Document.prototype.$restoreModifiedPathsSnapshot = function $restoreModifiedPathsSnapshot(snapshot) {
5314 this.$__.activePaths = snapshot.activePaths.clone();
5315 this.$__.version = snapshot.version;
5316 if (!this.$isSubdocument) {
5317 const subdocs = this.$getAllSubdocs();
5318 for (const child of subdocs) {
5319 if (snapshot.subdocSnapshot.has(child)) {
5320 child.$__.activePaths = snapshot.subdocSnapshot.get(child);
5321 }
5322 }
5323 }
5324
5325 return this;
5326};
5327
5328/**
5329 * Clear the document's modified paths.
5330 *
5331 * #### Example:
5332 *
5333 * const doc = await TestModel.findOne();
5334 *
5335 * doc.name = 'test';
5336 * doc.$isModified('name'); // true
5337 *
5338 * doc.$clearModifiedPaths();
5339 * doc.name; // 'test', `$clearModifiedPaths()` does **not** modify the document's data, only change tracking
5340 *
5341 * @api public
5342 * @return {Document} this
5343 * @method $clearModifiedPaths
5344 * @memberOf Document
5345 * @instance
5346 */
5347
5348Document.prototype.$clearModifiedPaths = function $clearModifiedPaths() {
5349 this.$__.activePaths.clear('modify');
5350 this.$__.activePaths.clear('init');
5351 this.$__.version = 0;
5352 if (!this.$isSubdocument) {
5353 const subdocs = this.$getAllSubdocs();
5354 for (const child of subdocs) {
5355 child.$clearModifiedPaths();
5356 }
5357 }
5358
5359 return this;
5360};
5361
5362/*!
5363 * Check if the given document only has primitive values
5364 */
5365
5366Document.prototype.$__hasOnlyPrimitiveValues = function $__hasOnlyPrimitiveValues() {
5367 return !this.$__.populated && !this.$__.wasPopulated && (this._doc == null || Object.values(this._doc).every(v => {
5368 return v == null
5369 || typeof v !== 'object'
5370 || (utils.isNativeObject(v) && !Array.isArray(v))
5371 || isBsonType(v, 'ObjectId')
5372 || isBsonType(v, 'Decimal128');
5373 }));
5374};
5375
5376/*!
5377 * Module exports.
5378 */
5379
5380Document.VERSION_WHERE = VERSION_WHERE;
5381Document.VERSION_INC = VERSION_INC;
5382Document.VERSION_ALL = VERSION_ALL;
5383Document.ValidationError = ValidationError;
5384module.exports = exports = Document;