UNPKG

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