UNPKG

@skybloxsystems/ticket-bot

Version:
1,623 lines (1,425 loc) 176 kB
'use strict'; /*! * Module dependencies. */ const Aggregate = require('./aggregate'); const ChangeStream = require('./cursor/ChangeStream'); const Document = require('./document'); const DocumentNotFoundError = require('./error/notFound'); const DivergentArrayError = require('./error/divergentArray'); const EventEmitter = require('events').EventEmitter; const MongooseBuffer = require('./types/buffer'); const MongooseError = require('./error/index'); const OverwriteModelError = require('./error/overwriteModel'); const PromiseProvider = require('./promise_provider'); const Query = require('./query'); const RemoveOptions = require('./options/removeOptions'); const SaveOptions = require('./options/saveOptions'); const Schema = require('./schema'); const ServerSelectionError = require('./error/serverSelection'); const ValidationError = require('./error/validation'); const VersionError = require('./error/version'); const ParallelSaveError = require('./error/parallelSave'); const applyQueryMiddleware = require('./helpers/query/applyQueryMiddleware'); const applyHooks = require('./helpers/model/applyHooks'); const applyMethods = require('./helpers/model/applyMethods'); const applyStaticHooks = require('./helpers/model/applyStaticHooks'); const applyStatics = require('./helpers/model/applyStatics'); const applyWriteConcern = require('./helpers/schema/applyWriteConcern'); const assignVals = require('./helpers/populate/assignVals'); const castBulkWrite = require('./helpers/model/castBulkWrite'); const createPopulateQueryFilter = require('./helpers/populate/createPopulateQueryFilter'); const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult'); 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 isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive'); const leanPopulateMap = require('./helpers/populate/leanPopulateMap'); const modifiedPaths = require('./helpers/update/modifiedPaths'); const parallelLimit = require('./helpers/parallelLimit'); const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscriminatorPipeline'); const removeDeselectedForeignField = require('./helpers/populate/removeDeselectedForeignField'); const util = require('util'); const utils = require('./utils'); const VERSION_WHERE = 1; const VERSION_INC = 2; const VERSION_ALL = VERSION_WHERE | VERSION_INC; const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol; const modelCollectionSymbol = Symbol('mongoose#Model#collection'); const modelDbSymbol = Symbol('mongoose#Model#db'); const modelSymbol = require('./helpers/symbols').modelSymbol; const subclassedSymbol = Symbol('mongoose#Model#subclassed'); const saveToObjectOptions = Object.assign({}, internalToObjectOptions, { bson: true }); /** * A Model is a class that's your primary tool for interacting with MongoDB. * An instance of a Model is called a [Document](./api.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()`](./api.html#mongoose_Mongoose-model) and * [`connection.model()`](./api.html#connection_Connection-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 [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](./api.html#query_Query-select). * @param {Boolean} [skipId=false] optional boolean. If true, mongoose doesn't add an `_id` field to the document. * @inherits Document http://mongoosejs.com/docs/api/document.html * @event `error`: If listening to this event, 'error' is emitted when a document was saved without passing a callback and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model. * @event `index`: Emitted after `Model#ensureIndexes` completes. If an error occurred it is passed with the event. * @event `index-single-start`: Emitted when an individual index starts within `Model#ensureIndexes`. The fields and options being used to build the index are also passed with the event. * @event `index-single-done`: Emitted when an individual index finishes within `Model#ensureIndexes`. If an error occurred it is passed with the event. The fields, options, and index name are also passed. * @api public */ function Model(doc, fields, skipId) { if (fields instanceof Schema) { throw new TypeError('2nd argument to `Model` must be a POJO or string, ' + '**not** a schema. Make sure you\'re calling `mongoose.model()`, not ' + '`mongoose.Model()`.'); } Document.call(this, doc, fields, skipId); } /*! * Inherits from Document. * * All Model.prototype features are available on * top level (non-sub) documents. */ Model.prototype.__proto__ = Document.prototype; Model.prototype.$isMongooseModelPrototype = true; /** * Connection the model uses. * * @api public * @property db * @memberOf Model * @instance */ Model.prototype.db; /** * Collection the model uses. * * 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: 'notanid' }).catch(noop); * * @api public * @fires error whenever any query or model function errors * @memberOf Model * @static events */ 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 _this = this; let saveOptions = {}; applyWriteConcern(this.$__schema, options); if (typeof options.writeConcern != 'undefined') { saveOptions.writeConcern = {}; if ('w' in options.writeConcern) { saveOptions.writeConcern.w = options.writeConcern.w; } if ('j' in options.writeConcern) { saveOptions.writeConcern.j = options.writeConcern.j; } if ('wtimeout' in options.writeConcern) { saveOptions.writeConcern.wtimeout = options.writeConcern.wtimeout; } } else { if ('w' in options) { saveOptions.w = options.w; } if ('j' in options) { saveOptions.j = options.j; } if ('wtimeout' in options) { saveOptions.wtimeout = options.wtimeout; } } if ('checkKeys' in options) { saveOptions.checkKeys = options.checkKeys; } const session = this.$session(); if (!saveOptions.hasOwnProperty('session')) { saveOptions.session = session; } if (Object.keys(saveOptions).length === 0) { saveOptions = null; } 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, function(err, ret) { if (err) { _setIsNew(_this, true); callback(err, null); return; } callback(null, ret); }); this.$__reset(); _setIsNew(this, false); // Make it possible to retry the insert this.$__.inserting = true; } else { // Make sure we don't treat it as a new object on error, // since it already exists this.$__.inserting = false; const delta = this.$__delta(); if (delta) { if (delta instanceof MongooseError) { callback(delta); return; } const where = this.$__where(delta[0]); if (where instanceof MongooseError) { callback(where); return; } _applyCustomWhere(this, where); this[modelCollectionSymbol].updateOne(where, delta[1], saveOptions, (err, ret) => { if (err) { this.$__undoReset(); callback(err); return; } ret.$where = where; callback(null, ret); }); } else { const optionsWithCustomValues = Object.assign({}, options, saveOptions); const where = this.$__where(); if (this.$__schema.options.optimisticConcurrency) { const key = this.$__schema.options.versionKey; const val = this.$__getValue(key); if (val != null) { where[key] = val; } } this.constructor.exists(where, optionsWithCustomValues). then((documentExists) => { if (!documentExists) { const matchedCount = 0; return callback(null, { $where: where, matchedCount }); } const matchedCount = 1; callback(null, { $where: where, matchedCount }); }). catch(callback); return; } // store the modified paths before the document is reset this.$__.modifiedPaths = this.modifiedPaths(); this.$__reset(); _setIsNew(this, false); } }; /*! * ignore */ Model.prototype.$__save = function(options, callback) { this.$__handleSave(options, (error, result) => { const hooks = this.$__schema.s.hooks; if (error) { return hooks.execPost('save:error', this, [this], { error: error }, (error) => { callback(error, this); }); } let numAffected = 0; if (get(options, 'safe.w') !== 0 && get(options, 'w') !== 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; } } // was this an update that required a version bump? if (this.$__.version && !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); 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](/docs/api.html#document_Document-isNew) is `true`, * or sends an [updateOne](/docs/api.html#document_Document-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://docs.mongodb.com/manual/reference/server-sessions/) associated with this save operation. If not specified, defaults to the [document's associated session](api.html#document_Document-$session). * @param {Object} [options.safe] (DEPRECATED) overrides [schema's safe option](http://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://docs.mongodb.com/manual/reference/write-concern/#w-option). Overrides the [schema-level `writeConcern` option](/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://docs.mongodb.com/manual/reference/write-concern/#j-option). Overrides the [schema-level `writeConcern` option](/docs/guide.html#writeConcern) * @param {Number} [options.wtimeout] sets a [timeout for the write concern](https://docs.mongodb.com/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](/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/#Restrictions-on-Field-Names) * @param {Boolean} [options.timestamps=true] if `false` and [timestamps](./guide.html#timestamps) are enabled, skip timestamps for this `save()`. * @param {Function} [fn] optional callback * @throws {DocumentNotFoundError} if this [save updates an existing document](api.html#document_Document-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|undefined} Returns undefined if used with callback or a Promise otherwise. * @api public * @see middleware http://mongoosejs.com/docs/middleware.html */ Model.prototype.save = function(options, fn) { let parallelSave; this.$op = 'save'; if (this.$__.saving) { parallelSave = new ParallelSaveError(this); } else { this.$__.saving = new ParallelSaveError(this); } if (typeof options === 'function') { fn = options; options = undefined; } options = new SaveOptions(options); if (options.hasOwnProperty('session')) { this.$session(options.session); } this.$__.$versionError = generateVersionError(this, this.modifiedPaths()); fn = this.constructor.$handleCallbackError(fn); return this.constructor.db.base._promiseOrCallback(fn, cb => { cb = this.constructor.$wrapCallback(cb); if (parallelSave) { this.$__handleReject(parallelSave); return cb(parallelSave); } this.$__.saveOptions = options; this.$__save(options, error => { this.$__.saving = undefined; delete this.$__.saveOptions; delete this.$__.$versionError; this.$op = null; if (error) { this.$__handleReject(error); return cb(error); } cb(null, this); }); }, this.constructor.events); }; Model.prototype.$save = Model.prototype.save; /*! * Determines whether versioning should be skipped for the given path * * @param {Document} self * @param {String} path * @return {Boolean} true if versioning should be skipped for the given path */ function shouldSkipVersioning(self, path) { const skipVersioning = self.$__schema.options.skipVersioning; if (!skipVersioning) return false; // Remove any array indexes from the path path = path.replace(/\.\d+\./, '.'); return skipVersioning[path]; } /*! * Apply the operation to the delta (update) clause as * well as track versioning for our where clause. * * @param {Document} self * @param {Object} where * @param {Object} delta * @param {Object} data * @param {Mixed} val * @param {String} [operation] */ function operand(self, where, delta, data, val, op) { // delta op || (op = '$set'); if (!delta[op]) delta[op] = {}; delta[op][data.path] = val; // disabled versioning? if (self.$__schema.options.versionKey === false) return; // path excluded from versioning? if (shouldSkipVersioning(self, data.path)) return; // already marked for versioning? if (VERSION_ALL === (VERSION_ALL & self.$__.version)) return; if (self.$__schema.options.optimisticConcurrency) { self.$__.version = VERSION_ALL; return; } switch (op) { case '$set': case '$unset': case '$pop': case '$pull': case '$pullAll': case '$push': case '$addToSet': break; default: // nothing to do return; } // ensure updates sent with positional notation are // editing the correct array element. // only increment the version if an array position changes. // modifying elements of an array is ok if position does not change. if (op === '$push' || op === '$addToSet' || op === '$pullAll' || op === '$pull') { self.$__.version = VERSION_INC; } else if (/^\$p/.test(op)) { // potentially changing array positions increment.call(self); } else if (Array.isArray(val)) { // $set an array increment.call(self); } else if (/\.\d+\.|\.\d+$/.test(data.path)) { // now handling $set, $unset // subpath of array self.$__.version = VERSION_WHERE; } } /*! * Compiles an update and where clause for a `val` with _atomics. * * @param {Document} self * @param {Object} where * @param {Object} delta * @param {Object} data * @param {Array} value */ function handleAtomics(self, where, delta, data, value) { if (delta.$set && delta.$set[data.path]) { // $set has precedence over other atomics return; } if (typeof value.$__getAtomics === 'function') { value.$__getAtomics().forEach(function(atomic) { const op = atomic[0]; const val = atomic[1]; operand(self, where, delta, data, val, op); }); return; } // legacy support for plugins const atomics = value[arrayAtomicsSymbol]; const ops = Object.keys(atomics); let i = ops.length; let val; let op; if (i === 0) { // $set if (utils.isMongooseObject(value)) { value = value.toObject({ depopulate: 1, _isNested: true }); } else if (value.valueOf) { value = value.valueOf(); } return operand(self, where, delta, data, value); } function iter(mem) { return utils.isMongooseObject(mem) ? mem.toObject({ depopulate: 1, _isNested: true }) : mem; } while (i--) { op = ops[i]; val = atomics[op]; if (utils.isMongooseObject(val)) { val = val.toObject({ depopulate: true, transform: false, _isNested: true }); } else if (Array.isArray(val)) { val = val.map(iter); } else if (val.valueOf) { val = val.valueOf(); } if (op === '$addToSet') { val = { $each: val }; } operand(self, where, delta, data, val, op); } } /** * Produces a special query document of the modified properties used in updates. * * @api private * @method $__delta * @memberOf Model * @instance */ Model.prototype.$__delta = function() { const dirty = this.$__dirty(); if (!dirty.length && VERSION_ALL !== this.$__.version) { return; } const where = {}; const delta = {}; const len = dirty.length; const divergent = []; let d = 0; where._id = this._doc._id; // If `_id` is an object, need to depopulate, but also need to be careful // because `_id` can technically be null (see gh-6406) if (get(where, '_id.$__', null) != null) { where._id = where._id.toObject({ transform: false, depopulate: true }); } for (; d < len; ++d) { const data = dirty[d]; let value = data.value; const match = checkDivergentArray(this, data.path, value); if (match) { divergent.push(match); continue; } const pop = this.$populated(data.path, true); if (!pop && this.$__.selected) { // If any array was selected using an $elemMatch projection, we alter the path and where clause // NOTE: MongoDB only supports projected $elemMatch on top level array. const pathSplit = data.path.split('.'); const top = pathSplit[0]; if (this.$__.selected[top] && this.$__.selected[top].$elemMatch) { // If the selected array entry was modified if (pathSplit.length > 1 && pathSplit[1] == 0 && typeof where[top] === 'undefined') { where[top] = this.$__.selected[top]; pathSplit[1] = '$'; data.path = pathSplit.join('.'); } // if the selected array was modified in any other way throw an error else { divergent.push(data.path); continue; } } } if (divergent.length) continue; if (value === undefined) { operand(this, where, delta, data, 1, '$unset'); } else if (value === null) { operand(this, where, delta, data, null); } else if (value.isMongooseArray && value.$path() && value[arrayAtomicsSymbol]) { // arrays and other custom types (support plugins etc) handleAtomics(this, where, delta, data, value); } else if (value[MongooseBuffer.pathSymbol] && Buffer.isBuffer(value)) { // MongooseBuffer value = value.toObject(); operand(this, where, delta, data, value); } else { value = utils.clone(value, { depopulate: true, transform: false, virtuals: false, getters: false, _isNested: true }); operand(this, where, delta, data, value); } } if (divergent.length) { return new DivergentArrayError(divergent); } if (this.$__.version) { this.$__version(where, delta); } return [where, delta]; }; /*! * Determine if array was populated with some form of filter and is now * being updated in a manner which could overwrite data unintentionally. * * @see https://github.com/Automattic/mongoose/issues/1334 * @param {Document} doc * @param {String} path * @return {String|undefined} */ function checkDivergentArray(doc, path, array) { // see if we populated this path const pop = doc.$populated(path, true); if (!pop && doc.$__.selected) { // If any array was selected using an $elemMatch projection, we deny the update. // NOTE: MongoDB only supports projected $elemMatch on top level array. const top = path.split('.')[0]; if (doc.$__.selected[top + '.$']) { return top; } } if (!(pop && array && array.isMongooseArray)) return; // If the array was populated using options that prevented all // documents from being returned (match, skip, limit) or they // deselected the _id field, $pop and $set of the array are // not safe operations. If _id was deselected, we do not know // how to remove elements. $pop will pop off the _id from the end // of the array in the db which is not guaranteed to be the // same as the last element we have here. $set of the entire array // would be similarly destructive as we never received all // elements of the array and potentially would overwrite data. const check = pop.options.match || pop.options.options && utils.object.hasOwnProperty(pop.options.options, 'limit') || // 0 is not permitted pop.options.options && pop.options.options.skip || // 0 is permitted pop.options.select && // deselected _id? (pop.options.select._id === 0 || /\s?-_id\s?/.test(pop.options.select)); if (check) { const atomics = array[arrayAtomicsSymbol]; if (Object.keys(atomics).length === 0 || atomics.$set || atomics.$pop) { return path; } } } /** * Appends versioning to the where and update clauses. * * @api private * @method $__version * @memberOf Model * @instance */ Model.prototype.$__version = function(where, delta) { const key = this.$__schema.options.versionKey; if (where === true) { // this is an insert if (key) { this.$__setValue(key, delta[key] = 0); } 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: * * Model.findById(id, function (err, doc) { * doc.increment(); * doc.save(function (err) { .. }) * }) * * @see versionKeys http://mongoosejs.com/docs/guide.html#versionKey * @api public */ function increment() { this.$__.version = VERSION_ALL; return this; } Model.prototype.increment = increment; /** * Returns a query object * * @api private * @method $__where * @memberOf Model * @instance */ Model.prototype.$__where = function _where(where) { where || (where = {}); if (!where._id) { where._id = this._doc._id; } if (this._doc._id === void 0) { return new MongooseError('No _id found on document!'); } return where; }; /** * Removes this document from the db. * * ####Example: * product.remove(function (err, product) { * if (err) return handleError(err); * Product.findById(product._id, function (err, product) { * console.log(product) // null * }) * }) * * * As an extra measure of flow control, remove will return a Promise (bound to `fn` if passed) so it could be chained, or hooked to receive errors * * ####Example: * product.remove().then(function (product) { * ... * }).catch(function (err) { * assert.ok(err) * }) * * @param {Object} [options] * @param {Session} [options.session=null] the [session](https://docs.mongodb.com/manual/reference/server-sessions/) associated with this operation. If not specified, defaults to the [document's associated session](api.html#document_Document-$session). * @param {function(err,product)} [fn] optional callback * @return {Promise} Promise * @api public */ Model.prototype.remove = function remove(options, fn) { if (typeof options === 'function') { fn = options; options = undefined; } options = new RemoveOptions(options); if (options.hasOwnProperty('session')) { this.$session(options.session); } this.$op = 'remove'; fn = this.constructor.$handleCallbackError(fn); return this.constructor.db.base._promiseOrCallback(fn, cb => { cb = this.constructor.$wrapCallback(cb); this.$__remove(options, (err, res) => { this.$op = null; cb(err, res); }); }, this.constructor.events); }; /*! * Alias for remove */ Model.prototype.$remove = Model.prototype.remove; Model.prototype.delete = Model.prototype.remove; /** * Removes this document from the db. Equivalent to `.remove()`. * * ####Example: * product = await product.deleteOne(); * await Product.findById(product._id); // null * * @param {function(err,product)} [fn] optional callback * @return {Promise} Promise * @api public */ Model.prototype.deleteOne = function deleteOne(options, fn) { if (typeof options === 'function') { fn = options; options = undefined; } if (!options) { options = {}; } fn = this.constructor.$handleCallbackError(fn); return this.constructor.db.base._promiseOrCallback(fn, cb => { cb = this.constructor.$wrapCallback(cb); this.$__deleteOne(options, cb); }, this.constructor.events); }; /*! * ignore */ Model.prototype.$__remove = function $__remove(options, cb) { if (this.$__.isDeleted) { return immediate(() => cb(null, this)); } const where = this.$__where(); if (where instanceof MongooseError) { return cb(where); } _applyCustomWhere(this, where); const session = this.$session(); if (!options.hasOwnProperty('session')) { options.session = session; } this[modelCollectionSymbol].deleteOne(where, options, err => { if (!err) { this.$__.isDeleted = true; this.$emit('remove', this); this.constructor.emit('remove', this); return cb(null, this); } this.$__.isDeleted = false; cb(err); }); }; /*! * ignore */ Model.prototype.$__deleteOne = Model.prototype.$__remove; /** * Returns another Model instance. * * ####Example: * * const doc = new Tank; * doc.model('User').findById(id, callback); * * @param {String} name model name * @api public */ Model.prototype.model = function model(name) { return this[modelDbSymbol].model(name); }; /** * Returns true if at least one document exists in the database that matches * the given `filter`, and false 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()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Function} [callback] callback * @return {Query} */ Model.exists = function exists(filter, options, callback) { _checkContext(this, 'exists'); if (typeof options === 'function') { callback = options; options = null; } const query = this.findOne(filter). select({ _id: 1 }). lean(). setOptions(options); if (typeof callback === 'function') { return query.exec(callback); } options = options || {}; if (!options.explain) { return query.then(doc => !!doc); } 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. * @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 = get(options, 'clone', true); _checkContext(this, 'discriminator'); if (utils.isObject(schema) && !schema.instanceOfSchema) { schema = new Schema(schema); } if (schema instanceof Schema && clone) { schema = schema.clone(); } schema = discriminator(this, name, schema, value, true); if (this.db.models[name]) { 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]; d.prototype.__proto__ = 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 */ function _checkContext(ctx, fnName) { // Check context, because it is easy to mistakenly type // `new Model.discriminator()` and get an incomprehensible error if (ctx == null || ctx === global) { throw new MongooseError('`Model.' + fnName + '()` cannot run without a ' + 'model as `this`. Make sure you are calling `MyModel.' + fnName + '()` ' + 'where `MyModel` is a Mongoose model.'); } else if (ctx[modelSymbol] == null) { throw new MongooseError('`Model.' + fnName + '()` cannot run without a ' + 'model as `this`. Make sure you are not calling ' + '`new Model.' + fnName + '()`'); } } // Model (class) features /*! * Give the constructor the ability to emit events. */ for (const i in EventEmitter.prototype) { Model[i] = EventEmitter.prototype[i]; } /** * This function is responsible for building [indexes](https://docs.mongodb.com/manual/indexes/), * unless [`autoIndex`](http://mongoosejs.com/docs/guide.html#autoIndex) is turned off. * * Mongoose calls this function automatically when a model is created using * [`mongoose.model()`](/docs/api.html#mongoose_Mongoose-model) or * [`connection.model()`](/docs/api.html#connection_Connection-model), so you * don't need to call it. This function is also idempotent, so you may call it * to get back a promise that will resolve when your indexes are finished * building as an alternative to [`MyModel.on('index')`](/docs/guide.html#indexes) * * ####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); * * Event.init().then(function(Event) { * // You can also use `Event.on('index')` if you prefer event emitters * // over promises. * console.log('Indexes are done building!'); * }); * * @api public * @param {Function} [callback] * @returns {Promise} */ Model.init = function init(callback) { _checkContext(this, 'init'); this.schema.emit('init', this); if (this.$init != null) { if (callback) { this.$init.then(() => callback(), err => callback(err)); return null; } return this.$init; } const Promise = PromiseProvider.get(); const autoIndex = utils.getOption('autoIndex', this.schema.options, this.db.config, this.db.base.options); const autoCreate = utils.getOption('autoCreate', this.schema.options, this.db.config, this.db.base.options); const _ensureIndexes = autoIndex ? cb => this.ensureIndexes({ _automatic: true }, cb) : cb => cb(); const _createCollection = autoCreate ? cb => this.createCollection({}, cb) : cb => cb(); this.$init = new Promise((resolve, reject) => { _createCollection(error => { if (error) { return reject(error); } _ensureIndexes(error => { if (error) { return reject(error); } resolve(this); }); }); }); if (callback) { this.$init.then(() => callback(), err => callback(err)); this.$caught = true; return null; } else { 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://docs.mongodb.com/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](http://mongodb.github.io/node-mongodb-native/3.1/api/Db.html#createCollection) * @param {Function} [callback] * @returns {Promise} */ Model.createCollection = function createCollection(options, callback) { _checkContext(this, 'createCollection'); if (typeof options === 'string') { throw new MongooseError('You can\'t specify a new collection name in Model.createCollection.' + 'This is not like Connection.createCollection. Only options are accepted here.'); } else if (typeof options === 'function') { callback = options; options = void 0; } const schemaCollation = get(this, ['schema', 'options', 'collation'], null); if (schemaCollation != null) { options = Object.assign({ collation: schemaCollation }, options); } const capped = get(this, ['schema', 'options', 'capped']); if (capped) { options = Object.assign({ capped: true }, capped, options); } const timeseries = get(this, ['schema', 'options', 'timeseries']); if (timeseries != null) { options = Object.assign({ timeseries }, options); } callback = this.$handleCallbackError(callback); return this.db.base._promiseOrCallback(callback, cb => { cb = this.$wrapCallback(cb); this.db.createCollection(this.$__collection.collectionName, options, utils.tick((err) => { if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) { return cb(err); } this.$__collection = this.db.collection(this.$__collection.collectionName, options); cb(null, this.$__collection); })); }, this.events); }; /** * 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](http://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(); * * @param {Object} [options] options to pass to `ensureIndexes()` * @param {Boolean} [options.background=null] if specified, overrides each index's `background` property * @param {Function} [callback] optional callback * @return {Promise|undefined} Returns `undefined` if callback is specified, returns a promise if no callback. * @api public */ Model.syncIndexes = function syncIndexes(options, callback) { _checkContext(this, 'syncIndexes'); callback = this.$handleCallbackError(callback); return this.db.base._promiseOrCallback(callback, cb => { cb = this.$wrapCallback(cb); this.createCollection(err => { if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) { return cb(err); } this.cleanIndexes((err, dropped) => { if (err != null) { return cb(err); } this.createIndexes(options, err => { if (err != null) { return cb(err); } cb(null, dropped); }); }); }); }, this.events); }; /** * Does a dry-run of Model.syncIndexes(), meaning that * the result of this function would be the result of * Model.syncIndexes(). * * @param {Object} options not used at all. * @param {Function} callback optional callback * @returns {Promise} which containts an object, {toDrop, toCreate}, which * are indexes that would be dropped in mongodb and indexes that would be created in mongodb. */ Model.diffIndexes = function diffIndexes(options, callback) { const toDrop = []; const toCreate = []; callback = this.$handleCallbackError(callback); return this.db.base._promiseOrCallback(callback, cb => { cb = this.$wrapCallback(cb); this.listIndexes((err, indexes) => { if (indexes === undefined) { indexes = []; } const schemaIndexes = this.schema.indexes(); // Iterate through the indexes created in mongodb and // compare against the indexes in the schema. for (const index of indexes) { let found = false; // Never try to drop `_id` index, MongoDB server doesn't allow it if (isDefaultIdIndex(index)) { continue; } for (const schemaIndex of schemaIndexes) { const key = schemaIndex[0]; const options = _decorateDiscriminatorIndexOptions(this, utils.clone(schemaIndex[1])); if (isIndexEqual(key, options, index)) { found = true; } } if (!found) { toDrop.push(index.name); } } // Iterate through the indexes created on the schema and // compare against the indexes in mongodb. for (const schemaIndex of schemaIndexes) { const key = schemaIndex[0]; let found = false; const options = _decorateDiscriminatorIndexOptions(this, utils.clone(schemaIndex[1])); for (const index of indexes) { if (isDefaultIdIndex(index)) { continue; } if (isIndexEqual(key, options, index)) { found = true; } } if (!found) { toCreate.push(key); } } cb(null, { toDrop, toCreate }); }); }); }; /** * Deletes all indexes that aren't defined in this model's schema. Used by * `syncIndexes()`. * * The returned promise resolves to a list of the dropped indexes' names as an array * * @param {Function} [callback] optional callback * @return {Promise|undefined} Returns `undefined` if callback is specified, returns a promise if no callback. * @api public */ Model.cleanIndexes = function cleanIndexes(callback) { _checkContext(this, 'cleanIndexes'); callback = this.$handleCallbackError(callback); return this.db.base._promiseOrCallback(callback, cb => { const collection = this.$__collection; this.listIndexes((err, indexes) => { if (err != null) { return cb(err); } const schemaIndexes = this.schema.indexes(); const toDrop = []; for (const index of indexes) { let found = false; // Never try to drop `_id` index, MongoDB server doesn't allow it if (isDefaultIdIndex(index)) { continue; } for (const schemaIndex of schemaIndexes) { const key = schemaIndex[0]; const options = _decorateDiscriminatorIndexOptions(this, utils.clone(schemaIndex[1])); if (isIndexEqual(key, options, index)) { found = true; } } if (!found) { toDrop.push(index.name); } } if (toDrop.length === 0) { return cb(null, []); } dropIndexes(toDrop, cb); }); function dropIndexes(toDrop, cb) { let remaining = toDrop.length; let error = false; toDrop.forEach(indexName => { collection.dropIndex(indexName, err => { if (err != null) { error = true; return cb(err); } if (!error) { --remaining || cb(null, toDrop); } }); }); } }); }; /** * Lists the indexes currently defined in MongoDB. This may or may not be * the same as the indexes defined in your schema depending on whether you * use the [`autoIndex` option](/docs/guide.html#autoIndex) and if you * build indexes manually. * * @param {Function} [cb] optional callback * @return {Promise|undefined} Returns `undefined` if callback is specified, returns a promise if no callback. * @api public */ Model.listIndexes = function init(callback) { _checkContext(this, 'listIndexes'); const _listIndexes = cb => { this.$__collection.listIndexes().toArray(cb); }; callback = this.$handleCallbackError(callback); return this.db.base._promiseOrCallback(callback, cb => { cb = this.$wrapCallback(cb); // Buffering if (this.$__collection.buffer) { this.$__collection.addQueue(_listIndexes, [cb]); } else { _listIndexes(cb); } }, this.events); }; /** * Sends `createIndex` commands to mongo for each index declared in the schema. * The `createIndex` commands are sent in series. * * ####Example: * * Event.ensureIndexes(function (err) { * if (err) return handleError(err); * }); * * After completion, an `index` event is emitted on this `Model` passing an error if one occurred. * * ####Example: * * const eventSchema = new Schema({ thing: { type: 'string', unique: true }}) * const Event = mongoose.model('Event', eventSchema); * * Event.on('index', function (err) { * if (err) console.error(err); // error occurred during index creation * }) * * _NOTE: It is not recommended that you run this in production. Index creation may impact database performance depending on your load. Use with caution._ * * @param {Object} [options] internal options * @param {Function} [cb] optional callback * @return {Promise} * @api public */ Mode