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

1746 lines
46 KiB
JavaScript

/*!
* Module dependencies.
*/
var Document = require('./document')
, MongooseArray = require('./types/array')
, MongooseBuffer = require('./types/buffer')
, MongooseError = require('./error')
, VersionError = require('./errors/version')
, Query = require('./query')
, Schema = require('./schema')
, utils = require('./utils')
, isMongooseObject = utils.isMongooseObject
, EventEmitter = utils.EventEmitter
, merge = utils.merge
, Promise = require('./promise')
, tick = utils.tick
var VERSION_WHERE = 1
, VERSION_INC = 2
, VERSION_ALL = VERSION_WHERE | VERSION_INC;
/**
* Model constructor
*
* @param {Object} doc values to with which to create the document
* @inherits Document
* @event `error`: If listening to this Model 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.
* @event `index`: Emitted after `Model#ensureIndexes` completes. If an error occurred it is passed with the event.
* @api public
*/
function Model (doc, fields, skipId) {
Document.call(this, doc, fields, skipId);
};
/*!
* Inherits from Document.
*
* All Model.prototype features are available on
* top level (non-sub) documents.
*/
Model.prototype.__proto__ = Document.prototype;
/**
* 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;
/**
* Returns what paths can be populated
*
* @param {Query} query object
* @return {Object|undefined} population paths
* @api private
*/
Model.prototype._getPopulationKeys = function getPopulationKeys (query) {
if (!(query && query.options.populate)) return;
var names = Object.keys(query.options.populate)
, n = names.length
, name
, paths = {}
, hasKeys
, schema
while (n--) {
name = names[n];
schema = this.schema.path(name);
hasKeys = true;
if (!schema) {
// if the path is not recognized, it's potentially embedded docs
// walk path atoms from right to left to find a matching path
var pieces = name.split('.')
, i = pieces.length;
while (i--) {
var path = pieces.slice(0, i).join('.')
, pathSchema = this.schema.path(path);
// loop until we find an array schema
if (pathSchema && pathSchema.caster) {
if (!paths[path]) {
paths[path] = { sub: {} };
}
paths[path].sub[pieces.slice(i).join('.')] = query.options.populate[name];
hasKeys || (hasKeys = true);
break;
}
}
} else {
paths[name] = query.options.populate[name];
hasKeys || (hasKeys = true);
}
}
return hasKeys && paths;
};
/**
* Populates an object
*
* @param {SchemaType} schema type for the oid
* @param {Object} oid object id or array of object ids
* @param {Object} query object specifying query conditions, fields, and options
* @param {Function} fn
* @api private
*/
Model.prototype._populate = function populate (schema, oid, query, fn) {
if (!Array.isArray(oid)) {
var conditions = query.conditions || {};
conditions._id = oid;
return this
.db.model(query.model || schema.options.ref)
.findOne(conditions, query.fields, query.options, fn);
}
if (!oid.length) {
return fn(null, oid);
}
var model = this.db.model(query.model || schema.caster.options.ref)
, conditions = query && query.conditions || {};
conditions._id || (conditions._id = { $in: oid });
model.find(conditions, query.fields, query.options, function (err, docs) {
if (err) return fn(err);
// user specified sort order?
if (query.options && query.options.sort) {
return fn(null, docs);
}
// put back in original id order (using a hash reduces complexity from n*n to 2n)
var docHash = {};
docs.forEach(function (doc) {
docHash[doc._id] = doc;
});
var arr = [];
oid.forEach(function (id) {
if (id in docHash) arr.push(docHash[id]);
});
fn(null, arr);
});
};
/**
* Performs auto-population of relations.
*
* @param {Object} doc document returned by mongo
* @param {Query} query query that originated the initialization
* @param {Function} fn
* @api private
*/
Model.prototype.init = function init (doc, query, fn) {
if ('function' == typeof query) {
fn = query;
query = null;
}
var populate = this._getPopulationKeys(query);
if (!populate) {
return Document.prototype.init.call(this, doc, fn);
}
// population from other models is necessary
var self = this;
init(doc, '', function (err) {
if (err) return fn(err);
Document.prototype.init.call(self, doc, fn);
});
return this;
function init (obj, prefix, fn) {
prefix = prefix || '';
var keys = Object.keys(obj)
, len = keys.length;
return next();
function next () {
if (--len < 0) return fn();
var i = keys[len]
, path = prefix + i
, schema = self.schema.path(path)
, total = 0
, inline = false
, poppath
if (!schema && obj[i] && 'Object' === obj[i].constructor.name) {
// assume nested object
return init(obj[i], path + '.', next);
}
if (!(obj[i] && schema && populate[path])) return next();
// this query object is re-used and passed around. we clone
// it to prevent query condition contamination between
// one populate call to the next.
poppath = utils.clone(populate[path]);
if (poppath.sub) {
obj[i].forEach(function (subobj) {
inline = true;
var pkeys = Object.keys(poppath.sub)
, pi = pkeys.length
, key
while (pi--) {
key = pkeys[pi];
if (subobj[key]) (function (key) {
total++;
self._populate(schema.schema.path(key), subobj[key], poppath.sub[key], done);
function done (err, doc) {
if (err) return error(err);
subobj[key] = doc;
if (--total < 1 && !inline) {
next();
}
}
})(key);
}
});
inline = false;
if (0 === total) return next();
} else {
self._populate(schema, obj[i], poppath, function (err, doc) {
if (err) return error(err);
obj[i] = doc;
next();
});
}
};
};
function error (err) {
if (error.err) return;
fn(error.err = err);
}
};
/*!
* Handles doc.save() callbacks
*/
function handleSave (promise, self) {
return tick(function handleSave (err, result) {
if (err) {
// If the initial insert fails provide a second chance.
// (If we did this all the time we would break updates)
if (self._inserting) {
self.isNew = true;
self.emit('isNew', true);
}
promise.error(err);
promise = self = null;
return;
}
self._storeShard();
var numAffected;
if (result) {
// when inserting, the array of created docs is returned
numAffected = result.length
? result.length
: result;
} else {
numAffected = 0;
}
// was this an update that required a version bump?
if (self.__version && !self._inserting) {
var doIncrement = VERSION_INC === (VERSION_INC & self.__version);
self.__version = undefined;
// increment version if was successful
if (numAffected > 0) {
if (doIncrement) {
var key = self.schema.options.versionKey;
var version = self.getValue(key) | 0;
self.setValue(key, version + 1);
}
} else {
// the update failed. pass an error back
promise.error(new VersionError);
promise = self = null;
return;
}
}
self.emit('save', self, numAffected);
promise.complete(self, numAffected);
promise = self = null;
});
}
/**
* Saves this document.
*
* ####Example:
*
* product.sold = Date.now();
* product.save(function (err, product) {
* if (err) ..
* })
*
* The `fn` callback is optional. If no `fn` is passed and validation fails, the validation error will be emitted on the connection used to create this model.
*
* var db = mongoose.createConnection(..);
* var schema = new Schema(..);
* var Product = db.model('Product', schema);
*
* db.on('error', handleError);
*
* However, if you desire more local error handling you can add an `error` listener to the model and handle errors there instead.
*
* Product.on('error', handleError);
*
* @param {Function} [fn] optional callback
* @api public
* @see middleware http://mongoosejs.com/docs/middleware.html
*/
Model.prototype.save = function save (fn) {
var promise = new Promise(fn)
, complete = handleSave(promise, this)
, options = {}
if (this.schema.options.safe) {
options.safe = this.schema.options.safe;
}
if (this.isNew) {
// send entire doc
var obj = this.toObject({ depopulate: 1 });
this._version(true, obj);
this.collection.insert(obj, options, complete);
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) {
var where = this._where(delta[0]);
this.collection.update(where, delta[1], options, complete);
} else {
process.nextTick(function () {
complete(null);
})
}
this._reset();
this.emit('isNew', false);
}
};
/*!
* 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 (false === self.schema.options.versionKey) 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 ('$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+/.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} val
*/
function handleAtomics (self, where, delta, data, val) {
if (delta.$set && delta.$set[data.path]) {
// $set has precedence over other atomics
return;
}
var atomics = val._atomics
, ops = Object.keys(atomics)
, schema = data.schema
, path = data.path
, i = ops.length
, val
, op;
if (0 === i) {
// $set
if (isMongooseObject(val)) {
val = val.toObject({ depopulate: 1 });
} else if (val.valueOf) {
val = val.valueOf();
}
return operand(self, where, delta, data, val);
}
while (i--) {
op = ops[i];
val = atomics[op];
if (isMongooseObject(val)) {
val = val.toObject({ depopulate: 1 })
} else if (Array.isArray(val)) {
val = val.map(function (mem) {
return isMongooseObject(mem)
? mem.toObject({ depopulate: 1 })
: mem;
})
} else if (val.valueOf) {
val = val.valueOf()
}
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
*/
Model.prototype._delta = function _delta () {
var dirty = this._dirty();
if (!dirty.length) return;
var self = this
, where = {}
, delta = {}
, len = dirty.length
, d = 0
, val
, obj
for (; d < len; ++d) {
var data = dirty[d]
var value = data.value
var schema = data.schema
if (undefined === value) {
operand(self, where, delta, data, 1, '$unset');
} else if (null === value) {
operand(self, where, delta, data, null);
} else if (value._path && value._atomics) {
// arrays and other custom types (support plugins etc)
handleAtomics(self, where, delta, data, value);
} else if (value._path && Buffer.isBuffer(value)) {
// MongooseBuffer
value = value.toObject();
operand(self, where, delta, data, value);
} else {
value = utils.clone(value);
operand(self, where, delta, data, value);
}
}
if (this.__version) {
this._version(where, delta);
}
return [where, delta];
}
/**
* Appends versioning to the where and update clauses.
*
* @api private
*/
Model.prototype._version = function _version (where, delta) {
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?
// TODO fail fast option?
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)) {
delta.$inc || (delta.$inc = {});
delta.$inc[key] = 1;
}
}
/**
* Signal that we desire an increment of this documents version.
*
* @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 which applies shardkeys if they exist.
*
* @api private
*/
Model.prototype._where = function _where (where) {
where || (where = {});
var paths
, len
if (this._shardval) {
paths = Object.keys(this._shardval)
len = paths.length
for (var i = 0; i < len; ++i) {
where[paths[i]] = this._shardval[paths[i]];
}
}
where._id = this._doc._id;
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
* })
* })
*
* @param {Function} [fn] optional callback
* @api public
*/
Model.prototype.remove = function remove (fn) {
if (this._removing) {
this._removing.addBack(fn);
return this;
}
var promise = this._removing = new Promise(fn)
, where = this._where()
, self = this
, options = {}
if (this.schema.options.safe) {
options.safe = this.schema.options.safe;
}
this.collection.remove(where, options, tick(function (err) {
if (err) {
promise.error(err);
promise = self = self._removing = where = options = null;
return;
}
self.emit('remove', self);
promise.complete();
promise = self = where = options = null;
}));
return this;
};
/**
* Register hooks override
*
* @api private
*/
Model.prototype._registerHooks = function registerHooks () {
Document.prototype._registerHooks.call(this);
};
/**
* 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);
};
// 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
*/
Model.init = function init () {
if (this.schema.options.autoIndex) {
this.ensureIndexes();
}
this.schema.emit('init', this);
};
/**
* Sends `ensureIndex` commands to mongo for each index declared in the schema.
*
* After completion, an `index` event is emitted on this `Model` passing an error if one occurred.
*
* _NOTE: It is not recommended that you run this in production. Index creation may impact database performance depending on your load. Use with caution._
*
* ####Example:
*
* Event.ensureIndexes(function (err) {
* if (err) return handleError(err);
* });
*
* @param {Function} [cb] optional callback
* @api public
*/
Model.ensureIndexes = function ensureIndexes (cb) {
var indexes = this.schema.indexes();
if (!indexes.length) {
return cb && cb();
}
var self = this
, safe = self.schema.options.safe
, count = indexes.length
, error
indexes.forEach(function (index) {
var options = index[1];
options.safe = safe;
self.collection.ensureIndex(index[0], options, tick(function (err) {
if (err) error = err;
if (--count) return;
self.emit('index', error);
cb && cb(error);
}));
});
}
/**
* Schema the model uses.
*
* @property schema
* @receiver Model
* @api public
*/
Model.schema;
/**
* Database 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;
/**
* Removes documents from the collection.
*
* ####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):
*
* Comment.remove({ _id: id }).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]
* @return {Query}
* @api public
*/
Model.remove = function remove (conditions, callback) {
if ('function' === typeof conditions) {
callback = conditions;
conditions = {};
}
var query = new Query(conditions).bind(this, 'remove');
if ('undefined' === typeof callback)
return query;
this._applyNamedScope(query);
return query.remove(callback);
};
/**
* Finds documents
*
* ####Examples:
*
* // retrieve only certain keys
* MyModel.find({ name: /john/i }, 'name friends', function () { })
*
* // pass options
* MyModel.find({ name: /john/i }, null, { skip: 10 } )
*
* @param {Object} conditions
* @param {Object} [fields] optional fields to select
* @param {Object} [options] optional
* @param {Function} [callback]
* @return {Query}
* @see field selection #query_Query-select
* @api public
*/
Model.find = function find (conditions, fields, options, callback) {
if ('function' == typeof conditions) {
callback = conditions;
conditions = {};
fields = null;
options = null;
} else if ('function' == typeof fields) {
callback = fields;
fields = null;
options = null;
} else if ('function' == typeof options) {
callback = options;
options = null;
}
var query = new Query(conditions, options);
query.bind(this, 'find');
query.select(fields);
if ('undefined' === typeof callback)
return query;
this._applyNamedScope(query);
return query.find(callback);
};
/**
* Merges the current named scope query into `query`.
*
* @param {Query} query
* @return {Query}
* @api private
*/
Model._applyNamedScope = function _applyNamedScope (query) {
var cQuery = this._cumulativeQuery;
if (cQuery) {
merge(query._conditions, cQuery._conditions);
if (query._fields && cQuery._fields)
merge(query._fields, cQuery._fields);
if (query.options && cQuery.options)
merge(query.options, cQuery.options);
delete this._cumulativeQuery;
}
return query;
}
/**
* Finds a single document by id.
*
* The `id` is cast to an ObjectId before sending the command.
*
* ####Example:
*
* Adventure.findById(id, callback);
*
* @param {ObjectId|HexId} id objectid, or a value that can be casted to one
* @param {Object} [fields] optional fields to select
* @param {Object} [options] optional
* @param {Function} [callback]
* @return {Query}
* @see field selection #query_Query-select
* @api public
*/
Model.findById = function findById (id, fields, options, callback) {
return this.findOne({ _id: id }, fields, options, callback);
};
/**
* Finds one document.
*
* The `conditions` are cast to their respective SchemaTypes before the command is sent.
*
* ####Example:
*
* Adventure.findOne({ type: 'iphone' }, 'name', { safe: true }, callback);
*
* @param {Object} conditions
* @param {Object} [fields] optional fields to select
* @param {Object} [options] optional
* @param {Function} [callback]
* @return {Query}
* @see field selection #query_Query-select
* @api public
*/
Model.findOne = function findOne (conditions, fields, options, callback) {
if ('function' == typeof options) {
callback = options;
options = null;
} else if ('function' == typeof fields) {
callback = fields;
fields = null;
options = null;
} else if ('function' == typeof conditions) {
callback = conditions;
conditions = {};
fields = null;
options = null;
}
var query = new Query(conditions, options).select(fields).bind(this, 'findOne');
if ('undefined' == typeof callback)
return query;
this._applyNamedScope(query);
return query.findOne(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 ('function' === typeof conditions)
callback = conditions, conditions = {};
var query = new Query(conditions).bind(this, 'count');
if ('undefined' == typeof callback)
return query;
this._applyNamedScope(query);
return query.count(callback);
};
/**
* Executes a DISTINCT command
*
* @param {String} field
* @param {Object} [conditions] optional
* @param {Function} [callback]
* @return {Query}
* @api public
*/
Model.distinct = function distinct (field, conditions, callback) {
var query = new Query(conditions).bind(this, 'distinct');
if ('undefined' == typeof callback) {
query._distinctArg = field;
return query;
}
this._applyNamedScope(query);
return query.distinct(field, 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) {
var q = new Query().bind(this, 'find');
return q.where.apply(q, 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.comments.length > 5');
*
* @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 q = new Query().bind(this, 'find');
return q.$where.apply(q, 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 - true to return the modified document rather than the original. defaults to true
* - `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:
*
* Although values are cast to their appropriate types when using the findAndModify helpers, 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);
* })
*
* @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
*/
Model.findOneAndUpdate = function (conditions, update, options, callback) {
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';
throw new TypeError(msg)
}
update = conditions;
conditions = undefined;
}
var fields;
if (options && options.fields) {
fields = options.fields;
options.fields = undefined;
}
var query = new Query(conditions);
query.setOptions(options);
query.select(fields);
query.bind(this, 'findOneAndUpdate', update);
if ('undefined' == typeof callback)
return query;
this._applyNamedScope(query);
return query.findOneAndUpdate(callback);
}
/**
* Issues a mongodb findAndModify update command by a documents id.
*
* 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 - true to return the modified document rather than the original. defaults to true
* - `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
*
* 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 - true to return the modified document rather than the original. defaults to true
* - `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
*
* ####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:
*
* Although values are cast to their appropriate types when using the findAndModify helpers, the following are *not* applied:
*
* - defaults
* - setters
* - validators
* - middleware
*
* If you need those features, use the traditional approach of first retrieving the document.
*
* Model.findById(id, function (err, doc) {
* if (err) ..
* doc.name = 'jason borne';
* doc.save(callback);
* })
*
* @param {ObjectId|HexId} id an ObjectId or string that can be cast to one.
* @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
*/
Model.findByIdAndUpdate = function (id, update, options, callback) {
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';
throw new TypeError(msg)
}
return this.findOneAndUpdate({_id: id }, undefined);
}
args = utils.args(arguments, 1);
args.unshift({ _id: id });
return this.findOneAndUpdate.apply(this, args);
}
/**
* 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
*
* @param {Object} conditions
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
* @api public
*/
Model.findOneAndRemove = function (conditions, options, callback) {
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';
throw new TypeError(msg)
}
if ('function' == typeof options) {
callback = options;
options = undefined;
}
var fields;
if (options) {
fields = options.select;
options.select = undefined;
}
var query = new Query(conditions);
query.setOptions(options);
query.select(fields);
query.bind(this, 'findOneAndRemove');
if ('undefined' == typeof callback)
return query;
this._applyNamedScope(query);
return query.findOneAndRemove(callback);
}
/**
* Issue a mongodb findAndModify remove command by a documents id.
*
* 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
*
* @param {ObjectId|HexString} id ObjectId or string that can be cast to one
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @see Model.findOneAndRemove #model_Model-findOneAndRemove
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
*/
Model.findByIdAndRemove = function (id, options, callback) {
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';
throw new TypeError(msg)
}
return this.findOneAndRemove({ _id: id }, options, callback);
}
/**
* Shortcut for creating a new Document that is automatically saved to the db if valid.
*
* ####Example:
*
* Candy.create({ type: 'jelly bean' }, { type: 'snickers' }, function (err, jellybean, snickers) {
* if (err) // ...
* });
*
* var array = [{ type: 'jelly bean' }, { type: 'snickers' }];
* Candy.create(array, function (err, jellybean, snickers) {
* if (err) // ...
* });
*
* @param {Array|Object...} doc
* @param {Function} fn callback
* @api public
*/
Model.create = function create (doc, fn) {
if (1 === arguments.length) {
return 'function' === typeof doc && doc(null);
}
var self = this
, docs = [null]
, promise
, count
, args
if (Array.isArray(doc)) {
args = doc;
} else {
args = utils.args(arguments, 0, arguments.length - 1);
fn = arguments[arguments.length - 1];
}
if (0 === args.length) return fn(null);
promise = new Promise(fn);
count = args.length;
args.forEach(function (arg, i) {
var doc = new self(arg);
docs[i+1] = doc;
doc.save(function (err) {
if (err) return promise.error(err);
--count || fn.apply(null, docs);
});
});
// TODO
// utilize collection.insertAll for batch processing?
};
/**
* Updates documents in the database without returning them.
*
* ####Examples:
*
* MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);
* MyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, function (err, numberAffected, raw) {
* if (err) return handleError(err);
* console.log('The number of updated documents was %d', numberAffected);
* 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)
*
* All `update` values are cast to their appropriate SchemaTypes before being sent.
*
* The `callback` function receives `(err, numberAffected, rawResponse)`.
*
* - `err` is the error if any occurred
* - `numberAffected` is the count of updated documents Mongo reported
* - `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)
*
* This helps prevent accidentally overwriting all documents in your collection with `{ name: 'jason borne' }`.
*
* ####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);
* })
*
* @param {Object} conditions
* @param {Object} update
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @api public
*/
Model.update = function update (conditions, doc, options, callback) {
if (arguments.length < 4) {
if ('function' === typeof options) {
// Scenario: update(conditions, doc, callback)
callback = options;
options = null;
} else if ('function' === typeof doc) {
// Scenario: update(doc, callback);
callback = doc;
doc = conditions;
conditions = {};
options = null;
}
}
var query = new Query(conditions, options).bind(this, 'update', doc);
if ('undefined' == typeof callback)
return query;
this._applyNamedScope(query);
return query.update(doc, callback);
};
/**
* Executes a mapReduce command.
*
* `o` is an object specifying all mapReduce options as well as the map and reduce functions. All options are delegated to the driver implementation.
*
* ####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.
* - `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.
* - `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;
* 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);
* });
* })
*
* @param {Object} o an object specifying map-reduce options
* @param {Function} callback
* @see http://www.mongodb.org/display/DOCS/MapReduce
* @api public
*/
Model.mapReduce = function mapReduce (o, callback) {
if ('function' != typeof callback) throw new Error('missing callback');
var self = this;
if (!Model.mapReduce.schema) {
var opts = { noId: true, noVirtualId: true, strict: false }
Model.mapReduce.schema = new Schema({}, opts);
}
if (!o.out) o.out = { inline: 1 };
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;
}
this.collection.mapReduce(null, null, o, function (err, ret, stats) {
if (err) return callback(err);
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;
return callback(err, model, stats);
}
callback(err, ret, stats);
});
}
/**
* Executes an aggregate command on this models collection.
*
* ####Example:
*
* // find the max age of all users
* Users.aggregate(
* { $group: { _id: null, maxAge: { $max: '$age' }}}
* , { $project: { _id: 0, maxAge: 1 }}
* , function (err, res) {
* if (err) return handleError(err);
* console.log(res); // [ { maxAge: 98 } ]
* });
*
* _NOTE: the documents returned are plain javascript objects, not mongoose documents cast to this models schema definition (since any shape of document can be returned)._
*
* _NOTE: this requires running MongoDB >= 2.1_
*
* @param {Array} array an array of pipeline commands
* @param {Object} [options]
* @param {Function} callback
* @see aggregation http://docs.mongodb.org/manual/applications/aggregation/
* @see driver http://mongodb.github.com/node-mongodb-native/api-generated/collection.html#aggregate
* @api public
*/
Model.aggregate = function aggregate () {
return this.collection.aggregate.apply(this.collection, arguments);
}
/*!
* Compiler utility.
*
* @param {String} name model name
* @param {Schema} schema
* @param {String} collectionName
* @param {Connection} connection
* @param {Mongoose} base mongoose instance
*/
Model.compile = function compile (name, schema, collectionName, connection, base) {
// generate new class
function model (doc, fields, skipId) {
if (!(this instanceof model))
return new model(doc, fields, skipId);
Model.call(this, doc, fields, skipId);
};
model.modelName = name;
model.__proto__ = Model;
model.prototype.__proto__ = Model.prototype;
model.prototype.db = connection;
model.prototype._setSchema(schema);
model.prototype.collection = connection.collection(
collectionName
, schema.options.capped
);
// apply methods
for (var i in schema.methods)
model.prototype[i] = schema.methods[i];
// apply statics
for (var i in schema.statics)
model[i] = schema.statics[i];
// apply named scopes
if (schema.namedScopes) schema.namedScopes.compile(model);
model.model = model.prototype.model;
model.options = model.prototype.options;
model.db = model.prototype.db;
model.schema = model.prototype.schema;
model.collection = model.prototype.collection;
model.base = base;
return model;
};
/*!
* Subclass this model with `conn`, `schema`, and `collection` settings.
*
* @param {Connection} conn
* @param {Schema} [schema]
* @param {String} [collection]
* @return {Model}
*/
Model.__subclass = function subclass (conn, schema, collection) {
// subclass model using this connection and collection name
var model = this;
var Model = function Model (doc, fields, skipId) {
if (!(this instanceof Model)) {
return new Model(doc, fields, skipId);
}
model.call(this, doc, fields, skipId);
}
Model.__proto__ = model;
Model.prototype.__proto__ = model.prototype;
Model.db = Model.prototype.db = conn;
var s = 'string' != typeof schema
? schema
: model.prototype.schema;
if (!collection) {
collection = model.prototype.schema.get('collection')
|| utils.toCollectionName(model.modelName);
}
Model.prototype.collection = conn.collection(collection, s && s.options.capped);
Model.collection = Model.prototype.collection;
Model.init();
return Model;
}
/*!
* Module exports.
*/
module.exports = exports = Model;