mongoose
Version:
Mongoose MongoDB ODM
1,452 lines (1,294 loc) • 189 kB
JavaScript
'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