Files
biomedjs/node_modules/mongoose/lib/model.js

3059 lines
84 KiB
JavaScript
Raw Permalink Normal View History

2015-11-24 22:08:58 -08:00
/* eslint no-unused-vars: 1 */
2014-09-14 07:04:16 -04:00
/*!
* Module dependencies.
*/
2015-11-24 22:08:58 -08:00
var Document = require('./document');
var MongooseError = require('./error');
var VersionError = MongooseError.VersionError;
var DivergentArrayError = MongooseError.DivergentArrayError;
var Query = require('./query');
var Aggregate = require('./aggregate');
var Schema = require('./schema');
var Types = require('./schema/index');
var utils = require('./utils');
var hasOwnProperty = utils.object.hasOwnProperty;
var isMongooseObject = utils.isMongooseObject;
var EventEmitter = require('events').EventEmitter;
var Promise = require('./promise');
var util = require('util');
var tick = utils.tick;
var async = require('async');
var PromiseProvider = require('./promise_provider');
var VERSION_WHERE = 1,
VERSION_INC = 2,
VERSION_ALL = VERSION_WHERE | VERSION_INC;
2014-09-14 07:04:16 -04:00
/**
* Model constructor
*
2015-11-24 22:08:58 -08:00
* Provides the interface to MongoDB collections as well as creates document instances.
*
* @param {Object} doc values with which to create the document
2014-09-14 07:04:16 -04:00
* @inherits Document
2015-11-24 22:08:58 -08:00
* @event `error`: If listening to this event, it 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.
2014-09-14 07:04:16 -04:00
* @event `index`: Emitted after `Model#ensureIndexes` completes. If an error occurred it is passed with the event.
2015-11-24 22:08:58 -08:00
* @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.
2014-09-14 07:04:16 -04:00
* @api public
*/
2015-11-24 22:08:58 -08:00
function Model(doc, fields, skipId) {
2014-09-14 07:04:16 -04:00
Document.call(this, doc, fields, skipId);
2015-11-24 22:08:58 -08:00
}
2014-09-14 07:04:16 -04:00
/*!
* Inherits from Document.
*
* All Model.prototype features are available on
* top level (non-sub) documents.
*/
Model.prototype.__proto__ = Document.prototype;
/**
* 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;
2015-11-24 22:08:58 -08:00
/**
* If this is a discriminator model, `baseModelName` is the name of
* the base model.
*
* @api public
* @property baseModelName
2014-09-14 07:04:16 -04:00
*/
2015-11-24 22:08:58 -08:00
Model.prototype.baseModelName;
Model.prototype.$__handleSave = function(options, callback) {
var _this = this;
if (!options.safe && this.schema.options.safe) {
options.safe = this.schema.options.safe;
}
if (typeof options.safe === 'boolean') {
options.safe = null;
}
if (this.isNew) {
// send entire doc
var toObjectOptions = {};
if (this.schema.options.toObject &&
this.schema.options.toObject.retainKeyOrder) {
toObjectOptions.retainKeyOrder = true;
}
toObjectOptions.depopulate = 1;
toObjectOptions._skipDepopulateTopLevel = true;
toObjectOptions.transform = false;
var obj = this.toObject(toObjectOptions);
if (!utils.object.hasOwnProperty(obj || {}, '_id')) {
// 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, options.safe, function(err, ret) {
if (err) {
_this.isNew = true;
_this.emit('isNew', true);
callback(err);
return;
}
callback(null, ret);
});
this.$__reset();
this.isNew = false;
this.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;
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
this.collection.update(where, delta[1], options.safe, function(err, ret) {
if (err) {
callback(err);
return;
}
callback(null, ret);
});
} else {
this.$__reset();
callback();
2014-09-14 07:04:16 -04:00
return;
}
2015-11-24 22:08:58 -08:00
this.emit('isNew', false);
}
};
/*!
* ignore
*/
Model.prototype.$__save = function(options, callback) {
var _this = this;
_this.$__handleSave(options, function(error, result) {
if (error) {
return callback(error);
}
_this.$__reset();
_this.$__storeShard();
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
var numAffected = 0;
2014-09-14 07:04:16 -04:00
if (result) {
2015-11-24 22:08:58 -08:00
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;
}
2014-09-14 07:04:16 -04:00
}
// was this an update that required a version bump?
2015-11-24 22:08:58 -08:00
if (_this.$__.version && !_this.$__.inserting) {
var doIncrement = VERSION_INC === (VERSION_INC & _this.$__.version);
_this.$__.version = undefined;
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
if (numAffected <= 0) {
2014-09-14 07:04:16 -04:00
// the update failed. pass an error back
2015-11-24 22:08:58 -08:00
var err = new VersionError();
return callback(err);
}
// increment version if was successful
if (doIncrement) {
var key = _this.schema.options.versionKey;
var version = _this.getValue(key) | 0;
_this.setValue(key, version + 1);
2014-09-14 07:04:16 -04:00
}
}
2015-11-24 22:08:58 -08:00
_this.emit('save', _this, numAffected);
callback(null, _this, numAffected);
2014-09-14 07:04:16 -04:00
});
2015-11-24 22:08:58 -08:00
};
2014-09-14 07:04:16 -04:00
/**
* Saves this document.
*
* ####Example:
*
* product.sold = Date.now();
2015-11-24 22:08:58 -08:00
* product.save(function (err, product, numAffected) {
2014-09-14 07:04:16 -04:00
* if (err) ..
* })
*
2015-11-24 22:08:58 -08:00
* The callback will receive three parameters
2014-09-14 07:04:16 -04:00
*
2015-11-24 22:08:58 -08:00
* 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.
2014-09-14 07:04:16 -04:00
*
2015-11-24 22:08:58 -08:00
* As an extra measure of flow control, save will return a Promise.
* ####Example:
* product.save().then(function(product) {
* ...
* });
2014-09-14 07:04:16 -04:00
*
2015-11-24 22:08:58 -08:00
* 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.
2014-09-14 07:04:16 -04:00
*
2015-11-24 22:08:58 -08:00
* @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.
2014-09-14 07:04:16 -04:00
* @param {Function} [fn] optional callback
2015-11-24 22:08:58 -08:00
* @return {Promise} Promise
2014-09-14 07:04:16 -04:00
* @api public
* @see middleware http://mongoosejs.com/docs/middleware.html
*/
2015-11-24 22:08:58 -08:00
Model.prototype.save = function(options, fn) {
if ('function' == typeof options) {
fn = options;
options = undefined;
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
if (!options) {
options = {};
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
if (options.__noPromise) {
return this.$__save(options, fn);
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
var _this = this;
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
return new Promise.ES6(function(resolve, reject) {
_this.$__save(options, function(error, doc, numAffected) {
if (error) {
fn && fn(error);
reject(error);
return;
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
fn && fn(null, doc, numAffected);
resolve(doc, numAffected);
});
});
2014-09-14 07:04:16 -04:00
};
2015-11-24 22:08:58 -08:00
/**
* 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];
}
2014-09-14 07:04:16 -04:00
/*!
* 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]
*/
2015-11-24 22:08:58 -08:00
function operand(self, where, delta, data, val, op) {
2014-09-14 07:04:16 -04:00
// delta
op || (op = '$set');
if (!delta[op]) delta[op] = {};
delta[op][data.path] = val;
// disabled versioning?
if (false === self.schema.options.versionKey) return;
2015-11-24 22:08:58 -08:00
// path excluded from versioning?
if (shouldSkipVersioning(self, data.path)) return;
2014-09-14 07:04:16 -04:00
// 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 ('$push' == op || '$pushAll' == op || '$addToSet' == op) {
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();
}
// now handling $set, $unset
else if (/\.\d+\.|\.\d+$/.test(data.path)) {
// 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
*/
2015-11-24 22:08:58 -08:00
function handleAtomics(self, where, delta, data, value) {
2014-09-14 07:04:16 -04:00
if (delta.$set && delta.$set[data.path]) {
// $set has precedence over other atomics
return;
}
if ('function' == typeof value.$__getAtomics) {
2015-11-24 22:08:58 -08:00
value.$__getAtomics().forEach(function(atomic) {
2014-09-14 07:04:16 -04:00
var op = atomic[0];
var val = atomic[1];
operand(self, where, delta, data, val, op);
2015-11-24 22:08:58 -08:00
});
2014-09-14 07:04:16 -04:00
return;
}
// legacy support for plugins
2015-11-24 22:08:58 -08:00
var atomics = value._atomics,
ops = Object.keys(atomics),
i = ops.length,
val,
op;
2014-09-14 07:04:16 -04:00
if (0 === i) {
// $set
if (isMongooseObject(value)) {
value = value.toObject({ depopulate: 1 });
} else if (value.valueOf) {
value = value.valueOf();
}
return operand(self, where, delta, data, value);
}
while (i--) {
op = ops[i];
val = atomics[op];
if (isMongooseObject(val)) {
2015-11-24 22:08:58 -08:00
val = val.toObject({ depopulate: 1 });
2014-09-14 07:04:16 -04:00
} else if (Array.isArray(val)) {
2015-11-24 22:08:58 -08:00
val = val.map(function(mem) {
2014-09-14 07:04:16 -04:00
return isMongooseObject(mem)
? mem.toObject({ depopulate: 1 })
: mem;
2015-11-24 22:08:58 -08:00
});
2014-09-14 07:04:16 -04:00
} else if (val.valueOf) {
2015-11-24 22:08:58 -08:00
val = val.valueOf();
2014-09-14 07:04:16 -04:00
}
if ('$addToSet' === op)
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
*/
2015-11-24 22:08:58 -08:00
Model.prototype.$__delta = function() {
2014-09-14 07:04:16 -04:00
var dirty = this.$__dirty();
if (!dirty.length && VERSION_ALL != this.$__.version) return;
2015-11-24 22:08:58 -08:00
var where = {},
delta = {},
len = dirty.length,
divergent = [],
d = 0;
where._id = this._doc._id;
2014-09-14 07:04:16 -04:00
for (; d < len; ++d) {
2015-11-24 22:08:58 -08:00
var data = dirty[d];
var value = data.value;
2014-09-14 07:04:16 -04:00
var match = checkDivergentArray(this, data.path, value);
if (match) {
divergent.push(match);
continue;
}
if (divergent.length) continue;
if (undefined === value) {
operand(this, where, delta, data, 1, '$unset');
} else if (null === value) {
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: 1 });
operand(this, where, delta, data, value);
}
}
if (divergent.length) {
return new DivergentArrayError(divergent);
}
if (this.$__.version) {
this.$__version(where, delta);
}
return [where, delta];
2015-11-24 22:08:58 -08:00
};
2014-09-14 07:04:16 -04:00
/*!
* Determine if array was populated with some form of filter and is now
* being updated in a manner which could overwrite data unintentionally.
*
2015-11-24 22:08:58 -08:00
* @see https://github.com/Automattic/mongoose/issues/1334
2014-09-14 07:04:16 -04:00
* @param {Document} doc
* @param {String} path
* @return {String|undefined}
*/
2015-11-24 22:08:58 -08:00
function checkDivergentArray(doc, path, array) {
2014-09-14 07:04:16 -04:00
// 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];
2015-11-24 22:08:58 -08:00
if ((doc.$__.selected[top] && doc.$__.selected[top].$elemMatch) ||
doc.$__.selected[top + '.$']) {
2014-09-14 07:04:16 -04:00
return top;
}
}
2015-11-24 22:08:58 -08:00
if (!(pop && array && array.isMongooseArray)) return;
if (!pop.options) {
return;
}
2014-09-14 07:04:16 -04:00
// 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 && hasOwnProperty(pop.options.options, 'limit') || // 0 is not permitted
pop.options.options && pop.options.options.skip || // 0 is permitted
pop.options.select && // deselected _id?
(0 === pop.options.select._id ||
2015-11-24 22:08:58 -08:00
/\s?-_id\s?/.test(pop.options.select));
2014-09-14 07:04:16 -04:00
if (check) {
var atomics = array._atomics;
if (0 === Object.keys(atomics).length || atomics.$set || atomics.$pop) {
return path;
}
}
}
/**
* Appends versioning to the where and update clauses.
*
* @api private
* @method $__version
* @memberOf Model
*/
2015-11-24 22:08:58 -08:00
Model.prototype.$__version = function(where, delta) {
2014-09-14 07:04:16 -04:00
var key = this.schema.options.versionKey;
if (true === where) {
// 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)) {
where[key] = this.getValue(key);
}
if (VERSION_INC === (VERSION_INC & this.$__.version)) {
2015-11-24 22:08:58 -08:00
if (!delta.$set || typeof delta.$set[key] === 'undefined') {
delta.$inc || (delta.$inc = {});
delta.$inc[key] = 1;
}
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
};
2014-09-14 07:04:16 -04:00
/**
* 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
*/
2015-11-24 22:08:58 -08:00
Model.prototype.increment = function increment() {
2014-09-14 07:04:16 -04:00
this.$__.version = VERSION_ALL;
return this;
2015-11-24 22:08:58 -08:00
};
2014-09-14 07:04:16 -04:00
/**
* Returns a query object which applies shardkeys if they exist.
*
* @api private
* @method $__where
* @memberOf Model
*/
2015-11-24 22:08:58 -08:00
Model.prototype.$__where = function _where(where) {
2014-09-14 07:04:16 -04:00
where || (where = {});
2015-11-24 22:08:58 -08:00
var paths,
len;
if (!where._id) {
where._id = this._doc._id;
}
2014-09-14 07:04:16 -04:00
if (this.$__.shardval) {
2015-11-24 22:08:58 -08:00
paths = Object.keys(this.$__.shardval);
len = paths.length;
2014-09-14 07:04:16 -04:00
for (var i = 0; i < len; ++i) {
where[paths[i]] = this.$__.shardval[paths[i]];
}
}
2015-11-24 22:08:58 -08:00
if (!this._doc._id) {
return new Error('No _id found on document!');
}
2014-09-14 07:04:16 -04:00
return where;
2015-11-24 22:08:58 -08:00
};
2014-09-14 07:04:16 -04:00
/**
* 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
* })
* })
*
2015-11-24 22:08:58 -08:00
*
* 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) {
* ...
* }).onRejected(function (err) {
* assert.ok(err)
* })
*
* @param {function(err,product)} [fn] optional callback
* @return {Promise} Promise
2014-09-14 07:04:16 -04:00
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.prototype.remove = function remove(options, fn) {
var Promise = PromiseProvider.get();
if ('function' == typeof options) {
fn = options;
options = undefined;
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
if (!options) {
options = {};
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
if (this.$__.removing) {
if (fn) {
this.$__.removing.then(
function(res) { fn(null, res); },
function(err) { fn(err); });
}
return this;
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
var _this = this;
var promise = this.$__.removing = new Promise.ES6(function(resolve, reject) {
var where = _this.$__where();
if (where instanceof Error) {
reject(where);
fn && fn(where);
2014-09-14 07:04:16 -04:00
return;
}
2015-11-24 22:08:58 -08:00
if (!options.safe && _this.schema.options.safe) {
options.safe = _this.schema.options.safe;
}
_this.collection.remove(where, options, function(err) {
if (!err) {
_this.emit('remove', _this);
resolve(_this);
fn && fn(null, _this);
return;
}
reject(err);
fn && fn(err);
return;
});
});
return promise;
2014-09-14 07:04:16 -04:00
};
/**
* Returns another Model instance.
*
* ####Example:
*
* var doc = new Tank;
* doc.model('User').findById(id, callback);
*
* @param {String} name model name
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.prototype.model = function model(name) {
2014-09-14 07:04:16 -04:00
return this.db.model(name);
};
2015-11-24 22:08:58 -08:00
/**
* 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 discriminator(name, schema) {
if (!(schema instanceof Schema)) {
throw new Error("You must pass a valid discriminator Schema");
}
if (this.schema.discriminatorMapping && !this.schema.discriminatorMapping.isRoot) {
throw new Error("Discriminator \"" + name +
"\" can only be a discriminator of the root model");
}
var key = this.schema.options.discriminatorKey;
if (schema.path(key)) {
throw new Error("Discriminator \"" + name +
"\" cannot have field with name \"" + key + "\"");
}
// merges base schema into new discriminator schema and sets new type field.
(function(schema, baseSchema) {
utils.merge(schema, baseSchema);
var obj = {};
obj[key] = { type: String, default: name };
schema.add(obj);
schema.discriminatorMapping = { key: key, value: name, isRoot: false };
if (baseSchema.options.collection) {
schema.options.collection = baseSchema.options.collection;
}
// throws error if options are invalid
(function(a, b) {
a = utils.clone(a);
b = utils.clone(b);
delete a.toJSON;
delete a.toObject;
delete b.toJSON;
delete b.toObject;
delete a._id;
delete b._id;
if (!utils.deepEqual(a, b)) {
throw new Error("Discriminator options are not customizable " +
"(except toJSON, toObject, _id)");
}
})(schema.options, baseSchema.options);
var toJSON = schema.options.toJSON;
var toObject = schema.options.toObject;
var _id = schema.options._id;
schema.options = utils.clone(baseSchema.options);
if (toJSON) schema.options.toJSON = toJSON;
if (toObject) schema.options.toObject = toObject;
if (typeof _id !== 'undefined') {
schema.options._id = _id;
}
schema.callQueue = baseSchema.callQueue.concat(schema.callQueue.slice(schema._defaultMiddleware.length));
schema._requiredpaths = undefined; // reset just in case Schema#requiredPaths() was called on either schema
})(schema, this.schema);
if (!this.discriminators) {
this.discriminators = {};
}
if (!this.schema.discriminatorMapping) {
this.schema.discriminatorMapping = { key: key, value: null, isRoot: true };
}
if (this.discriminators[name]) {
throw new Error("Discriminator with name \"" + name + "\" already exists");
}
this.discriminators[name] = this.db.model(name, schema, this.collection.name);
this.discriminators[name].prototype.__proto__ = this.prototype;
Object.defineProperty(this.discriminators[name], 'baseModelName', {
value: this.modelName,
configurable: true,
writable: false
});
// apply methods and statics
applyMethods(this.discriminators[name], schema);
applyStatics(this.discriminators[name], schema);
return this.discriminators[name];
};
2014-09-14 07:04:16 -04:00
// Model (class) features
/*!
* Give the constructor the ability to emit events.
*/
for (var i in EventEmitter.prototype)
Model[i] = EventEmitter.prototype[i];
/**
* Called when the model compiles.
*
* @api private
*/
2015-11-24 22:08:58 -08:00
Model.init = function init() {
if ((this.schema.options.autoIndex) ||
(this.schema.options.autoIndex === null && this.db.config.autoIndex)) {
2014-09-14 07:04:16 -04:00
this.ensureIndexes();
}
this.schema.emit('init', this);
};
/**
* Sends `ensureIndex` commands to mongo for each index declared in the schema.
*
* ####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._
*
2015-11-24 22:08:58 -08:00
* The `ensureIndex` commands are not sent in parallel. This is to avoid the `MongoError: cannot add index with a background operation in progress` error. See [this ticket](https://github.com/Automattic/mongoose/issues/1365) for more information.
2014-09-14 07:04:16 -04:00
*
* @param {Function} [cb] optional callback
2015-11-24 22:08:58 -08:00
* @return {Promise}
2014-09-14 07:04:16 -04:00
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.ensureIndexes = function ensureIndexes(cb) {
var promise = new Promise(cb);
2014-09-14 07:04:16 -04:00
var indexes = this.schema.indexes();
if (!indexes.length) {
2015-11-24 22:08:58 -08:00
process.nextTick(promise.fulfill.bind(promise));
return promise;
2014-09-14 07:04:16 -04:00
}
// Indexes are created one-by-one to support how MongoDB < 2.4 deals
// with background indexes.
2015-11-24 22:08:58 -08:00
var self = this;
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
var done = function(err) {
if (err && self.schema.options.emitIndexErrors) {
self.emit('error', err);
}
2014-09-14 07:04:16 -04:00
self.emit('index', err);
2015-11-24 22:08:58 -08:00
promise.resolve(err);
};
var indexSingleDone = function(err, fields, options, name) {
self.emit('index-single-done', err, fields, options, name);
};
var indexSingleStart = function(fields, options) {
self.emit('index-single-start', fields, options);
};
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
var create = function() {
2014-09-14 07:04:16 -04:00
var index = indexes.shift();
if (!index) return done();
2015-11-24 22:08:58 -08:00
var indexFields = index[0];
2014-09-14 07:04:16 -04:00
var options = index[1];
2015-11-24 22:08:58 -08:00
_handleSafe(options);
indexSingleStart(indexFields, options);
self.collection.ensureIndex(indexFields, options, tick(function(err,name) {
indexSingleDone(err,indexFields, options, name);
if (err) {
return done(err);
} else {
create();
}
2014-09-14 07:04:16 -04:00
}));
2015-11-24 22:08:58 -08:00
};
2014-09-14 07:04:16 -04:00
create();
2015-11-24 22:08:58 -08:00
return promise;
};
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;
}
}
2014-09-14 07:04:16 -04:00
}
/**
* 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;
2015-11-24 22:08:58 -08:00
/**
* Registered discriminators for this model.
*
* @property discriminators
* @receiver Model
* @api public
*/
Model.discriminators;
2014-09-14 07:04:16 -04:00
/**
* Removes documents from the collection.
*
* ####Example:
*
* Comment.remove({ title: 'baby born from alien father' }, function (err) {
*
* });
*
* ####Note:
*
* To remove documents without waiting for a response from MongoDB, do not pass a `callback`, then call `exec` on the returned [Query](#query-js):
*
* var query = Comment.remove({ _id: id });
* query.exec();
*
* ####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]
2015-11-24 22:08:58 -08:00
* @return {Promise} Promise
2014-09-14 07:04:16 -04:00
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.remove = function remove(conditions, callback) {
2014-09-14 07:04:16 -04:00
if ('function' === typeof conditions) {
callback = conditions;
conditions = {};
}
2015-11-24 22:08:58 -08:00
// get the mongodb collection object
var mq = new Query(conditions, {}, this, this.collection);
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
return mq.remove(callback);
2014-09-14 07:04:16 -04:00
};
/**
* 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
2015-11-24 22:08:58 -08:00
* @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo)
2014-09-14 07:04:16 -04:00
* @param {Object} [options] optional
* @param {Function} [callback]
* @return {Query}
* @see field selection #query_Query-select
* @see promise #promise-js
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.find = function find(conditions, projection, options, callback) {
2014-09-14 07:04:16 -04:00
if ('function' == typeof conditions) {
callback = conditions;
conditions = {};
2015-11-24 22:08:58 -08:00
projection = null;
2014-09-14 07:04:16 -04:00
options = null;
2015-11-24 22:08:58 -08:00
} else if ('function' == typeof projection) {
callback = projection;
projection = null;
2014-09-14 07:04:16 -04:00
options = null;
} else if ('function' == typeof options) {
callback = options;
options = null;
}
2015-11-24 22:08:58 -08:00
// get the raw mongodb collection object
var mq = new Query({}, {}, this, this.collection);
mq.select(projection);
mq.setOptions(options);
if (this.schema.discriminatorMapping && mq.selectedInclusively()) {
mq.select(this.schema.options.discriminatorKey);
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
return mq.find(conditions, callback);
};
2014-09-14 07:04:16 -04:00
/**
2015-11-24 22:08:58 -08:00
* Finds a single document by its _id field. `findById(id)` is almost*
* equivalent to `findOne({ _id: id })`.
2014-09-14 07:04:16 -04:00
*
* The `id` is cast based on the Schema before sending the command.
*
2015-11-24 22:08:58 -08:00
* Note: `findById()` triggers `findOne` hooks.
*
* * Except for how it treats `undefined`. Because the MongoDB driver
* deletes keys that have value `undefined`, `findById(undefined)` gets
* translated to `findById({ _id: null })`.
*
2014-09-14 07:04:16 -04:00
* ####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) {});
*
2015-11-24 22:08:58 -08:00
* @param {Object|String|Number} id value of `_id` to query by
* @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo)
2014-09-14 07:04:16 -04:00
* @param {Object} [options] optional
* @param {Function} [callback]
* @return {Query}
* @see field selection #query_Query-select
* @see lean queries #query_Query-lean
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.findById = function findById(id, projection, options, callback) {
if (typeof id === 'undefined') {
id = null;
}
return this.findOne({ _id: id }, projection, options, callback);
2014-09-14 07:04:16 -04:00
};
/**
* Finds one document.
*
* The `conditions` are cast to their respective SchemaTypes before the command is sent.
*
* ####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);
*
2015-11-24 22:08:58 -08:00
* @param {Object} [conditions]
* @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo)
2014-09-14 07:04:16 -04:00
* @param {Object} [options] optional
* @param {Function} [callback]
* @return {Query}
* @see field selection #query_Query-select
* @see lean queries #query_Query-lean
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.findOne = function findOne(conditions, projection, options, callback) {
2014-09-14 07:04:16 -04:00
if ('function' == typeof options) {
callback = options;
options = null;
2015-11-24 22:08:58 -08:00
} else if ('function' == typeof projection) {
callback = projection;
projection = null;
2014-09-14 07:04:16 -04:00
options = null;
} else if ('function' == typeof conditions) {
callback = conditions;
conditions = {};
2015-11-24 22:08:58 -08:00
projection = null;
2014-09-14 07:04:16 -04:00
options = null;
}
2015-11-24 22:08:58 -08:00
// get the mongodb collection object
var mq = new Query({}, {}, this, this.collection);
mq.select(projection);
mq.setOptions(options);
if (this.schema.discriminatorMapping && mq.selectedInclusively()) {
mq.select(this.schema.options.discriminatorKey);
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
return mq.findOne(conditions, callback);
2014-09-14 07:04:16 -04:00
};
/**
* 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
*/
2015-11-24 22:08:58 -08:00
Model.count = function count(conditions, callback) {
2014-09-14 07:04:16 -04:00
if ('function' === typeof conditions)
callback = conditions, conditions = {};
2015-11-24 22:08:58 -08:00
// get the mongodb collection object
var mq = new Query({}, {}, this, this.collection);
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
return mq.count(conditions, callback);
2014-09-14 07:04:16 -04:00
};
/**
2015-11-24 22:08:58 -08:00
* Creates a Query for a `distinct` operation.
*
* Passing a `callback` immediately executes the query.
2014-09-14 07:04:16 -04:00
*
* ####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);
* })
*
2015-11-24 22:08:58 -08:00
* var query = Link.distinct('url');
* query.exec(callback);
*
2014-09-14 07:04:16 -04:00
* @param {String} field
* @param {Object} [conditions] optional
* @param {Function} [callback]
* @return {Query}
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.distinct = function distinct(field, conditions, callback) {
// get the mongodb collection object
var mq = new Query({}, {}, this, this.collection);
2014-09-14 07:04:16 -04:00
if ('function' == typeof conditions) {
callback = conditions;
conditions = {};
}
2015-11-24 22:08:58 -08:00
return mq.distinct(field, conditions, callback);
2014-09-14 07:04:16 -04:00
};
/**
* 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
*/
2015-11-24 22:08:58 -08:00
Model.where = function where(path, val) {
// get the mongodb collection object
var mq = new Query({}, {}, this, this.collection).find({});
return mq.where.apply(mq, arguments);
2014-09-14 07:04:16 -04:00
};
/**
* 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.comments.length > 5').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
*/
2015-11-24 22:08:58 -08:00
Model.$where = function $where() {
var mq = new Query({}, {}, this, this.collection).find({});
return mq.$where.apply(mq, arguments);
2014-09-14 07:04:16 -04:00
};
/**
* 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:
*
2015-11-24 22:08:58 -08:00
* - `new`: bool - if true, return the modified document rather than the original. defaults to false (changed in 4.0)
2014-09-14 07:04:16 -04:00
* - `upsert`: bool - creates the object if it doesn't exist. defaults to false.
* - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
* - `select`: sets the document fields to return
*
* ####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 borne' }, options, callback)
*
* // is sent as
* Model.findOneAndUpdate(query, { $set: { name: 'jason borne' }}, options, callback)
*
* This helps prevent accidentally overwriting your document with `{ name: 'jason borne' }`.
*
* ####Note:
*
2015-11-24 22:08:58 -08:00
* Values are cast to their appropriate types when using the findAndModify helpers.
* However, the below are never executed.
2014-09-14 07:04:16 -04:00
*
* - defaults
* - setters
*
2015-11-24 22:08:58 -08:00
* `findAndModify` helpers support limited defaults and validation. You can
* enable these by setting the `setDefaultsOnInsert` and `runValidators` options,
* respectively.
2014-09-14 07:04:16 -04:00
*
2015-11-24 22:08:58 -08:00
* If you need full-fledged validation, use the traditional approach of first
* retrieving the document.
*
* Model.findById(id, function (err, doc) {
2014-09-14 07:04:16 -04:00
* if (err) ..
* doc.name = 'jason borne';
* doc.save(callback);
2015-11-24 22:08:58 -08:00
* });
2014-09-14 07:04:16 -04:00
*
* @param {Object} [conditions]
* @param {Object} [update]
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.findOneAndUpdate = function(conditions, update, options, callback) {
2014-09-14 07:04:16 -04:00
if ('function' == typeof options) {
callback = options;
options = null;
}
else if (1 === arguments.length) {
if ('function' == typeof conditions) {
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';
2015-11-24 22:08:58 -08:00
throw new TypeError(msg);
2014-09-14 07:04:16 -04:00
}
update = conditions;
conditions = undefined;
}
var fields;
if (options && options.fields) {
fields = options.fields;
options.fields = undefined;
}
2015-11-24 22:08:58 -08:00
update = utils.clone(update, { depopulate: 1 });
if (this.schema.options.versionKey && options && options.upsert) {
if (!update.$setOnInsert) {
update.$setOnInsert = {};
}
update.$setOnInsert[this.schema.options.versionKey] = 0;
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
var mq = new Query({}, {}, this, this.collection);
mq.select(fields);
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
return mq.findOneAndUpdate(conditions, update, options, callback);
};
2014-09-14 07:04:16 -04:00
/**
2015-11-24 22:08:58 -08:00
* Issues a mongodb findAndModify update command by a document's _id field.
* `findByIdAndUpdate(id, ...)` is equivalent to `findOneAndUpdate({ _id: id }, ...)`.
2014-09-14 07:04:16 -04:00
*
2015-11-24 22:08:58 -08:00
* 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.
*
* This function triggers `findOneAndUpdate` middleware.
2014-09-14 07:04:16 -04:00
*
* ####Options:
*
2015-11-24 22:08:58 -08:00
* - `new`: bool - true to return the modified document rather than the original. defaults to false
2014-09-14 07:04:16 -04:00
* - `upsert`: bool - creates the object if it doesn't exist. defaults to false.
* - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
* - `select`: sets the document fields to return
*
* ####Examples:
*
* A.findByIdAndUpdate(id, update, options, callback) // executes
* A.findByIdAndUpdate(id, update, options) // returns Query
* A.findByIdAndUpdate(id, update, callback) // executes
* A.findByIdAndUpdate(id, update) // returns Query
* A.findByIdAndUpdate() // returns Query
*
* ####Note:
*
* All top level update keys which are not `atomic` operation names are treated as set operations:
*
* ####Example:
*
* Model.findByIdAndUpdate(id, { name: 'jason borne' }, options, callback)
*
* // is sent as
* Model.findByIdAndUpdate(id, { $set: { name: 'jason borne' }}, options, callback)
*
* This helps prevent accidentally overwriting your document with `{ name: 'jason borne' }`.
*
* ####Note:
*
2015-11-24 22:08:58 -08:00
* Values are cast to their appropriate types when using the findAndModify helpers.
* However, the below are never executed.
2014-09-14 07:04:16 -04:00
*
* - defaults
* - setters
*
2015-11-24 22:08:58 -08:00
* `findAndModify` helpers support limited defaults and validation. You can
* enable these by setting the `setDefaultsOnInsert` and `runValidators` options,
* respectively.
*
* If you need full-fledged validation, use the traditional approach of first
* retrieving the document.
2014-09-14 07:04:16 -04:00
*
* Model.findById(id, function (err, doc) {
* if (err) ..
* doc.name = 'jason borne';
* doc.save(callback);
2015-11-24 22:08:58 -08:00
* });
2014-09-14 07:04:16 -04:00
*
2015-11-24 22:08:58 -08:00
* @param {Object|Number|String} id value of `_id` to query by
2014-09-14 07:04:16 -04:00
* @param {Object} [update]
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @see Model.findOneAndUpdate #model_Model.findOneAndUpdate
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.findByIdAndUpdate = function(id, update, options, callback) {
2014-09-14 07:04:16 -04:00
var args;
if (1 === arguments.length) {
if ('function' == typeof id) {
var msg = 'Model.findByIdAndUpdate(): First argument must not be a function.\n\n'
+ ' ' + this.modelName + '.findByIdAndUpdate(id, callback)\n'
+ ' ' + this.modelName + '.findByIdAndUpdate(id)\n'
+ ' ' + this.modelName + '.findByIdAndUpdate()\n';
2015-11-24 22:08:58 -08:00
throw new TypeError(msg);
2014-09-14 07:04:16 -04:00
}
return this.findOneAndUpdate({_id: id }, undefined);
}
args = utils.args(arguments, 1);
2015-11-24 22:08:58 -08:00
// if a model is passed in instead of an id
if (id instanceof Document) {
id = id._id;
}
if (id) {
args.unshift({ _id: id });
}
2014-09-14 07:04:16 -04:00
return this.findOneAndUpdate.apply(this, args);
2015-11-24 22:08:58 -08:00
};
2014-09-14 07:04:16 -04:00
/**
* Issue a mongodb findAndModify remove command.
*
* Finds a matching document, removes it, passing the found document (if any) to the callback.
*
* Executes immediately if `callback` is passed else a Query object is returned.
*
* ####Options:
*
* - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
* - `select`: sets the document fields to return
*
* ####Examples:
*
* A.findOneAndRemove(conditions, options, callback) // executes
* A.findOneAndRemove(conditions, options) // return Query
* A.findOneAndRemove(conditions, callback) // executes
* A.findOneAndRemove(conditions) // returns Query
* A.findOneAndRemove() // returns Query
*
2015-11-24 22:08:58 -08:00
* Values are cast to their appropriate types when using the findAndModify helpers.
* However, the below are never executed.
2014-09-14 07:04:16 -04:00
*
* - defaults
* - setters
*
2015-11-24 22:08:58 -08:00
* `findAndModify` helpers support limited defaults and validation. You can
* enable these by setting the `setDefaultsOnInsert` and `runValidators` options,
* respectively.
*
* If you need full-fledged validation, use the traditional approach of first
* retrieving the document.
2014-09-14 07:04:16 -04:00
*
* Model.findById(id, function (err, doc) {
* if (err) ..
2015-11-24 22:08:58 -08:00
* doc.name = 'jason borne';
* doc.save(callback);
* });
2014-09-14 07:04:16 -04:00
*
* @param {Object} conditions
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.findOneAndRemove = function(conditions, options, callback) {
2014-09-14 07:04:16 -04:00
if (1 === arguments.length && 'function' == typeof conditions) {
var msg = 'Model.findOneAndRemove(): First argument must not be a function.\n\n'
+ ' ' + this.modelName + '.findOneAndRemove(conditions, callback)\n'
+ ' ' + this.modelName + '.findOneAndRemove(conditions)\n'
+ ' ' + this.modelName + '.findOneAndRemove()\n';
2015-11-24 22:08:58 -08:00
throw new TypeError(msg);
2014-09-14 07:04:16 -04:00
}
if ('function' == typeof options) {
callback = options;
options = undefined;
}
var fields;
if (options) {
fields = options.select;
options.select = undefined;
}
2015-11-24 22:08:58 -08:00
var mq = new Query({}, {}, this, this.collection);
mq.select(fields);
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
return mq.findOneAndRemove(conditions, options, callback);
};
2014-09-14 07:04:16 -04:00
/**
2015-11-24 22:08:58 -08:00
* Issue a mongodb findAndModify remove command by a document's _id field. `findByIdAndRemove(id, ...)` is equivalent to `findOneAndRemove({ _id: id }, ...)`.
2014-09-14 07:04:16 -04:00
*
* Finds a matching document, removes it, passing the found document (if any) to the callback.
*
* Executes immediately if `callback` is passed, else a `Query` object is returned.
*
* ####Options:
*
* - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
* - `select`: sets the document fields to return
*
* ####Examples:
*
* A.findByIdAndRemove(id, options, callback) // executes
* A.findByIdAndRemove(id, options) // return Query
* A.findByIdAndRemove(id, callback) // executes
* A.findByIdAndRemove(id) // returns Query
* A.findByIdAndRemove() // returns Query
*
2015-11-24 22:08:58 -08:00
* @param {Object|Number|String} id value of `_id` to query by
2014-09-14 07:04:16 -04:00
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @see Model.findOneAndRemove #model_Model.findOneAndRemove
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
*/
2015-11-24 22:08:58 -08:00
Model.findByIdAndRemove = function(id, options, callback) {
2014-09-14 07:04:16 -04:00
if (1 === arguments.length && 'function' == typeof id) {
var msg = 'Model.findByIdAndRemove(): First argument must not be a function.\n\n'
+ ' ' + this.modelName + '.findByIdAndRemove(id, callback)\n'
+ ' ' + this.modelName + '.findByIdAndRemove(id)\n'
+ ' ' + this.modelName + '.findByIdAndRemove()\n';
2015-11-24 22:08:58 -08:00
throw new TypeError(msg);
2014-09-14 07:04:16 -04:00
}
return this.findOneAndRemove({ _id: id }, options, callback);
2015-11-24 22:08:58 -08:00
};
2014-09-14 07:04:16 -04:00
/**
* Shortcut for creating a new Document that is automatically saved to the db if valid.
*
* ####Example:
*
2015-11-24 22:08:58 -08:00
* // pass individual docs
2014-09-14 07:04:16 -04:00
* Candy.create({ type: 'jelly bean' }, { type: 'snickers' }, function (err, jellybean, snickers) {
* if (err) // ...
* });
*
2015-11-24 22:08:58 -08:00
* // pass an array
2014-09-14 07:04:16 -04:00
* var array = [{ type: 'jelly bean' }, { type: 'snickers' }];
2015-11-24 22:08:58 -08:00
* Candy.create(array, function (err, candies) {
2014-09-14 07:04:16 -04:00
* if (err) // ...
2015-11-24 22:08:58 -08:00
*
* var jellybean = candies[0];
* var snickers = candies[1];
* // ...
2014-09-14 07:04:16 -04:00
* });
*
2015-11-24 22:08:58 -08:00
* // callback is optional; use the returned promise if you like:
* var promise = Candy.create({ type: 'jawbreaker' });
* promise.then(function (jawbreaker) {
* // ...
* })
*
* @param {Array|Object...} doc(s)
* @param {Function} [callback] callback
* @return {Promise}
2014-09-14 07:04:16 -04:00
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.create = function create(doc, callback) {
var args,
cb;
2014-09-14 07:04:16 -04:00
if (Array.isArray(doc)) {
args = doc;
2015-11-24 22:08:58 -08:00
cb = callback;
2014-09-14 07:04:16 -04:00
} else {
2015-11-24 22:08:58 -08:00
var last = arguments[arguments.length - 1];
if ('function' == typeof last) {
cb = last;
args = utils.args(arguments, 0, arguments.length - 1);
} else {
args = utils.args(arguments);
}
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
var Promise = PromiseProvider.get();
var ModelConstructor = this;
var promise = new Promise.ES6(function(resolve, reject) {
if (args.length === 0) {
process.nextTick(function() {
cb && cb(null);
resolve(null);
});
return;
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
var toExecute = [];
args.forEach(function(doc) {
toExecute.push(function(callback) {
var toSave = new ModelConstructor(doc);
var callbackWrapper = function(error, doc) {
if (error) {
return callback(error);
}
callback(null, doc);
};
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
// Hack to avoid getting a promise because of
// $__registerHooksFromSchema
if (toSave.$__original_save) {
toSave.$__original_save({ __noPromise: true }, callbackWrapper);
} else {
toSave.save({ __noPromise: true }, callbackWrapper);
}
});
});
async.parallel(toExecute, function(error, savedDocs) {
if (error) {
cb && cb(error);
reject(error);
return;
}
if (doc instanceof Array) {
resolve(savedDocs);
cb && cb.call(ModelConstructor, null, savedDocs);
} else {
resolve.apply(promise, savedDocs);
if (cb) {
savedDocs.unshift(null);
cb.apply(ModelConstructor, savedDocs);
}
}
2014-09-14 07:04:16 -04:00
});
});
2015-11-24 22:08:58 -08:00
return promise;
};
/**
* Shortcut for creating a new Document from existing raw data, pre-saved in the DB.
* The document returned has no paths marked as modified initially.
*
* ####Example:
*
* // hydrate previous data into a Mongoose document
* var mongooseCandy = Candy.hydrate({ _id: '54108337212ffb6d459f854c', type: 'jelly bean' });
*
* @param {Object} obj
* @return {Document}
* @api public
*/
Model.hydrate = function(obj) {
var model = require('./queryhelpers').createModel(this, obj);
model.init(obj);
return model;
2014-09-14 07:04:16 -04:00
};
/**
* Updates documents in the database without returning them.
*
* ####Examples:
*
* MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);
2015-11-24 22:08:58 -08:00
* MyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, function (err, raw) {
2014-09-14 07:04:16 -04:00
* if (err) return handleError(err);
* console.log('The raw response from Mongo was ', raw);
* });
*
* ####Valid options:
*
* - `safe` (boolean) safe mode (defaults to value set in schema (true))
* - `upsert` (boolean) whether to create the doc if it doesn't match (false)
* - `multi` (boolean) whether multiple documents should be updated (false)
* - `strict` (boolean) overrides the `strict` option for this update
2015-11-24 22:08:58 -08:00
* - `overwrite` (boolean) disables update-only mode, allowing you to overwrite the doc (false)
2014-09-14 07:04:16 -04:00
*
* All `update` values are cast to their appropriate SchemaTypes before being sent.
*
2015-11-24 22:08:58 -08:00
* The `callback` function receives `(err, rawResponse)`.
2014-09-14 07:04:16 -04:00
*
* - `err` is the error if any occurred
* - `rawResponse` is the full response from Mongo
*
* ####Note:
*
* All top level keys which are not `atomic` operation names are treated as set operations:
*
* ####Example:
*
* var query = { name: 'borne' };
* Model.update(query, { name: 'jason borne' }, options, callback)
*
* // is sent as
* Model.update(query, { $set: { name: 'jason borne' }}, options, callback)
2015-11-24 22:08:58 -08:00
* // if overwrite option is false. If overwrite is true, sent without the $set wrapper.
2014-09-14 07:04:16 -04:00
*
* This helps prevent accidentally overwriting all documents in your collection with `{ name: 'jason borne' }`.
*
* ####Note:
*
* Be careful to not use an existing model instance for the update clause (this won't work and can cause weird behavior like infinite loops). Also, ensure that the update clause does not have an _id property, which causes Mongo to return a "Mod on _id not allowed" error.
*
* ####Note:
*
* To update documents without waiting for a response from MongoDB, do not pass a `callback`, then call `exec` on the returned [Query](#query-js):
*
* Comment.update({ _id: id }, { $set: { text: 'changed' }}).exec();
*
* ####Note:
*
* Although values are casted to their appropriate types when using update, the following are *not* applied:
*
* - defaults
* - setters
* - validators
* - middleware
*
* If you need those features, use the traditional approach of first retrieving the document.
*
* Model.findOne({ name: 'borne' }, function (err, doc) {
* if (err) ..
* doc.name = 'jason borne';
* doc.save(callback);
* })
*
2015-11-24 22:08:58 -08:00
* @see strict http://mongoosejs.com/docs/guide.html#strict
* @see response http://docs.mongodb.org/v2.6/reference/command/update/#output
2014-09-14 07:04:16 -04:00
* @param {Object} conditions
2015-11-24 22:08:58 -08:00
* @param {Object} doc
2014-09-14 07:04:16 -04:00
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.update = function update(conditions, doc, options, callback) {
var mq = new Query({}, {}, this, this.collection);
// gh-2406
// make local deep copy of conditions
if (conditions instanceof Document) {
conditions = conditions.toObject();
} else {
conditions = utils.clone(conditions, { retainKeyOrder: true });
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
options = typeof options === 'function' ? options : utils.clone(options);
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
return mq.update(conditions, doc, options, callback);
2014-09-14 07:04:16 -04:00
};
/**
* Executes a mapReduce command.
*
2015-11-24 22:08:58 -08:00
* `o` is an object specifying all mapReduce options as well as the map and reduce functions. All options are delegated to the driver implementation. See [node-mongodb-native mapReduce() documentation](http://mongodb.github.io/node-mongodb-native/api-generated/collection.html#mapreduce) for more detail about options.
2014-09-14 07:04:16 -04:00
*
* ####Example:
*
* var o = {};
* o.map = function () { emit(this.name, 1) }
* o.reduce = function (k, vals) { return vals.length }
* User.mapReduce(o, function (err, results) {
* console.log(results)
* })
*
* ####Other options:
*
* - `query` {Object} query filter object.
2015-11-24 22:08:58 -08:00
* - `sort` {Object} sort input objects using this key
2014-09-14 07:04:16 -04:00
* - `limit` {Number} max number of documents
* - `keeptemp` {Boolean, default:false} keep temporary data
* - `finalize` {Function} finalize function
* - `scope` {Object} scope variables exposed to map/reduce/finalize during execution
* - `jsMode` {Boolean, default:false} it is possible to make the execution stay in JS. Provided in MongoDB > 2.0.X
* - `verbose` {Boolean, default:false} provide statistics on job execution time.
2015-11-24 22:08:58 -08:00
* - `readPreference` {String}
2014-09-14 07:04:16 -04:00
* - `out*` {Object, default: {inline:1}} sets the output target for the map reduce job.
*
* ####* out options:
*
* - `{inline:1}` the results are returned in an array
* - `{replace: 'collectionName'}` add the results to collectionName: the results replace the collection
* - `{reduce: 'collectionName'}` add the results to collectionName: if dups are detected, uses the reducer / finalize functions
* - `{merge: 'collectionName'}` add the results to collectionName: if dups exist the new docs overwrite the old
*
* If `options.out` is set to `replace`, `merge`, or `reduce`, a Model instance is returned that can be used for further querying. Queries run against this model are all executed with the `lean` option; meaning only the js object is returned and no Mongoose magic is applied (getters, setters, etc).
*
* ####Example:
*
* var o = {};
* o.map = function () { emit(this.name, 1) }
* o.reduce = function (k, vals) { return vals.length }
* o.out = { replace: 'createdCollectionNameForResults' }
* o.verbose = true;
2015-11-24 22:08:58 -08:00
*
2014-09-14 07:04:16 -04:00
* User.mapReduce(o, function (err, model, stats) {
* console.log('map reduce took %d ms', stats.processtime)
* model.find().where('value').gt(10).exec(function (err, docs) {
* console.log(docs);
* });
* })
*
2015-11-24 22:08:58 -08:00
* // a promise is returned so you may instead write
* var promise = User.mapReduce(o);
* promise.then(function (model, stats) {
* console.log('map reduce took %d ms', stats.processtime)
* return model.find().where('value').gt(10).exec();
* }).then(function (docs) {
* console.log(docs);
* }).then(null, handleError).end()
*
2014-09-14 07:04:16 -04:00
* @param {Object} o an object specifying map-reduce options
2015-11-24 22:08:58 -08:00
* @param {Function} [callback] optional callback
2014-09-14 07:04:16 -04:00
* @see http://www.mongodb.org/display/DOCS/MapReduce
2015-11-24 22:08:58 -08:00
* @return {Promise}
2014-09-14 07:04:16 -04:00
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.mapReduce = function mapReduce(o, callback) {
var promise = new Promise(callback);
2014-09-14 07:04:16 -04:00
var self = this;
if (!Model.mapReduce.schema) {
2015-11-24 22:08:58 -08:00
var opts = { noId: true, noVirtualId: true, strict: false };
2014-09-14 07:04:16 -04:00
Model.mapReduce.schema = new Schema({}, opts);
}
if (!o.out) o.out = { inline: 1 };
2015-11-24 22:08:58 -08:00
if (false !== o.verbose) o.verbose = true;
2014-09-14 07:04:16 -04:00
o.map = String(o.map);
o.reduce = String(o.reduce);
if (o.query) {
var q = new Query(o.query);
q.cast(this);
o.query = q._conditions;
q = undefined;
}
2015-11-24 22:08:58 -08:00
this.collection.mapReduce(null, null, o, function(err, ret, stats) {
if (err) return promise.error(err);
2014-09-14 07:04:16 -04:00
if (ret.findOne && ret.mapReduce) {
// returned a collection, convert to Model
var model = Model.compile(
'_mapreduce_' + ret.collectionName
, Model.mapReduce.schema
, ret.collectionName
, self.db
, self.base);
model._mapreduce = true;
2015-11-24 22:08:58 -08:00
return promise.fulfill(model, stats);
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
promise.fulfill(ret, stats);
2014-09-14 07:04:16 -04:00
});
2015-11-24 22:08:58 -08:00
return promise;
};
/**
* geoNear support for Mongoose
*
* ####Options:
* - `lean` {Boolean} return the raw object
* - All options supported by the driver are also supported
*
* ####Example:
*
* // Legacy point
* Model.geoNear([1,3], { maxDistance : 5, spherical : true }, function(err, results, stats) {
* console.log(results);
* });
*
* // geoJson
* var point = { type : "Point", coordinates : [9,9] };
* Model.geoNear(point, { maxDistance : 5, spherical : true }, function(err, results, stats) {
* console.log(results);
* });
*
* @param {Object/Array} GeoJSON point or legacy coordinate pair [x,y] to search near
* @param {Object} options for the qurery
* @param {Function} [callback] optional callback for the query
* @return {Promise}
* @see http://docs.mongodb.org/manual/core/2dsphere/
* @see http://mongodb.github.io/node-mongodb-native/api-generated/collection.html?highlight=geonear#geoNear
* @api public
*/
Model.geoNear = function(near, options, callback) {
if ('function' == typeof options) {
callback = options;
options = {};
}
var self = this;
var Promise = PromiseProvider.get();
if (!near) {
return new Promise.ES6(function(resolve, reject) {
var error = new Error('Must pass a near option to geoNear');
reject(error);
callback && callback(error);
return;
});
}
var x,y;
var _this = this;
return new Promise.ES6(function(resolve, reject) {
var handler = function(err, res) {
if (err) {
reject(err);
callback && callback(err);
return;
}
if (options.lean) {
resolve(res.results, res.stats);
callback && callback(null, res.results, res.stats);
return;
}
var count = res.results.length;
// if there are no results, fulfill the promise now
if (count == 0) {
resolve(res.results, res.stats);
callback && callback(null, res.results, res.stats);
return;
}
var errSeen = false;
for (var i = 0; i < res.results.length; i++) {
var temp = res.results[i].obj;
res.results[i].obj = new self();
res.results[i].obj.init(temp, function(err) {
if (err && !errSeen) {
errSeen = true;
reject(err);
callback && callback(err);
return;
}
if (--count <= 0) {
resolve(res.results, res.stats);
callback && callback(null, res.results, res.stats);
}
});
}
};
if (Array.isArray(near)) {
if (near.length !== 2) {
var error = new Error('If using legacy coordinates, must be an array ' +
'of size 2 for geoNear');
reject(error);
callback && callback(error);
return;
}
x = near[0];
y = near[1];
_this.collection.geoNear(x, y, options, handler);
} else {
if (near.type != 'Point' || !Array.isArray(near.coordinates)) {
error = new Error('Must pass either a legacy coordinate array or ' +
'GeoJSON Point to geoNear');
reject(error);
callback && callback(error);
return;
}
_this.collection.geoNear(near, options, handler);
}
});
};
2014-09-14 07:04:16 -04:00
/**
2015-11-24 22:08:58 -08:00
* Performs [aggregations](http://docs.mongodb.org/manual/applications/aggregation/) on the models collection.
*
* If a `callback` is passed, the `aggregate` is executed and a `Promise` is returned. If a callback is not passed, the `aggregate` itself is returned.
2014-09-14 07:04:16 -04:00
*
* ####Example:
*
2015-11-24 22:08:58 -08:00
* // Find the max balance of all accounts
2014-09-14 07:04:16 -04:00
* Users.aggregate(
2015-11-24 22:08:58 -08:00
* { $group: { _id: null, maxBalance: { $max: '$balance' }}}
* , { $project: { _id: 0, maxBalance: 1 }}
2014-09-14 07:04:16 -04:00
* , function (err, res) {
* if (err) return handleError(err);
2015-11-24 22:08:58 -08:00
* console.log(res); // [ { maxBalance: 98000 } ]
* });
*
* // Or use the aggregation pipeline builder.
* Users.aggregate()
* .group({ _id: null, maxBalance: { $max: '$balance' } })
* .select('-id maxBalance')
* .exec(function (err, res) {
* if (err) return handleError(err);
* console.log(res); // [ { maxBalance: 98 } ]
2014-09-14 07:04:16 -04:00
* });
*
* ####NOTE:
*
2015-11-24 22:08:58 -08:00
* - Arguments are not cast to the model's schema because `$project` operators allow redefining the "shape" of the documents at any stage of the pipeline, which may leave documents in an incompatible format.
* - The documents returned are plain javascript objects, not mongoose documents (since any shape of document can be returned).
2014-09-14 07:04:16 -04:00
* - Requires MongoDB >= 2.1
*
2015-11-24 22:08:58 -08:00
* @see Aggregate #aggregate_Aggregate
* @see MongoDB http://docs.mongodb.org/manual/applications/aggregation/
* @param {Object|Array} [...] aggregation pipeline operator(s) or operator array
* @param {Function} [callback]
* @return {Aggregate|Promise}
2014-09-14 07:04:16 -04:00
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.aggregate = function aggregate() {
var args = [].slice.call(arguments),
aggregate,
callback;
if ('function' === typeof args[args.length - 1]) {
callback = args.pop();
}
if (1 === args.length && util.isArray(args[0])) {
aggregate = new Aggregate(args[0]);
} else {
aggregate = new Aggregate(args);
}
aggregate.model(this);
if ('undefined' === typeof callback) {
return aggregate;
}
return aggregate.exec(callback);
};
/**
* Implements `$geoSearch` functionality for Mongoose
*
* ####Example:
*
* var options = { near: [10, 10], maxDistance: 5 };
* Locations.geoSearch({ type : "house" }, options, function(err, res) {
* console.log(res);
* });
*
* ####Options:
* - `near` {Array} x,y point to search for
* - `maxDistance` {Number} the maximum distance from the point near that a result can be
* - `limit` {Number} The maximum number of results to return
* - `lean` {Boolean} return the raw object instead of the Mongoose Model
*
* @param {Object} condition an object that specifies the match condition (required)
* @param {Object} options for the geoSearch, some (near, maxDistance) are required
* @param {Function} [callback] optional callback
* @return {Promise}
* @see http://docs.mongodb.org/manual/reference/command/geoSearch/
* @see http://docs.mongodb.org/manual/core/geohaystack/
* @api public
*/
Model.geoSearch = function(conditions, options, callback) {
if ('function' == typeof options) {
callback = options;
options = {};
}
var promise = new Promise(callback);
if (conditions == undefined || !utils.isObject(conditions)) {
return promise.error(new Error("Must pass conditions to geoSearch"));
}
if (!options.near) {
return promise.error(new Error("Must specify the near option in geoSearch"));
}
if (!Array.isArray(options.near)) {
return promise.error(new Error("near option must be an array [x, y]"));
}
// send the conditions in the options object
options.search = conditions;
var self = this;
this.collection.geoHaystackSearch(options.near[0], options.near[1], options, function(err, res) {
// have to deal with driver problem. Should be fixed in a soon-ish release
// (7/8/2013)
if (err || res.errmsg) {
if (!err) err = new Error(res.errmsg);
if (res && res.code !== undefined) err.code = res.code;
return promise.error(err);
}
var count = res.results.length;
if (options.lean || (count == 0)) return promise.fulfill(res.results, res.stats);
var errSeen = false;
for (var i = 0; i < res.results.length; i++) {
var temp = res.results[i];
res.results[i] = new self();
res.results[i].init(temp, {}, function(err) {
if (err && !errSeen) {
errSeen = true;
return promise.error(err);
}
--count || (!errSeen && promise.fulfill(res.results, res.stats));
});
}
});
return promise;
};
2014-09-14 07:04:16 -04:00
/**
* Populates document references.
*
* ####Available options:
*
* - path: space delimited path(s) to populate
* - select: optional fields to select
* - match: optional query conditions to match
* - model: optional name of the model to use for population
* - options: optional query options like sort, limit, etc
*
* ####Examples:
*
* // populates a single object
* User.findById(id, function (err, user) {
* var opts = [
* { path: 'company', match: { x: 1 }, select: 'name' }
* , { path: 'notes', options: { limit: 10 }, model: 'override' }
* ]
*
* User.populate(user, opts, function (err, user) {
* console.log(user);
* })
* })
*
* // populates an array of objects
* User.find(match, function (err, users) {
* var opts = [{ path: 'company', match: { x: 1 }, select: 'name' }]
*
2015-11-24 22:08:58 -08:00
* var promise = User.populate(users, opts);
* promise.then(console.log).end();
2014-09-14 07:04:16 -04:00
* })
*
* // imagine a Weapon model exists with two saved documents:
* // { _id: 389, name: 'whip' }
* // { _id: 8921, name: 'boomerang' }
*
* var user = { name: 'Indiana Jones', weapon: 389 }
* Weapon.populate(user, { path: 'weapon', model: 'Weapon' }, function (err, user) {
* console.log(user.weapon.name) // whip
* })
*
* // populate many plain objects
* var users = [{ name: 'Indiana Jones', weapon: 389 }]
* users.push({ name: 'Batman', weapon: 8921 })
* Weapon.populate(users, { path: 'weapon' }, function (err, users) {
* users.forEach(function (user) {
* console.log('%s uses a %s', users.name, user.weapon.name)
* // Indiana Jones uses a whip
* // Batman uses a boomerang
* })
* })
* // Note that we didn't need to specify the Weapon model because
* // we were already using it's populate() method.
*
* @param {Document|Array} docs Either a single document or array of documents to populate.
* @param {Object} options A hash of key/val (path, options) used for population.
2015-11-24 22:08:58 -08:00
* @param {Function} [cb(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`.
* @return {Promise}
2014-09-14 07:04:16 -04:00
* @api public
*/
2015-11-24 22:08:58 -08:00
Model.populate = function(docs, paths, cb) {
var promise = new Promise(cb);
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
// always resolve on nextTick for consistent async behavior
function resolve() {
2014-09-14 07:04:16 -04:00
var args = utils.args(arguments);
2015-11-24 22:08:58 -08:00
process.nextTick(function() {
promise.resolve.apply(promise, args);
2014-09-14 07:04:16 -04:00
});
}
// normalized paths
2015-11-24 22:08:58 -08:00
paths = utils.populate(paths);
2014-09-14 07:04:16 -04:00
var pending = paths.length;
if (0 === pending) {
2015-11-24 22:08:58 -08:00
resolve(null, docs);
return promise;
2014-09-14 07:04:16 -04:00
}
// each path has its own query options and must be executed separately
var i = pending;
var path;
2015-11-24 22:08:58 -08:00
var model = this;
2014-09-14 07:04:16 -04:00
while (i--) {
path = paths[i];
2015-11-24 22:08:58 -08:00
if ('function' === typeof path.model) model = path.model;
populate(model, docs, path, subPopulate.call(model, docs, path, next));
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
return promise;
function next(err) {
if (err) return resolve(err);
2014-09-14 07:04:16 -04:00
if (--pending) return;
2015-11-24 22:08:58 -08:00
resolve(null, docs);
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
};
2014-09-14 07:04:16 -04:00
/*!
2015-11-24 22:08:58 -08:00
* Populates deeply if `populate` option is present.
*
* @param {Document|Array} docs
* @param {Object} options
* @param {Function} cb
* @return {Function}
* @api private
2014-09-14 07:04:16 -04:00
*/
2015-11-24 22:08:58 -08:00
function subPopulate(docs, options, cb) {
var model = this;
var prefix = options.path + '.';
var pop = options.populate;
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
if (!pop) {
return cb;
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
// normalize as array
if (!Array.isArray(pop)) {
pop = [pop];
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
return function(err) {
var pending = pop.length;
function next(err) {
if (err) return cb(err);
if (--pending) return;
cb();
}
if (err || !pending) return cb(err);
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
pop.forEach(function(subOptions) {
// path needs parent's path prefixed to it
if (!subOptions._originalPath) {
subOptions._originalPath = subOptions.path;
subOptions.path = prefix + subOptions.path;
}
var schema = model.schema._getSchema(prefix);
if (typeof subOptions.model === 'string') {
subOptions.model = model.model(subOptions.model);
} else if (schema && schema.caster && schema.caster.options &&
schema.caster.options.ref) {
var subSchema = model.model(schema.caster.options.ref).schema.
_getSchema(subOptions._originalPath);
if (subSchema && subSchema.options && subSchema.options.ref) {
subOptions.model = model.model(subSchema.options.ref);
}
}
Model.populate.call(subOptions.model || model, docs, subOptions, next);
});
};
}
/*!
* Populates `docs`
*/
var excludeIdReg = /\s?-_id\s?/,
excludeIdRegGlobal = /\s?-_id\s?/g;
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
function populate(model, docs, options, cb) {
var modelsMap, rawIds;
2014-09-14 07:04:16 -04:00
// normalize single / multiple docs passed
if (!Array.isArray(docs)) {
docs = [docs];
}
if (0 === docs.length || docs.every(utils.isNullOrUndefined)) {
return cb();
}
2015-11-24 22:08:58 -08:00
modelsMap = getModelsMapForPopulate(model, docs, options);
rawIds = getIdsForAndAddIdsInMapPopulate(modelsMap);
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
var i, len = modelsMap.length,
mod, match, select, promise, vals = [];
promise = new Promise(function(err, vals, options, assignmentOpts) {
if (err) return cb(err);
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
var lean = options.options && options.options.lean,
len = vals.length,
rawOrder = {}, rawDocs = {}, key, val;
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
// optimization:
// record the document positions as returned by
// the query result.
for (var i = 0; i < len; i++) {
val = vals[i];
key = String(utils.getValue('_id', val));
rawDocs[key] = val;
rawOrder[key] = i;
// flag each as result of population
if (!lean) val.$__.wasPopulated = true;
}
assignVals({
rawIds: rawIds,
rawDocs: rawDocs,
rawOrder: rawOrder,
docs: docs,
path: options.path,
options: assignmentOpts
});
cb();
});
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
var _remaining = len;
2014-09-14 07:04:16 -04:00
for (i = 0; i < len; i++) {
2015-11-24 22:08:58 -08:00
mod = modelsMap[i];
select = mod.options.select;
if (mod.options.match) {
match = utils.object.shallowCopy(mod.options.match);
} else {
match = {};
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
var ids = utils.array.flatten(mod.ids, function(item) {
// no need to include undefined values in our query
return undefined !== item;
});
ids = utils.array.unique(ids);
if (0 === ids.length || ids.every(utils.isNullOrUndefined)) {
return cb();
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
match._id || (match._id = {
$in: ids
});
var assignmentOpts = {};
assignmentOpts.sort = mod.options.options && mod.options.options.sort || undefined;
assignmentOpts.excludeId = excludeIdReg.test(select) || (select && 0 === select._id);
if (assignmentOpts.excludeId) {
// override the exclusion from the query so we can use the _id
// for document matching during assignment. we'll delete the
// _id back off before returning the result.
if ('string' == typeof select) {
select = select.replace(excludeIdRegGlobal, ' ');
} else {
// preserve original select conditions by copying
select = utils.object.shallowCopy(select);
delete select._id;
}
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
if (mod.options.options && mod.options.options.limit) {
assignmentOpts.originalLimit = mod.options.options.limit;
mod.options.options.limit = mod.options.options.limit * ids.length;
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
mod.Model.find(match, select, mod.options.options, next.bind(this, mod.options, assignmentOpts));
}
function next(options, assignmentOpts, err, valsFromDb) {
if (err) return promise.resolve(err);
vals = vals.concat(valsFromDb);
if (--_remaining === 0) {
promise.resolve(err, vals, options, assignmentOpts);
2014-09-14 07:04:16 -04:00
}
}
2015-11-24 22:08:58 -08:00
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
function getModelsMapForPopulate(model, docs, options) {
var i, doc, len = docs.length,
available = {},
map = [],
modelNameFromQuery = options.model && options.model.modelName || options.model,
schema, refPath, Model, currentOptions, modelNames, modelName, discriminatorKey, modelForFindSchema;
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
schema = model._getSchema(options.path);
if (schema && schema.caster) {
schema = schema.caster;
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
if (!schema && model.discriminators) {
discriminatorKey = model.schema.discriminatorMapping.key;
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
refPath = schema && schema.options && schema.options.refPath;
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
for (i = 0; i < len; i++) {
doc = docs[i];
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
if (refPath) {
modelNames = utils.getValue(refPath, doc);
2014-09-14 07:04:16 -04:00
} else {
2015-11-24 22:08:58 -08:00
if (!modelNameFromQuery) {
var schemaForCurrentDoc;
if (!schema && discriminatorKey) {
modelForFindSchema = utils.getValue(discriminatorKey, doc);
if (modelForFindSchema) {
schemaForCurrentDoc = model.db.model(modelForFindSchema)._getSchema(options.path);
if (schemaForCurrentDoc && schemaForCurrentDoc.caster) {
schemaForCurrentDoc = schemaForCurrentDoc.caster;
}
}
} else {
schemaForCurrentDoc = schema;
}
modelNames = [
schemaForCurrentDoc && schemaForCurrentDoc.options && schemaForCurrentDoc.options.ref // declared in schema
|| model.modelName // an ad-hoc structure
];
} else {
modelNames = [modelNameFromQuery]; // query options
}
}
if (!modelNames)
continue;
if (!Array.isArray(modelNames)) {
modelNames = [modelNames];
}
var k = modelNames.length;
while (k--) {
modelName = modelNames[k];
if (!available[modelName]) {
Model = model.db.model(modelName);
currentOptions = {
model: Model
};
if (schema && !discriminatorKey) {
options.model = Model;
}
utils.merge(currentOptions, options);
available[modelName] = {
Model: Model,
options: currentOptions,
docs: [doc],
ids: []
};
map.push(available[modelName]);
} else {
available[modelName].docs.push(doc);
}
2014-09-14 07:04:16 -04:00
}
}
2015-11-24 22:08:58 -08:00
return map;
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
function getIdsForAndAddIdsInMapPopulate(modelsMap) {
var rawIds = [], // for the correct position
i, j, doc, docs, id, len, len2, ret, isDocument, options, path;
len2 = modelsMap.length;
for (j = 0; j < len2; j++) {
docs = modelsMap[j].docs;
len = docs.length;
options = modelsMap[j].options;
path = options.path;
for (i = 0; i < len; i++) {
ret = undefined;
doc = docs[i];
id = String(utils.getValue("_id", doc));
isDocument = !!doc.$__;
if (!ret || Array.isArray(ret) && 0 === ret.length) {
ret = utils.getValue(path, doc);
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
if (ret) {
ret = convertTo_id(ret);
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
options._docs[id] = Array.isArray(ret) ? ret.slice() : ret;
}
rawIds.push(ret);
modelsMap[j].ids.push(ret);
if (isDocument) {
// cache original populated _ids and model used
doc.populated(path, options._docs[id], options);
}
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
return rawIds;
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
/*!
* Retrieve the _id of `val` if a Document or Array of Documents.
*
* @param {Array|Document|Any} val
* @return {Array|Document|Any}
*/
function convertTo_id(val) {
if (val instanceof Model) return val._id;
if (Array.isArray(val)) {
for (var i = 0; i < val.length; ++i) {
if (val[i] instanceof Model) {
val[i] = val[i]._id;
}
}
return val;
}
return val;
2014-09-14 07:04:16 -04:00
}
/*!
* Assigns documents returned from a population query back
* to the original document path.
*/
2015-11-24 22:08:58 -08:00
function assignVals(o) {
2014-09-14 07:04:16 -04:00
// replace the original ids in our intermediate _ids structure
// with the documents found by query
assignRawDocsToIdStructure(o.rawIds, o.rawDocs, o.rawOrder, o.options);
// now update the original documents being populated using the
// result structure that contains real documents.
var docs = o.docs;
var path = o.path;
var rawIds = o.rawIds;
var options = o.options;
for (var i = 0; i < docs.length; ++i) {
2015-11-24 22:08:58 -08:00
if (utils.getValue(path, docs[i]) == null)
continue;
utils.setValue(path, rawIds[i], docs[i], function(val) {
2014-09-14 07:04:16 -04:00
return valueFilter(val, options);
});
}
}
/*!
* 1) Apply backwards compatible find/findOne behavior to sub documents
*
* find logic:
* a) filter out non-documents
* b) remove _id from sub docs when user specified
*
* findOne
* a) if no doc found, set to null
* b) remove _id from sub docs when user specified
*
* 2) Remove _ids when specified by users query.
*
* background:
* _ids are left in the query even when user excludes them so
* that population mapping can occur.
*/
2015-11-24 22:08:58 -08:00
function valueFilter(val, assignmentOpts) {
2014-09-14 07:04:16 -04:00
if (Array.isArray(val)) {
// find logic
var ret = [];
2015-11-24 22:08:58 -08:00
var numValues = val.length;
for (var i = 0; i < numValues; ++i) {
2014-09-14 07:04:16 -04:00
var subdoc = val[i];
if (!isDoc(subdoc)) continue;
maybeRemoveId(subdoc, assignmentOpts);
ret.push(subdoc);
2015-11-24 22:08:58 -08:00
if (assignmentOpts.originalLimit &&
ret.length >= assignmentOpts.originalLimit) {
break;
}
}
// Since we don't want to have to create a new mongoosearray, make sure to
// modify the array in place
while (val.length > ret.length) {
Array.prototype.pop.apply(val, []);
}
for (i = 0; i < ret.length; ++i) {
val[i] = ret[i];
2014-09-14 07:04:16 -04:00
}
2015-11-24 22:08:58 -08:00
return val;
2014-09-14 07:04:16 -04:00
}
// findOne
if (isDoc(val)) {
maybeRemoveId(val, assignmentOpts);
return val;
}
return null;
}
/*!
* Remove _id from `subdoc` if user specified "lean" query option
*/
2015-11-24 22:08:58 -08:00
function maybeRemoveId(subdoc, assignmentOpts) {
2014-09-14 07:04:16 -04:00
if (assignmentOpts.excludeId) {
if ('function' == typeof subdoc.setValue) {
2015-11-24 22:08:58 -08:00
delete subdoc._doc._id;
2014-09-14 07:04:16 -04:00
} else {
delete subdoc._id;
}
}
}
/*!
* Determine if `doc` is a document returned
* by a populate query.
*/
2015-11-24 22:08:58 -08:00
function isDoc(doc) {
2014-09-14 07:04:16 -04:00
if (null == doc)
return false;
var type = typeof doc;
if ('string' == type)
return false;
if ('number' == type)
return false;
if (Buffer.isBuffer(doc))
return false;
if ('ObjectID' == doc.constructor.name)
return false;
// only docs
return true;
}
/*!
* Assign `vals` returned by mongo query to the `rawIds`
* structure returned from utils.getVals() honoring
* query sort order if specified by user.
*
* This can be optimized.
*
* Rules:
*
* if the value of the path is not an array, use findOne rules, else find.
* for findOne the results are assigned directly to doc path (including null results).
* for find, if user specified sort order, results are assigned directly
* else documents are put back in original order of array if found in results
*
* @param {Array} rawIds
* @param {Array} vals
* @param {Boolean} sort
* @api private
*/
2015-11-24 22:08:58 -08:00
function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, recursed) {
2014-09-14 07:04:16 -04:00
// honor user specified sort order
var newOrder = [];
var sorting = options.sort && rawIds.length > 1;
var doc;
var sid;
var id;
for (var i = 0; i < rawIds.length; ++i) {
id = rawIds[i];
if (Array.isArray(id)) {
// handle [ [id0, id2], [id3] ]
assignRawDocsToIdStructure(id, resultDocs, resultOrder, options, true);
newOrder.push(id);
continue;
}
if (null === id && !sorting) {
// keep nulls for findOne unless sorting, which always
// removes them (backward compat)
newOrder.push(id);
continue;
}
sid = String(id);
if (recursed) {
// apply find behavior
// assign matching documents in original order unless sorting
doc = resultDocs[sid];
if (doc) {
if (sorting) {
newOrder[resultOrder[sid]] = doc;
} else {
newOrder.push(doc);
}
} else {
newOrder.push(id);
}
} else {
// apply findOne behavior - if document in results, assign, else assign null
newOrder[i] = doc = resultDocs[sid] || null;
}
}
rawIds.length = 0;
if (newOrder.length) {
// reassign the documents based on corrected order
// forEach skips over sparse entries in arrays so we
// can safely use this to our advantage dealing with sorted
// result sets too.
2015-11-24 22:08:58 -08:00
newOrder.forEach(function(doc, i) {
2014-09-14 07:04:16 -04:00
rawIds[i] = doc;
});
}
}
/**
* Finds the schema for `path`. This is different than
* calling `schema.path` as it also resolves paths with
* positional selectors (something.$.another.$.path).
*
* @param {String} path
* @return {Schema}
* @api private
*/
2015-11-24 22:08:58 -08:00
Model._getSchema = function _getSchema(path) {
return this.schema._getSchema(path);
};
2014-09-14 07:04:16 -04:00
/*!
* Compiler utility.
*
* @param {String} name model name
* @param {Schema} schema
* @param {String} collectionName
* @param {Connection} connection
* @param {Mongoose} base mongoose instance
*/
2015-11-24 22:08:58 -08:00
Model.compile = function compile(name, schema, collectionName, connection, base) {
2014-09-14 07:04:16 -04:00
var versioningEnabled = false !== schema.options.versionKey;
if (versioningEnabled && !schema.paths[schema.options.versionKey]) {
// add versioning to top level documents only
var o = {};
o[schema.options.versionKey] = Number;
schema.add(o);
}
// generate new class
2015-11-24 22:08:58 -08:00
function model(doc, fields, skipId) {
2014-09-14 07:04:16 -04:00
if (!(this instanceof model))
return new model(doc, fields, skipId);
Model.call(this, doc, fields, skipId);
2015-11-24 22:08:58 -08:00
}
2014-09-14 07:04:16 -04:00
2015-11-24 22:08:58 -08:00
model.hooks = schema.s.hooks.clone();
2014-09-14 07:04:16 -04:00
model.base = base;
model.modelName = name;
model.__proto__ = Model;
model.prototype.__proto__ = Model.prototype;
model.model = Model.prototype.model;
model.db = model.prototype.db = connection;
2015-11-24 22:08:58 -08:00
model.discriminators = model.prototype.discriminators = undefined;
2014-09-14 07:04:16 -04:00
model.prototype.$__setSchema(schema);
var collectionOptions = {
2015-11-24 22:08:58 -08:00
bufferCommands: schema.options.bufferCommands,
capped: schema.options.capped
2014-09-14 07:04:16 -04:00
};
model.prototype.collection = connection.collection(
collectionName
, collectionOptions
);
2015-11-24 22:08:58 -08:00
// apply methods and statics
applyMethods(model, schema);
applyStatics(model, schema);
2014-09-14 07:04:16 -04:00
model.schema = model.prototype.schema;
model.collection = model.prototype.collection;
return model;
};
2015-11-24 22:08:58 -08:00
/*!
* Register methods for this model
*
* @param {Model} model
* @param {Schema} schema
*/
var applyMethods = function(model, schema) {
for (var i in schema.methods) {
if (typeof schema.methods[i] === 'function') {
model.prototype[i] = schema.methods[i];
} else {
(function(_i) {
Object.defineProperty(model.prototype, _i, {
get: function() {
var h = {};
for (var k in schema.methods[_i]) {
h[k] = schema.methods[_i][k].bind(this);
}
return h;
},
configurable: true
});
})(i);
}
}
};
/*!
* Register statics for this model
* @param {Model} model
* @param {Schema} schema
*/
var applyStatics = function(model, schema) {
for (var i in schema.statics) {
model[i] = schema.statics[i];
}
};
2014-09-14 07:04:16 -04:00
/*!
* Subclass this model with `conn`, `schema`, and `collection` settings.
*
* @param {Connection} conn
* @param {Schema} [schema]
* @param {String} [collection]
* @return {Model}
*/
2015-11-24 22:08:58 -08:00
Model.__subclass = function subclass(conn, schema, collection) {
2014-09-14 07:04:16 -04:00
// subclass model using this connection and collection name
var model = this;
2015-11-24 22:08:58 -08:00
var Model = function Model(doc, fields, skipId) {
2014-09-14 07:04:16 -04:00
if (!(this instanceof Model)) {
return new Model(doc, fields, skipId);
}
model.call(this, doc, fields, skipId);
2015-11-24 22:08:58 -08:00
};
2014-09-14 07:04:16 -04:00
Model.__proto__ = model;
Model.prototype.__proto__ = model.prototype;
Model.db = Model.prototype.db = conn;
var s = schema && 'string' != typeof schema
? schema
: model.prototype.schema;
2015-11-24 22:08:58 -08:00
var options = s.options || {};
2014-09-14 07:04:16 -04:00
if (!collection) {
collection = model.prototype.schema.get('collection')
2015-11-24 22:08:58 -08:00
|| utils.toCollectionName(model.modelName, options);
2014-09-14 07:04:16 -04:00
}
var collectionOptions = {
2015-11-24 22:08:58 -08:00
bufferCommands: s ? options.bufferCommands : true,
capped: s && options.capped
2014-09-14 07:04:16 -04:00
};
Model.prototype.collection = conn.collection(collection, collectionOptions);
Model.collection = Model.prototype.collection;
Model.init();
return Model;
2015-11-24 22:08:58 -08:00
};
2014-09-14 07:04:16 -04:00
/*!
* Module exports.
*/
module.exports = exports = Model;