'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} 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(schemaIndexKeysObject); } } return toCreate; } function getIndexesToDrop(schema, schemaIndexes, dbIndexes) { const toDrop = []; for (const dbIndex of dbIndexes) { let found = false; // Never try to drop `_id` index, MongoDB server doesn't allow it if (isDefaultIdIndex(dbIndex)) { continue; } for (const [schemaIndexKeysObject, schemaIndexOptions] of schemaIndexes) { const options = decorateDiscriminatorIndexOptions(schema, clone(schemaIndexOptions)); applySchemaCollation(schemaIndexKeysObject, options, schema.options); if (isIndexEqual(schemaIndexKeysObject, options, dbIndex)) { found = true; break; } } if (!found) { toDrop.push(dbIndex.name); } } return toDrop; } /** * Deletes all indexes that aren't defined in this model's schema. Used by * `syncIndexes()`. * * The returned promise resolves to a list of the dropped indexes' names as an array * * @param {Function} [callback] optional callback * @return {Promise|undefined} Returns `undefined` if callback is specified, returns a promise if no callback. * @api public */ Model.cleanIndexes = async function cleanIndexes(options) { _checkContext(this, 'cleanIndexes'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') { throw new MongooseError('Model.cleanIndexes() no longer accepts a callback'); } const model = this; const collection = model.$__collection; if (Array.isArray(options && options.toDrop)) { const res = await _dropIndexes(options.toDrop, collection); return res; } const res = await model.diffIndexes(); return await _dropIndexes(res.toDrop, collection); }; async function _dropIndexes(toDrop, collection) { if (toDrop.length === 0) { return []; } await Promise.all(toDrop.map(indexName => collection.dropIndex(indexName))); return toDrop; } /** * Lists the indexes currently defined in MongoDB. This may or may not be * the same as the indexes defined in your schema depending on whether you * use the [`autoIndex` option](https://mongoosejs.com/docs/guide.html#autoIndex) and if you * build indexes manually. * * @return {Promise} * @api public */ Model.listIndexes = async function listIndexes() { _checkContext(this, 'listIndexes'); if (typeof arguments[0] === 'function') { throw new MongooseError('Model.listIndexes() no longer accepts a callback'); } if (this.$__collection.buffer) { await new Promise(resolve => { this.$__collection.addQueue(resolve); }); } return this.$__collection.listIndexes().toArray(); }; /** * Sends `createIndex` commands to mongo for each index declared in the schema. * The `createIndex` commands are sent in series. * * #### Example: * * Event.ensureIndexes(function (err) { * if (err) return handleError(err); * }); * * After completion, an `index` event is emitted on this `Model` passing an error if one occurred. * * #### Example: * * const eventSchema = new Schema({ thing: { type: 'string', unique: true } }) * const Event = mongoose.model('Event', eventSchema); * * Event.on('index', function (err) { * if (err) console.error(err); // error occurred during index creation * }) * * _NOTE: It is not recommended that you run this in production. Index creation may impact database performance depending on your load. Use with caution._ * * @param {Object} [options] internal options * @return {Promise} * @api public */ Model.ensureIndexes = async function ensureIndexes(options) { _checkContext(this, 'ensureIndexes'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') { throw new MongooseError('Model.ensureIndexes() no longer accepts a callback'); } await new Promise((resolve, reject) => { _ensureIndexes(this, options, (err) => { if (err != null) { return reject(err); } resolve(); }); }); }; /** * Similar to `ensureIndexes()`, except for it uses the [`createIndex`](https://mongodb.github.io/node-mongodb-native/4.9/classes/Db.html#createIndex) * function. * * @param {Object} [options] internal options * @return {Promise} * @api public */ Model.createIndexes = async function createIndexes(options) { _checkContext(this, 'createIndexes'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') { throw new MongooseError('Model.createIndexes() no longer accepts a callback'); } return this.ensureIndexes(options); }; /*! * ignore */ function _ensureIndexes(model, options, callback) { const indexes = model.schema.indexes(); let indexError; options = options || {}; const done = function(err) { if (err && !model.$caught) { model.emit('error', err); } model.emit('index', err || indexError); callback && callback(err || indexError); }; for (const index of indexes) { if (isDefaultIdIndex(index)) { utils.warn('mongoose: Cannot specify a custom index on `_id` for ' + 'model name "' + model.modelName + '", ' + 'MongoDB does not allow overwriting the default `_id` index. See ' + 'https://bit.ly/mongodb-id-index'); } } if (!indexes.length) { immediate(function() { done(); }); return; } // Indexes are created one-by-one to support how MongoDB < 2.4 deals // with background indexes. const indexSingleDone = function(err, fields, options, name) { model.emit('index-single-done', err, fields, options, name); }; const indexSingleStart = function(fields, options) { model.emit('index-single-start', fields, options); }; const baseSchema = model.schema._baseSchema; const baseSchemaIndexes = baseSchema ? baseSchema.indexes() : []; immediate(function() { // If buffering is off, do this manually. if (options._automatic && !model.collection.collection) { model.collection.addQueue(create, []); } else { create(); } }); function create() { if (options._automatic) { if (model.schema.options.autoIndex === false || (model.schema.options.autoIndex == null && model.db.config.autoIndex === false)) { return done(); } } const index = indexes.shift(); if (!index) { return done(); } if (options._automatic && index[1]._autoIndex === false) { return create(); } if (baseSchemaIndexes.find(i => utils.deepEqual(i, index))) { return create(); } const indexFields = clone(index[0]); const indexOptions = clone(index[1]); delete indexOptions._autoIndex; decorateDiscriminatorIndexOptions(model.schema, indexOptions); applyWriteConcern(model.schema, indexOptions); applySchemaCollation(indexFields, indexOptions, model.schema.options); indexSingleStart(indexFields, options); if ('background' in options) { indexOptions.background = options.background; } if ('toCreate' in options) { if (options.toCreate.length === 0) { return done(); } } model.collection.createIndex(indexFields, indexOptions).then( name => { indexSingleDone(null, indexFields, indexOptions, name); create(); }, err => { if (!indexError) { indexError = err; } if (!model.$caught) { model.emit('error', err); } indexSingleDone(err, indexFields, indexOptions); create(); } ); } } /** * Schema the model uses. * * @property schema * @static * @api public * @memberOf Model */ Model.schema; /** * Connection instance the model uses. * * @property db * @static * @api public * @memberOf Model */ Model.db; /** * Collection the model uses. * * @property collection * @api public * @memberOf Model */ Model.collection; /** * Internal collection the model uses. * * @property collection * @api private * @memberOf Model */ Model.$__collection; /** * Base Mongoose instance the model uses. * * @property base * @api public * @memberOf Model */ Model.base; /** * Registered discriminators for this model. * * @property discriminators * @api public * @memberOf Model */ Model.discriminators; /** * Translate any aliases fields/conditions so the final query or document object is pure * * #### Example: * * await Character.find(Character.translateAliases({ * '名': 'Eddard Stark' // Alias for 'name' * }); * * By default, `translateAliases()` overwrites raw fields with aliased fields. * So if `n` is an alias for `name`, `{ n: 'alias', name: 'raw' }` will resolve to `{ name: 'alias' }`. * However, you can set the `errorOnDuplicates` option to throw an error if there are potentially conflicting paths. * The `translateAliases` option for queries uses `errorOnDuplicates`. * * #### Note: * * Only translate arguments of object type anything else is returned raw * * @param {Object} fields fields/conditions that may contain aliased keys * @param {Boolean} [errorOnDuplicates] if true, throw an error if there's both a key and an alias for that key in `fields` * @return {Object} the translated 'pure' fields/conditions */ Model.translateAliases = function translateAliases(fields, errorOnDuplicates) { _checkContext(this, 'translateAliases'); const translate = (key, value) => { let alias; const translated = []; const fieldKeys = key.split('.'); let currentSchema = this.schema; for (const i in fieldKeys) { const name = fieldKeys[i]; if (currentSchema && currentSchema.aliases[name]) { alias = currentSchema.aliases[name]; if (errorOnDuplicates && alias in fields) { throw new MongooseError(`Provided object has both field "${name}" and its alias "${alias}"`); } // Alias found, translated.push(alias); } else { alias = name; // Alias not found, so treat as un-aliased key translated.push(name); } // Check if aliased path is a schema if (currentSchema && currentSchema.paths[alias]) { currentSchema = currentSchema.paths[alias].schema; } else currentSchema = null; } const translatedKey = translated.join('.'); if (fields instanceof Map) fields.set(translatedKey, value); else fields[translatedKey] = value; if (translatedKey !== key) { // We'll be using the translated key instead if (fields instanceof Map) { // Delete from map fields.delete(key); } else { // Delete from object delete fields[key]; // We'll be using the translated key instead } } return fields; }; if (typeof fields === 'object') { // Fields is an object (query conditions or document fields) if (fields instanceof Map) { // A Map was supplied for (const field of new Map(fields)) { fields = translate(field[0], field[1]); } } else { // Infer a regular object was supplied for (const key of Object.keys(fields)) { fields = translate(key, fields[key]); if (key[0] === '$') { if (Array.isArray(fields[key])) { for (const i in fields[key]) { // Recursively translate nested queries fields[key][i] = this.translateAliases(fields[key][i]); } } else { this.translateAliases(fields[key]); } } } } return fields; } else { // Don't know typeof fields return fields; } }; /** * Deletes the first document that matches `conditions` from the collection. * It returns an object with the property `deletedCount` indicating how many documents were deleted. * Behaves like `remove()`, but deletes at most one document regardless of the * `single` option. * * #### Example: * * await Character.deleteOne({ name: 'Eddard Stark' }); // returns {deletedCount: 1} * * #### Note: * * This function triggers `deleteOne` query hooks. Read the * [middleware docs](https://mongoosejs.com/docs/middleware.html#naming) to learn more. * * @param {Object} conditions * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @api public */ Model.deleteOne = function deleteOne(conditions, options) { _checkContext(this, 'deleteOne'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.prototype.deleteOne() no longer accepts a callback'); } const mq = new this.Query({}, {}, this, this.$__collection); mq.setOptions(options); return mq.deleteOne(conditions); }; /** * Deletes all of the documents that match `conditions` from the collection. * It returns an object with the property `deletedCount` containing the number of documents deleted. * Behaves like `remove()`, but deletes all documents that match `conditions` * regardless of the `single` option. * * #### Example: * * await Character.deleteMany({ name: /Stark/, age: { $gte: 18 } }); // returns {deletedCount: x} where x is the number of documents deleted. * * #### Note: * * This function triggers `deleteMany` query hooks. Read the * [middleware docs](https://mongoosejs.com/docs/middleware.html#naming) to learn more. * * @param {Object} conditions * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @api public */ Model.deleteMany = function deleteMany(conditions, options) { _checkContext(this, 'deleteMany'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.deleteMany() no longer accepts a callback'); } const mq = new this.Query({}, {}, this, this.$__collection); mq.setOptions(options); return mq.deleteMany(conditions); }; /** * Finds documents. * * Mongoose casts the `filter` to match the model's schema before the command is sent. * See our [query casting tutorial](https://mongoosejs.com/docs/tutorials/query_casting.html) for * more information on how Mongoose casts `filter`. * * #### Example: * * // find all documents * await MyModel.find({}); * * // find all documents named john and at least 18 * await MyModel.find({ name: 'john', age: { $gte: 18 } }).exec(); * * // executes, name LIKE john and only selecting the "name" and "friends" fields * await MyModel.find({ name: /john/i }, 'name friends').exec(); * * // passing options * await MyModel.find({ name: /john/i }, null, { skip: 10 }).exec(); * * @param {Object|ObjectId} filter * @param {Object|String|String[]} [projection] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()) * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see field selection https://mongoosejs.com/docs/api/query.html#Query.prototype.select() * @see query casting https://mongoosejs.com/docs/tutorials/query_casting.html * @api public */ Model.find = function find(conditions, projection, options) { _checkContext(this, 'find'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Model.find() no longer accepts a callback'); } const mq = new this.Query({}, {}, this, this.$__collection); mq.select(projection); mq.setOptions(options); return mq.find(conditions); }; /** * Finds a single document by its _id field. `findById(id)` is almost* * equivalent to `findOne({ _id: id })`. If you want to query by a document's * `_id`, use `findById()` instead of `findOne()`. * * The `id` is cast based on the Schema before sending the command. * * This function triggers the following middleware. * * - `findOne()` * * \* Except for how it treats `undefined`. If you use `findOne()`, you'll see * that `findOne(undefined)` and `findOne({ _id: undefined })` are equivalent * to `findOne({})` and return arbitrary documents. However, mongoose * translates `findById(undefined)` into `findOne({ _id: null })`. * * #### Example: * * // Find the adventure with the given `id`, or `null` if not found * await Adventure.findById(id).exec(); * * // select only the adventures name and length * await Adventure.findById(id, 'name length').exec(); * * @param {Any} id value of `_id` to query by * @param {Object|String|String[]} [projection] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()) * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @return {Query} * @see field selection https://mongoosejs.com/docs/api/query.html#Query.prototype.select() * @see lean queries https://mongoosejs.com/docs/tutorials/lean.html * @see findById in Mongoose https://masteringjs.io/tutorials/mongoose/find-by-id * @api public */ Model.findById = function findById(id, projection, options) { _checkContext(this, 'findById'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.findById() no longer accepts a callback'); } if (typeof id === 'undefined') { id = null; } return this.findOne({ _id: id }, projection, options); }; /** * Finds one document. * * The `conditions` are cast to their respective SchemaTypes before the command is sent. * * *Note:* `conditions` is optional, and if `conditions` is null or undefined, * mongoose will send an empty `findOne` command to MongoDB, which will return * an arbitrary document. If you're querying by `_id`, use `findById()` instead. * * #### Example: * * // Find one adventure whose `country` is 'Croatia', otherwise `null` * await Adventure.findOne({ country: 'Croatia' }).exec(); * * // Model.findOne() no longer accepts a callback * * // Select only the adventures name and length * await Adventure.findOne({ country: 'Croatia' }, 'name length').exec(); * * @param {Object} [conditions] * @param {Object|String|String[]} [projection] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()) * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see field selection https://mongoosejs.com/docs/api/query.html#Query.prototype.select() * @see lean queries https://mongoosejs.com/docs/tutorials/lean.html * @api public */ Model.findOne = function findOne(conditions, projection, options) { _checkContext(this, 'findOne'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.findOne() no longer accepts a callback'); } const mq = new this.Query({}, {}, this, this.$__collection); mq.select(projection); mq.setOptions(options); return mq.findOne(conditions); }; /** * Estimates the number of documents in the MongoDB collection. Faster than * using `countDocuments()` for large collections because * `estimatedDocumentCount()` uses collection metadata rather than scanning * the entire collection. * * #### Example: * * const numAdventures = await Adventure.estimatedDocumentCount(); * * @param {Object} [options] * @return {Query} * @api public */ Model.estimatedDocumentCount = function estimatedDocumentCount(options) { _checkContext(this, 'estimatedDocumentCount'); const mq = new this.Query({}, {}, this, this.$__collection); return mq.estimatedDocumentCount(options); }; /** * Counts number of documents matching `filter` in a database collection. * * #### Example: * * Adventure.countDocuments({ type: 'jungle' }, function (err, count) { * console.log('there are %d jungle adventures', count); * }); * * If you want to count all documents in a large collection, * use the [`estimatedDocumentCount()` function](https://mongoosejs.com/docs/api/model.html#Model.estimatedDocumentCount()) * instead. If you call `countDocuments({})`, MongoDB will always execute * a full collection scan and **not** use any indexes. * * The `countDocuments()` function is similar to `count()`, but there are a * [few operators that `countDocuments()` does not support](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#countDocuments). * Below are the operators that `count()` supports but `countDocuments()` does not, * and the suggested replacement: * * - `$where`: [`$expr`](https://www.mongodb.com/docs/manual/reference/operator/query/expr/) * - `$near`: [`$geoWithin`](https://www.mongodb.com/docs/manual/reference/operator/query/geoWithin/) with [`$center`](https://www.mongodb.com/docs/manual/reference/operator/query/center/#op._S_center) * - `$nearSphere`: [`$geoWithin`](https://www.mongodb.com/docs/manual/reference/operator/query/geoWithin/) with [`$centerSphere`](https://www.mongodb.com/docs/manual/reference/operator/query/centerSphere/#op._S_centerSphere) * * @param {Object} filter * @return {Query} * @api public */ Model.countDocuments = function countDocuments(conditions, options) { _checkContext(this, 'countDocuments'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.countDocuments() no longer accepts a callback'); } const mq = new this.Query({}, {}, this, this.$__collection); if (options != null) { mq.setOptions(options); } return mq.countDocuments(conditions); }; /** * Counts number of documents that match `filter` in a database collection. * * This method is deprecated. If you want to count the number of documents in * a collection, e.g. `count({})`, use the [`estimatedDocumentCount()` function](https://mongoosejs.com/docs/api/model.html#Model.estimatedDocumentCount()) * instead. Otherwise, use the [`countDocuments()`](https://mongoosejs.com/docs/api/model.html#Model.countDocuments()) function instead. * * #### Example: * * const count = await Adventure.count({ type: 'jungle' }); * console.log('there are %d jungle adventures', count); * * @deprecated * @param {Object} [filter] * @return {Query} * @api public */ Model.count = function count(conditions) { _checkContext(this, 'count'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') { throw new MongooseError('Model.count() no longer accepts a callback'); } const mq = new this.Query({}, {}, this, this.$__collection); return mq.count(conditions); }; /** * Creates a Query for a `distinct` operation. * * #### Example: * * const query = Link.distinct('url'); * query.exec(); * * @param {String} field * @param {Object} [conditions] optional * @return {Query} * @api public */ Model.distinct = function distinct(field, conditions) { _checkContext(this, 'distinct'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') { throw new MongooseError('Model.distinct() no longer accepts a callback'); } const mq = new this.Query({}, {}, this, this.$__collection); return mq.distinct(field, conditions); }; /** * Creates a Query, applies the passed conditions, and returns the Query. * * For example, instead of writing: * * User.find({ age: { $gte: 21, $lte: 65 } }); * * we can instead write: * * User.where('age').gte(21).lte(65).exec(); * * Since the Query class also supports `where` you can continue chaining * * User * .where('age').gte(21).lte(65) * .where('name', /^b/i) * ... etc * * @param {String} path * @param {Object} [val] optional value * @return {Query} * @api public */ Model.where = function where(path, val) { _checkContext(this, 'where'); void val; // eslint const mq = new this.Query({}, {}, this, this.$__collection).find({}); return mq.where.apply(mq, arguments); }; /** * Creates a `Query` and specifies a `$where` condition. * * Sometimes you need to query for things in mongodb using a JavaScript expression. You can do so via `find({ $where: javascript })`, or you can use the mongoose shortcut method $where via a Query chain or from your mongoose Model. * * Blog.$where('this.username.indexOf("val") !== -1').exec(function (err, docs) {}); * * @param {String|Function} argument is a javascript string or anonymous function * @method $where * @memberOf Model * @return {Query} * @see Query.$where https://mongoosejs.com/docs/api/query.html#Query.prototype.$where * @api public */ Model.$where = function $where() { _checkContext(this, '$where'); const mq = new this.Query({}, {}, this, this.$__collection).find({}); return mq.$where.apply(mq, arguments); }; /** * Issues a mongodb findOneAndUpdate command. * * Finds a matching document, updates it according to the `update` arg, passing any `options`, and returns the found document (if any) to the callback. The query executes if `callback` is passed else a Query object is returned. * * #### Example: * * A.findOneAndUpdate(conditions, update, options) // returns Query * A.findOneAndUpdate(conditions, update) // returns Query * A.findOneAndUpdate() // returns Query * * #### Note: * * All top level update keys which are not `atomic` operation names are treated as set operations: * * #### Example: * * const query = { name: 'borne' }; * Model.findOneAndUpdate(query, { name: 'jason bourne' }, options) * * // is sent as * Model.findOneAndUpdate(query, { $set: { name: 'jason bourne' }}, options) * * #### Note: * * `findOneAndX` and `findByIdAndX` functions support limited validation that * you can enable by setting the `runValidators` option. * * If you need full-fledged validation, use the traditional approach of first * retrieving the document. * * const doc = await Model.findById(id); * doc.name = 'jason bourne'; * await doc.save(); * * @param {Object} [conditions] * @param {Object} [update] * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {String} [options.returnDocument='before'] Has two possible values, `'before'` and `'after'`. By default, it will return the document before the update was applied. * @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 {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.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. * @param {Boolean} [options.overwrite=false] If set to `true`, Mongoose will convert this `findOneAndUpdate()` to a `findOneAndReplace()`. This option is deprecated and only supported for backwards compatiblity. * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()) * @param {Boolean} [options.new=false] if true, return the modified document rather than the original * @param {Object|String} [options.fields] Field selection. Equivalent to `.select(fields).findOneAndUpdate()` * @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0 * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. * @param {Boolean} [options.runValidators] if true, runs [update validators](https://mongoosejs.com/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema * @param {Boolean} [options.setDefaultsOnInsert=true] If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ * @api public */ Model.findOneAndUpdate = function(conditions, update, options) { _checkContext(this, 'findOneAndUpdate'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Model.findOneAndUpdate() no longer accepts a callback'); } if (arguments.length === 1) { update = conditions; conditions = null; options = null; } let fields; if (options) { fields = options.fields || options.projection; } update = clone(update, { depopulate: true, _isNested: true }); _decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey); const mq = new this.Query({}, {}, this, this.$__collection); mq.select(fields); return mq.findOneAndUpdate(conditions, update, options); }; /** * Decorate the update with a version key, if necessary * @api private */ function _decorateUpdateWithVersionKey(update, options, versionKey) { if (!versionKey || !(options && options.upsert || false)) { return; } const updatedPaths = modifiedPaths(update); if (!updatedPaths[versionKey]) { if (options.overwrite) { update[versionKey] = 0; } else { if (!update.$setOnInsert) { update.$setOnInsert = {}; } update.$setOnInsert[versionKey] = 0; } } } /** * Issues a mongodb findOneAndUpdate command by a document's _id field. * `findByIdAndUpdate(id, ...)` is equivalent to `findOneAndUpdate({ _id: id }, ...)`. * * Finds a matching document, updates it according to the `update` arg, * passing any `options`, and returns the found document (if any). * * This function triggers the following middleware. * * - `findOneAndUpdate()` * * #### Example: * * A.findByIdAndUpdate(id, update, options) // returns Query * A.findByIdAndUpdate(id, update) // returns Query * A.findByIdAndUpdate() // returns Query * * #### Note: * * All top level update keys which are not `atomic` operation names are treated as set operations: * * #### Example: * * Model.findByIdAndUpdate(id, { name: 'jason bourne' }, options) * * // is sent as * Model.findByIdAndUpdate(id, { $set: { name: 'jason bourne' }}, options) * * This helps prevent accidentally overwriting your document with `{ name: 'jason bourne' }`. * To prevent this behaviour, see the `overwrite` option * * #### Note: * * `findOneAndX` and `findByIdAndX` functions support limited validation. You can * enable validation by setting the `runValidators` option. * * If you need full-fledged validation, use the traditional approach of first * retrieving the document. * * const doc = await Model.findById(id) * doc.name = 'jason bourne'; * await doc.save(); * * @param {Object|Number|String} id value of `_id` to query by * @param {Object} [update] * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {String} [options.returnDocument='before'] Has two possible values, `'before'` and `'after'`. By default, it will return the document before the update was applied. * @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 {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.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. * @param {Boolean} [options.overwrite=false] If set to `true`, Mongoose will convert this `findByIdAndUpdate()` to a `findByIdAndReplace()`. This option is deprecated and only supported for backwards compatiblity. * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. * @param {Boolean} [options.runValidators] if true, runs [update validators](https://mongoosejs.com/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema * @param {Boolean} [options.setDefaultsOnInsert=true] If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Boolean} [options.new=false] if true, return the modified document rather than the original * @param {Object|String} [options.select] sets the document fields to return. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Model.findOneAndUpdate https://mongoosejs.com/docs/api/model.html#Model.findOneAndUpdate() * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ * @api public */ Model.findByIdAndUpdate = function(id, update, options) { _checkContext(this, 'findByIdAndUpdate'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Model.findByIdAndUpdate() no longer accepts a callback'); } // if a model is passed in instead of an id if (id instanceof Document) { id = id._id; } return this.findOneAndUpdate.call(this, { _id: id }, update, options); }; /** * Issue a MongoDB `findOneAndDelete()` command. * * Finds a matching document, removes it, and returns the found document (if any). * * This function triggers the following middleware. * * - `findOneAndDelete()` * * This function differs slightly from `Model.findOneAndRemove()` in that * `findOneAndRemove()` becomes a [MongoDB `findAndModify()` command](https://www.mongodb.com/docs/manual/reference/method/db.collection.findAndModify/), * as opposed to a `findOneAndDelete()` command. For most mongoose use cases, * this distinction is purely pedantic. You should use `findOneAndDelete()` * unless you have a good reason not to. * * #### Example: * * A.findOneAndDelete(conditions, options) // return Query * A.findOneAndDelete(conditions) // returns Query * A.findOneAndDelete() // returns Query * * `findOneAndX` and `findByIdAndX` functions support limited validation. You can * enable validation by setting the `runValidators` option. * * If you need full-fledged validation, use the traditional approach of first * retrieving the document. * * const doc = await Model.findById(id) * doc.name = 'jason bourne'; * await doc.save(); * * @param {Object} conditions * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()) * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html). * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. * @param {Object|String} [options.select] sets the document fields to return. * @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0 * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @api public */ Model.findOneAndDelete = function(conditions, options) { _checkContext(this, 'findOneAndDelete'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.findOneAndDelete() no longer accepts a callback'); } let fields; if (options) { fields = options.select; options.select = undefined; } const mq = new this.Query({}, {}, this, this.$__collection); mq.select(fields); return mq.findOneAndDelete(conditions, options); }; /** * Issue a MongoDB `findOneAndDelete()` command by a document's _id field. * In other words, `findByIdAndDelete(id)` is a shorthand for * `findOneAndDelete({ _id: id })`. * * This function triggers the following middleware. * * - `findOneAndDelete()` * * @param {Object|Number|String} id value of `_id` to query by * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Model.findOneAndRemove https://mongoosejs.com/docs/api/model.html#Model.findOneAndRemove() * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ */ Model.findByIdAndDelete = function(id, options) { _checkContext(this, 'findByIdAndDelete'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.findByIdAndDelete() no longer accepts a callback'); } return this.findOneAndDelete({ _id: id }, options); }; /** * Issue a MongoDB `findOneAndReplace()` command. * * Finds a matching document, replaces it with the provided doc, and returns the document. * * This function triggers the following query middleware. * * - `findOneAndReplace()` * * #### Example: * * A.findOneAndReplace(filter, replacement, options) // return Query * A.findOneAndReplace(filter, replacement) // returns Query * A.findOneAndReplace() // returns Query * * @param {Object} filter Replace the first document that matches this filter * @param {Object} [replacement] Replace with this document * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {String} [options.returnDocument='before'] Has two possible values, `'before'` and `'after'`. By default, it will return the document before the update was applied. * @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 {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.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. * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()) * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) * @param {Object|String} [options.select] sets the document fields to return. * @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0 * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @api public */ Model.findOneAndReplace = function(filter, replacement, options) { _checkContext(this, 'findOneAndReplace'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Model.findOneAndReplace() no longer accepts a callback'); } let fields; if (options) { fields = options.select; options.select = undefined; } const mq = new this.Query({}, {}, this, this.$__collection); mq.select(fields); return mq.findOneAndReplace(filter, replacement, options); }; /** * Issue a mongodb findOneAndRemove command. * * Finds a matching document, removes it, and returns the found document (if any). * * This function triggers the following middleware. * * - `findOneAndRemove()` * * #### Example: * * A.findOneAndRemove(conditions, options) // return Query * A.findOneAndRemove(conditions) // returns Query * A.findOneAndRemove() // returns Query * * `findOneAndX` and `findByIdAndX` functions support limited validation. You can * enable validation by setting the `runValidators` option. * * If you need full-fledged validation, use the traditional approach of first * retrieving the document. * * const doc = await Model.findById(id); * doc.name = 'jason bourne'; * await doc.save(); * * @param {Object} conditions * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html). * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()) * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) * @param {Object|String} [options.select] sets the document fields to return. * @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0 * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ * @api public */ Model.findOneAndRemove = function(conditions, options) { _checkContext(this, 'findOneAndRemove'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Model.findOneAndRemove() no longer accepts a callback'); } let fields; if (options) { fields = options.select; options.select = undefined; } const mq = new this.Query({}, {}, this, this.$__collection); mq.select(fields); return mq.findOneAndRemove(conditions, options); }; /** * Issue a mongodb findOneAndRemove command by a document's _id field. `findByIdAndRemove(id, ...)` is equivalent to `findOneAndRemove({ _id: id }, ...)`. * * Finds a matching document, removes it, and returns the found document (if any). * * This function triggers the following middleware. * * - `findOneAndRemove()` * * #### Example: * * A.findByIdAndRemove(id, options) // return Query * A.findByIdAndRemove(id) // returns Query * A.findByIdAndRemove() // returns Query * * @param {Object|Number|String} id value of `_id` to query by * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html). * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()) * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) * @param {Object|String} [options.select] sets the document fields to return. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Model.findOneAndRemove https://mongoosejs.com/docs/api/model.html#Model.findOneAndRemove() * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ */ Model.findByIdAndRemove = function(id, options) { _checkContext(this, 'findByIdAndRemove'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Model.findByIdAndRemove() no longer accepts a callback'); } return this.findOneAndRemove({ _id: id }, options); }; /** * Shortcut for saving one or more documents to the database. * `MyModel.create(docs)` does `new MyModel(doc).save()` for every doc in * docs. * * This function triggers the following middleware. * * - `save()` * * #### Example: * * // Insert one new `Character` document * await Character.create({ name: 'Jean-Luc Picard' }); * * // Insert multiple new `Character` documents * await Character.create([{ name: 'Will Riker' }, { name: 'Geordi LaForge' }]); * * // Create a new character within a transaction. Note that you **must** * // pass an array as the first parameter to `create()` if you want to * // specify options. * await Character.create([{ name: 'Jean-Luc Picard' }], { session }); * * @param {Array|Object} docs Documents to insert, as a spread or array * @param {Object} [options] Options passed down to `save()`. To specify `options`, `docs` **must** be an array, not a spread. See [Model.save](https://mongoosejs.com/docs/api/model.html#Model.prototype.save()) for available options. * @param {Boolean} [options.ordered] saves the docs in series rather than parallel. * @param {Boolean} [options.aggregateErrors] Aggregate Errors instead of throwing the first one that occurs. Default: false * @return {Promise} * @api public */ Model.create = async function create(doc, options) { if (typeof options === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.create() no longer accepts a callback'); } _checkContext(this, 'create'); let args; const discriminatorKey = this.schema.options.discriminatorKey; if (Array.isArray(doc)) { args = doc; options = options != null && typeof options === 'object' ? options : {}; } else { const last = arguments[arguments.length - 1]; options = {}; const hasCallback = typeof last === 'function' || typeof options === 'function' || typeof arguments[2] === 'function'; if (hasCallback) { throw new MongooseError('Model.create() no longer accepts a callback'); } else { args = [...arguments]; // For backwards compatibility with 6.x, because of gh-5061 Mongoose 6.x and // older would treat a falsy last arg as a callback. We don't want to throw // an error here, because it would look strange if `Test.create({}, void 0)` // threw a callback error. But we also don't want to create an unnecessary document. if (args.length > 1 && !last) { args.pop(); } } if (args.length === 2 && args[0] != null && args[1] != null && args[0].session == null && last && getConstructorName(last.session) === 'ClientSession' && !this.schema.path('session')) { // Probably means the user is running into the common mistake of trying // to use a spread to specify options, see gh-7535 utils.warn('WARNING: to pass a `session` to `Model.create()` in ' + 'Mongoose, you **must** pass an array as the first argument. See: ' + 'https://mongoosejs.com/docs/api/model.html#Model.create()'); } } if (args.length === 0) { return Array.isArray(doc) ? [] : null; } let res = []; const immediateError = typeof options.aggregateErrors === 'boolean' ? !options.aggregateErrors : true; delete options.aggregateErrors; // dont pass on the option to "$save" if (options.ordered) { for (let i = 0; i < args.length; i++) { try { const doc = args[i]; const Model = this.discriminators && doc[discriminatorKey] != null ? this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this.discriminators, doc[discriminatorKey]) : this; if (Model == null) { throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` + `found for model "${this.modelName}"`); } let toSave = doc; if (!(toSave instanceof Model)) { toSave = new Model(toSave); } await toSave.$save(options); res.push(toSave); } catch (err) { if (!immediateError) { res.push(err); } else { throw err; } } } return res; } else { // ".bind(Promise)" is required, otherwise results in "TypeError: Promise.allSettled called on non-object" const promiseType = !immediateError ? Promise.allSettled.bind(Promise) : Promise.all.bind(Promise); let p = promiseType(args.map(async doc => { const Model = this.discriminators && doc[discriminatorKey] != null ? this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this.discriminators, doc[discriminatorKey]) : this; if (Model == null) { throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` + `found for model "${this.modelName}"`); } let toSave = doc; if (!(toSave instanceof Model)) { toSave = new Model(toSave); } await toSave.$save(options); return toSave; })); // chain the mapper, only if "allSettled" is used if (!immediateError) { p = p.then(presult => presult.map(v => v.status === 'fulfilled' ? v.value : v.reason)); } res = await p; } if (!Array.isArray(doc) && args.length === 1) { return res[0]; } return res; }; /** * _Requires a replica set running MongoDB >= 3.6.0._ Watches the * underlying collection for changes using * [MongoDB change streams](https://www.mongodb.com/docs/manual/changeStreams/). * * This function does **not** trigger any middleware. In particular, it * does **not** trigger aggregate middleware. * * The ChangeStream object is an event emitter that emits the following events: * * - 'change': A change occurred, see below example * - 'error': An unrecoverable error occurred. In particular, change streams currently error out if they lose connection to the replica set primary. Follow [this GitHub issue](https://github.com/Automattic/mongoose/issues/6799) for updates. * - 'end': Emitted if the underlying stream is closed * - 'close': Emitted if the underlying stream is closed * * #### Example: * * const doc = await Person.create({ name: 'Ned Stark' }); * const changeStream = Person.watch().on('change', change => console.log(change)); * // Will print from the above `console.log()`: * // { _id: { _data: ... }, * // operationType: 'delete', * // ns: { db: 'mydb', coll: 'Person' }, * // documentKey: { _id: 5a51b125c5500f5aa094c7bd } } * await doc.remove(); * * @param {Array} [pipeline] * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#watch) * @param {Boolean} [options.hydrate=false] if true and `fullDocument: 'updateLookup'` is set, Mongoose will automatically hydrate `fullDocument` into a fully fledged Mongoose document * @return {ChangeStream} mongoose-specific change stream wrapper, inherits from EventEmitter * @api public */ Model.watch = function(pipeline, options) { _checkContext(this, 'watch'); const changeStreamThunk = cb => { pipeline = pipeline || []; prepareDiscriminatorPipeline(pipeline, this.schema, 'fullDocument'); if (this.$__collection.buffer) { this.$__collection.addQueue(() => { if (this.closed) { return; } const driverChangeStream = this.$__collection.watch(pipeline, options); cb(null, driverChangeStream); }); } else { const driverChangeStream = this.$__collection.watch(pipeline, options); cb(null, driverChangeStream); } }; options = options || {}; options.model = this; return new ChangeStream(changeStreamThunk, pipeline, options); }; /** * _Requires MongoDB >= 3.6.0._ Starts a [MongoDB session](https://www.mongodb.com/docs/manual/release-notes/3.6/#client-sessions) * for benefits like causal consistency, [retryable writes](https://www.mongodb.com/docs/manual/core/retryable-writes/), * and [transactions](https://thecodebarbarian.com/a-node-js-perspective-on-mongodb-4-transactions.html). * * Calling `MyModel.startSession()` is equivalent to calling `MyModel.db.startSession()`. * * This function does not trigger any middleware. * * #### Example: * * const session = await Person.startSession(); * let doc = await Person.findOne({ name: 'Ned Stark' }, null, { session }); * await doc.remove(); * // `doc` will always be null, even if reading from a replica set * // secondary. Without causal consistency, it is possible to * // get a doc back from the below query if the query reads from a * // secondary that is experiencing replication lag. * doc = await Person.findOne({ name: 'Ned Stark' }, null, { session, readPreference: 'secondary' }); * * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/MongoClient.html#startSession) * @param {Boolean} [options.causalConsistency=true] set to false to disable causal consistency * @return {Promise} promise that resolves to a MongoDB driver `ClientSession` * @api public */ Model.startSession = function() { _checkContext(this, 'startSession'); return this.db.startSession.apply(this.db, arguments); }; /** * Shortcut for validating an array of documents and inserting them into * MongoDB if they're all valid. This function is faster than `.create()` * because it only sends one operation to the server, rather than one for each * document. * * Mongoose always validates each document **before** sending `insertMany` * to MongoDB. So if one document has a validation error, no documents will * be saved, unless you set * [the `ordered` option to false](https://www.mongodb.com/docs/manual/reference/method/db.collection.insertMany/#error-handling). * * This function does **not** trigger save middleware. * * This function triggers the following middleware. * * - `insertMany()` * * #### Example: * * await Movies.insertMany([ * { name: 'Star Wars' }, * { name: 'The Empire Strikes Back' } * ]); * * @param {Array|Object|*} doc(s) * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#insertMany) * @param {Boolean} [options.ordered=true] if true, will fail fast on the first error encountered. If false, will insert all the documents it can and report errors later. An `insertMany()` with `ordered = false` is called an "unordered" `insertMany()`. * @param {Boolean} [options.rawResult=false] if false, the returned promise resolves to the documents that passed mongoose document validation. If `true`, will return the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/InsertManyResult.html) with a `mongoose` property that contains `validationErrors` and `results` if this is an unordered `insertMany`. * @param {Boolean} [options.lean=false] if `true`, skips hydrating and validating the documents. This option is useful if you need the extra performance, but Mongoose won't validate the documents before inserting. * @param {Number} [options.limit=null] this limits the number of documents being processed (validation/casting) by mongoose in parallel, this does **NOT** send the documents in batches to MongoDB. Use this option if you're processing a large number of documents and your app is running out of memory. * @param {String|Object|Array} [options.populate=null] populates the result documents. This option is a no-op if `rawResult` is set. * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. * @return {Promise} resolving to the raw result from the MongoDB driver if `options.rawResult` was `true`, or the documents that passed validation, otherwise * @api public */ Model.insertMany = async function insertMany(arr, options) { _checkContext(this, 'insertMany'); if (typeof options === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.insertMany() no longer accepts a callback'); } return new Promise((resolve, reject) => { this.$__insertMany(arr, options, (err, res) => { if (err != null) { return reject(err); } resolve(res); }); }); }; /** * ignore * * @param {Array} arr * @param {Object} options * @param {Function} callback * @api private * @memberOf Model * @method $__insertMany * @static */ Model.$__insertMany = function(arr, options, callback) { const _this = this; if (typeof options === 'function') { callback = options; options = null; } callback = callback || utils.noop; options = options || {}; const limit = options.limit || 1000; const rawResult = !!options.rawResult; const ordered = typeof options.ordered === 'boolean' ? options.ordered : true; const throwOnValidationError = typeof options.throwOnValidationError === 'boolean' ? options.throwOnValidationError : false; const lean = !!options.lean; if (!Array.isArray(arr)) { arr = [arr]; } const validationErrors = []; const validationErrorsToOriginalOrder = new Map(); const results = ordered ? null : new Array(arr.length); const toExecute = arr.map((doc, index) => callback => { if (!(doc instanceof _this)) { try { doc = new _this(doc); } catch (err) { return callback(err); } } if (options.session != null) { doc.$session(options.session); } // If option `lean` is set to true bypass validation if (lean) { // we have to execute callback at the nextTick to be compatible // with parallelLimit, as `results` variable has TDZ issue if we // execute the callback synchronously return immediate(() => callback(null, doc)); } doc.$validate().then( () => { callback(null, doc); }, error => { if (ordered === false) { validationErrors.push(error); validationErrorsToOriginalOrder.set(error, index); results[index] = error; return callback(null, null); } callback(error); } ); }); parallelLimit(toExecute, limit, function(error, docs) { if (error) { callback(error, null); return; } const originalDocIndex = new Map(); const validDocIndexToOriginalIndex = new Map(); for (let i = 0; i < docs.length; ++i) { originalDocIndex.set(docs[i], i); } // We filter all failed pre-validations by removing nulls const docAttributes = docs.filter(function(doc) { return doc != null; }); for (let i = 0; i < docAttributes.length; ++i) { validDocIndexToOriginalIndex.set(i, originalDocIndex.get(docAttributes[i])); } // Make sure validation errors are in the same order as the // original documents, so if both doc1 and doc2 both fail validation, // `Model.insertMany([doc1, doc2])` will always have doc1's validation // error before doc2's. Re: gh-12791. if (validationErrors.length > 0) { validationErrors.sort((err1, err2) => { return validationErrorsToOriginalOrder.get(err1) - validationErrorsToOriginalOrder.get(err2); }); } // Quickly escape while there aren't any valid docAttributes if (docAttributes.length === 0) { if (rawResult) { const res = { acknowledged: true, insertedCount: 0, insertedIds: {}, mongoose: { validationErrors: validationErrors } }; return callback(null, res); } callback(null, []); return; } const docObjects = docAttributes.map(function(doc) { if (doc.$__schema.options.versionKey) { doc[doc.$__schema.options.versionKey] = 0; } const shouldSetTimestamps = (!options || options.timestamps !== false) && doc.initializeTimestamps && (!doc.$__ || doc.$__.timestamps !== false); if (shouldSetTimestamps) { return doc.initializeTimestamps().toObject(internalToObjectOptions); } return doc.toObject(internalToObjectOptions); }); _this.$__collection.insertMany(docObjects, options).then( res => { for (const attribute of docAttributes) { attribute.$__reset(); _setIsNew(attribute, false); } if (ordered === false && throwOnValidationError && validationErrors.length > 0) { for (let i = 0; i < results.length; ++i) { if (results[i] === void 0) { results[i] = docs[i]; } } return callback(new MongooseBulkWriteError( validationErrors, results, res, 'insertMany' )); } if (rawResult) { if (ordered === false) { for (let i = 0; i < results.length; ++i) { if (results[i] === void 0) { results[i] = docs[i]; } } // Decorate with mongoose validation errors in case of unordered, // because then still do `insertMany()` res.mongoose = { validationErrors: validationErrors, results: results }; } return callback(null, res); } if (options.populate != null) { return _this.populate(docAttributes, options.populate).then( docs => { callback(null, docs); }, err => { if (err != null) { err.insertedDocs = docAttributes; } throw err; } ); } callback(null, docAttributes); }, error => { // `writeErrors` is a property reported by the MongoDB driver, // just not if there's only 1 error. if (error.writeErrors == null && (error.result && error.result.result && error.result.result.writeErrors) != null) { error.writeErrors = error.result.result.writeErrors; } // `insertedDocs` is a Mongoose-specific property const hasWriteErrors = error && error.writeErrors; const erroredIndexes = new Set((error && error.writeErrors || []).map(err => err.index)); if (error.writeErrors != null) { for (let i = 0; i < error.writeErrors.length; ++i) { const originalIndex = validDocIndexToOriginalIndex.get(error.writeErrors[i].index); error.writeErrors[i] = { ...error.writeErrors[i], index: originalIndex }; if (!ordered) { results[originalIndex] = error.writeErrors[i]; } } } if (!ordered) { for (let i = 0; i < results.length; ++i) { if (results[i] === void 0) { results[i] = docs[i]; } } error.results = results; } let firstErroredIndex = -1; error.insertedDocs = docAttributes. filter((doc, i) => { const isErrored = !hasWriteErrors || erroredIndexes.has(i); if (ordered) { if (firstErroredIndex > -1) { return i < firstErroredIndex; } if (isErrored) { firstErroredIndex = i; } } return !isErrored; }). map(function setIsNewForInsertedDoc(doc) { doc.$__reset(); _setIsNew(doc, false); return doc; }); if (rawResult && ordered === false) { error.mongoose = { validationErrors: validationErrors, results: results }; } callback(error, null); } ); }); }; /*! * ignore */ function _setIsNew(doc, val) { doc.$isNew = val; doc.$emit('isNew', val); doc.constructor.emit('isNew', val); const subdocs = doc.$getAllSubdocs(); for (const subdoc of subdocs) { subdoc.$isNew = val; subdoc.$emit('isNew', val); } } /** * Sends multiple `insertOne`, `updateOne`, `updateMany`, `replaceOne`, * `deleteOne`, and/or `deleteMany` operations to the MongoDB server in one * command. This is faster than sending multiple independent operations (e.g. * if you use `create()`) because with `bulkWrite()` there is only one round * trip to MongoDB. * * Mongoose will perform casting on all operations you provide. * * This function does **not** trigger any middleware, neither `save()`, nor `update()`. * If you need to trigger * `save()` middleware for every document use [`create()`](https://mongoosejs.com/docs/api/model.html#Model.create()) instead. * * #### Example: * * Character.bulkWrite([ * { * insertOne: { * document: { * name: 'Eddard Stark', * title: 'Warden of the North' * } * } * }, * { * updateOne: { * filter: { name: 'Eddard Stark' }, * // If you were using the MongoDB driver directly, you'd need to do * // `update: { $set: { title: ... } }` but mongoose adds $set for * // you. * update: { title: 'Hand of the King' } * } * }, * { * deleteOne: { * filter: { name: 'Eddard Stark' } * } * } * ]).then(res => { * // Prints "1 1 1" * console.log(res.insertedCount, res.modifiedCount, res.deletedCount); * }); * * The [supported operations](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#db.collection.bulkWrite) are: * * - `insertOne` * - `updateOne` * - `updateMany` * - `deleteOne` * - `deleteMany` * - `replaceOne` * * @param {Array} ops * @param {Object} [ops.insertOne.document] The document to insert * @param {Object} [ops.updateOne.filter] Update the first document that matches this filter * @param {Object} [ops.updateOne.update] An object containing [update operators](https://www.mongodb.com/docs/manual/reference/operator/update/) * @param {Boolean} [ops.updateOne.upsert=false] If true, insert a doc if none match * @param {Boolean} [ops.updateOne.timestamps=true] If false, do not apply [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) to the operation * @param {Object} [ops.updateOne.collation] The [MongoDB collation](https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-34-collations) to use * @param {Array} [ops.updateOne.arrayFilters] The [array filters](https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-array-filters.html) used in `update` * @param {Object} [ops.updateMany.filter] Update all the documents that match this filter * @param {Object} [ops.updateMany.update] An object containing [update operators](https://www.mongodb.com/docs/manual/reference/operator/update/) * @param {Boolean} [ops.updateMany.upsert=false] If true, insert a doc if no documents match `filter` * @param {Boolean} [ops.updateMany.timestamps=true] If false, do not apply [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) to the operation * @param {Object} [ops.updateMany.collation] The [MongoDB collation](https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-34-collations) to use * @param {Array} [ops.updateMany.arrayFilters] The [array filters](https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-array-filters.html) used in `update` * @param {Object} [ops.deleteOne.filter] Delete the first document that matches this filter * @param {Object} [ops.deleteMany.filter] Delete all documents that match this filter * @param {Object} [ops.replaceOne.filter] Replace the first document that matches this filter * @param {Object} [ops.replaceOne.replacement] The replacement document * @param {Boolean} [ops.replaceOne.upsert=false] If true, insert a doc if no documents match `filter` * @param {Object} [options] * @param {Boolean} [options.ordered=true] If true, execute writes in order and stop at the first error. If false, execute writes in parallel and continue until all writes have either succeeded or errored. * @param {ClientSession} [options.session=null] The session associated with this bulk write. See [transactions docs](https://mongoosejs.com/docs/transactions.html). * @param {String|number} [options.w=1] The [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/). See [`Query#w()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.w()) for more information. * @param {number} [options.wtimeout=null] The [write concern timeout](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). * @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option) * @param {Boolean} [options.skipValidation=false] Set to true to skip Mongoose schema validation on bulk write operations. Mongoose currently runs validation on `insertOne` and `replaceOne` operations by default. * @param {Boolean} [options.bypassDocumentValidation=false] If true, disable [MongoDB server-side schema validation](https://www.mongodb.com/docs/manual/core/schema-validation/) for all writes in this bulk. * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. * @param {Boolean} [options.strict=null] Overwrites the [`strict` option](https://mongoosejs.com/docs/guide.html#strict) on schema. If false, allows filtering and writing fields not defined in the schema for all writes in this bulk. * @return {Promise} resolves to a [`BulkWriteOpResult`](https://mongodb.github.io/node-mongodb-native/4.9/classes/BulkWriteResult.html) if the operation succeeds * @api public */ Model.bulkWrite = async function bulkWrite(ops, options) { _checkContext(this, 'bulkWrite'); if (typeof options === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.bulkWrite() no longer accepts a callback'); } options = options || {}; const ordered = options.ordered == null ? true : options.ordered; const validations = ops.map(op => castBulkWrite(this, op, options)); return new Promise((resolve, reject) => { if (ordered) { each(validations, (fn, cb) => fn(cb), error => { if (error) { return reject(error); } if (ops.length === 0) { return resolve(getDefaultBulkwriteResult()); } try { this.$__collection.bulkWrite(ops, options, (error, res) => { if (error) { return reject(error); } resolve(res); }); } catch (err) { return reject(err); } }); return; } let remaining = validations.length; let validOps = []; let validationErrors = []; const results = []; if (remaining === 0) { completeUnorderedValidation.call(this); } else { for (let i = 0; i < validations.length; ++i) { validations[i]((err) => { if (err == null) { validOps.push(i); } else { validationErrors.push({ index: i, error: err }); results[i] = err; } if (--remaining <= 0) { completeUnorderedValidation.call(this); } }); } } validationErrors = validationErrors. sort((v1, v2) => v1.index - v2.index). map(v => v.error); function completeUnorderedValidation() { const validOpIndexes = validOps; validOps = validOps.sort().map(index => ops[index]); if (validOps.length === 0) { return resolve(getDefaultBulkwriteResult()); } this.$__collection.bulkWrite(validOps, options, (error, res) => { if (error) { if (validationErrors.length > 0) { error.mongoose = error.mongoose || {}; error.mongoose.validationErrors = validationErrors; } return reject(error); } for (let i = 0; i < validOpIndexes.length; ++i) { results[validOpIndexes[i]] = null; } if (validationErrors.length > 0) { if (options.throwOnValidationError) { return reject(new MongooseBulkWriteError( validationErrors, results, res, 'bulkWrite' )); } else { res.mongoose = res.mongoose || {}; res.mongoose.validationErrors = validationErrors; res.mongoose.results = results; } } resolve(res); }); } }); }; /** * takes an array of documents, gets the changes and inserts/updates documents in the database * according to whether or not the document is new, or whether it has changes or not. * * `bulkSave` uses `bulkWrite` under the hood, so it's mostly useful when dealing with many documents (10K+) * * @param {Array} documents * @param {Object} [options] options passed to the underlying `bulkWrite()` * @param {Boolean} [options.timestamps] defaults to `null`, when set to false, mongoose will not add/update timestamps to the documents. * @param {ClientSession} [options.session=null] The session associated with this bulk write. See [transactions docs](https://mongoosejs.com/docs/transactions.html). * @param {String|number} [options.w=1] The [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/). See [`Query#w()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.w()) for more information. * @param {number} [options.wtimeout=null] The [write concern timeout](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). * @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option) * */ Model.bulkSave = async function bulkSave(documents, options) { options = options || {}; if (options.timestamps != null) { for (const document of documents) { document.$__.saveOptions = document.$__.saveOptions || {}; document.$__.saveOptions.timestamps = options.timestamps; } } else { for (const document of documents) { if (document.$__.timestamps != null) { document.$__.saveOptions = document.$__.saveOptions || {}; document.$__.saveOptions.timestamps = document.$__.timestamps; } } } await Promise.all(documents.map(buildPreSavePromise)); const writeOperations = this.buildBulkWriteOperations(documents, { skipValidation: true, timestamps: options.timestamps }); const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, options).then( (res) => ({ bulkWriteResult: res, bulkWriteError: null }), (err) => ({ bulkWriteResult: null, bulkWriteError: err }) ); await Promise.all( documents.map(async(document) => { const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => { const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id; return writeErrorDocumentId.toString() === document._id.toString(); }); if (documentError == null) { await handleSuccessfulWrite(document); } }) ); if (bulkWriteError && bulkWriteError.writeErrors && bulkWriteError.writeErrors.length) { throw bulkWriteError; } return bulkWriteResult; }; function buildPreSavePromise(document) { return new Promise((resolve, reject) => { document.schema.s.hooks.execPre('save', document, (err) => { if (err) { reject(err); return; } resolve(); }); }); } function handleSuccessfulWrite(document) { return new Promise((resolve, reject) => { if (document.$isNew) { _setIsNew(document, false); } document.$__reset(); document.schema.s.hooks.execPost('save', document, [document], {}, (err) => { if (err) { reject(err); return; } resolve(); }); }); } /** * Apply defaults to the given document or POJO. * * @param {Object|Document} obj object or document to apply defaults on * @returns {Object|Document} * @api public */ Model.applyDefaults = function applyDefaults(doc) { if (doc.$__ != null) { applyDefaultsHelper(doc, doc.$__.fields, doc.$__.exclude); for (const subdoc of doc.$getAllSubdocs()) { applyDefaults(subdoc, subdoc.$__.fields, subdoc.$__.exclude); } return doc; } applyDefaultsToPOJO(doc, this.schema); return doc; }; /** * Cast the given POJO to the model's schema * * #### Example: * * const Test = mongoose.model('Test', Schema({ num: Number })); * * const obj = Test.castObject({ num: '42' }); * obj.num; // 42 as a number * * Test.castObject({ num: 'not a number' }); // Throws a ValidationError * * @param {Object} obj object or document to cast * @param {Object} options options passed to castObject * @param {Boolean} options.ignoreCastErrors If set to `true` will not throw a ValidationError and only return values that were successfully cast. * @returns {Object} POJO casted to the model's schema * @throws {ValidationError} if casting failed for at least one path * @api public */ Model.castObject = function castObject(obj, options) { options = options || {}; const ret = {}; const schema = this.schema; const paths = Object.keys(schema.paths); for (const path of paths) { const schemaType = schema.path(path); if (!schemaType || !schemaType.$isMongooseArray) { continue; } const val = get(obj, path); pushNestedArrayPaths(paths, val, path); } let error = null; for (const path of paths) { const schemaType = schema.path(path); if (schemaType == null) { continue; } let val = get(obj, path, void 0); if (val == null) { continue; } const pieces = path.indexOf('.') === -1 ? [path] : path.split('.'); let cur = ret; for (let i = 0; i < pieces.length - 1; ++i) { if (cur[pieces[i]] == null) { cur[pieces[i]] = isNaN(pieces[i + 1]) ? {} : []; } cur = cur[pieces[i]]; } if (schemaType.$isMongooseDocumentArray) { continue; } if (schemaType.$isSingleNested || schemaType.$isMongooseDocumentArrayElement) { try { val = Model.castObject.call(schemaType.caster, val); } catch (err) { if (!options.ignoreCastErrors) { error = error || new ValidationError(); error.addError(path, err); } continue; } cur[pieces[pieces.length - 1]] = val; continue; } try { val = schemaType.cast(val); cur[pieces[pieces.length - 1]] = val; } catch (err) { if (!options.ignoreCastErrors) { error = error || new ValidationError(); error.addError(path, err); } continue; } } if (error != null) { throw error; } return ret; }; /** * Build bulk write operations for `bulkSave()`. * * @param {Array} documents The array of documents to build write operations of * @param {Object} options * @param {Boolean} options.skipValidation defaults to `false`, when set to true, building the write operations will bypass validating the documents. * @param {Boolean} options.timestamps defaults to `null`, when set to false, mongoose will not add/update timestamps to the documents. * @return {Array} Returns a array of all Promises the function executes to be awaited. * @api private */ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, options) { if (!Array.isArray(documents)) { throw new Error(`bulkSave expects an array of documents to be passed, received \`${documents}\` instead`); } setDefaultOptions(); const writeOperations = documents.reduce((accumulator, document, i) => { if (!options.skipValidation) { if (!(document instanceof Document)) { throw new Error(`documents.${i} was not a mongoose document, documents must be an array of mongoose documents (instanceof mongoose.Document).`); } const validationError = document.validateSync(); if (validationError) { throw validationError; } } const isANewDocument = document.isNew; if (isANewDocument) { const writeOperation = { insertOne: { document } }; utils.injectTimestampsOption(writeOperation.insertOne, options.timestamps); accumulator.push(writeOperation); return accumulator; } const delta = document.$__delta(); const isDocumentWithChanges = delta != null && !utils.isEmptyObject(delta[0]); if (isDocumentWithChanges) { const where = document.$__where(delta[0]); const changes = delta[1]; _applyCustomWhere(document, where); document.$__version(where, delta); const writeOperation = { updateOne: { filter: where, update: changes } }; utils.injectTimestampsOption(writeOperation.updateOne, options.timestamps); accumulator.push(writeOperation); return accumulator; } return accumulator; }, []); return writeOperations; function setDefaultOptions() { options = options || {}; if (options.skipValidation == null) { options.skipValidation = false; } } }; /** * Shortcut for creating a new Document from existing raw data, pre-saved in the DB. * The document returned has no paths marked as modified initially. * * #### Example: * * // hydrate previous data into a Mongoose document * const mongooseCandy = Candy.hydrate({ _id: '54108337212ffb6d459f854c', type: 'jelly bean' }); * * @param {Object} obj * @param {Object|String|String[]} [projection] optional projection containing which fields should be selected for this document * @param {Object} [options] optional options * @param {Boolean} [options.setters=false] if true, apply schema setters when hydrating * @return {Document} document instance * @api public */ Model.hydrate = function(obj, projection, options) { _checkContext(this, 'hydrate'); if (projection != null) { if (obj != null && obj.$__ != null) { obj = obj.toObject(internalToObjectOptions); } obj = applyProjection(obj, projection); } const document = require('./queryhelpers').createModel(this, obj, projection); document.$init(obj, options); return document; }; /** * Same as `updateOne()`, except MongoDB will update _all_ documents that match * `filter` (as opposed to just the first one) regardless of the value of * the `multi` option. * * **Note** updateMany will _not_ fire update middleware. Use `pre('updateMany')` * and `post('updateMany')` instead. * * #### Example: * * const res = await Person.updateMany({ name: /Stark$/ }, { isDeleted: true }); * res.matchedCount; // Number of documents matched * res.modifiedCount; // Number of documents modified * res.acknowledged; // Boolean indicating everything went smoothly. * res.upsertedId; // null or an id containing a document that had to be upserted. * res.upsertedCount; // Number indicating how many documents had to be upserted. Will either be 0 or 1. * * This function triggers the following middleware. * * - `updateMany()` * * @param {Object} filter * @param {Object|Array} update * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @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. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output * @see UpdateResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/UpdateResult.html * @api public */ Model.updateMany = function updateMany(conditions, doc, options) { _checkContext(this, 'updateMany'); return _update(this, 'updateMany', conditions, doc, options); }; /** * Update _only_ the first document that matches `filter`. * * - Use `replaceOne()` if you want to overwrite an entire document rather than using atomic operators like `$set`. * * #### Example: * * const res = await Person.updateOne({ name: 'Jean-Luc Picard' }, { ship: 'USS Enterprise' }); * res.matchedCount; // Number of documents matched * res.modifiedCount; // Number of documents modified * res.acknowledged; // Boolean indicating everything went smoothly. * res.upsertedId; // null or an id containing a document that had to be upserted. * res.upsertedCount; // Number indicating how many documents had to be upserted. Will either be 0 or 1. * * This function triggers the following middleware. * * - `updateOne()` * * @param {Object} filter * @param {Object|Array} update * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @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. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output * @see UpdateResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/UpdateResult.html * @api public */ Model.updateOne = function updateOne(conditions, doc, options) { _checkContext(this, 'updateOne'); return _update(this, 'updateOne', conditions, doc, options); }; /** * Replace the existing document with the given document (no atomic operators like `$set`). * * #### Example: * * const res = await Person.replaceOne({ _id: 24601 }, { name: 'Jean Valjean' }); * res.matchedCount; // Number of documents matched * res.modifiedCount; // Number of documents modified * res.acknowledged; // Boolean indicating everything went smoothly. * res.upsertedId; // null or an id containing a document that had to be upserted. * res.upsertedCount; // Number indicating how many documents had to be upserted. Will either be 0 or 1. * * This function triggers the following middleware. * * - `replaceOne()` * * @param {Object} filter * @param {Object} doc * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @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. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see UpdateResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/UpdateResult.html * @return {Query} * @api public */ Model.replaceOne = function replaceOne(conditions, doc, options) { _checkContext(this, 'replaceOne'); const versionKey = this && this.schema && this.schema.options && this.schema.options.versionKey || null; if (versionKey && !doc[versionKey]) { doc[versionKey] = 0; } return _update(this, 'replaceOne', conditions, doc, options); }; /** * Common code for `updateOne()`, `updateMany()`, `replaceOne()`, and `update()` * because they need to do the same thing * @api private */ function _update(model, op, conditions, doc, options) { const mq = new model.Query({}, {}, model, model.collection); // gh-2406 // make local deep copy of conditions if (conditions instanceof Document) { conditions = conditions.toObject(); } else { conditions = clone(conditions); } options = typeof options === 'function' ? options : clone(options); const versionKey = model && model.schema && model.schema.options && model.schema.options.versionKey || null; _decorateUpdateWithVersionKey(doc, options, versionKey); return mq[op](conditions, doc, options); } /** * Performs [aggregations](https://www.mongodb.com/docs/manual/aggregation/) on the models collection. * * If a `callback` is passed, the `aggregate` is executed and a `Promise` is returned. If a callback is not passed, the `aggregate` itself is returned. * * This function triggers the following middleware. * * - `aggregate()` * * #### Example: * * // Find the max balance of all accounts * const res = await Users.aggregate([ * { $group: { _id: null, maxBalance: { $max: '$balance' }}}, * { $project: { _id: 0, maxBalance: 1 }} * ]); * * console.log(res); // [ { maxBalance: 98000 } ] * * // Or use the aggregation pipeline builder. * const res = await Users.aggregate(). * group({ _id: null, maxBalance: { $max: '$balance' } }). * project('-id maxBalance'). * exec(); * console.log(res); // [ { maxBalance: 98 } ] * * #### Note: * * - Mongoose does **not** cast aggregation pipelines to the model's schema because `$project` and `$group` operators allow redefining the "shape" of the documents at any stage of the pipeline, which may leave documents in an incompatible format. You can use the [mongoose-cast-aggregation plugin](https://github.com/AbdelrahmanHafez/mongoose-cast-aggregation) to enable minimal casting for aggregation pipelines. * - The documents returned are plain javascript objects, not mongoose documents (since any shape of document can be returned). * * #### More About Aggregations: * * - [Mongoose `Aggregate`](https://mongoosejs.com/docs/api/aggregate.html) * - [An Introduction to Mongoose Aggregate](https://masteringjs.io/tutorials/mongoose/aggregate) * - [MongoDB Aggregation docs](https://www.mongodb.com/docs/manual/applications/aggregation/) * * @see Aggregate https://mongoosejs.com/docs/api/aggregate.html#Aggregate() * @see MongoDB https://www.mongodb.com/docs/manual/applications/aggregation/ * @param {Array} [pipeline] aggregation pipeline as an array of objects * @param {Object} [options] aggregation options * @return {Aggregate} * @api public */ Model.aggregate = function aggregate(pipeline, options) { _checkContext(this, 'aggregate'); if (typeof options === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.aggregate() no longer accepts a callback'); } const aggregate = new Aggregate(pipeline || []); aggregate.model(this); if (options != null) { aggregate.option(options); } if (typeof callback === 'undefined') { return aggregate; } return aggregate; }; /** * Casts and validates the given object against this model's schema, passing the * given `context` to custom validators. * * #### Example: * * const Model = mongoose.model('Test', Schema({ * name: { type: String, required: true }, * age: { type: Number, required: true } * }); * * try { * await Model.validate({ name: null }, ['name']) * } catch (err) { * err instanceof mongoose.Error.ValidationError; // true * Object.keys(err.errors); // ['name'] * } * * @param {Object} obj * @param {Object|Array|String} pathsOrOptions * @param {Object} [context] * @return {Promise|undefined} * @api public */ Model.validate = async function validate(obj, pathsOrOptions, context) { if ((arguments.length < 3) || (arguments.length === 3 && typeof arguments[2] === 'function')) { // For convenience, if we're validating a document or an object, make `context` default to // the model so users don't have to always pass `context`, re: gh-10132, gh-10346 context = obj; } if (typeof context === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Model.validate() no longer accepts a callback'); } let schema = this.schema; const discriminatorKey = schema.options.discriminatorKey; if (schema.discriminators != null && obj != null && obj[discriminatorKey] != null) { schema = getSchemaDiscriminatorByValue(schema, obj[discriminatorKey]) || schema; } let paths = Object.keys(schema.paths); if (pathsOrOptions != null) { const _pathsToValidate = typeof pathsOrOptions === 'string' ? new Set(pathsOrOptions.split(' ')) : Array.isArray(pathsOrOptions) ? new Set(pathsOrOptions) : new Set(paths); paths = paths.filter(p => { if (pathsOrOptions.pathsToSkip) { if (Array.isArray(pathsOrOptions.pathsToSkip)) { if (pathsOrOptions.pathsToSkip.find(x => x == p)) { return false; } } else if (typeof pathsOrOptions.pathsToSkip == 'string') { if (pathsOrOptions.pathsToSkip.includes(p)) { return false; } } } const pieces = p.split('.'); let cur = pieces[0]; for (const piece of pieces) { if (_pathsToValidate.has(cur)) { return true; } cur += '.' + piece; } return _pathsToValidate.has(p); }); } for (const path of paths) { const schemaType = schema.path(path); if (!schemaType || !schemaType.$isMongooseArray || schemaType.$isMongooseDocumentArray) { continue; } const val = get(obj, path); pushNestedArrayPaths(paths, val, path); } let remaining = paths.length; let error = null; return new Promise((resolve, reject) => { for (const path of paths) { const schemaType = schema.path(path); if (schemaType == null) { _checkDone(); continue; } const pieces = path.indexOf('.') === -1 ? [path] : path.split('.'); let cur = obj; for (let i = 0; i < pieces.length - 1; ++i) { cur = cur[pieces[i]]; } let val = get(obj, path, void 0); if (val != null) { try { val = schemaType.cast(val); cur[pieces[pieces.length - 1]] = val; } catch (err) { error = error || new ValidationError(); error.addError(path, err); _checkDone(); continue; } } schemaType.doValidate(val, err => { if (err) { error = error || new ValidationError(); error.addError(path, err); } _checkDone(); }, context, { path: path }); } function _checkDone() { if (--remaining <= 0) { if (error) { reject(error); } else { resolve(); } } } }); }; /** * Populates document references. * * Changed in Mongoose 6: the model you call `populate()` on should be the * "local field" model, **not** the "foreign field" model. * * #### Available top-level options: * * - path: space delimited path(s) to populate * - select: optional fields to select * - match: optional query conditions to match * - model: optional name of the model to use for population * - options: optional query options like sort, limit, etc * - justOne: optional boolean, if true Mongoose will always set `path` to a document, or `null` if no document was found. If false, Mongoose will always set `path` to an array, which will be empty if no documents are found. Inferred from schema by default. * - strictPopulate: optional boolean, set to `false` to allow populating paths that aren't in the schema. * * #### Example: * * const Dog = mongoose.model('Dog', new Schema({ name: String, breed: String })); * const Person = mongoose.model('Person', new Schema({ * name: String, * pet: { type: mongoose.ObjectId, ref: 'Dog' } * })); * * const pets = await Pet.create([ * { name: 'Daisy', breed: 'Beagle' }, * { name: 'Einstein', breed: 'Catalan Sheepdog' } * ]); * * // populate many plain objects * const users = [ * { name: 'John Wick', dog: pets[0]._id }, * { name: 'Doc Brown', dog: pets[1]._id } * ]; * await User.populate(users, { path: 'dog', select: 'name' }); * users[0].dog.name; // 'Daisy' * users[0].dog.breed; // undefined because of `select` * * @param {Document|Array} docs Either a single document or array of documents to populate. * @param {Object|String} options Either the paths to populate or an object specifying all parameters * @param {string} [options.path=null] The path to populate. * @param {string|PopulateOptions} [options.populate=null] Recursively populate paths in the populated documents. See [deep populate docs](https://mongoosejs.com/docs/populate.html#deep-populate). * @param {boolean} [options.retainNullValues=false] By default, Mongoose removes null and undefined values from populated arrays. Use this option to make `populate()` retain `null` and `undefined` array entries. * @param {boolean} [options.getters=false] If true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](https://mongoosejs.com/docs/schematypes.html#schematype-options). * @param {boolean} [options.clone=false] When you do `BlogPost.find().populate('author')`, blog posts with the same author will share 1 copy of an `author` doc. Enable this option to make Mongoose clone populated docs before assigning them. * @param {Object|Function} [options.match=null] Add an additional filter to the populate query. Can be a filter object containing [MongoDB query syntax](https://www.mongodb.com/docs/manual/tutorial/query-documents/), or a function that returns a filter object. * @param {Boolean} [options.skipInvalidIds=false] By default, Mongoose throws a cast error if `localField` and `foreignField` schemas don't line up. If you enable this option, Mongoose will instead filter out any `localField` properties that cannot be casted to `foreignField`'s schema type. * @param {Number} [options.perDocumentLimit=null] For legacy reasons, `limit` with `populate()` may give incorrect results because it only executes a single query for every document being populated. If you set `perDocumentLimit`, Mongoose will ensure correct `limit` per document by executing a separate query for each document to `populate()`. For example, `.find().populate({ path: 'test', perDocumentLimit: 2 })` will execute 2 additional queries if `.find()` returns 2 documents. * @param {Boolean} [options.strictPopulate=true] Set to false to allow populating paths that aren't defined in the given model's schema. * @param {Object} [options.options=null] Additional options like `limit` and `lean`. * @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document. * @param {Function} [callback(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`. * @return {Promise} * @api public */ Model.populate = async function populate(docs, paths) { _checkContext(this, 'populate'); if (typeof paths === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.populate() no longer accepts a callback'); } const _this = this; // normalized paths paths = utils.populate(paths); // data that should persist across subPopulate calls const cache = {}; return new Promise((resolve, reject) => { _populate(_this, docs, paths, cache, (err, res) => { if (err) { return reject(err); } resolve(res); }); }); }; /** * Populate helper * * @param {Model} model the model to use * @param {Document|Array} docs Either a single document or array of documents to populate. * @param {Object} paths * @param {never} cache Unused * @param {Function} [callback] Optional callback, executed upon completion. Receives `err` and the `doc(s)`. * @return {Function} * @api private */ function _populate(model, docs, paths, cache, callback) { let pending = paths.length; if (paths.length === 0) { return callback(null, docs); } // each path has its own query options and must be executed separately for (const path of paths) { populate(model, docs, path, next); } function next(err) { if (err) { return callback(err, null); } if (--pending) { return; } callback(null, docs); } } /*! * Populates `docs` */ const excludeIdReg = /\s?-_id\s?/; const excludeIdRegGlobal = /\s?-_id\s?/g; function populate(model, docs, options, callback) { const populateOptions = options; if (options.strictPopulate == null) { if (options._localModel != null && options._localModel.schema._userProvidedOptions.strictPopulate != null) { populateOptions.strictPopulate = options._localModel.schema._userProvidedOptions.strictPopulate; } else if (options._localModel != null && model.base.options.strictPopulate != null) { populateOptions.strictPopulate = model.base.options.strictPopulate; } else if (model.base.options.strictPopulate != null) { populateOptions.strictPopulate = model.base.options.strictPopulate; } } // normalize single / multiple docs passed if (!Array.isArray(docs)) { docs = [docs]; } if (docs.length === 0 || docs.every(utils.isNullOrUndefined)) { return callback(); } const modelsMap = getModelsMapForPopulate(model, docs, populateOptions); if (modelsMap instanceof MongooseError) { return immediate(function() { callback(modelsMap); }); } const len = modelsMap.length; let vals = []; function flatten(item) { // no need to include undefined values in our query return undefined !== item; } let _remaining = len; let hasOne = false; const params = []; for (let i = 0; i < len; ++i) { const mod = modelsMap[i]; let select = mod.options.select; let ids = utils.array.flatten(mod.ids, flatten); ids = utils.array.unique(ids); const assignmentOpts = {}; assignmentOpts.sort = mod && mod.options && mod.options.options && mod.options.options.sort || void 0; assignmentOpts.excludeId = excludeIdReg.test(select) || (select && select._id === 0); // Lean transform may delete `_id`, which would cause assignment // to fail. So delay running lean transform until _after_ // `_assign()` if (mod.options && mod.options.options && mod.options.options.lean && mod.options.options.lean.transform) { mod.options.options._leanTransform = mod.options.options.lean.transform; mod.options.options.lean = true; } if (ids.length === 0 || ids.every(utils.isNullOrUndefined)) { // Ensure that we set to 0 or empty array even // if we don't actually execute a query to make sure there's a value // and we know this path was populated for future sets. See gh-7731, gh-8230 --_remaining; _assign(model, [], mod, assignmentOpts); continue; } hasOne = true; if (typeof populateOptions.foreignField === 'string') { mod.foreignField.clear(); mod.foreignField.add(populateOptions.foreignField); } const match = createPopulateQueryFilter(ids, mod.match, mod.foreignField, mod.model, mod.options.skipInvalidIds); if (assignmentOpts.excludeId) { // override the exclusion from the query so we can use the _id // for document matching during assignment. we'll delete the // _id back off before returning the result. if (typeof select === 'string') { select = select.replace(excludeIdRegGlobal, ' '); } else { // preserve original select conditions by copying select = utils.object.shallowCopy(select); delete select._id; } } if (mod.options.options && mod.options.options.limit != null) { assignmentOpts.originalLimit = mod.options.options.limit; } else if (mod.options.limit != null) { assignmentOpts.originalLimit = mod.options.limit; } params.push([mod, match, select, assignmentOpts, _next]); } if (!hasOne) { // If models but no docs, skip further deep populate. if (modelsMap.length !== 0) { return callback(); } // If no models to populate but we have a nested populate, // keep trying, re: gh-8946 if (populateOptions.populate != null) { const opts = utils.populate(populateOptions.populate).map(pop => Object.assign({}, pop, { path: populateOptions.path + '.' + pop.path })); model.populate(docs, opts).then(res => { callback(null, res); }, err => { callback(err); }); return; } return callback(); } for (const arr of params) { _execPopulateQuery.apply(null, arr); } function _next(err, valsFromDb) { if (err != null) { return callback(err, null); } vals = vals.concat(valsFromDb); if (--_remaining === 0) { _done(); } } function _done() { for (const arr of params) { const mod = arr[0]; const assignmentOpts = arr[3]; for (const val of vals) { mod.options._childDocs.push(val); } try { _assign(model, vals, mod, assignmentOpts); } catch (err) { return callback(err); } } for (const arr of params) { removeDeselectedForeignField(arr[0].foreignField, arr[0].options, vals); } for (const arr of params) { const mod = arr[0]; if (mod.options && mod.options.options && mod.options.options._leanTransform) { for (const doc of vals) { mod.options.options._leanTransform(doc); } } } callback(); } } /*! * ignore */ function _execPopulateQuery(mod, match, select, assignmentOpts, callback) { let subPopulate = clone(mod.options.populate); const queryOptions = Object.assign({ skip: mod.options.skip, limit: mod.options.limit, perDocumentLimit: mod.options.perDocumentLimit }, mod.options.options); if (mod.count) { delete queryOptions.skip; } if (queryOptions.perDocumentLimit != null) { queryOptions.limit = queryOptions.perDocumentLimit; delete queryOptions.perDocumentLimit; } else if (queryOptions.limit != null) { queryOptions.limit = queryOptions.limit * mod.ids.length; } const query = mod.model.find(match, select, queryOptions); // If we're doing virtual populate and projection is inclusive and foreign // field is not selected, automatically select it because mongoose needs it. // If projection is exclusive and client explicitly unselected the foreign // field, that's the client's fault. for (const foreignField of mod.foreignField) { if (foreignField !== '_id' && query.selectedInclusively() && !isPathSelectedInclusive(query._fields, foreignField)) { query.select(foreignField); } } // If using count, still need the `foreignField` so we can match counts // to documents, otherwise we would need a separate `count()` for every doc. if (mod.count) { for (const foreignField of mod.foreignField) { query.select(foreignField); } } // If we need to sub-populate, call populate recursively if (subPopulate) { // If subpopulating on a discriminator, skip check for non-existent // paths. Because the discriminator may not have the path defined. if (mod.model.baseModelName != null) { if (Array.isArray(subPopulate)) { subPopulate.forEach(pop => { pop.strictPopulate = false; }); } else if (typeof subPopulate === 'string') { subPopulate = { path: subPopulate, strictPopulate: false }; } else { subPopulate.strictPopulate = false; } } const basePath = mod.options._fullPath || mod.options.path; if (Array.isArray(subPopulate)) { for (const pop of subPopulate) { pop._fullPath = basePath + '.' + pop.path; } } else if (typeof subPopulate === 'object') { subPopulate._fullPath = basePath + '.' + subPopulate.path; } query.populate(subPopulate); } query.exec().then( docs => { for (const val of docs) { leanPopulateMap.set(val, mod.model); } callback(null, docs); }, err => { callback(err); } ); } /*! * ignore */ function _assign(model, vals, mod, assignmentOpts) { const options = mod.options; const isVirtual = mod.isVirtual; const justOne = mod.justOne; let _val; const lean = options && options.options && options.options.lean || false; const len = vals.length; const rawOrder = {}; const rawDocs = {}; let key; let val; // Clone because `assignRawDocsToIdStructure` will mutate the array const allIds = clone(mod.allIds); // optimization: // record the document positions as returned by // the query result. for (let i = 0; i < len; i++) { val = vals[i]; if (val == null) { continue; } for (const foreignField of mod.foreignField) { _val = utils.getValue(foreignField, val); if (Array.isArray(_val)) { _val = utils.array.unique(utils.array.flatten(_val)); for (let __val of _val) { if (__val instanceof Document) { __val = __val._id; } key = String(__val); if (rawDocs[key]) { if (Array.isArray(rawDocs[key])) { rawDocs[key].push(val); rawOrder[key].push(i); } else { rawDocs[key] = [rawDocs[key], val]; rawOrder[key] = [rawOrder[key], i]; } } else { if (isVirtual && !justOne) { rawDocs[key] = [val]; rawOrder[key] = [i]; } else { rawDocs[key] = val; rawOrder[key] = i; } } } } else { if (_val instanceof Document) { _val = _val._id; } key = String(_val); if (rawDocs[key]) { if (Array.isArray(rawDocs[key])) { rawDocs[key].push(val); rawOrder[key].push(i); } else if (isVirtual || rawDocs[key].constructor !== val.constructor || String(rawDocs[key]._id) !== String(val._id)) { // May need to store multiple docs with the same id if there's multiple models // if we have discriminators or a ref function. But avoid converting to an array // if we have multiple queries on the same model because of `perDocumentLimit` re: gh-9906 rawDocs[key] = [rawDocs[key], val]; rawOrder[key] = [rawOrder[key], i]; } } else { rawDocs[key] = val; rawOrder[key] = i; } } // flag each as result of population if (!lean) { val.$__.wasPopulated = val.$__.wasPopulated || true; } } } assignVals({ originalModel: model, // If virtual, make sure to not mutate original field rawIds: mod.isVirtual ? allIds : mod.allIds, allIds: allIds, unpopulatedValues: mod.unpopulatedValues, foreignField: mod.foreignField, rawDocs: rawDocs, rawOrder: rawOrder, docs: mod.docs, path: options.path, options: assignmentOpts, justOne: mod.justOne, isVirtual: mod.isVirtual, allOptions: mod, populatedModel: mod.model, lean: lean, virtual: mod.virtual, count: mod.count, match: mod.match }); } /** * Compiler utility. * * @param {String|Function} name model name or class extending Model * @param {Schema} schema * @param {String} collectionName * @param {Connection} connection * @param {Mongoose} base mongoose instance * @api private */ Model.compile = function compile(name, schema, collectionName, connection, base) { const versioningEnabled = schema.options.versionKey !== false; if (versioningEnabled && !schema.paths[schema.options.versionKey]) { // add versioning to top level documents only const o = {}; o[schema.options.versionKey] = Number; schema.add(o); } let model; if (typeof name === 'function' && name.prototype instanceof Model) { model = name; name = model.name; schema.loadClass(model, false); model.prototype.$isMongooseModelPrototype = true; } else { // generate new class model = function model(doc, fields, skipId) { model.hooks.execPreSync('createModel', doc); if (!(this instanceof model)) { return new model(doc, fields, skipId); } const discriminatorKey = model.schema.options.discriminatorKey; if (model.discriminators == null || doc == null || doc[discriminatorKey] == null) { Model.call(this, doc, fields, skipId); return; } // If discriminator key is set, use the discriminator instead (gh-7586) const Discriminator = model.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(model.discriminators, doc[discriminatorKey]); if (Discriminator != null) { return new Discriminator(doc, fields, skipId); } // Otherwise, just use the top-level model Model.call(this, doc, fields, skipId); }; } model.hooks = schema.s.hooks.clone(); model.base = base; model.modelName = name; if (!(model.prototype instanceof Model)) { Object.setPrototypeOf(model, Model); Object.setPrototypeOf(model.prototype, Model.prototype); } model.model = function model(name) { return this.db.model(name); }; model.db = connection; model.prototype.db = connection; model.prototype[modelDbSymbol] = connection; model.discriminators = model.prototype.discriminators = undefined; model[modelSymbol] = true; model.events = new EventEmitter(); schema._preCompile(); model.prototype.$__setSchema(schema); const _userProvidedOptions = schema._userProvidedOptions || {}; const collectionOptions = { schemaUserProvidedOptions: _userProvidedOptions, capped: schema.options.capped, Promise: model.base.Promise, modelName: name }; if (schema.options.autoCreate !== void 0) { collectionOptions.autoCreate = schema.options.autoCreate; } model.prototype.collection = connection.collection( collectionName, collectionOptions ); model.prototype.$collection = model.prototype.collection; model.prototype[modelCollectionSymbol] = model.prototype.collection; // apply methods and statics applyMethods(model, schema); applyStatics(model, schema); applyHooks(model, schema); applyStaticHooks(model, schema.s.hooks, schema.statics); model.schema = model.prototype.$__schema; model.collection = model.prototype.collection; model.$__collection = model.collection; // Create custom query constructor model.Query = function() { Query.apply(this, arguments); }; Object.setPrototypeOf(model.Query.prototype, Query.prototype); model.Query.base = Query.base; model.Query.prototype.constructor = Query; applyQueryMiddleware(model.Query, model); applyQueryMethods(model, schema.query); return model; }; /** * Register custom query methods for this model * * @param {Model} model * @param {Schema} schema * @api private */ function applyQueryMethods(model, methods) { for (const i in methods) { model.Query.prototype[i] = methods[i]; } } /** * Subclass this model with `conn`, `schema`, and `collection` settings. * * @param {Connection} conn * @param {Schema} [schema] * @param {String} [collection] * @return {Model} * @api private * @memberOf Model * @static * @method __subclass */ Model.__subclass = function subclass(conn, schema, collection) { // subclass model using this connection and collection name const _this = this; const Model = function Model(doc, fields, skipId) { if (!(this instanceof Model)) { return new Model(doc, fields, skipId); } _this.call(this, doc, fields, skipId); }; Object.setPrototypeOf(Model, _this); Object.setPrototypeOf(Model.prototype, _this.prototype); Model.db = conn; Model.prototype.db = conn; Model.prototype[modelDbSymbol] = conn; _this[subclassedSymbol] = _this[subclassedSymbol] || []; _this[subclassedSymbol].push(Model); if (_this.discriminators != null) { Model.discriminators = {}; for (const key of Object.keys(_this.discriminators)) { Model.discriminators[key] = _this.discriminators[key]. __subclass(_this.db, _this.discriminators[key].schema, collection); } } const s = schema && typeof schema !== 'string' ? schema : _this.prototype.$__schema; const options = s.options || {}; const _userProvidedOptions = s._userProvidedOptions || {}; if (!collection) { collection = _this.prototype.$__schema.get('collection') || utils.toCollectionName(_this.modelName, this.base.pluralize()); } const collectionOptions = { schemaUserProvidedOptions: _userProvidedOptions, capped: s && options.capped }; Model.prototype.collection = conn.collection(collection, collectionOptions); Model.prototype.$collection = Model.prototype.collection; Model.prototype[modelCollectionSymbol] = Model.prototype.collection; Model.collection = Model.prototype.collection; Model.$__collection = Model.collection; // Errors handled internally, so ignore Model.init().catch(() => {}); return Model; }; /** * Helper for console.log. Given a model named 'MyModel', returns the string * `'Model { MyModel }'`. * * #### Example: * * const MyModel = mongoose.model('Test', Schema({ name: String })); * MyModel.inspect(); // 'Model { Test }' * console.log(MyModel); // Prints 'Model { Test }' * * @api public */ Model.inspect = function() { return `Model { ${this.modelName} }`; }; if (util.inspect.custom) { // Avoid Node deprecation warning DEP0079 Model[util.inspect.custom] = Model.inspect; } /*! * Module exports. */ module.exports = exports = Model;