UNPKG

mongoose

Version:
1,658 lines (1,453 loc) 165 kB
'use strict'; /*! * Module dependencies. */ const DivergentArrayError = require('./error/divergentArray'); const EventEmitter = require('events').EventEmitter; const InternalCache = require('./internal'); const MongooseBuffer = require('./types/buffer'); const MongooseError = require('./error/index'); const MixedSchema = require('./schema/mixed'); const ModifiedPathsSnapshot = require('./modifiedPathsSnapshot'); const ObjectExpectedError = require('./error/objectExpected'); const ObjectParameterError = require('./error/objectParameter'); const ParallelValidateError = require('./error/parallelValidate'); const Schema = require('./schema'); const StrictModeError = require('./error/strict'); const ValidationError = require('./error/validation'); const ValidatorError = require('./error/validator'); const $__hasIncludedChildren = require('./helpers/projection/hasIncludedChildren'); const applyDefaults = require('./helpers/document/applyDefaults'); const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths'); const clone = require('./helpers/clone'); const compile = require('./helpers/document/compile').compile; const defineKey = require('./helpers/document/compile').defineKey; const firstKey = require('./helpers/firstKey'); const flatten = require('./helpers/common').flatten; const getEmbeddedDiscriminatorPath = require('./helpers/document/getEmbeddedDiscriminatorPath'); const getKeysInSchemaOrder = require('./helpers/schema/getKeysInSchemaOrder'); const getSubdocumentStrictValue = require('./helpers/schema/getSubdocumentStrictValue'); const handleSpreadDoc = require('./helpers/document/handleSpreadDoc'); const immediate = require('./helpers/immediate'); const isBsonType = require('./helpers/isBsonType'); const isDefiningProjection = require('./helpers/projection/isDefiningProjection'); const isExclusive = require('./helpers/projection/isExclusive'); const isPathExcluded = require('./helpers/projection/isPathExcluded'); const inspect = require('util').inspect; const internalToObjectOptions = require('./options').internalToObjectOptions; const markArraySubdocsPopulated = require('./helpers/populate/markArraySubdocsPopulated'); const minimize = require('./helpers/minimize'); const mpath = require('mpath'); const parentPaths = require('./helpers/path/parentPaths'); const queryhelpers = require('./queryHelpers'); const utils = require('./utils'); const isPromise = require('./helpers/isPromise'); const deepEqual = utils.deepEqual; const isMongooseObject = utils.isMongooseObject; const arrayAtomicsBackupSymbol = require('./helpers/symbols').arrayAtomicsBackupSymbol; const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol; const documentArrayParent = require('./helpers/symbols').documentArrayParent; const documentIsModified = require('./helpers/symbols').documentIsModified; const documentModifiedPaths = require('./helpers/symbols').documentModifiedPaths; const documentSchemaSymbol = require('./helpers/symbols').documentSchemaSymbol; const getSymbol = require('./helpers/symbols').getSymbol; const modelSymbol = require('./helpers/symbols').modelSymbol; const populateModelSymbol = require('./helpers/symbols').populateModelSymbol; const scopeSymbol = require('./helpers/symbols').scopeSymbol; const schemaMixedSymbol = require('./schema/symbols').schemaMixedSymbol; const getDeepestSubdocumentForPath = require('./helpers/document/getDeepestSubdocumentForPath'); const sessionNewDocuments = require('./helpers/symbols').sessionNewDocuments; let DocumentArray; let MongooseArray; let Embedded; const specialProperties = utils.specialProperties; const VERSION_WHERE = 1; const VERSION_INC = 2; const VERSION_ALL = VERSION_WHERE | VERSION_INC; /** * The core Mongoose document constructor. You should not call this directly, * the Mongoose [Model constructor](./api/model.html#Model) calls this for you. * * @param {Object} obj the values to set * @param {Object} [fields] optional object containing the fields which were selected in the query returning this document and any populated paths data * @param {Object} [options] various configuration options for the document * @param {Boolean} [options.defaults=true] if `false`, skip applying default values to this document. * @inherits NodeJS EventEmitter https://nodejs.org/api/events.html#class-eventemitter * @event `init`: Emitted on a document after it has been retrieved from the db and fully hydrated by Mongoose. * @event `save`: Emitted when the document is successfully saved * @api private */ function Document(obj, fields, skipId, options) { if (typeof skipId === 'object' && skipId != null) { options = skipId; skipId = options.skipId; } options = Object.assign({}, options); // Support `browserDocument.js` syntax if (this.$__schema == null) { const _schema = utils.isObject(fields) && !fields.instanceOfSchema ? new Schema(fields) : fields; this.$__setSchema(_schema); fields = skipId; skipId = options; options = arguments[4] || {}; } this.$__ = new InternalCache(); // Avoid setting `isNew` to `true`, because it is `true` by default if (options.isNew != null && options.isNew !== true) { this.$isNew = options.isNew; } if (options.priorDoc != null) { this.$__.priorDoc = options.priorDoc; } if (skipId) { this.$__.skipId = skipId; } if (obj != null && typeof obj !== 'object') { throw new ObjectParameterError(obj, 'obj', 'Document'); } let defaults = true; if (options.defaults !== undefined) { this.$__.defaults = options.defaults; defaults = options.defaults; } const schema = this.$__schema; if (typeof fields === 'boolean' || fields === 'throw') { if (fields !== true) { this.$__.strictMode = fields; } fields = undefined; } else if (schema.options.strict !== true) { this.$__.strictMode = schema.options.strict; } const requiredPaths = schema.requiredPaths(true); for (const path of requiredPaths) { this.$__.activePaths.require(path); } let exclude = null; // determine if this doc is a result of a query with // excluded fields if (utils.isPOJO(fields) && Object.keys(fields).length > 0) { exclude = isExclusive(fields); this.$__.selected = fields; this.$__.exclude = exclude; } const hasIncludedChildren = exclude === false && fields ? $__hasIncludedChildren(fields) : null; if (this._doc == null) { this.$__buildDoc(obj, fields, skipId, exclude, hasIncludedChildren, false); // By default, defaults get applied **before** setting initial values // Re: gh-6155 if (defaults) { applyDefaults(this, fields, exclude, hasIncludedChildren, true, null, { skipParentChangeTracking: true }); } } if (obj) { // Skip set hooks if (this.$__original_set) { this.$__original_set(obj, undefined, true, options); } else { this.$set(obj, undefined, true, options); } if (obj instanceof Document) { this.$isNew = obj.$isNew; } } // Function defaults get applied **after** setting initial values so they // see the full doc rather than an empty one, unless they opt out. // Re: gh-3781, gh-6155 if (options.willInit && defaults) { if (options.skipDefaults) { this.$__.skipDefaults = options.skipDefaults; } } else if (defaults) { applyDefaults(this, fields, exclude, hasIncludedChildren, false, options.skipDefaults); } if (!this.$__.strictMode && obj) { const _this = this; const keys = Object.keys(this._doc); keys.forEach(function(key) { // Avoid methods, virtuals, existing fields, and `$` keys. The latter is to avoid overwriting // Mongoose internals. if (!(key in schema.tree) && !(key in schema.methods) && !(key in schema.virtuals) && !key.startsWith('$')) { defineKey({ prop: key, subprops: null, prototype: _this }); } }); } applyQueue(this); } Document.prototype.$isMongooseDocumentPrototype = true; /** * Boolean flag specifying if the document is new. If you create a document * using `new`, this document will be considered "new". `$isNew` is how * Mongoose determines whether `save()` should use `insertOne()` to create * a new document or `updateOne()` to update an existing document. * * #### Example: * * const user = new User({ name: 'John Smith' }); * user.$isNew; // true * * await user.save(); // Sends an `insertOne` to MongoDB * * On the other hand, if you load an existing document from the database * using `findOne()` or another [query operation](https://mongoosejs.com/docs/queries.html), * `$isNew` will be false. * * #### Example: * * const user = await User.findOne({ name: 'John Smith' }); * user.$isNew; // false * * Mongoose sets `$isNew` to `false` immediately after `save()` succeeds. * That means Mongoose sets `$isNew` to false **before** `post('save')` hooks run. * In `post('save')` hooks, `$isNew` will be `false` if `save()` succeeded. * * #### Example: * * userSchema.post('save', function() { * this.$isNew; // false * }); * await User.create({ name: 'John Smith' }); * * For subdocuments, `$isNew` is true if either the parent has `$isNew` set, * or if you create a new subdocument. * * #### Example: * * // Assume `Group` has a document array `users` * const group = await Group.findOne(); * group.users[0].$isNew; // false * * group.users.push({ name: 'John Smith' }); * group.users[1].$isNew; // true * * @api public * @property $isNew * @memberOf Document * @instance */ Object.defineProperty(Document.prototype, 'isNew', { get: function() { return this.$isNew; }, set: function(value) { this.$isNew = value; } }); /** * Hash containing current validation errors. * * @api public * @property errors * @memberOf Document * @instance */ Object.defineProperty(Document.prototype, 'errors', { get: function() { return this.$errors; }, set: function(value) { this.$errors = value; } }); /*! * ignore */ Document.prototype.$isNew = true; /*! * Document exposes the NodeJS event emitter API, so you can use * `on`, `once`, etc. */ utils.each( ['on', 'once', 'emit', 'listeners', 'removeListener', 'setMaxListeners', 'removeAllListeners', 'addListener'], function(emitterFn) { Document.prototype[emitterFn] = function() { // Delay creating emitter until necessary because emitters take up a lot of memory, // especially for subdocuments. if (!this.$__.emitter) { if (emitterFn === 'emit') { return; } this.$__.emitter = new EventEmitter(); this.$__.emitter.setMaxListeners(0); } return this.$__.emitter[emitterFn].apply(this.$__.emitter, arguments); }; Document.prototype[`$${emitterFn}`] = Document.prototype[emitterFn]; }); Document.prototype.constructor = Document; for (const i in EventEmitter.prototype) { Document[i] = EventEmitter.prototype[i]; } /** * The document's internal schema. * * @api private * @property schema * @memberOf Document * @instance */ Document.prototype.$__schema; /** * The document's schema. * * @api public * @property schema * @memberOf Document * @instance */ Document.prototype.schema; /** * Empty object that you can use for storing properties on the document. This * is handy for passing data to middleware without conflicting with Mongoose * internals. * * #### Example: * * schema.pre('save', function() { * // Mongoose will set `isNew` to `false` if `save()` succeeds * this.$locals.wasNew = this.isNew; * }); * * schema.post('save', function() { * // Prints true if `isNew` was set before `save()` * console.log(this.$locals.wasNew); * }); * * @api public * @property $locals * @memberOf Document * @instance */ Object.defineProperty(Document.prototype, '$locals', { configurable: false, enumerable: false, get: function() { if (this.$__.locals == null) { this.$__.locals = {}; } return this.$__.locals; }, set: function(v) { this.$__.locals = v; } }); /** * Legacy alias for `$isNew`. * * @api public * @property isNew * @memberOf Document * @see $isNew https://mongoosejs.com/docs/api/document.html#Document.prototype.$isNew * @instance */ Document.prototype.isNew; /** * Set this property to add additional query filters when Mongoose saves this document and `isNew` is false. * * #### Example: * * // Make sure `save()` never updates a soft deleted document. * schema.pre('save', function() { * this.$where = { isDeleted: false }; * }); * * @api public * @property $where * @memberOf Document * @instance */ Object.defineProperty(Document.prototype, '$where', { configurable: false, enumerable: false, writable: true }); /** * The string version of this documents _id. * * #### Note: * * This getter exists on all documents by default. The getter can be disabled by setting the `id` [option](https://mongoosejs.com/docs/guide.html#id) of its `Schema` to false at construction time. * * new Schema({ name: String }, { id: false }); * * @api public * @see Schema options https://mongoosejs.com/docs/guide.html#options * @property id * @memberOf Document * @instance */ Document.prototype.id; /** * Hash containing current validation $errors. * * @api public * @property $errors * @memberOf Document * @instance */ Document.prototype.$errors; /** * A string containing the current operation that Mongoose is executing * on this document. May be `null`, `'save'`, `'validate'`, or `'remove'`. * * #### Example: * * const doc = new Model({ name: 'test' }); * doc.$op; // null * * const promise = doc.save(); * doc.$op; // 'save' * * await promise; * doc.$op; // null * * @api public * @property $op * @memberOf Document * @instance */ Object.defineProperty(Document.prototype, '$op', { get: function() { return this.$__.op || null; }, set: function(value) { this.$__.op = value; } }); /*! * ignore */ function $applyDefaultsToNested(val, path, doc) { if (val == null) { return; } const paths = Object.keys(doc.$__schema.paths); const plen = paths.length; const pathPieces = path.indexOf('.') === -1 ? [path] : path.split('.'); for (let i = 0; i < plen; ++i) { let curPath = ''; const p = paths[i]; if (!p.startsWith(path + '.')) { continue; } const type = doc.$__schema.paths[p]; const pieces = type.splitPath().slice(pathPieces.length); const len = pieces.length; if (type.defaultValue === void 0) { continue; } let cur = val; for (let j = 0; j < len; ++j) { if (cur == null) { break; } const piece = pieces[j]; if (j === len - 1) { if (cur[piece] !== void 0) { break; } try { const def = type.getDefault(doc, false); if (def !== void 0) { cur[piece] = def; } } catch (err) { doc.invalidate(path + '.' + curPath, err); break; } break; } curPath += (!curPath.length ? '' : '.') + piece; cur[piece] = cur[piece] || {}; cur = cur[piece]; } } } /** * Builds the default doc structure * * @param {Object} obj * @param {Object} [fields] * @param {Boolean} [skipId] * @param {Boolean} [exclude] * @param {Object} [hasIncludedChildren] * @api private * @method $__buildDoc * @memberOf Document * @instance */ Document.prototype.$__buildDoc = function(obj, fields, skipId, exclude, hasIncludedChildren) { const doc = {}; const paths = Object.keys(this.$__schema.paths). // Don't build up any paths that are underneath a map, we don't know // what the keys will be filter(p => !p.includes('$*')); const plen = paths.length; let ii = 0; for (; ii < plen; ++ii) { const p = paths[ii]; if (p === '_id') { if (skipId) { continue; } if (obj && '_id' in obj) { continue; } } const path = this.$__schema.paths[p].splitPath(); const len = path.length; const last = len - 1; let curPath = ''; let doc_ = doc; let included = false; for (let i = 0; i < len; ++i) { const piece = path[i]; if (!curPath.length) { curPath = piece; } else { curPath += '.' + piece; } // support excluding intermediary levels if (exclude === true) { if (curPath in fields) { break; } } else if (exclude === false && fields && !included) { if (curPath in fields) { included = true; } else if (!hasIncludedChildren[curPath]) { break; } } if (i < last) { doc_ = doc_[piece] || (doc_[piece] = {}); } } } this._doc = doc; }; /*! * Converts to POJO when you use the document for querying */ Document.prototype.toBSON = function() { return this.toObject(internalToObjectOptions); }; /** * Hydrates this document with the data in `doc`. Does not run setters or mark any paths modified. * * Called internally after a document is returned from MongoDB. Normally, * you do **not** need to call this function on your own. * * This function triggers `init` [middleware](https://mongoosejs.com/docs/middleware.html). * Note that `init` hooks are [synchronous](https://mongoosejs.com/docs/middleware.html#synchronous). * * @param {Object} doc raw document returned by mongo * @param {Object} [opts] * @param {Boolean} [opts.hydratedPopulatedDocs=false] If true, hydrate and mark as populated any paths that are populated in the raw document * @param {Function} [fn] * @api public * @memberOf Document * @instance */ Document.prototype.init = function(doc, opts, fn) { if (typeof opts === 'function') { fn = opts; opts = null; } this.$__init(doc, opts); if (fn) { fn(null, this); } return this; }; /** * Alias for [`.init`](https://mongoosejs.com/docs/api/document.html#Document.prototype.init()) * * @api public */ Document.prototype.$init = function() { return this.constructor.prototype.init.apply(this, arguments); }; /** * Internal "init" function * * @param {Document} doc * @param {Object} [opts] * @returns {Document} this * @api private */ Document.prototype.$__init = function(doc, opts) { this.$isNew = false; opts = opts || {}; // handle docs with populated paths // If doc._id is not null or undefined if (doc._id != null && opts.populated && opts.populated.length) { const id = String(doc._id); for (const item of opts.populated) { if (item.isVirtual) { this.$populated(item.path, utils.getValue(item.path, doc), item); } else { this.$populated(item.path, item._docs[id], item); } if (item._childDocs == null) { continue; } for (const child of item._childDocs) { if (child == null || child.$__ == null) { continue; } child.$__.parent = this; } item._childDocs = []; } } init(this, doc, this._doc, opts); markArraySubdocsPopulated(this, opts.populated); this.$emit('init', this); this.constructor.emit('init', this); const hasIncludedChildren = this.$__.exclude === false && this.$__.selected ? $__hasIncludedChildren(this.$__.selected) : null; applyDefaults(this, this.$__.selected, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults); return this; }; /** * Init helper. * * @param {Object} self document instance * @param {Object} obj raw mongodb doc * @param {Object} doc object we are initializing * @param {Object} [opts] Optional Options * @param {Boolean} [opts.setters] Call `applySetters` instead of `cast` * @param {String} [prefix] Prefix to add to each path * @api private */ function init(self, obj, doc, opts, prefix) { prefix = prefix || ''; if (obj.$__ != null) { obj = obj._doc; } const keys = Object.keys(obj); const len = keys.length; let schemaType; let path; let i; const strict = self.$__.strictMode; const docSchema = self.$__schema; for (let index = 0; index < len; ++index) { i = keys[index]; // avoid prototype pollution if (specialProperties.has(i)) { continue; } path = prefix ? prefix + i : i; schemaType = docSchema.path(path); // Should still work if not a model-level discriminator, but should not be // necessary. This is *only* to catch the case where we queried using the // base model and the discriminated model has a projection if (docSchema.$isRootDiscriminator && !self.$__isSelected(path)) { continue; } const value = obj[i]; if (!schemaType && utils.isPOJO(value)) { // assume nested object if (!doc[i]) { doc[i] = {}; if (!strict && !(i in docSchema.tree) && !(i in docSchema.methods) && !(i in docSchema.virtuals)) { self[i] = doc[i]; } } init(self, value, doc[i], opts, path + '.'); } else if (!schemaType) { doc[i] = value; if (!strict && !prefix) { self[i] = value; } } else { // Retain order when overwriting defaults if (doc.hasOwnProperty(i) && value !== void 0 && !opts.hydratedPopulatedDocs) { delete doc[i]; } if (value === null) { doc[i] = schemaType._castNullish(null); } else if (value !== undefined) { const wasPopulated = value.$__ == null ? null : value.$__.wasPopulated; if (schemaType && !wasPopulated && !opts.hydratedPopulatedDocs) { try { if (opts && opts.setters) { // Call applySetters with `init = false` because otherwise setters are a noop const overrideInit = false; doc[i] = schemaType.applySetters(value, self, overrideInit); } else { doc[i] = schemaType.cast(value, self, true); } } catch (e) { self.invalidate(e.path, new ValidatorError({ path: e.path, message: e.message, type: 'cast', value: e.value, reason: e })); } } else if (schemaType && opts.hydratedPopulatedDocs) { doc[i] = schemaType.cast(value, self, true, undefined, { hydratedPopulatedDocs: true }); if (doc[i] && doc[i].$__ && doc[i].$__.wasPopulated) { self.$populated(path, doc[i].$__.wasPopulated.value, doc[i].$__.wasPopulated.options); } else if (Array.isArray(doc[i]) && doc[i].length && doc[i][0]?.$__?.wasPopulated) { self.$populated(path, doc[i].map(populatedDoc => populatedDoc?.$__?.wasPopulated?.value).filter(val => val != null), doc[i][0].$__.wasPopulated.options); } } else { doc[i] = value; } } // mark as hydrated if (!self.$isModified(path)) { self.$__.activePaths.init(path); } } } } /** * Sends an updateOne command with this document `_id` as the query selector. * * #### Example: * * weirdCar.updateOne({$inc: {wheels:1}}, { w: 1 }); * * #### Valid options: * * - same as in [Model.updateOne](https://mongoosejs.com/docs/api/model.html#Model.updateOne) * * @see Model.updateOne https://mongoosejs.com/docs/api/model.html#Model.updateOne * @param {Object} doc * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean()) and the [Mongoose lean tutorial](https://mongoosejs.com/docs/tutorials/lean.html). * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. * @return {Query} * @api public * @memberOf Document * @instance */ Document.prototype.updateOne = function updateOne(doc, options, callback) { const query = this.constructor.updateOne({ _id: this._doc._id }, doc, options); const self = this; query.pre(function queryPreUpdateOne(cb) { self.constructor._middleware.execPre('updateOne', self, [self], cb); }); query.post(function queryPostUpdateOne(cb) { self.constructor._middleware.execPost('updateOne', self, [self], {}, cb); }); if (this.$session() != null) { if (!('session' in query.options)) { query.options.session = this.$session(); } } if (callback != null) { return query.exec(callback); } return query; }; /** * Sends a replaceOne command with this document `_id` as the query selector. * * #### Valid options: * * - same as in [Model.replaceOne](https://mongoosejs.com/docs/api/model.html#Model.replaceOne()) * * @see Model.replaceOne https://mongoosejs.com/docs/api/model.html#Model.replaceOne() * @param {Object} doc * @param {Object} [options] * @param {Function} [callback] * @return {Query} * @api public * @memberOf Document * @instance */ Document.prototype.replaceOne = function replaceOne() { const args = [...arguments]; args.unshift({ _id: this._doc._id }); return this.constructor.replaceOne.apply(this.constructor, args); }; /** * Getter/setter around the session associated with this document. Used to * automatically set `session` if you `save()` a doc that you got from a * query with an associated session. * * #### Example: * * const session = MyModel.startSession(); * const doc = await MyModel.findOne().session(session); * doc.$session() === session; // true * doc.$session(null); * doc.$session() === null; // true * * If this is a top-level document, setting the session propagates to all child * docs. * * @param {ClientSession} [session] overwrite the current session * @return {ClientSession} * @method $session * @api public * @memberOf Document */ Document.prototype.$session = function $session(session) { if (arguments.length === 0) { if (this.$__.session != null && this.$__.session.hasEnded) { this.$__.session = null; return null; } return this.$__.session; } if (session != null && session.hasEnded) { throw new MongooseError('Cannot set a document\'s session to a session that has ended. Make sure you haven\'t ' + 'called `endSession()` on the session you are passing to `$session()`.'); } if (session == null && this.$__.session == null) { return; } this.$__.session = session; if (!this.$isSubdocument) { const subdocs = this.$getAllSubdocs(); for (const child of subdocs) { child.$session(session); } } return session; }; /** * Getter/setter around whether this document will apply timestamps by * default when using `save()` and `bulkSave()`. * * #### Example: * * const TestModel = mongoose.model('Test', new Schema({ name: String }, { timestamps: true })); * const doc = new TestModel({ name: 'John Smith' }); * * doc.$timestamps(); // true * * doc.$timestamps(false); * await doc.save(); // Does **not** apply timestamps * * @param {Boolean} [value] overwrite the current session * @return {Document|boolean|undefined} When used as a getter (no argument), a boolean will be returned indicating the timestamps option state or if unset "undefined" will be used, otherwise will return "this" * @method $timestamps * @api public * @memberOf Document */ Document.prototype.$timestamps = function $timestamps(value) { if (arguments.length === 0) { if (this.$__.timestamps != null) { return this.$__.timestamps; } if (this.$__schema) { return this.$__schema.options.timestamps; } return undefined; } const currentValue = this.$timestamps(); if (value !== currentValue) { this.$__.timestamps = value; } return this; }; /** * Overwrite all values in this document with the values of `obj`, except * for immutable properties. Behaves similarly to `set()`, except for it * unsets all properties that aren't in `obj`. * * @param {Object} obj the object to overwrite this document with * @method overwrite * @memberOf Document * @instance * @api public * @return {Document} this */ Document.prototype.overwrite = function overwrite(obj) { const keys = Array.from(new Set(Object.keys(this._doc).concat(Object.keys(obj)))); for (const key of keys) { if (key === '_id') { continue; } // Explicitly skip version key if (this.$__schema.options.versionKey && key === this.$__schema.options.versionKey) { continue; } if (this.$__schema.options.discriminatorKey && key === this.$__schema.options.discriminatorKey) { continue; } this.$set(key, obj[key]); } return this; }; /** * Alias for `set()`, used internally to avoid conflicts * * @param {String|Object} path path or object of key/vals to set * @param {Any} val the value to set * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for "on-the-fly" attributes * @param {Object} [options] optionally specify options that modify the behavior of the set * @param {Boolean} [options.merge=false] if true, setting a [nested path](https://mongoosejs.com/docs/subdocs.html#subdocuments-versus-nested-paths) will merge existing values rather than overwrite the whole object. So `doc.set('nested', { a: 1, b: 2 })` becomes `doc.set('nested.a', 1); doc.set('nested.b', 2);` * @return {Document} this * @method $set * @memberOf Document * @instance * @api public */ Document.prototype.$set = function $set(path, val, type, options) { if (utils.isPOJO(type)) { options = type; type = undefined; } const merge = options && options.merge; const adhoc = type && type !== true; const constructing = type === true; let adhocs; let keys; let i = 0; let pathtype; let key; let prefix; const userSpecifiedStrict = options && 'strict' in options; let strict = userSpecifiedStrict ? options.strict : this.$__.strictMode; if (adhoc) { adhocs = this.$__.adhocPaths || (this.$__.adhocPaths = {}); adhocs[path] = this.$__schema.interpretAsType(path, type, this.$__schema.options); } if (path == null) { [path, val] = [val, path]; } else if (typeof path !== 'string') { // new Document({ key: val }) if (path instanceof Document) { if (path.$__isNested) { path = path.toObject(); } else { // This ternary is to support gh-7898 (copying virtuals if same schema) // while not breaking gh-10819, which for some reason breaks if we use toObject() path = path.$__schema === this.$__schema ? applyVirtuals(path, { ...path._doc }) : path._doc; } } if (path == null) { [path, val] = [val, path]; } prefix = val ? val + '.' : ''; keys = getKeysInSchemaOrder(this.$__schema, path); const len = keys.length; // `_skipMinimizeTopLevel` is because we may have deleted the top-level // nested key to ensure key order. const _skipMinimizeTopLevel = options && options._skipMinimizeTopLevel || false; if (len === 0 && _skipMinimizeTopLevel) { delete options._skipMinimizeTopLevel; if (val) { this.$set(val, {}); } return this; } options = Object.assign({}, options, { _skipMinimizeTopLevel: false }); for (let i = 0; i < len; ++i) { key = keys[i]; const pathName = prefix ? prefix + key : key; pathtype = this.$__schema.pathType(pathName); const valForKey = path[key]; // On initial set, delete any nested keys if we're going to overwrite // them to ensure we keep the user's key order. if (type === true && !prefix && valForKey != null && pathtype === 'nested' && this._doc[key] != null) { delete this._doc[key]; } if (utils.isNonBuiltinObject(valForKey) && pathtype === 'nested') { this.$set(pathName, valForKey, constructing, Object.assign({}, options, { _skipMarkModified: true })); $applyDefaultsToNested(this.$get(pathName), pathName, this); continue; } else if (strict) { // Don't overwrite defaults with undefined keys (gh-3981) (gh-9039) if (constructing && valForKey === void 0 && this.$get(pathName) !== void 0) { continue; } if (pathtype === 'adhocOrUndefined') { pathtype = getEmbeddedDiscriminatorPath(this, pathName, { typeOnly: true }); } if (pathtype === 'real' || pathtype === 'virtual') { this.$set(pathName, valForKey, constructing, options); } else if (pathtype === 'nested' && valForKey instanceof Document) { this.$set(pathName, valForKey.toObject({ transform: false }), constructing, options); } else if (strict === 'throw') { if (pathtype === 'nested') { throw new ObjectExpectedError(key, valForKey); } else { throw new StrictModeError(key); } } else if (pathtype === 'nested' && valForKey == null) { this.$set(pathName, valForKey, constructing, options); } } else { this.$set(pathName, valForKey, constructing, options); } } // Ensure all properties are in correct order const orderedDoc = {}; const orderedKeys = Object.keys(this.$__schema.tree); for (let i = 0, len = orderedKeys.length; i < len; ++i) { (key = orderedKeys[i]) && (this._doc.hasOwnProperty(key)) && (orderedDoc[key] = undefined); } this._doc = Object.assign(orderedDoc, this._doc); return this; } let pathType = this.$__schema.pathType(path); let parts = null; if (pathType === 'adhocOrUndefined') { parts = path.indexOf('.') === -1 ? [path] : path.split('.'); pathType = getEmbeddedDiscriminatorPath(this, parts, { typeOnly: true }); } if (pathType === 'adhocOrUndefined' && !userSpecifiedStrict) { // May be path underneath non-strict schema if (parts == null) { parts = path.indexOf('.') === -1 ? [path] : path.split('.'); } const subdocStrict = getSubdocumentStrictValue(this.$__schema, parts); if (subdocStrict !== undefined) { strict = subdocStrict; } } // Assume this is a Mongoose document that was copied into a POJO using // `Object.assign()` or `{...doc}` val = handleSpreadDoc(val, true); // if this doc is being constructed we should not trigger getters const priorVal = (() => { if (this.$__.priorDoc != null) { return this.$__.priorDoc.$__getValue(path); } if (constructing) { return void 0; } return this.$__getValue(path); })(); if (pathType === 'nested' && val) { if (typeof val === 'object' && val != null) { if (val.$__ != null) { val = val.toObject(internalToObjectOptions); } if (val == null) { this.invalidate(path, new MongooseError.CastError('Object', val, path)); return this; } const wasModified = this.$isModified(path); const hasInitialVal = this.$__.savedState != null && this.$__.savedState.hasOwnProperty(path); if (this.$__.savedState != null && !this.$isNew && !this.$__.savedState.hasOwnProperty(path)) { const initialVal = this.$__getValue(path); this.$__.savedState[path] = initialVal; const keys = Object.keys(initialVal || {}); for (const key of keys) { this.$__.savedState[path + '.' + key] = initialVal[key]; } } if (!merge) { this.$__setValue(path, null); cleanModifiedSubpaths(this, path); } else { return this.$set(val, path, constructing, options); } const keys = getKeysInSchemaOrder(this.$__schema, val, path); this.$__setValue(path, {}); for (const key of keys) { this.$set(path + '.' + key, val[key], constructing, { ...options, _skipMarkModified: true }); } if (priorVal != null && (!wasModified || hasInitialVal) && utils.deepEqual(hasInitialVal ? this.$__.savedState[path] : priorVal, val)) { this.unmarkModified(path); } else { this.markModified(path); } return this; } this.invalidate(path, new MongooseError.CastError('Object', val, path)); return this; } let schema; if (parts == null) { parts = path.indexOf('.') === -1 ? [path] : path.split('.'); } // Might need to change path for top-level alias if (typeof this.$__schema.aliases[parts[0]] === 'string') { parts[0] = this.$__schema.aliases[parts[0]]; } if (pathType === 'adhocOrUndefined' && strict) { // check for roots that are Mixed types let mixed; for (i = 0; i < parts.length; ++i) { const subpath = parts.slice(0, i + 1).join('.'); // If path is underneath a virtual, bypass everything and just set it. if (i + 1 < parts.length && this.$__schema.pathType(subpath) === 'virtual') { mpath.set(path, val, this); return this; } schema = this.$__schema.path(subpath); if (schema == null) { continue; } if (schema instanceof MixedSchema) { // allow changes to sub paths of mixed types mixed = true; break; } else if (schema.$isSchemaMap && schema.$__schemaType instanceof MixedSchema && i < parts.length - 1) { // Map of mixed and not the last element in the path resolves to mixed mixed = true; schema = schema.$__schemaType; break; } } if (schema == null) { // Check for embedded discriminators schema = getEmbeddedDiscriminatorPath(this, path); } if (!mixed && !schema) { if (strict === 'throw') { throw new StrictModeError(path); } return this; } } else if (pathType === 'virtual') { schema = this.$__schema.virtualpath(path); schema.applySetters(val, this); return this; } else { schema = this.$__path(path); } // gh-4578, if setting a deeply nested path that doesn't exist yet, create it let cur = this._doc; let curPath = ''; for (i = 0; i < parts.length - 1; ++i) { cur = cur[parts[i]]; curPath += (curPath.length !== 0 ? '.' : '') + parts[i]; if (!cur) { this.$set(curPath, {}); // Hack re: gh-5800. If nested field is not selected, it probably exists // so `MongoServerError: cannot use the part (nested of nested.num) to // traverse the element ({nested: null})` is not likely. If user gets // that error, its their fault for now. We should reconsider disallowing // modifying not selected paths for 6.x if (!this.$__isSelected(curPath)) { this.unmarkModified(curPath); } cur = this.$__getValue(curPath); } } let pathToMark; // When using the $set operator the path to the field must already exist. // Else mongodb throws: "LEFT_SUBFIELD only supports Object" if (parts.length <= 1) { pathToMark = path; } else { const len = parts.length; for (i = 0; i < len; ++i) { const subpath = parts.slice(0, i + 1).join('.'); if (this.$get(subpath, null, { getters: false }) === null) { pathToMark = subpath; break; } } if (!pathToMark) { pathToMark = path; } } if (!schema) { this.$__set(pathToMark, path, options, constructing, parts, schema, val, priorVal); if (pathType === 'nested' && val == null) { cleanModifiedSubpaths(this, path); } return this; } // If overwriting a subdocument path, make sure to clear out // any errors _before_ setting, so new errors that happen // get persisted. Re: #9080 if (schema.$isSingleNested || schema.$isMongooseArray) { _markValidSubpaths(this, path); } if (val != null && merge && schema.$isSingleNested) { if (val instanceof Document) { val = val.toObject({ virtuals: false, transform: false }); } const keys = Object.keys(val); for (const key of keys) { this.$set(path + '.' + key, val[key], constructing, options); } return this; } let shouldSet = true; try { // If the user is trying to set a ref path to a document with // the correct model name, treat it as populated const refMatches = (() => { if (schema.options == null) { return false; } if (!(val instanceof Document)) { return false; } const model = val.constructor; // Check ref const refOpt = typeof schema.options.ref === 'function' && !schema.options.ref[modelSymbol] ? schema.options.ref.call(this, this) : schema.options.ref; const ref = refOpt?.modelName || refOpt; if (ref != null && (ref === model.modelName || ref === model.baseModelName)) { return true; } // Check refPath const refPath = schema.options.refPath; if (refPath == null) { return false; } const modelName = val.get(refPath); return modelName === model.modelName || modelName === model.baseModelName; })(); let didPopulate = false; if (refMatches && val instanceof Document && (!val.$__.wasPopulated || utils.deepEqual(val.$__.wasPopulated.value, val._doc._id))) { const unpopulatedValue = (schema && schema.$isSingleNested) ? schema.cast(val, this) : val._doc._id; this.$populated(path, unpopulatedValue, { [populateModelSymbol]: val.constructor }); val.$__.wasPopulated = { value: unpopulatedValue }; didPopulate = true; } let popOpts; const typeKey = this.$__schema.options.typeKey; if (schema.options && Array.isArray(schema.options[typeKey]) && schema.options[typeKey].length && schema.options[typeKey][0] && schema.options[typeKey][0].ref && _isManuallyPopulatedArray(val, schema.options[typeKey][0].ref)) { popOpts = { [populateModelSymbol]: val[0].constructor }; this.$populated(path, val.map(function(v) { return v._doc._id; }), popOpts); for (const doc of val) { doc.$__.wasPopulated = { value: doc._doc._id }; } didPopulate = true; } if (!refMatches || !schema.$isSingleNested || !val.$__) { // If this path is underneath a single nested schema, we'll call the setter // later in `$__set()` because we don't take `_doc` when we iterate through // a single nested doc. That's to make sure we get the correct context. // Otherwise we would double-call the setter, see gh-7196. let setterContext = this; if (this.$__schema.singleNestedPaths[path] != null && parts.length > 1) { setterContext = getDeepestSubdocumentForPath(this, parts, this.schema); } if (options != null && options.overwriteImmutable) { val = schema.applySetters(val, setterContext, false, priorVal, { overwriteImmutable: true }); } else { val = schema.applySetters(val, setterContext, false, priorVal); } } if (Array.isArray(val) && !Array.isArray(schema) && schema.$isMongooseDocumentArray && val.length !== 0 && val[0] != null && val[0].$__ != null && val[0].$__.populated != null) { const populatedPaths = Object.keys(val[0].$__.populated); for (const populatedPath of populatedPaths) { this.$populated(path + '.' + populatedPath, val.map(v => v.$populated(populatedPath)), val[0].$__.populated[populatedPath].options); } didPopulate = true; } if (!didPopulate && this.$__.populated) { // If this array partially contains populated documents, convert them // all to ObjectIds re: #8443 if (Array.isArray(val) && this.$__.populated[path]) { for (let i = 0; i < val.length; ++i) { if (val[i] instanceof Document) { val.set(i, val[i]._doc._id, true); } } } delete this.$__.populated[path]; } if (val != null && schema.$isSingleNested) { _checkImmutableSubpaths(val, schema, priorVal); } this.$markValid(path); } catch (e) { if (e instanceof MongooseError.StrictModeError && e.isImmutableError) { this.invalidate(path, e); } else if (e instanceof MongooseError.CastError) { this.invalidate(e.path, e); if (e.$originalErrorPath) { this.invalidate(path, new MongooseError.CastError(schema.instance, val, path, e.$originalErrorPath)); } } else { this.invalidate(path, new MongooseError.CastError(schema.instance, val, path, e)); } shouldSet = false; } if (shouldSet) { let savedState = null; let savedStatePath = null; if (!constructing) { const doc = this.$isSubdocument ? this.ownerDocument() : this; savedState = doc.$__.savedState; savedStatePath = this.$isSubdocument ? this.$__.fullPath + '.' + path : path; doc.$__saveInitialState(savedStatePath); } this.$__set(pathToMark, path, options, constructing, parts, schema, val, priorVal); const isInTransaction = !!this.$__.session?.transaction; const isModifiedWithinTransaction = this.$__.session && this.$__.session[sessionNewDocuments] && this.$__.session[sessionNewDocuments].has(this) && this.$__.session[sessionNewDocuments].get(this).modifiedPaths && !this.$__.session[sessionNewDocuments].get(this).modifiedPaths.has(savedStatePath); if (savedState != null && savedState.hasOwnProperty(savedStatePath) && (!isInTransaction || isModifiedWithinTransaction) && utils.deepEqual(val, savedState[savedStatePath])) { this.unmarkModified(path); } } if (schema.$isSingleNested && (this.isDirectModified(path) || val == null)) { cleanModifiedSubpaths(this, path); } return this; }; /*! * ignore */ function _isManuallyPopulatedArray(val, ref) { if (!Array.isArray(val)) { return false; } if (val.length === 0) { return false; } for (const el of val) { if (!(el instanceof Document)) { return false; } const modelName = el.constructor.modelName; if (modelName == null) { return false; } if (el.constructor.modelName != ref && el.constructor.baseModelName != ref) { return false; } } return true; } /** * Sets the value of a path, or many paths. * Alias for [`.$set`](https://mongoosejs.com/docs/api/document.html#Document.prototype.$set()). * * #### Example: * * // path, value * doc.set(path, value) * * // object * doc.set({ * path : value * , path2 : { * path : value * } * }) * * // on-the-fly cast to number * doc.set(path, value, Number) * * // on-the-fly cast to string * doc.set(path, value, String) * * // changing strict mode behavior * doc.set(path, value, { strict: false }); * * @param {String|Object} path path or object of key/vals to set * @param {Any} val the value to set * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for "on-the-fly" attributes * @param {Object} [options] optionally specify options that modify the behavior of the set * @return {Document} this * @api public * @method set * @memberOf Document * @instance */ Document.prototype.set = Document.prototype.$set; /** * Determine if we should mark this change as modified. * * @param {never} pathToMark UNUSED * @param {String|Symbol} path * @param {Object} options * @param {Any} constructing * @param {never} parts UNUSED * @param {Schema} schema * @param {Any} val * @param {Any} priorVal * @return {Boolean} * @api private * @method $__shouldModify * @memberOf Document * @instance */ Document.prototype.$__shouldModify = function(pathToMark, path, options, constructing, parts, schema, val, priorVal) { if (options && options._skipMarkModified) { return false; } if (this.$isNew) { return true; } // Is path already modified? If so, always modify. We may unmark modified later. if (path in this.$__.activePaths.getStatePaths('modify')) { return true; } if (val === void 0 && !this.$__isSelected(path)) { // when a path is not selected in a query, its initial // value will be undefined. return true; } if (val === void 0 && path in this.$__.activePaths.getStatePaths('default')) { // we're just unsetting the default value which was never saved return false; } // gh-3992: if setting a populated field to a doc, don't mark modified // if they have the same _id if (this.$populated(path) && val instanceof Document && deepEqual(val._doc._id, priorVal)) { return false; } if (!deepEqual(val, priorVal !== undefined ? priorVal : utils.getValue(path, this))) { return true; } if (!constructing && val !== null && val !== undefined && path in this.$__.activePaths.getStatePaths('default') && deepEqual(val, schema.getDefault(this, constructing))) { // a path with a default was $unset on the server // and the user is setting it to the same value again return true; } return false; }; /** * Handles the actual setting of the value and marking the path