UNPKG

mongoose

Version:
1,452 lines (1,294 loc) 189 kB
'use strict'; /*! * Module dependencies. */ const Aggregate = require('./aggregate'); const ChangeStream = require('./cursor/changeStream'); const Document = require('./document'); const DocumentNotFoundError = require('./error/notFound'); const EventEmitter = require('events').EventEmitter; const Kareem = require('kareem'); const MongooseBulkWriteError = require('./error/bulkWriteError'); const MongooseError = require('./error/index'); const ObjectParameterError = require('./error/objectParameter'); 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 applyEmbeddedDiscriminators = require('./helpers/discriminator/applyEmbeddedDiscriminators'); const applyHooks = require('./helpers/model/applyHooks'); const applyMethods = require('./helpers/model/applyMethods'); const applyProjection = require('./helpers/projection/applyProjection'); const applyReadConcern = require('./helpers/schema/applyReadConcern'); const applySchemaCollation = require('./helpers/indexes/applySchemaCollation'); const applyStaticHooks = require('./helpers/model/applyStaticHooks'); const applyStatics = require('./helpers/model/applyStatics'); const applyTimestampsHelper = require('./helpers/document/applyTimestamps'); const applyWriteConcern = require('./helpers/schema/applyWriteConcern'); const applyVirtualsHelper = require('./helpers/document/applyVirtuals'); const assignVals = require('./helpers/populate/assignVals'); const castBulkWrite = require('./helpers/model/castBulkWrite'); const clone = require('./helpers/clone'); const createPopulateQueryFilter = require('./helpers/populate/createPopulateQueryFilter'); const decorateUpdateWithVersionKey = require('./helpers/update/decorateUpdateWithVersionKey'); const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult'); const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue'); const discriminator = require('./helpers/model/discriminator'); 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 isTimeseriesIndex = require('./helpers/indexes/isTimeseriesIndex'); const { getRelatedDBIndexes, getRelatedSchemaIndexes } = require('./helpers/indexes/getRelatedIndexes'); const decorateDiscriminatorIndexOptions = require('./helpers/indexes/decorateDiscriminatorIndexOptions'); const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive'); const leanPopulateMap = require('./helpers/populate/leanPopulateMap'); const parallelLimit = require('./helpers/parallelLimit'); 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 util = require('util'); const utils = require('./utils'); const minimize = require('./helpers/minimize'); const MongooseBulkSaveIncompleteError = require('./error/bulkSaveIncompleteError'); const ObjectExpectedError = require('./error/objectExpected'); const decorateBulkWriteResult = require('./helpers/model/decorateBulkWriteResult'); 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 { VERSION_INC, VERSION_WHERE, VERSION_ALL } = Document; const saveToObjectOptions = Object.assign({}, internalToObjectOptions, { bson: true }); /** * 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` constructor must be a POJO or string, ' + '**not** a schema. Make sure you\'re calling `mongoose.model()`, not ' + '`mongoose.Model()`.'); } if (typeof doc === 'string') { throw new TypeError('First argument to `Model` constructor must be an object, ' + '**not** a string. 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; /** * Changes the Connection instance this model uses to make requests to MongoDB. * This function is most useful for changing the Connection that a Model defined using `mongoose.model()` uses * after initialization. * * #### Example: * * await mongoose.connect('mongodb://127.0.0.1:27017/db1'); * const UserModel = mongoose.model('User', mongoose.Schema({ name: String })); * UserModel.connection === mongoose.connection; // true * * const conn2 = await mongoose.createConnection('mongodb://127.0.0.1:27017/db2').asPromise(); * UserModel.useConnection(conn2); // `UserModel` now stores documents in `db2`, not `db1` * * UserModel.connection === mongoose.connection; // false * UserModel.connection === conn2; // true * * conn2.model('User') === UserModel; // true * mongoose.model('User'); // Throws 'MissingSchemaError' * * Note: `useConnection()` does **not** apply any [connection-level plugins](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.plugin()) from the new connection. * If you use `useConnection()` to switch a model's connection, the model will still have the old connection's plugins. * * @function useConnection * @param [Connection] connection The new connection to use * @return [Model] this * @api public */ Model.useConnection = function useConnection(connection) { if (!connection) { throw new Error('Please provide a connection.'); } if (this.db) { delete this.db.models[this.modelName]; delete this.prototype.db; delete this.prototype[modelDbSymbol]; delete this.prototype.collection; delete this.prototype.$collection; delete this.prototype[modelCollectionSymbol]; } this.db = connection; const collection = connection.collection(this.modelName, connection.options); this.prototype.collection = collection; this.prototype.$collection = collection; this.prototype[modelCollectionSymbol] = collection; this.prototype.db = connection; this.prototype[modelDbSymbol] = connection; this.collection = collection; this.$__collection = collection; connection.models[this.modelName] = this; return this; }; /** * 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(); const asyncLocalStorage = this[modelDbSymbol].base.transactionAsyncLocalStorage?.getStore(); if (session != null) { saveOptions.session = session; } else if (!options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { // Only set session from asyncLocalStorage if `session` option wasn't originally passed in options saveOptions.session = asyncLocalStorage.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 (options.pathsToSave) { for (const key in delta[1]['$set']) { if (options.pathsToSave.includes(key)) { continue; } else if (options.pathsToSave.some(pathToSave => key.slice(0, pathToSave.length) === pathToSave && key.charAt(pathToSave.length) === '.')) { continue; } else { delete delta[1]['$set'][key]; } } } 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); const update = delta[1]; if (this.$__schema.options.minimize) { for (const updateOp of Object.values(update)) { if (updateOp == null) { continue; } for (const key of Object.keys(updateOp)) { if (updateOp[key] == null || typeof updateOp[key] !== 'object') { continue; } if (!utils.isPOJO(updateOp[key])) { continue; } minimize(updateOp[key]); if (Object.keys(updateOp[key]).length === 0) { delete updateOp[key]; update.$unset = update.$unset || {}; update.$unset[key] = 1; } } } } this[modelCollectionSymbol].updateOne(where, update, saveOptions).then( ret => { if (ret == null) { ret = { $where: where }; } else { ret.$where = where; } callback(null, ret); }, err => { this.$__undoReset(); callback(err); } ); } else { handleEmptyUpdate.call(this); return; } // store the modified paths before the document is reset this.$__.modifiedPaths = this.modifiedPaths(); this.$__reset(); _setIsNew(this, false); function handleEmptyUpdate() { 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; } } applyReadConcern(this.$__schema, optionsWithCustomValues); this.constructor.collection.findOne(where, optionsWithCustomValues) .then(documentExists => { const matchedCount = !documentExists ? 0 : 1; callback(null, { $where: where, matchedCount }); }) .catch(callback); } }; /*! * ignore */ Model.prototype.$__save = function(options, callback) { this.$__handleSave(options, (error, result) => { if (error) { error = this.$__schema._transformDuplicateKeyError(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()`. * @param {Array} [options.pathsToSave] An array of paths that tell mongoose to only validate and save the paths in `pathsToSave`. * @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; /** * 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; } } }; /** * 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 = function increment() { this.$__.version = VERSION_ALL; return this; }; /** * 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; }; /** * Delete this document from the db. Returns a Query instance containing a `deleteOne` operation by this document's `_id`. * * #### Example: * * await product.deleteOne(); * await Product.findById(product._id); // null * * Since `deleteOne()` returns a Query, the `deleteOne()` will **not** execute unless you use either `await`, `.then()`, `.catch()`, or [`.exec()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.exec()) * * #### Example: * * product.deleteOne(); // Doesn't do anything * product.deleteOne().exec(); // Deletes the document, returns a promise * * @return {Query} Query * @api public */ Model.prototype.deleteOne = 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 self = this; const where = this.$__where(); if (where instanceof Error) { throw where; } const query = self.constructor.deleteOne(where, options); if (this.$session() != null) { if (!('session' in query.options)) { query.options.session = this.$session(); } } query.pre(function queryPreDeleteOne(cb) { self.constructor._middleware.execPre('deleteOne', self, [self], cb); }); query.pre(function callSubdocPreHooks(cb) { each(self.$getAllSubdocs(), (subdoc, cb) => { subdoc.constructor._middleware.execPre('deleteOne', subdoc, [subdoc], cb); }, cb); }); query.pre(function skipIfAlreadyDeleted(cb) { if (self.$__.isDeleted) { return cb(Kareem.skipWrappedFunction()); } return cb(); }); query.post(function callSubdocPostHooks(cb) { each(self.$getAllSubdocs(), (subdoc, cb) => { subdoc.constructor._middleware.execPost('deleteOne', subdoc, [subdoc], {}, cb); }, cb); }); query.post(function queryPostDeleteOne(cb) { self.constructor._middleware.execPost('deleteOne', self, [self], {}, cb); }); return query; }; /** * Returns the model instance used to create this document if no `name` specified. * If `name` specified, returns the model with the given `name`. * * #### Example: * * const doc = new Tank({}); * doc.$model() === Tank; // true * await doc.$model('User').findById(id); * * @param {String} [name] model name * @method $model * @api public * @return {Model} */ Model.prototype.$model = function $model(name) { if (arguments.length === 0) { return this.constructor; } return this[modelDbSymbol].model(name); }; /** * Returns the model instance used to create this document if no `name` specified. * If `name` specified, returns the model with the given `name`. * * #### Example: * * const doc = new Tank({}); * doc.$model() === Tank; // true * await doc.$model('User').findById(id); * * @param {String} [name] model name * @method model * @api public * @return {Model} */ Model.prototype.model = Model.prototype.$model; /** * 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; const overwriteModels = typeof options.overwriteModels === 'boolean' ? options.overwriteModels : false; _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, overwriteModels); if (this.db.models[name] && !schema.options.overwriteModels && !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 initializing the underlying connection in MongoDB based on schema options. * This function performs the following operations: * * - `createCollection()` unless [`autoCreate`](https://mongoosejs.com/docs/guide.html#autoCreate) option is turned off * - `ensureIndexes()` unless [`autoIndex`](https://mongoosejs.com/docs/guide.html#autoIndex) option is turned off * - `createSearchIndex()` on all schema search indexes if `autoSearchIndex` is enabled. * * Mongoose calls this function automatically when a model is a 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 _createSearchIndexes = async() => { const autoSearchIndex = utils.getOption( 'autoSearchIndex', this.schema.options, conn.config, conn.base.options ); if (!autoSearchIndex) { return; } const results = []; for (const searchIndex of this.schema._searchIndexes) { results.push(await this.createSearchIndex(searchIndex)); } return results; }; const _createCollection = async() => { let autoCreate = utils.getOption( 'autoCreate', this.schema.options, conn.config // No base.options here because we don't want to take the base value if the connection hasn't // set it yet ); if (autoCreate == null) { // `autoCreate` may later be set when the connection is opened, so wait for connect before checking await conn._waitForConnect(true); autoCreate = utils.getOption( 'autoCreate', this.schema.options, conn.config, conn.base.options ); } if (!autoCreate) { return; } return await this.createCollection(); }; this.$init = _createCollection(). then(() => _ensureIndexes()). then(() => _createSearchIndexes()); 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 shouldSkip = await new Promise((resolve, reject) => { this.hooks.execPre('createCollection', this, [options], (err) => { if (err != null) { if (err instanceof Kareem.skipWrappedFunction) { return resolve(true); } return reject(err); } resolve(); }); }); 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 { if (!shouldSkip) { await this.db.createCollection(this.$__collection.collectionName, options); } } catch (err) { if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) { await new Promise((resolve, reject) => { const _opts = { error: err }; this.hooks.execPost('createCollection', this, [null], _opts, (err) => { if (err != null) { return reject(err); } resolve(); }); }); } } await new Promise((resolve, reject) => { this.hooks.execPost('createCollection', this, [this.$__collection], (err) => { if (err != null) { return reject(err); } resolve(); }); }); 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 * @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher * @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 autoCreate = options?.autoCreate ?? this.schema.options?.autoCreate ?? this.db.config.autoCreate ?? true; if (autoCreate) { try { await this.createCollection(); } catch (err) { if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) { throw err; } } } const diffIndexesResult = await this.diffIndexes({ indexOptionsToCreate: true }); const dropped = await this.cleanIndexes({ ...options, toDrop: diffIndexesResult.toDrop }); await this.createIndexes({ ...options, toCreate: diffIndexesResult.toCreate }); return dropped; }; /** * Create an [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/). * This function only works when connected to MongoDB Atlas. * * #### Example: * * const schema = new Schema({ name: { type: String, unique: true } }); * const Customer = mongoose.model('Customer', schema); * await Customer.createSearchIndex({ name: 'test', definition: { mappings: { dynamic: true } } }); * * @param {Object} description index options, including `name` and `definition` * @param {String} description.name * @param {Object} description.definition * @return {Promise} * @api public */ Model.createSearchIndex = async function createSearchIndex(description) { _checkContext(this, 'createSearchIndex'); return await this.$__collection.createSearchIndex(description); }; /** * Update an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/). * This function only works when connected to MongoDB Atlas. * * #### Example: * * const schema = new Schema({ name: { type: String, unique: true } }); * const Customer = mongoose.model('Customer', schema); * await Customer.updateSearchIndex('test', { mappings: { dynamic: true } }); * * @param {String} name * @param {Object} definition * @return {Promise} * @api public */ Model.updateSearchIndex = async function updateSearchIndex(name, definition) { _checkContext(this, 'updateSearchIndex'); return await this.$__collection.updateSearchIndex(name, definition); }; /** * Delete an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) by name. * This function only works when connected to MongoDB Atlas. * * #### Example: * * const schema = new Schema({ name: { type: String, unique: true } }); * const Customer = mongoose.model('Customer', schema); * await Customer.dropSearchIndex('test'); * * @param {String} name * @return {Promise} * @api public */ Model.dropSearchIndex = async function dropSearchIndex(name) { _checkContext(this, 'dropSearchIndex'); return await this.$__collection.dropSearchIndex(name); }; /** * List all [Atlas search indexes](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) on this model's collection. * This function only works when connected to MongoDB Atlas. * * #### Example: * * const schema = new Schema({ name: { type: String, unique: true } }); * const Customer = mongoose.model('Customer', schema); * * await Customer.createSearchIndex({ name: 'test', definition: { mappings: { dynamic: true } } }); * const res = await Customer.listSearchIndexes(); // Includes `[{ name: 'test' }]` * * @param {Object} [options] * @return {Promise<Array>} * @api public */ Model.listSearchIndexes = async function listSearchIndexes(options) { _checkContext(this, 'listSearchIndexes'); const cursor = await this.$__collection.listSearchIndexes(options); return await cursor.toArray(); }; /** * 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 index specs containing the keys of indexes that `syncIndexes()` will create * * @param {Object} [options] * @param {Boolean} [options.indexOptionsToCreate=false] If true, `toCreate` will include both the index spec and the index options, not just the index spec * @return {Promise<Object>} contains the indexes that would be dropped in MongoDB and indexes that would be created in MongoDB as `{ toDrop: string[], toCreate: string[] }`. */ Model.diffIndexes = async function diffIndexes(options) { 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().catch(err => { if (err.codeName == 'NamespaceNotFound') { return undefined; } throw err; }); if (dbIndexes === undefined) { dbIndexes = []; } dbIndexes = getRelatedDBIndexes(model, dbIndexes