mongoose
Version:
Mongoose MongoDB ODM
1,726 lines (1,522 loc) • 124 kB
JavaScript
/*!
* 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