UNPKG

mongoose

Version:
1,726 lines (1,522 loc) 124 kB
/*! * Module dependencies. */ var Aggregate = require('./aggregate'); var Document = require('./document'); var DocumentNotFoundError = require('./error').DocumentNotFoundError; var DivergentArrayError = require('./error').DivergentArrayError; var Error = require('./error'); var EventEmitter = require('events').EventEmitter; var OverwriteModelError = require('./error').OverwriteModelError; var PromiseProvider = require('./promise_provider'); var Query = require('./query'); var Schema = require('./schema'); var VersionError = require('./error').VersionError; var applyHooks = require('./services/model/applyHooks'); var applyMethods = require('./services/model/applyMethods'); var applyStatics = require('./services/model/applyStatics'); var cast = require('./cast'); var castUpdate = require('./services/query/castUpdate'); var discriminator = require('./services/model/discriminator'); var isPathSelectedInclusive = require('./services/projection/isPathSelectedInclusive'); var get = require('lodash.get'); var getSchemaTypes = require('./services/populate/getSchemaTypes'); var mpath = require('mpath'); var parallel = require('async/parallel'); var parallelLimit = require('async/parallelLimit'); var setDefaultsOnInsert = require('./services/setDefaultsOnInsert'); var util = require('util'); var utils = require('./utils'); var VERSION_WHERE = 1, VERSION_INC = 2, VERSION_ALL = VERSION_WHERE | VERSION_INC; /** * Model constructor * * Provides the interface to MongoDB collections as well as creates document instances. * * @param {Object} doc values with which to create the document * @inherits Document http://mongoosejs.com/docs/api.html#document-js * @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, true); } /*! * 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 */ Model.prototype.db; /** * Collection the model uses. * * @api public * @property collection */ Model.prototype.collection; /** * The name of the model * * @api public * @property modelName */ Model.prototype.modelName; /** * Additional properties to attach to the query when calling `save()` and * `isNew` is false. * * @api public * @property $where */ Model.prototype.$where; /** * If this is a discriminator model, `baseModelName` is the name of * the base model. * * @api public * @property baseModelName */ Model.prototype.baseModelName; Model.prototype.$__handleSave = function(options, callback) { var _this = this; var i; var keys; var len; if (!options.safe && this.schema.options.safe) { options.safe = this.schema.options.safe; } if (typeof options.safe === 'boolean') { options.safe = null; } var safe = options.safe ? utils.clone(options.safe, { retainKeyOrder: true }) : options.safe; if (this.isNew) { // send entire doc var toObjectOptions = {}; toObjectOptions.retainKeyOrder = this.schema.options.retainKeyOrder; toObjectOptions.depopulate = 1; toObjectOptions._skipDepopulateTopLevel = true; toObjectOptions.transform = false; toObjectOptions.flattenDecimals = false; var obj = this.toObject(toObjectOptions); 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 my mongodb necessarily // match the schema definition. setTimeout(function() { callback(new Error('document must have an _id before saving')); }, 0); return; } this.$__version(true, obj); this.collection.insert(obj, safe, function(err, ret) { if (err) { _this.isNew = true; _this.emit('isNew', true); _this.constructor.emit('isNew', true); callback(err); return; } callback(null, ret); }); this.$__reset(); this.isNew = false; this.emit('isNew', false); this.constructor.emit('isNew', 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; var delta = this.$__delta(); if (delta) { if (delta instanceof Error) { callback(delta); return; } var where = this.$__where(delta[0]); if (where instanceof Error) { callback(where); return; } if (this.$where) { keys = Object.keys(this.$where); len = keys.length; for (i = 0; i < len; ++i) { where[keys[i]] = this.$where[keys[i]]; } } this.collection.update(where, delta[1], safe, function(err, ret) { if (err) { callback(err); return; } ret.$where = where; callback(null, ret); }); } else { this.$__reset(); callback(); return; } this.emit('isNew', false); this.constructor.emit('isNew', false); } }; /*! * ignore */ Model.prototype.$__save = function(options, callback) { var _this = this; _this.$__handleSave(options, function(error, result) { if (error) { return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { callback(error); }); } // store the modified paths before the document is reset var modifiedPaths = _this.modifiedPaths(); _this.$__reset(); var numAffected = 0; if (result) { if (Array.isArray(result)) { numAffected = result.length; } else if (result.result && result.result.n !== undefined) { numAffected = result.result.n; } else if (result.result && result.result.nModified !== undefined) { numAffected = result.result.nModified; } else { numAffected = result; } } if (_this.schema.options && _this.schema.options.saveErrorIfNotFound && numAffected <= 0) { error = new DocumentNotFoundError(result.$where); return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { callback(error); }); } // was this an update that required a version bump? if (_this.$__.version && !_this.$__.inserting) { var doIncrement = VERSION_INC === (VERSION_INC & _this.$__.version); _this.$__.version = undefined; var key = _this.schema.options.versionKey; var version = _this.getValue(key) || 0; if (numAffected <= 0) { // the update failed. pass an error back var err = new VersionError(_this, version, modifiedPaths); return callback(err); } // increment version if was successful if (doIncrement) { _this.setValue(key, version + 1); } } _this.emit('save', _this, numAffected); _this.constructor.emit('save', _this, numAffected); callback(null, _this, numAffected); }); }; /** * Saves this document. * * ####Example: * * product.sold = Date.now(); * product.save(function (err, product, numAffected) { * if (err) .. * }) * * The callback will receive three parameters * * 1. `err` if an error occurred * 2. `product` which is the saved `product` * 3. `numAffected` will be 1 when the document was successfully persisted to MongoDB, otherwise 0. Unless you tweak mongoose's internals, you don't need to worry about checking this parameter for errors - checking `err` is sufficient to make sure your document was properly saved. * * As an extra measure of flow control, save will return a Promise. * ####Example: * product.save().then(function(product) { * ... * }); * * For legacy reasons, mongoose stores object keys in reverse order on initial * save. That is, `{ a: 1, b: 2 }` will be saved as `{ b: 2, a: 1 }` in * MongoDB. To override this behavior, set * [the `toObject.retainKeyOrder` option](http://mongoosejs.com/docs/api.html#document_Document-toObject) * to true on your schema. * * @param {Object} [options] options optional options * @param {Object} [options.safe] overrides [schema's safe option](http://mongoosejs.com//docs/guide.html#safe) * @param {Boolean} [options.validateBeforeSave] set to false to save without validating. * @param {Function} [fn] optional callback * @return {Promise} Promise * @api public * @see middleware http://mongoosejs.com/docs/middleware.html */ Model.prototype.save = function(options, fn) { if (typeof options === 'function') { fn = options; options = undefined; } if (!options) { options = {}; } if (fn) { fn = this.constructor.$wrapCallback(fn); } return this.$__save(options, fn); }; /*! * 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) { var 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; switch (op) { case '$set': case '$unset': case '$pop': case '$pull': case '$pullAll': case '$push': case '$pushAll': 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 === '$pushAll' || op === '$addToSet') { self.$__.version = VERSION_INC; } else if (/^\$p/.test(op)) { // potentially changing array positions self.increment(); } else if (Array.isArray(val)) { // $set an array self.increment(); } 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) { var op = atomic[0]; var val = atomic[1]; var usePushEach = false; if ('usePushEach' in get(self, 'constructor.base.options', {})) { usePushEach = self.constructor.base.get('usePushEach'); } if ('usePushEach' in self.schema.options) { usePushEach = self.schema.options.usePushEach; } if (usePushEach && op === '$pushAll') { op = '$push'; val = { $each: val }; } operand(self, where, delta, data, val, op); }); return; } // legacy support for plugins var atomics = value._atomics, ops = Object.keys(atomics), i = ops.length, val, 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 */ Model.prototype.$__delta = function() { var dirty = this.$__dirty(); if (!dirty.length && VERSION_ALL !== this.$__.version) return; var where = {}, delta = {}, len = dirty.length, divergent = [], d = 0; where._id = this._doc._id; if (where._id.toObject) { where._id = where._id.toObject({ transform: false, depopulate: true }); } for (; d < len; ++d) { var data = dirty[d]; var value = data.value; var match = checkDivergentArray(this, data.path, value); if (match) { divergent.push(match); continue; } var 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. var pathSplit = data.path.split('.'); var 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 (undefined === value) { operand(this, where, delta, data, 1, '$unset'); } else if (value === null) { operand(this, where, delta, data, null); } else if (value._path && value._atomics) { // arrays and other custom types (support plugins etc) handleAtomics(this, where, delta, data, value); } else if (value._path && Buffer.isBuffer(value)) { // MongooseBuffer value = value.toObject(); operand(this, where, delta, data, value); } else { value = utils.clone(value, { depopulate: true, transform: false, virtuals: false, retainKeyOrder: true, _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 var 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. var 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 similarily destructive as we never received all // elements of the array and potentially would overwrite data. var 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) { var atomics = array._atomics; 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 */ Model.prototype.$__version = function(where, delta) { var 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)) { var 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 */ Model.prototype.increment = function increment() { this.$__.version = VERSION_ALL; return this; }; /** * Returns a query object * * @api private * @method $__where * @memberOf Model */ Model.prototype.$__where = function _where(where) { where || (where = {}); if (!where._id) { where._id = this._doc._id; } if (this._doc._id == null) { return new Error('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 recive errors * * ####Example: * product.remove().then(function (product) { * ... * }).catch(function (err) { * assert.ok(err) * }) * * @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; } var _this = this; if (!options) { options = {}; } if (this.$__.removing) { if (fn) { this.$__.removing.then( function(res) { fn(null, res); }, function(err) { fn(err); }); } return this; } if (this.$__.isDeleted) { setImmediate(function() { fn(null, _this); }); return this; } var Promise = PromiseProvider.get(); if (fn) { fn = this.constructor.$wrapCallback(fn); } this.$__.removing = new Promise.ES6(function(resolve, reject) { var where = _this.$__where(); if (where instanceof Error) { reject(where); fn && fn(where); return; } if (!options.safe && _this.schema.options.safe) { options.safe = _this.schema.options.safe; } _this.collection.remove(where, options, function(err) { if (!err) { _this.$__.isDeleted = true; _this.emit('remove', _this); _this.constructor.emit('remove', _this); resolve(_this); fn && fn(null, _this); return; } _this.$__.isDeleted = false; reject(err); fn && fn(err); }); }); return this.$__.removing; }; /** * Returns another Model instance. * * ####Example: * * var doc = new Tank; * doc.model('User').findById(id, callback); * * @param {String} name model name * @api public */ Model.prototype.model = function model(name) { return this.db.model(name); }; /** * Adds a discriminator type. * * ####Example: * * function BaseSchema() { * Schema.apply(this, arguments); * * this.add({ * name: String, * createdAt: Date * }); * } * util.inherits(BaseSchema, Schema); * * var PersonSchema = new BaseSchema(); * var BossSchema = new BaseSchema({ department: String }); * * var Person = mongoose.model('Person', PersonSchema); * var Boss = Person.discriminator('Boss', BossSchema); * * @param {String} name discriminator model name * @param {Schema} schema discriminator model schema * @api public */ Model.discriminator = function(name, schema) { var model; if (typeof name === 'function') { model = name; name = utils.getFunctionName(model); if (!(model.prototype instanceof Model)) { throw new Error('The provided class ' + name + ' must extend Model'); } } schema = discriminator(this, name, schema); if (this.db.models[name]) { throw new OverwriteModelError(name); } schema.$isRootDiscriminator = true; model = this.db.model(model || name, schema, this.collection.name); this.discriminators[name] = model; var 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); return d; }; // Model (class) features /*! * Give the constructor the ability to emit events. */ for (var i in EventEmitter.prototype) { Model[i] = EventEmitter.prototype[i]; } /** * Performs any async initialization of this model against MongoDB. Currently, * this function is only responsible for building [indexes](https://docs.mongodb.com/manual/indexes/), * unless [`autoIndex`](http://mongoosejs.com/docs/guide.html#autoIndex) is turned off. * * This function is called automatically, 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')` * * ####Example: * * var 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. * var 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) { this.schema.emit('init', this); if (this.$init) { return this.$init; } var _this = this; var Promise = PromiseProvider.get(); this.$init = new Promise.ES6(function(resolve, reject) { if ((_this.schema.options.autoIndex) || (_this.schema.options.autoIndex == null && _this.db.config.autoIndex)) { _this.ensureIndexes({ _automatic: true, __noPromise: true }, function(error) { if (error) { callback && callback(error); return reject(error); } callback && callback(null, _this); resolve(_this); }); } else { resolve(_this); } }); return this.$init; }; /** * 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: * * var eventSchema = new Schema({ thing: { type: 'string', unique: true }}) * var 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 */ Model.ensureIndexes = function ensureIndexes(options, callback) { if (typeof options === 'function') { callback = options; options = null; } if (options && options.__noPromise) { _ensureIndexes(this, options, callback); return; } if (callback) { callback = this.$wrapCallback(callback); } var _this = this; var Promise = PromiseProvider.get(); return new Promise.ES6(function(resolve, reject) { _ensureIndexes(_this, options || {}, function(error) { if (error) { callback && callback(error); reject(error); } callback && callback(); resolve(); }); }); }; /** * Similar to `ensureIndexes()`, except for it uses the [`createIndex`](http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#createIndex) * function. The `ensureIndex()` function checks to see if an index with that * name already exists, and, if not, does not attempt to create the index. * `createIndex()` bypasses this check. * * @param {Object} [options] internal options * @param {Function} [cb] optional callback * @return {Promise} * @api public */ Model.createIndexes = function createIndexes(options, callback) { if (typeof options === 'function') { callback = options; options = {}; } options = options || {}; options.createIndex = true; return this.ensureIndexes(options, callback); }; function _ensureIndexes(model, options, callback) { var indexes = model.schema.indexes(); options = options || {}; var done = function(err) { if (err && model.schema.options.emitIndexErrors) { model.emit('error', err); } model.emit('index', err); callback && callback(err); }; if (!indexes.length) { setImmediate(function() { done(); }); return; } // Indexes are created one-by-one to support how MongoDB < 2.4 deals // with background indexes. var indexSingleDone = function(err, fields, options, name) { model.emit('index-single-done', err, fields, options, name); }; var indexSingleStart = function(fields, options) { model.emit('index-single-start', fields, options); }; var create = function() { if (options._automatic) { if (model.schema.options.autoIndex === false || (model.schema.options.autoIndex == null && model.db.config.autoIndex === false)) { return done(); } } var index = indexes.shift(); if (!index) return done(); var indexFields = index[0]; var indexOptions = index[1]; _handleSafe(options); indexSingleStart(indexFields, options); var methodName = options.createIndex ? 'createIndex' : 'ensureIndex'; model.collection[methodName](indexFields, indexOptions, utils.tick(function(err, name) { indexSingleDone(err, indexFields, indexOptions, name); if (err) { return done(err); } create(); })); }; setImmediate(function() { // If buffering is off, do this manually. if (options._automatic && !model.collection.collection) { model.collection.addQueue(create, []); } else { create(); } }); } function _handleSafe(options) { if (options.safe) { if (typeof options.safe === 'boolean') { options.w = options.safe; delete options.safe; } if (typeof options.safe === 'object') { options.w = options.safe.w; options.j = options.safe.j; options.wtimeout = options.safe.wtimeout; delete options.safe; } } } /** * Schema the model uses. * * @property schema * @receiver Model * @api public */ Model.schema; /*! * Connection instance the model uses. * * @property db * @receiver Model * @api public */ Model.db; /*! * Collection the model uses. * * @property collection * @receiver Model * @api public */ Model.collection; /** * Base Mongoose instance the model uses. * * @property base * @receiver Model * @api public */ Model.base; /** * Registered discriminators for this model. * * @property discriminators * @receiver Model * @api public */ Model.discriminators; /** * Translate any aliases fields/conditions so the final query or document object is pure * * ####Example: * * Character * .find(Character.translateAliases({ * '名': 'Eddard Stark' // Alias for 'name' * }) * .exec(function(err, characters) {}) * * ####Note: * Only translate arguments of object type anything else is returned raw * * @param {Object} raw fields/conditions that may contain aliased keys * @return {Object} the translated 'pure' fields/conditions */ Model.translateAliases = function translateAliases(fields) { var aliases = this.schema.aliases; if (typeof fields === 'object') { // Fields is an object (query conditions or document fields) for (var key in fields) { if (aliases[key]) { fields[aliases[key]] = fields[key]; delete fields[key]; } } return fields; } else { // Don't know typeof fields return fields; } }; /** * Removes all documents that match `conditions` from the collection. * To remove just the first document that matches `conditions`, set the `single` * option to true. * * ####Example: * * Character.remove({ name: 'Eddard Stark' }, function (err) {}); * * ####Note: * * This method sends a remove command directly to MongoDB, no Mongoose documents * are involved. Because no Mongoose documents are involved, _no middleware * (hooks) are executed_. * * @param {Object} conditions * @param {Function} [callback] * @return {Query} * @api public */ Model.remove = function remove(conditions, callback) { if (typeof conditions === 'function') { callback = conditions; conditions = {}; } // get the mongodb collection object var mq = new this.Query({}, {}, this, this.collection); if (callback) { callback = this.$wrapCallback(callback); } return mq.remove(conditions, callback); }; /** * Deletes the first document that matches `conditions` from the collection. * Behaves like `remove()`, but deletes at most one document regardless of the * `single` option. * * ####Example: * * Character.deleteOne({ name: 'Eddard Stark' }, function (err) {}); * * ####Note: * * Like `Model.remove()`, this function does **not** trigger `pre('remove')` or `post('remove')` hooks. * * @param {Object} conditions * @param {Function} [callback] * @return {Query} * @api public */ Model.deleteOne = function deleteOne(conditions, callback) { if (typeof conditions === 'function') { callback = conditions; conditions = {}; } // get the mongodb collection object var mq = new this.Query(conditions, {}, this, this.collection); if (callback) { callback = this.$wrapCallback(callback); } return mq.deleteOne(callback); }; /** * Deletes all of the documents that match `conditions` from the collection. * Behaves like `remove()`, but deletes all documents that match `conditions` * regardless of the `single` option. * * ####Example: * * Character.deleteMany({ name: /Stark/, age: { $gte: 18 } }, function (err) {}); * * ####Note: * * Like `Model.remove()`, this function does **not** trigger `pre('remove')` or `post('remove')` hooks. * * @param {Object} conditions * @param {Function} [callback] * @return {Query} * @api public */ Model.deleteMany = function deleteMany(conditions, callback) { if (typeof conditions === 'function') { callback = conditions; conditions = {}; } // get the mongodb collection object var mq = new this.Query(conditions, {}, this, this.collection); if (callback) { callback = this.$wrapCallback(callback); } return mq.deleteMany(callback); }; /** * Finds documents * * The `conditions` are cast to their respective SchemaTypes before the command is sent. * * ####Examples: * * // named john and at least 18 * MyModel.find({ name: 'john', age: { $gte: 18 }}); * * // executes immediately, passing results to callback * MyModel.find({ name: 'john', age: { $gte: 18 }}, function (err, docs) {}); * * // name LIKE john and only selecting the "name" and "friends" fields, executing immediately * MyModel.find({ name: /john/i }, 'name friends', function (err, docs) { }) * * // passing options * MyModel.find({ name: /john/i }, null, { skip: 10 }) * * // passing options and executing immediately * MyModel.find({ name: /john/i }, null, { skip: 10 }, function (err, docs) {}); * * // executing a query explicitly * var query = MyModel.find({ name: /john/i }, null, { skip: 10 }) * query.exec(function (err, docs) {}); * * // using the promise returned from executing a query * var query = MyModel.find({ name: /john/i }, null, { skip: 10 }); * var promise = query.exec(); * promise.addBack(function (err, docs) {}); * * @param {Object} conditions * @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo) * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Function} [callback] * @return {Query} * @see field selection #query_Query-select * @see promise #promise-js * @api public */ Model.find = function find(conditions, projection, options, callback) { if (typeof conditions === 'function') { callback = conditions; conditions = {}; projection = null; options = null; } else if (typeof projection === 'function') { callback = projection; projection = null; options = null; } else if (typeof options === 'function') { callback = options; options = null; } var mq = new this.Query({}, {}, this, this.collection); mq.select(projection); mq.setOptions(options); if (this.schema.discriminatorMapping && this.schema.discriminatorMapping.isRoot && mq.selectedInclusively()) { // Need to select discriminator key because original schema doesn't have it mq.select(this.schema.options.discriminatorKey); } if (callback) { callback = this.$wrapCallback(callback); } return mq.find(conditions, callback); }; /** * Finds a single document by its _id field. `findById(id)` is almost* * equivalent to `findOne({ _id: id })`. If you want to query by a document's * `_id`, use `findById()` instead of `findOne()`. * * The `id` is cast based on the Schema before sending the command. * * This function triggers the following middleware: * - `findOne()` * * \* Except for how it treats `undefined`. If you use `findOne()`, you'll see * that `findOne(undefined)` and `findOne({ _id: undefined })` are equivalent * to `findOne({})` and return arbitrary documents. However, mongoose * translates `findById(undefined)` into `findOne({ _id: null })`. * * ####Example: * * // find adventure by id and execute immediately * Adventure.findById(id, function (err, adventure) {}); * * // same as above * Adventure.findById(id).exec(callback); * * // select only the adventures name and length * Adventure.findById(id, 'name length', function (err, adventure) {}); * * // same as above * Adventure.findById(id, 'name length').exec(callback); * * // include all properties except for `length` * Adventure.findById(id, '-length').exec(function (err, adventure) {}); * * // passing options (in this case return the raw js objects, not mongoose documents by passing `lean` * Adventure.findById(id, 'name', { lean: true }, function (err, doc) {}); * * // same as above * Adventure.findById(id, 'name').lean().exec(function (err, doc) {}); * * @param {Object|String|Number} id value of `_id` to query by * @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo) * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Function} [callback] * @return {Query} * @see field selection #query_Query-select * @see lean queries #query_Query-lean * @api public */ Model.findById = function findById(id, projection, options, callback) { if (typeof id === 'undefined') { id = null; } if (callback) { callback = this.$wrapCallback(callback); } return this.findOne({_id: id}, projection, options, callback); }; /** * Finds one document. * * The `conditions` are cast to their respective SchemaTypes before the command is sent. * * *Note:* `conditions` is optional, and if `conditions` is null or undefined, * mongoose will send an empty `findOne` command to MongoDB, which will return * an arbitrary document. If you're querying by `_id`, use `findById()` instead. * * ####Example: * * // find one iphone adventures - iphone adventures?? * Adventure.findOne({ type: 'iphone' }, function (err, adventure) {}); * * // same as above * Adventure.findOne({ type: 'iphone' }).exec(function (err, adventure) {}); * * // select only the adventures name * Adventure.findOne({ type: 'iphone' }, 'name', function (err, adventure) {}); * * // same as above * Adventure.findOne({ type: 'iphone' }, 'name').exec(function (err, adventure) {}); * * // specify options, in this case lean * Adventure.findOne({ type: 'iphone' }, 'name', { lean: true }, callback); * * // same as above * Adventure.findOne({ type: 'iphone' }, 'name', { lean: true }).exec(callback); * * // chaining findOne queries (same as above) * Adventure.findOne({ type: 'iphone' }).select('name').lean().exec(callback); * * @param {Object} [conditions] * @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo) * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Function} [callback] * @return {Query} * @see field selection #query_Query-select * @see lean queries #query_Query-lean * @api public */ Model.findOne = function findOne(conditions, projection, options, callback) { if (typeof options === 'function') { callback = options; options = null; } else if (typeof projection === 'function') { callback = projection; projection = null; options = null; } else if (typeof conditions === 'function') { callback = conditions; conditions = {}; projection = null; options = null; } // get the mongodb collection object var mq = new this.Query({}, {}, this, this.collection); mq.select(projection); mq.setOptions(options); if (this.schema.discriminatorMapping && this.schema.discriminatorMapping.isRoot && mq.selectedInclusively()) { mq.select(this.schema.options.discriminatorKey); } if (callback) { callback = this.$wrapCallback(callback); } return mq.findOne(conditions, callback); }; /** * Counts number of matching documents in a database collection. * * ####Example: * * Adventure.count({ type: 'jungle' }, function (err, count) { * if (err) .. * console.log('there are %d jungle adventures', count); * }); * * @param {Object} conditions * @param {Function} [callback] * @return {Query} * @api public */ Model.count = function count(conditions, callback) { if (typeof conditions === 'function') { callback = conditions; conditions = {}; } // get the mongodb collection object var mq = new this.Query({}, {}, this, this.collection); if (callback) { callback = this.$wrapCallback(callback); } return mq.count(conditions, callback); }; /** * Creates a Query for a `distinct` operation. * * Passing a `callback` immediately executes the query. * * ####Example * * Link.distinct('url', { clicks: {$gt: 100}}, function (err, result) { * if (err) return handleError(err); * * assert(Array.isArray(result)); * console.log('unique urls with more than 100 clicks', result); * }) * * var query = Link.distinct('url'); * query.exec(callback); * * @param {String} field * @param {Object} [conditions] optional * @param {Function} [callback] * @return {Query} * @api public */ Model.distinct = function distinct(field, conditions, callback) { // get the mongodb collection object var mq = new this.Query({}, {}, this, this.collection); if (typeof conditions === 'function') { callback = conditions; conditions = {}; } if (callback) { callback = this.$wrapCallback(callback); } return mq.distinct(field, conditions, callback); }; /** * Creates a Query, applies the passed conditions, and returns the Query. * * For example, instead of writing: * * User.find({age: {$gte: 21, $lte: 65}}, callback); * * we can instead write: * * User.where('age').gte(21).lte(65).exec(callback); * * Since the Query class also supports `where` you can continue chaining * * User * .where('age').gte(21).lte(65) * .where('name', /^b/i) * ... etc * * @param {String} path * @param {Object} [val] optional value * @return {Query} * @api public */ Model.where = function where(path, val) { void val; // eslint // get the mongodb collection object var mq = new this.Query({}, {}, this, this.collection).find({}); return mq.where.apply(mq, arguments); }; /** * Creates a `Query` and specifies a `$where` condition. * * Sometimes you need to query for things in mongodb using a JavaScript expression. You can do so via `find({ $where: javascript })`, or you can use the mongoose shortcut method $where via a Query chain or from your mongoose Model. * * Blog.$where('this.username.indexOf("val") !== -1').exec(function (err, docs) {}); * * @param {String|Function} argument is a javascript string or anonymous function * @method $where * @memberOf Model * @return {Query} * @see Query.$where #query_Query-%24where * @api public */ Model.$where = function $where() { var mq = new this.Query({}, {}, this, this.collection).find({}); return mq.$where.apply(mq, arguments); }; /** * Issues a mongodb findAndModify update command. * * Finds a matching document, updates it according to the `update` arg, passing any `options`, and returns the found document (if any) to the callback. The query executes immediately if `callback` is passed else a Query object is returned. * * ####Options: * * - `new`: bool - if true, return the modified document rather than the original. defaults to false (changed in 4.0) * - `upsert`: bool - creates the object if it doesn't exist. defaults to false. * - `fields`: {Object|String} - Field selection. Equivalent to `.select(fields).findOneAndUpdate()` * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0 * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update * - `runValidators`: if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema. * - `setDefaultsOnInsert`: if this and `upsert` are true, mongoose will apply the [defaults](http://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. This option only works on MongoDB >= 2.4 because it relies on [MongoDB's `$setOnInsert` operator](https://docs.mongodb.org/v2.4/reference/operator/update/setOnInsert/). * - `passRawResult`: if true, passes the [raw result from the MongoDB driver as the third callback parameter](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify) * - `strict`: overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict) for this update * - `runSettersOnQuery`: bool - if true, run all setters defined on the associated model's schema for all fields defined in the query and the update. * * ####Examples: * * A.findOneAndUpdate(conditions, update, options, callback) // executes * A.findOneAndUpdate(conditions, update, options) // returns Query * A.findOneAndUpdate(conditions, update, callback) // executes * A.findOneAndUpdate(conditions, update) // returns Query * A.findOneAndUpdate() // returns Query * * ####Note: * * All top level update keys which are not `atomic` operation names are treated as set operations: * * ####Example: * * var query = { name: 'borne' }; * Model.findOneAndUpdate(query, { name: 'jason bourne' }, options, callback) * * // is sent as * Model.findOneAndUpdate(query, { $set: { name: 'jason bourne' }}, options, callback) * * This helps prevent accidentally overwriting your document with `{ name: 'jason bourne' }`. * * ####Note: * * Values are cast to their appropriate types when using the findAndModify helpers. * However, the below are not executed by default. * * - defaults. Use the `setDefaultsOnInsert` option to override. * - setters. Use the `runSettersOnQuery` option to override. * * `findAndModify` helpers support limited validation. You can * enable these by setting the `runValidators` options, * respectively. * * If you need full-fledged validation, use the traditional approach of first * retrieving the document. * * Model.findById(id, function (err, doc) { * if (err) .. * doc.name = 'jason bourne'; * doc.save(callback); * }); * * @param {Object} [conditions] * @param {Object} [update] * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](http://mongoosejs.com/docs/api.html#query_Query-lean). * @param {Function} [callback] * @return {Query} * @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command * @api public */ Model.findOneAndUpdate = function(conditions, update, options, callback) { if (typeof options === 'function') { callback = options; options = null; } else if (arguments.length === 1) { if (typeof conditions === 'function') { var msg = 'Model.findOneAndUpdate(): First argument must not be a function.\n\n' + ' ' + this.modelName + '.findOneAndUpdate(conditions, update, options, callback)\n' + ' ' + this.modelName + '.findOneAndUpdate(conditions, update, options)\n' + ' ' + this.modelName + '.findOneAndUpdate(conditions, update)\n' + ' ' + this.modelName + '.findOneAndUpdate(update)\n' + ' ' + this.modelName + '.findOneAndUpdate()\n'; throw new TypeError(msg); } update = conditions; conditions = undefined; } if (callback) { cal