UNPKG

mongoose

Version:
1,551 lines (1,363 loc) 178 kB
'use strict'; /*! * Module dependencies. */ const Aggregate = require('./aggregate'); const ChangeStream = require('./cursor/ChangeStream'); const Document = require('./document'); const DocumentNotFoundError = require('./error/notFound'); const DivergentArrayError = require('./error/divergentArray'); const EventEmitter = require('events').EventEmitter; const MongooseBuffer = require('./types/buffer'); const MongooseError = require('./error/index'); const OverwriteModelError = require('./error/overwriteModel'); const Query = require('./query'); const SaveOptions = require('./options/saveOptions'); const Schema = require('./schema'); const ValidationError = require('./error/validation'); const VersionError = require('./error/version'); const ParallelSaveError = require('./error/parallelSave'); const applyDefaultsHelper = require('./helpers/document/applyDefaults'); const applyDefaultsToPOJO = require('./helpers/model/applyDefaultsToPOJO'); const applyQueryMiddleware = require('./helpers/query/applyQueryMiddleware'); const applyHooks = require('./helpers/model/applyHooks'); const applyMethods = require('./helpers/model/applyMethods'); const applyProjection = require('./helpers/projection/applyProjection'); const applySchemaCollation = require('./helpers/indexes/applySchemaCollation'); const applyStaticHooks = require('./helpers/model/applyStaticHooks'); const applyStatics = require('./helpers/model/applyStatics'); const applyWriteConcern = require('./helpers/schema/applyWriteConcern'); const assignVals = require('./helpers/populate/assignVals'); const castBulkWrite = require('./helpers/model/castBulkWrite'); const clone = require('./helpers/clone'); const createPopulateQueryFilter = require('./helpers/populate/createPopulateQueryFilter'); const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult'); const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue'); const discriminator = require('./helpers/model/discriminator'); const firstKey = require('./helpers/firstKey'); const each = require('./helpers/each'); const get = require('./helpers/get'); const getConstructorName = require('./helpers/getConstructorName'); const getDiscriminatorByValue = require('./helpers/discriminator/getDiscriminatorByValue'); const getModelsMapForPopulate = require('./helpers/populate/getModelsMapForPopulate'); const immediate = require('./helpers/immediate'); const internalToObjectOptions = require('./options').internalToObjectOptions; const isDefaultIdIndex = require('./helpers/indexes/isDefaultIdIndex'); const isIndexEqual = require('./helpers/indexes/isIndexEqual'); const { getRelatedDBIndexes, getRelatedSchemaIndexes } = require('./helpers/indexes/getRelatedIndexes'); const isPathExcluded = require('./helpers/projection/isPathExcluded'); const decorateDiscriminatorIndexOptions = require('./helpers/indexes/decorateDiscriminatorIndexOptions'); const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive'); const leanPopulateMap = require('./helpers/populate/leanPopulateMap'); const modifiedPaths = require('./helpers/update/modifiedPaths'); const parallelLimit = require('./helpers/parallelLimit'); const parentPaths = require('./helpers/path/parentPaths'); const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscriminatorPipeline'); const pushNestedArrayPaths = require('./helpers/model/pushNestedArrayPaths'); const removeDeselectedForeignField = require('./helpers/populate/removeDeselectedForeignField'); const setDottedPath = require('./helpers/path/setDottedPath'); const STATES = require('./connectionstate'); const util = require('util'); const utils = require('./utils'); const MongooseBulkWriteError = require('./error/bulkWriteError'); const VERSION_WHERE = 1; const VERSION_INC = 2; const VERSION_ALL = VERSION_WHERE | VERSION_INC; const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol; const modelCollectionSymbol = Symbol('mongoose#Model#collection'); const modelDbSymbol = Symbol('mongoose#Model#db'); const modelSymbol = require('./helpers/symbols').modelSymbol; const subclassedSymbol = Symbol('mongoose#Model#subclassed'); const saveToObjectOptions = Object.assign({}, internalToObjectOptions, { bson: true, flattenObjectIds: false }); /** * A Model is a class that's your primary tool for interacting with MongoDB. * An instance of a Model is called a [Document](https://mongoosejs.com/docs/api/document.html#Document). * * In Mongoose, the term "Model" refers to subclasses of the `mongoose.Model` * class. You should not use the `mongoose.Model` class directly. The * [`mongoose.model()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.model()) and * [`connection.model()`](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.model()) functions * create subclasses of `mongoose.Model` as shown below. * * #### Example: * * // `UserModel` is a "Model", a subclass of `mongoose.Model`. * const UserModel = mongoose.model('User', new Schema({ name: String })); * * // You can use a Model to create new documents using `new`: * const userDoc = new UserModel({ name: 'Foo' }); * await userDoc.save(); * * // You also use a model to create queries: * const userFromDb = await UserModel.findOne({ name: 'Foo' }); * * @param {Object} doc values for initial set * @param {Object} [fields] optional object containing the fields that were selected in the query which returned this document. You do **not** need to set this parameter to ensure Mongoose handles your [query projection](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()). * @param {Boolean} [skipId=false] optional boolean. If true, mongoose doesn't add an `_id` field to the document. * @inherits Document https://mongoosejs.com/docs/api/document.html * @event `error`: If listening to this event, 'error' is emitted when a document was saved and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model. * @event `index`: Emitted after `Model#ensureIndexes` completes. If an error occurred it is passed with the event. * @event `index-single-start`: Emitted when an individual index starts within `Model#ensureIndexes`. The fields and options being used to build the index are also passed with the event. * @event `index-single-done`: Emitted when an individual index finishes within `Model#ensureIndexes`. If an error occurred it is passed with the event. The fields, options, and index name are also passed. * @api public */ function Model(doc, fields, skipId) { if (fields instanceof Schema) { throw new TypeError('2nd argument to `Model` must be a POJO or string, ' + '**not** a schema. Make sure you\'re calling `mongoose.model()`, not ' + '`mongoose.Model()`.'); } Document.call(this, doc, fields, skipId); } /** * Inherits from Document. * * All Model.prototype features are available on * top level (non-sub) documents. * @api private */ Object.setPrototypeOf(Model.prototype, Document.prototype); Model.prototype.$isMongooseModelPrototype = true; /** * Connection the model uses. * * @api public * @property db * @memberOf Model * @instance */ Model.prototype.db; /** * The collection instance this model uses. * A Mongoose collection is a thin wrapper around a [MongoDB Node.js driver collection]([MongoDB Node.js driver collection](https://mongodb.github.io/node-mongodb-native/Next/classes/Collection.html)). * Using `Model.collection` means you bypass Mongoose middleware, validation, and casting. * * This property is read-only. Modifying this property is a no-op. * * @api public * @property collection * @memberOf Model * @instance */ Model.prototype.collection; /** * Internal collection the model uses. * * This property is read-only. Modifying this property is a no-op. * * @api private * @property collection * @memberOf Model * @instance */ Model.prototype.$__collection; /** * The name of the model * * @api public * @property modelName * @memberOf Model * @instance */ Model.prototype.modelName; /** * Additional properties to attach to the query when calling `save()` and * `isNew` is false. * * @api public * @property $where * @memberOf Model * @instance */ Model.prototype.$where; /** * If this is a discriminator model, `baseModelName` is the name of * the base model. * * @api public * @property baseModelName * @memberOf Model * @instance */ Model.prototype.baseModelName; /** * Event emitter that reports any errors that occurred. Useful for global error * handling. * * #### Example: * * MyModel.events.on('error', err => console.log(err.message)); * * // Prints a 'CastError' because of the above handler * await MyModel.findOne({ _id: 'Not a valid ObjectId' }).catch(noop); * * @api public * @property events * @fires error whenever any query or model function errors * @memberOf Model * @static */ Model.events; /** * Compiled middleware for this model. Set in `applyHooks()`. * * @api private * @property _middleware * @memberOf Model * @static */ Model._middleware; /*! * ignore */ function _applyCustomWhere(doc, where) { if (doc.$where == null) { return; } for (const key of Object.keys(doc.$where)) { where[key] = doc.$where[key]; } } /*! * ignore */ Model.prototype.$__handleSave = function(options, callback) { const saveOptions = {}; applyWriteConcern(this.$__schema, options); if (typeof options.writeConcern !== 'undefined') { saveOptions.writeConcern = {}; if ('w' in options.writeConcern) { saveOptions.writeConcern.w = options.writeConcern.w; } if ('j' in options.writeConcern) { saveOptions.writeConcern.j = options.writeConcern.j; } if ('wtimeout' in options.writeConcern) { saveOptions.writeConcern.wtimeout = options.writeConcern.wtimeout; } } else { if ('w' in options) { saveOptions.w = options.w; } if ('j' in options) { saveOptions.j = options.j; } if ('wtimeout' in options) { saveOptions.wtimeout = options.wtimeout; } } if ('checkKeys' in options) { saveOptions.checkKeys = options.checkKeys; } const session = this.$session(); if (!saveOptions.hasOwnProperty('session') && session != null) { saveOptions.session = session; } if (this.$isNew) { // send entire doc const obj = this.toObject(saveToObjectOptions); if ((obj || {})._id === void 0) { // documents must have an _id else mongoose won't know // what to update later if more changes are made. the user // wouldn't know what _id was generated by mongodb either // nor would the ObjectId generated by mongodb necessarily // match the schema definition. immediate(function() { callback(new MongooseError('document must have an _id before saving')); }); return; } this.$__version(true, obj); this[modelCollectionSymbol].insertOne(obj, saveOptions).then( ret => callback(null, ret), err => { _setIsNew(this, true); callback(err, null); } ); this.$__reset(); _setIsNew(this, false); // Make it possible to retry the insert this.$__.inserting = true; return; } // Make sure we don't treat it as a new object on error, // since it already exists this.$__.inserting = false; const delta = this.$__delta(); if (delta) { if (delta instanceof MongooseError) { callback(delta); return; } const where = this.$__where(delta[0]); if (where instanceof MongooseError) { callback(where); return; } _applyCustomWhere(this, where); this[modelCollectionSymbol].updateOne(where, delta[1], saveOptions).then( ret => { ret.$where = where; callback(null, ret); }, err => { this.$__undoReset(); callback(err); } ); } else { const optionsWithCustomValues = Object.assign({}, options, saveOptions); const where = this.$__where(); const optimisticConcurrency = this.$__schema.options.optimisticConcurrency; if (optimisticConcurrency && !Array.isArray(optimisticConcurrency)) { const key = this.$__schema.options.versionKey; const val = this.$__getValue(key); if (val != null) { where[key] = val; } } this.constructor.collection.findOne(where, optionsWithCustomValues) .then(documentExists => { const matchedCount = !documentExists ? 0 : 1; callback(null, { $where: where, matchedCount }); }) .catch(callback); return; } // store the modified paths before the document is reset this.$__.modifiedPaths = this.modifiedPaths(); this.$__reset(); _setIsNew(this, false); }; /*! * ignore */ Model.prototype.$__save = function(options, callback) { this.$__handleSave(options, (error, result) => { if (error) { const hooks = this.$__schema.s.hooks; return hooks.execPost('save:error', this, [this], { error: error }, (error) => { callback(error, this); }); } let numAffected = 0; const writeConcern = options != null ? options.writeConcern != null ? options.writeConcern.w : options.w : 0; if (writeConcern !== 0) { // Skip checking if write succeeded if writeConcern is set to // unacknowledged writes, because otherwise `numAffected` will always be 0 if (result != null) { if (Array.isArray(result)) { numAffected = result.length; } else if (result.matchedCount != null) { numAffected = result.matchedCount; } else { numAffected = result; } } const versionBump = this.$__.version; // was this an update that required a version bump? if (versionBump && !this.$__.inserting) { const doIncrement = VERSION_INC === (VERSION_INC & this.$__.version); this.$__.version = undefined; const key = this.$__schema.options.versionKey; const version = this.$__getValue(key) || 0; if (numAffected <= 0) { // the update failed. pass an error back this.$__undoReset(); const err = this.$__.$versionError || new VersionError(this, version, this.$__.modifiedPaths); return callback(err); } // increment version if was successful if (doIncrement) { this.$__setValue(key, version + 1); } } if (result != null && numAffected <= 0) { this.$__undoReset(); error = new DocumentNotFoundError(result.$where, this.constructor.modelName, numAffected, result); const hooks = this.$__schema.s.hooks; return hooks.execPost('save:error', this, [this], { error: error }, (error) => { callback(error, this); }); } } this.$__.saving = undefined; this.$__.savedState = {}; this.$emit('save', this, numAffected); this.constructor.emit('save', this, numAffected); callback(null, this); }); }; /*! * ignore */ function generateVersionError(doc, modifiedPaths) { const key = doc.$__schema.options.versionKey; if (!key) { return null; } const version = doc.$__getValue(key) || 0; return new VersionError(doc, version, modifiedPaths); } /** * Saves this document by inserting a new document into the database if [document.isNew](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew) is `true`, * or sends an [updateOne](https://mongoosejs.com/docs/api/document.html#Document.prototype.updateOne()) operation with just the modified paths if `isNew` is `false`. * * #### Example: * * product.sold = Date.now(); * product = await product.save(); * * If save is successful, the returned promise will fulfill with the document * saved. * * #### Example: * * const newProduct = await product.save(); * newProduct === product; // true * * @param {Object} [options] options optional options * @param {Session} [options.session=null] the [session](https://www.mongodb.com/docs/manual/reference/server-sessions/) associated with this save operation. If not specified, defaults to the [document's associated session](https://mongoosejs.com/docs/api/document.html#Document.prototype.session()). * @param {Object} [options.safe] (DEPRECATED) overrides [schema's safe option](https://mongoosejs.com/docs/guide.html#safe). Use the `w` option instead. * @param {Boolean} [options.validateBeforeSave] set to false to save without validating. * @param {Boolean} [options.validateModifiedOnly=false] if `true`, Mongoose will only validate modified paths, as opposed to modified paths and `required` paths. * @param {Number|String} [options.w] set the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#w-option). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.j] set to true for MongoDB to wait until this `save()` has been [journaled before resolving the returned promise](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern). * @param {Boolean} [options.checkKeys=true] the MongoDB driver prevents you from saving keys that start with '$' or contain '.' by default. Set this option to `false` to skip that check. See [restrictions on field names](https://docs.mongodb.com/manual/reference/limits/#mongodb-limit-Restrictions-on-Field-Names) * @param {Boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`. * @throws {DocumentNotFoundError} if this [save updates an existing document](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew) but the document doesn't exist in the database. For example, you will get this error if the document is [deleted between when you retrieved the document and when you saved it](documents.html#updating). * @return {Promise} * @api public * @see middleware https://mongoosejs.com/docs/middleware.html */ Model.prototype.save = async function save(options) { if (typeof options === 'function' || typeof arguments[1] === 'function') { throw new MongooseError('Model.prototype.save() no longer accepts a callback'); } let parallelSave; this.$op = 'save'; if (this.$__.saving) { parallelSave = new ParallelSaveError(this); } else { this.$__.saving = new ParallelSaveError(this); } options = new SaveOptions(options); if (options.hasOwnProperty('session')) { this.$session(options.session); } if (this.$__.timestamps != null) { options.timestamps = this.$__.timestamps; } this.$__.$versionError = generateVersionError(this, this.modifiedPaths()); if (parallelSave) { this.$__handleReject(parallelSave); throw parallelSave; } this.$__.saveOptions = options; await new Promise((resolve, reject) => { this.$__save(options, error => { this.$__.saving = null; this.$__.saveOptions = null; this.$__.$versionError = null; this.$op = null; if (error != null) { this.$__handleReject(error); return reject(error); } resolve(); }); }); return this; }; Model.prototype.$save = Model.prototype.save; /** * Determines whether versioning should be skipped for the given path * * @param {Document} self * @param {String} path * @return {Boolean} true if versioning should be skipped for the given path * @api private */ function shouldSkipVersioning(self, path) { const skipVersioning = self.$__schema.options.skipVersioning; if (!skipVersioning) return false; // Remove any array indexes from the path path = path.replace(/\.\d+\./, '.'); return skipVersioning[path]; } /** * Apply the operation to the delta (update) clause as * well as track versioning for our where clause. * * @param {Document} self * @param {Object} where Unused * @param {Object} delta * @param {Object} data * @param {Mixed} val * @param {String} [op] * @api private */ function operand(self, where, delta, data, val, op) { // delta op || (op = '$set'); if (!delta[op]) delta[op] = {}; delta[op][data.path] = val; // disabled versioning? if (self.$__schema.options.versionKey === false) return; // path excluded from versioning? if (shouldSkipVersioning(self, data.path)) return; // already marked for versioning? if (VERSION_ALL === (VERSION_ALL & self.$__.version)) return; if (self.$__schema.options.optimisticConcurrency) { return; } switch (op) { case '$set': case '$unset': case '$pop': case '$pull': case '$pullAll': case '$push': case '$addToSet': case '$inc': break; default: // nothing to do return; } // ensure updates sent with positional notation are // editing the correct array element. // only increment the version if an array position changes. // modifying elements of an array is ok if position does not change. if (op === '$push' || op === '$addToSet' || op === '$pullAll' || op === '$pull') { if (/\.\d+\.|\.\d+$/.test(data.path)) { increment.call(self); } else { self.$__.version = VERSION_INC; } } else if (/^\$p/.test(op)) { // potentially changing array positions increment.call(self); } else if (Array.isArray(val)) { // $set an array increment.call(self); } else if (/\.\d+\.|\.\d+$/.test(data.path)) { // now handling $set, $unset // subpath of array self.$__.version = VERSION_WHERE; } } /** * Compiles an update and where clause for a `val` with _atomics. * * @param {Document} self * @param {Object} where * @param {Object} delta * @param {Object} data * @param {Array} value * @api private */ function handleAtomics(self, where, delta, data, value) { if (delta.$set && delta.$set[data.path]) { // $set has precedence over other atomics return; } if (typeof value.$__getAtomics === 'function') { value.$__getAtomics().forEach(function(atomic) { const op = atomic[0]; const val = atomic[1]; operand(self, where, delta, data, val, op); }); return; } // legacy support for plugins const atomics = value[arrayAtomicsSymbol]; const ops = Object.keys(atomics); let i = ops.length; let val; let op; if (i === 0) { // $set if (utils.isMongooseObject(value)) { value = value.toObject({ depopulate: 1, _isNested: true }); } else if (value.valueOf) { value = value.valueOf(); } return operand(self, where, delta, data, value); } function iter(mem) { return utils.isMongooseObject(mem) ? mem.toObject({ depopulate: 1, _isNested: true }) : mem; } while (i--) { op = ops[i]; val = atomics[op]; if (utils.isMongooseObject(val)) { val = val.toObject({ depopulate: true, transform: false, _isNested: true }); } else if (Array.isArray(val)) { val = val.map(iter); } else if (val.valueOf) { val = val.valueOf(); } if (op === '$addToSet') { val = { $each: val }; } operand(self, where, delta, data, val, op); } } /** * Produces a special query document of the modified properties used in updates. * * @api private * @method $__delta * @memberOf Model * @instance */ Model.prototype.$__delta = function() { const dirty = this.$__dirty(); const optimisticConcurrency = this.$__schema.options.optimisticConcurrency; if (optimisticConcurrency) { if (Array.isArray(optimisticConcurrency)) { const optCon = new Set(optimisticConcurrency); const modPaths = this.modifiedPaths(); if (modPaths.find(path => optCon.has(path))) { this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE; } } else { this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE; } } if (!dirty.length && VERSION_ALL !== this.$__.version) { return; } const where = {}; const delta = {}; const len = dirty.length; const divergent = []; let d = 0; where._id = this._doc._id; // If `_id` is an object, need to depopulate, but also need to be careful // because `_id` can technically be null (see gh-6406) if ((where && where._id && where._id.$__ || null) != null) { where._id = where._id.toObject({ transform: false, depopulate: true }); } for (; d < len; ++d) { const data = dirty[d]; let value = data.value; const match = checkDivergentArray(this, data.path, value); if (match) { divergent.push(match); continue; } const pop = this.$populated(data.path, true); if (!pop && this.$__.selected) { // If any array was selected using an $elemMatch projection, we alter the path and where clause // NOTE: MongoDB only supports projected $elemMatch on top level array. const pathSplit = data.path.split('.'); const top = pathSplit[0]; if (this.$__.selected[top] && this.$__.selected[top].$elemMatch) { // If the selected array entry was modified if (pathSplit.length > 1 && pathSplit[1] == 0 && typeof where[top] === 'undefined') { where[top] = this.$__.selected[top]; pathSplit[1] = '$'; data.path = pathSplit.join('.'); } // if the selected array was modified in any other way throw an error else { divergent.push(data.path); continue; } } } // If this path is set to default, and either this path or one of // its parents is excluded, don't treat this path as dirty. if (this.$isDefault(data.path) && this.$__.selected) { if (data.path.indexOf('.') === -1 && isPathExcluded(this.$__.selected, data.path)) { continue; } const pathsToCheck = parentPaths(data.path); if (pathsToCheck.find(path => isPathExcluded(this.$__.isSelected, path))) { continue; } } if (divergent.length) continue; if (value === undefined) { operand(this, where, delta, data, 1, '$unset'); } else if (value === null) { operand(this, where, delta, data, null); } else if (utils.isMongooseArray(value) && value.$path() && value[arrayAtomicsSymbol]) { // arrays and other custom types (support plugins etc) handleAtomics(this, where, delta, data, value); } else if (value[MongooseBuffer.pathSymbol] && Buffer.isBuffer(value)) { // MongooseBuffer value = value.toObject(); operand(this, where, delta, data, value); } else { if (this.$__.primitiveAtomics && this.$__.primitiveAtomics[data.path] != null) { const val = this.$__.primitiveAtomics[data.path]; const op = firstKey(val); operand(this, where, delta, data, val[op], op); } else { value = clone(value, { depopulate: true, transform: false, virtuals: false, getters: false, omitUndefined: true, _isNested: true }); operand(this, where, delta, data, value); } } } if (divergent.length) { return new DivergentArrayError(divergent); } if (this.$__.version) { this.$__version(where, delta); } if (Object.keys(delta).length === 0) { return [where, null]; } return [where, delta]; }; /** * Determine if array was populated with some form of filter and is now * being updated in a manner which could overwrite data unintentionally. * * @see https://github.com/Automattic/mongoose/issues/1334 * @param {Document} doc * @param {String} path * @param {Any} array * @return {String|undefined} * @api private */ function checkDivergentArray(doc, path, array) { // see if we populated this path const pop = doc.$populated(path, true); if (!pop && doc.$__.selected) { // If any array was selected using an $elemMatch projection, we deny the update. // NOTE: MongoDB only supports projected $elemMatch on top level array. const top = path.split('.')[0]; if (doc.$__.selected[top + '.$']) { return top; } } if (!(pop && utils.isMongooseArray(array))) return; // If the array was populated using options that prevented all // documents from being returned (match, skip, limit) or they // deselected the _id field, $pop and $set of the array are // not safe operations. If _id was deselected, we do not know // how to remove elements. $pop will pop off the _id from the end // of the array in the db which is not guaranteed to be the // same as the last element we have here. $set of the entire array // would be similarly destructive as we never received all // elements of the array and potentially would overwrite data. const check = pop.options.match || pop.options.options && utils.object.hasOwnProperty(pop.options.options, 'limit') || // 0 is not permitted pop.options.options && pop.options.options.skip || // 0 is permitted pop.options.select && // deselected _id? (pop.options.select._id === 0 || /\s?-_id\s?/.test(pop.options.select)); if (check) { const atomics = array[arrayAtomicsSymbol]; if (Object.keys(atomics).length === 0 || atomics.$set || atomics.$pop) { return path; } } } /** * Appends versioning to the where and update clauses. * * @api private * @method $__version * @memberOf Model * @instance */ Model.prototype.$__version = function(where, delta) { const key = this.$__schema.options.versionKey; if (where === true) { // this is an insert if (key) { setDottedPath(delta, key, 0); this.$__setValue(key, 0); } return; } if (key === false) { return; } // updates // only apply versioning if our versionKey was selected. else // there is no way to select the correct version. we could fail // fast here and force them to include the versionKey but // thats a bit intrusive. can we do this automatically? if (!this.$__isSelected(key)) { return; } // $push $addToSet don't need the where clause set if (VERSION_WHERE === (VERSION_WHERE & this.$__.version)) { const value = this.$__getValue(key); if (value != null) where[key] = value; } if (VERSION_INC === (VERSION_INC & this.$__.version)) { if (get(delta.$set, key, null) != null) { // Version key is getting set, means we'll increment the doc's version // after a successful save, so we should set the incremented version so // future saves don't fail (gh-5779) ++delta.$set[key]; } else { delta.$inc = delta.$inc || {}; delta.$inc[key] = 1; } } }; /*! * ignore */ function increment() { this.$__.version = VERSION_ALL; return this; } /** * Signal that we desire an increment of this documents version. * * #### Example: * * const doc = await Model.findById(id); * doc.increment(); * await doc.save(); * * @see versionKeys https://mongoosejs.com/docs/guide.html#versionKey * @memberOf Model * @method increment * @api public */ Model.prototype.increment = increment; /** * Returns a query object * * @api private * @method $__where * @memberOf Model * @instance */ Model.prototype.$__where = function _where(where) { where || (where = {}); if (!where._id) { where._id = this._doc._id; } if (this._doc._id === void 0) { return new MongooseError('No _id found on document!'); } return where; }; /** * Removes this document from the db. Equivalent to `.remove()`. * * #### Example: * * product = await product.deleteOne(); * await Product.findById(product._id); // null * * @return {Promise} Promise * @api public */ Model.prototype.deleteOne = async function deleteOne(options) { if (typeof options === 'function' || typeof arguments[1] === 'function') { throw new MongooseError('Model.prototype.deleteOne() no longer accepts a callback'); } if (!options) { options = {}; } if (options.hasOwnProperty('session')) { this.$session(options.session); } const res = await new Promise((resolve, reject) => { this.$__deleteOne(options, (err, res) => { if (err != null) { return reject(err); } resolve(res); }); }); return res; }; /*! * ignore */ Model.prototype.$__deleteOne = function $__deleteOne(options, cb) { if (this.$__.isDeleted) { return immediate(() => cb(null, this)); } const where = this.$__where(); if (where instanceof MongooseError) { return cb(where); } _applyCustomWhere(this, where); const session = this.$session(); if (!options.hasOwnProperty('session')) { options.session = session; } this[modelCollectionSymbol].deleteOne(where, options).then( () => { this.$__.isDeleted = true; this.$emit('deleteOne', this); this.constructor.emit('deleteOne', this); return cb(null, this); }, err => { this.$__.isDeleted = false; cb(err); } ); }; /** * Returns another Model instance. * * #### Example: * * const doc = new Tank; * await doc.model('User').findById(id); * * @param {String} name model name * @method model * @api public * @return {Model} */ Model.prototype.model = function model(name) { return this[modelDbSymbol].model(name); }; /** * Returns another Model instance. * * #### Example: * * const doc = new Tank; * await doc.model('User').findById(id); * * @param {String} name model name * @method $model * @api public * @return {Model} */ Model.prototype.$model = function $model(name) { return this[modelDbSymbol].model(name); }; /** * Returns a document with `_id` only if at least one document exists in the database that matches * the given `filter`, and `null` otherwise. * * Under the hood, `MyModel.exists({ answer: 42 })` is equivalent to * `MyModel.findOne({ answer: 42 }).select({ _id: 1 }).lean()` * * #### Example: * * await Character.deleteMany({}); * await Character.create({ name: 'Jean-Luc Picard' }); * * await Character.exists({ name: /picard/i }); // { _id: ... } * await Character.exists({ name: /riker/i }); // null * * This function triggers the following middleware. * * - `findOne()` * * @param {Object} filter * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @return {Query} */ Model.exists = function exists(filter, options) { _checkContext(this, 'exists'); if (typeof arguments[2] === 'function') { throw new MongooseError('Model.exists() no longer accepts a callback'); } const query = this.findOne(filter). select({ _id: 1 }). lean(). setOptions(options); return query; }; /** * Adds a discriminator type. * * #### Example: * * function BaseSchema() { * Schema.apply(this, arguments); * * this.add({ * name: String, * createdAt: Date * }); * } * util.inherits(BaseSchema, Schema); * * const PersonSchema = new BaseSchema(); * const BossSchema = new BaseSchema({ department: String }); * * const Person = mongoose.model('Person', PersonSchema); * const Boss = Person.discriminator('Boss', BossSchema); * new Boss().__t; // "Boss". `__t` is the default `discriminatorKey` * * const employeeSchema = new Schema({ boss: ObjectId }); * const Employee = Person.discriminator('Employee', employeeSchema, 'staff'); * new Employee().__t; // "staff" because of 3rd argument above * * @param {String} name discriminator model name * @param {Schema} schema discriminator model schema * @param {Object|String} [options] If string, same as `options.value`. * @param {String} [options.value] the string stored in the `discriminatorKey` property. If not specified, Mongoose uses the `name` parameter. * @param {Boolean} [options.clone=true] By default, `discriminator()` clones the given `schema`. Set to `false` to skip cloning. * @param {Boolean} [options.overwriteModels=false] by default, Mongoose does not allow you to define a discriminator with the same name as another discriminator. Set this to allow overwriting discriminators with the same name. * @param {Boolean} [options.mergeHooks=true] By default, Mongoose merges the base schema's hooks with the discriminator schema's hooks. Set this option to `false` to make Mongoose use the discriminator schema's hooks instead. * @param {Boolean} [options.mergePlugins=true] By default, Mongoose merges the base schema's plugins with the discriminator schema's plugins. Set this option to `false` to make Mongoose use the discriminator schema's plugins instead. * @return {Model} The newly created discriminator model * @api public */ Model.discriminator = function(name, schema, options) { let model; if (typeof name === 'function') { model = name; name = utils.getFunctionName(model); if (!(model.prototype instanceof Model)) { throw new MongooseError('The provided class ' + name + ' must extend Model'); } } options = options || {}; const value = utils.isPOJO(options) ? options.value : options; const clone = typeof options.clone === 'boolean' ? options.clone : true; const mergePlugins = typeof options.mergePlugins === 'boolean' ? options.mergePlugins : true; _checkContext(this, 'discriminator'); if (utils.isObject(schema) && !schema.instanceOfSchema) { schema = new Schema(schema); } if (schema instanceof Schema && clone) { schema = schema.clone(); } schema = discriminator(this, name, schema, value, mergePlugins, options.mergeHooks); if (this.db.models[name] && !schema.options.overwriteModels) { throw new OverwriteModelError(name); } schema.$isRootDiscriminator = true; schema.$globalPluginsApplied = true; model = this.db.model(model || name, schema, this.$__collection.name); this.discriminators[name] = model; const d = this.discriminators[name]; Object.setPrototypeOf(d.prototype, this.prototype); Object.defineProperty(d, 'baseModelName', { value: this.modelName, configurable: true, writable: false }); // apply methods and statics applyMethods(d, schema); applyStatics(d, schema); if (this[subclassedSymbol] != null) { for (const submodel of this[subclassedSymbol]) { submodel.discriminators = submodel.discriminators || {}; submodel.discriminators[name] = model.__subclass(model.db, schema, submodel.collection.name); } } return d; }; /** * Make sure `this` is a model * @api private */ function _checkContext(ctx, fnName) { // Check context, because it is easy to mistakenly type // `new Model.discriminator()` and get an incomprehensible error if (ctx == null || ctx === global) { throw new MongooseError('`Model.' + fnName + '()` cannot run without a ' + 'model as `this`. Make sure you are calling `MyModel.' + fnName + '()` ' + 'where `MyModel` is a Mongoose model.'); } else if (ctx[modelSymbol] == null) { throw new MongooseError('`Model.' + fnName + '()` cannot run without a ' + 'model as `this`. Make sure you are not calling ' + '`new Model.' + fnName + '()`'); } } // Model (class) features /*! * Give the constructor the ability to emit events. */ for (const i in EventEmitter.prototype) { Model[i] = EventEmitter.prototype[i]; } /** * This function is responsible for building [indexes](https://www.mongodb.com/docs/manual/indexes/), * unless [`autoIndex`](https://mongoosejs.com/docs/guide.html#autoIndex) is turned off. * * Mongoose calls this function automatically when a model is created using * [`mongoose.model()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.model()) or * [`connection.model()`](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.model()), so you * don't need to call `init()` to trigger index builds. * * However, you _may_ need to call `init()` to get back a promise that will resolve when your indexes are finished. * Calling `await Model.init()` is helpful if you need to wait for indexes to build before continuing. * For example, if you want to wait for unique indexes to build before continuing with a test case. * * #### Example: * * const eventSchema = new Schema({ thing: { type: 'string', unique: true } }) * // This calls `Event.init()` implicitly, so you don't need to call * // `Event.init()` on your own. * const Event = mongoose.model('Event', eventSchema); * * await Event.init(); * console.log('Indexes are done building!'); * * @api public * @returns {Promise} */ Model.init = function init() { _checkContext(this, 'init'); if (typeof arguments[0] === 'function') { throw new MongooseError('Model.init() no longer accepts a callback'); } this.schema.emit('init', this); if (this.$init != null) { return this.$init; } const conn = this.db; const _ensureIndexes = async() => { const autoIndex = utils.getOption( 'autoIndex', this.schema.options, conn.config, conn.base.options ); if (!autoIndex) { return; } return await this.ensureIndexes({ _automatic: true }); }; const _createCollection = async() => { if ((conn.readyState === STATES.connecting || conn.readyState === STATES.disconnected) && conn._shouldBufferCommands()) { await new Promise(resolve => { conn._queue.push({ fn: resolve }); }); } const autoCreate = utils.getOption( 'autoCreate', this.schema.options, conn.config, conn.base.options ); if (!autoCreate) { return; } return await this.createCollection(); }; this.$init = _createCollection().then(() => _ensureIndexes()); const _catch = this.$init.catch; const _this = this; this.$init.catch = function() { _this.$caught = true; return _catch.apply(_this.$init, arguments); }; return this.$init; }; /** * Create the collection for this model. By default, if no indexes are specified, * mongoose will not create the collection for the model until any documents are * created. Use this method to create the collection explicitly. * * Note 1: You may need to call this before starting a transaction * See https://www.mongodb.com/docs/manual/core/transactions/#transactions-and-operations * * Note 2: You don't have to call this if your schema contains index or unique field. * In that case, just use `Model.init()` * * #### Example: * * const userSchema = new Schema({ name: String }) * const User = mongoose.model('User', userSchema); * * User.createCollection().then(function(collection) { * console.log('Collection is created!'); * }); * * @api public * @param {Object} [options] see [MongoDB driver docs](https://mongodb.github.io/node-mongodb-native/4.9/classes/Db.html#createCollection) * @returns {Promise} */ Model.createCollection = async function createCollection(options) { _checkContext(this, 'createCollection'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') { throw new MongooseError('Model.createCollection() no longer accepts a callback'); } const collectionOptions = this && this.schema && this.schema.options && this.schema.options.collectionOptions; if (collectionOptions != null) { options = Object.assign({}, collectionOptions, options); } const schemaCollation = this && this.schema && this.schema.options && this.schema.options.collation; if (schemaCollation != null) { options = Object.assign({ collation: schemaCollation }, options); } const capped = this && this.schema && this.schema.options && this.schema.options.capped; if (capped != null) { if (typeof capped === 'number') { options = Object.assign({ capped: true, size: capped }, options); } else if (typeof capped === 'object') { options = Object.assign({ capped: true }, capped, options); } } const timeseries = this && this.schema && this.schema.options && this.schema.options.timeseries; if (timeseries != null) { options = Object.assign({ timeseries }, options); if (options.expireAfterSeconds != null) { // do nothing } else if (options.expires != null) { utils.expires(options); } else if (this.schema.options.expireAfterSeconds != null) { options.expireAfterSeconds = this.schema.options.expireAfterSeconds; } else if (this.schema.options.expires != null) { options.expires = this.schema.options.expires; utils.expires(options); } } const clusteredIndex = this && this.schema && this.schema.options && this.schema.options.clusteredIndex; if (clusteredIndex != null) { options = Object.assign({ clusteredIndex: { ...clusteredIndex, unique: true } }, options); } try { await this.db.createCollection(this.$__collection.collectionName, options); } catch (err) { if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) { throw err; } } return this.$__collection; }; /** * Makes the indexes in MongoDB match the indexes defined in this model's * schema. This function will drop any indexes that are not defined in * the model's schema except the `_id` index, and build any indexes that * are in your schema but not in MongoDB. * * See the [introductory blog post](https://thecodebarbarian.com/whats-new-in-mongoose-5-2-syncindexes) * for more information. * * #### Example: * * const schema = new Schema({ name: { type: String, unique: true } }); * const Customer = mongoose.model('Customer', schema); * await Customer.collection.createIndex({ age: 1 }); // Index is not in schema * // Will drop the 'age' index and create an index on `name` * await Customer.syncIndexes(); * * You should be careful about running `syncIndexes()` on production applications under heavy load, * because index builds are expensive operations, and unexpected index drops can lead to degraded * performance. Before running `syncIndexes()`, you can use the [`diffIndexes()` function](#Model.diffIndexes()) * to check what indexes `syncIndexes()` will drop and create. * * #### Example: * * const { toDrop, toCreate } = await Model.diffIndexes(); * toDrop; // Array of strings containing names of indexes that `syncIndexes()` will drop * toCreate; // Array of strings containing names of indexes that `syncIndexes()` will create * * @param {Object} [options] options to pass to `ensureIndexes()` * @param {Boolean} [options.background=null] if specified, overrides each index's `background` property * @return {Promise} * @api public */ Model.syncIndexes = async function syncIndexes(options) { _checkContext(this, 'syncIndexes'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') { throw new MongooseError('Model.syncIndexes() no longer accepts a callback'); } const model = this; try { await model.createCollection(); } catch (err) { if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) { throw err; } } const diffIndexesResult = await model.diffIndexes(); const dropped = await model.cleanIndexes({ ...options, toDrop: diffIndexesResult.toDrop }); await model.createIndexes({ ...options, toCreate: diffIndexesResult.toCreate }); return dropped; }; /** * Does a dry-run of `Model.syncIndexes()`, returning the indexes that `syncIndexes()` would drop and create if you were to run `syncIndexes()`. * * #### Example: * * const { toDrop, toCreate } = await Model.diffIndexes(); * toDrop; // Array of strings containing names of indexes that `syncIndexes()` will drop * toCreate; // Array of strings containing names of indexes that `syncIndexes()` will create * * @param {Object} [options] * @return {Promise<Object>} contains the indexes that would be dropped in MongoDB and indexes that would be created in MongoDB as `{ toDrop: string[], toCreate: string[] }`. */ Model.diffIndexes = async function diffIndexes() { if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') { throw new MongooseError('Model.syncIndexes() no longer accepts a callback'); } const model = this; let dbIndexes = await model.listIndexes(); if (dbIndexes === undefined) { dbIndexes = []; } dbIndexes = getRelatedDBIndexes(model, dbIndexes); const schema = model.schema; const schemaIndexes = getRelatedSchemaIndexes(model, schema.indexes()); const toDrop = getIndexesToDrop(schema, schemaIndexes, dbIndexes); const toCreate = getIndexesToCreate(schema, schemaIndexes, dbIndexes, toDrop); return { toDrop, toCreate }; }; function getIndexesToCreate(schema, schemaIndexes, dbIndexes, toDrop) { const toCreate = []; for (const [schemaIndexKeysObject, schemaIndexOptions] of schemaIndexes) { let found = false; const options = decorateDiscriminatorIndexOptions(schema, clone(schemaIndexOptions)); for (const index of dbIndexes) { if (isDefaultIdIndex(index)) { continue; } if ( isIndexEqual(schemaIndexKeysObject, options, index) && !toDrop.includes(index.name) ) { found = true; break; } } if (!found) { toCreate.push(schemaIndexKey