@skybloxsystems/ticket-bot
Version:
1,623 lines (1,425 loc) • 176 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 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