mongoose
Version:
Mongoose MongoDB ORM
1,059 lines (902 loc) • 25.1 kB
JavaScript
/**
* Module dependencies.
*/
var utils = require('./utils')
, merge = utils.merge
, Promise = require('./promise')
, Document = require('./document')
, inGroupsOf = utils.inGroupsOf;
/**
* Query constructor
*
* @api private
*/
function Query (criteria, options) {
options = this.options = options || {};
this.safe = options.safe
// normalize population options
var pop = this.options.populate;
this.options.populate = {};
if (pop && Array.isArray(pop)) {
for (var i = 0, l = pop.length; i < l; i++) {
this.options.populate[pop[i]] = [];
}
}
this._conditions = {};
if (criteria) this.find(criteria);
}
/**
* Binds this query to a model.
* @param {Function} param
* @return {Query}
* @api public
*/
Query.prototype.bind = function bind (model, op, updateArg) {
this.model = model;
this.op = op;
if (op === 'update') this._updateArg = updateArg;
return this;
};
/**
* Executes the query returning a promise.
*
* Examples:
* query.run();
* query.run(callback);
* query.run('update');
* query.run('find', callback);
*
* @param {String|Function} op (optional)
* @param {Function} callback (optional)
* @return {Promise}
* @api public
*/
Query.prototype.run =
Query.prototype.exec = function (op, callback) {
var promise = new Promise();
switch (typeof op) {
case 'function':
callback = op;
op = null;
break;
case 'string':
this.op = op;
break;
}
if (callback) promise.addBack(callback);
if (!this.op) {
promise.complete();
return promise;
}
if ('update' == this.op) {
this.update(this._updateArg, promise.resolve.bind(promise));
return promise;
}
if ('distinct' == this.op) {
this.distinct(this._distinctArg, promise.resolve.bind(promise));
return promise;
}
this[this.op](promise.resolve.bind(promise));
return promise;
};
/**
* Finds documents.
*
* @param {Object} criteria
* @param {Function} callback
* @api public
*/
Query.prototype.find = function (criteria, callback) {
this.op = 'find';
if ('function' === typeof criteria) {
callback = criteria;
criteria = {};
} else if (criteria instanceof Query) {
// TODO Merge options, too
merge(this._conditions, criteria._conditions);
} else if (criteria instanceof Document) {
merge(this._conditions, criteria.toObject());
} else {
merge(this._conditions, criteria);
}
if (!callback) return this;
return this.execFind(callback);
};
/**
* Casts obj, or if obj is not present, then this._conditions,
* based on the model's schema.
*
* @param {Function} model
* @param {Object} obj (optional)
* @api public
*/
Query.prototype.cast = function (model, obj) {
obj || (obj= this._conditions);
var schema = model.schema
, paths = Object.keys(obj)
, i = paths.length
, any$conditionals
, schematype
, nested
, path
, type
, val;
while (i--) {
path = paths[i];
val = obj[path];
if (path === '$or') {
var k = val.length
, orComponentQuery;
while (k--) {
orComponentQuery = new Query(val[k]);
orComponentQuery.cast(model);
val[k] = orComponentQuery._conditions;
}
} else if (path === '$where') {
type = typeof val;
if ('string' !== type && 'function' !== type) {
throw new Error("Must have a string or function for $where");
}
if ('function' === type) {
obj[path] = val.toString();
}
continue;
} else {
schematype = schema.path(path);
if (!schematype) {
// Handle potential embedded array queries
var split = path.split('.')
, j = split.length
, pathFirstHalf
, pathLastHalf
, remainingConds
, castingQuery;
// Find the part of the var path that is a path of the Schema
while (j--) {
pathFirstHalf = split.slice(0, j).join('.');
schematype = schema.path(pathFirstHalf);
if (schematype) break;
}
// If a substring of the input path resolves to an actual real path...
if (schematype) {
// Apply the casting; similar code for $elemMatch in schema/array.js
if (schematype.caster && schematype.caster.schema) {
remainingConds = {};
pathLastHalf = split.slice(j).join('.');
remainingConds[pathLastHalf] = val;
castingQuery = new Query(remainingConds);
castingQuery.cast(schematype.caster);
obj[path] = castingQuery._conditions[pathLastHalf];
} else {
obj[path] = val;
}
}
} else if (val === null || val === undefined) {
continue;
} else if (val.constructor == Object) {
any$conditionals = Object.keys(val).some(function (k) {
return k.charAt(0) === '$';
});
if (!any$conditionals) {
obj[path] = schematype.castForQuery(val);
} else {
var ks = Object.keys(val)
, k = ks.length
, $cond;
while (k--) {
$cond = ks[k];
nested = val[$cond];
if ('$exists' === $cond) {
if ('boolean' !== typeof nested) {
throw new Error("$exists parameter must be Boolean");
}
continue;
}
if ('$not' === $cond) {
this.cast(model, val[$cond]);
} else {
val[$cond] = schematype.castForQuery($cond, nested);
}
}
}
} else {
obj[path] = schematype.castForQuery(val);
}
}
}
};
/**
* Returns default options.
* @api private
*/
Query.prototype._optionsForExec = function (model) {
var options = utils.clone(this.options);
if (! ('safe' in options)) options.safe = model.options.safe;
return options;
};
/**
* 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.
*
* @param {String|Function} js is a javascript string or anonymous function
* @return {Query}
* @api public
*/
Query.prototype.$where = function (js) {
this._conditions['$where'] = js;
return this;
};
/**
* `where` enables a very nice sugary api for doing your queries.
* For example, instead of writing:
* User.find({age: {$gte: 21, $lte: 65}}, callback);
* we can instead write more readably:
* User.where('age').gte(21).lte(65);
* Moreover, you can also chain a bunch of these together like:
* User
* .where('age').gte(21).lte(65)
* .where('name', /^b/i) // All names that begin where b or B
* .where('friends').slice(10);
* @param {String} path
* @param {Object} val (optional)
* @return {Query}
* @api public
*/
Query.prototype.where = function (path, val) {
var conds;
if (arguments.length === 2) {
this._conditions[path] = val;
}
this._currPath = path;
return this;
};
// $lt, $lte, $gt, $gte can be used on Numbers or Dates
'gt gte lt lte ne in nin all size maxDistance'.split(' ').forEach( function ($conditional) {
Query.prototype['$' + $conditional] =
Query.prototype[$conditional] = function (path, val) {
if (arguments.length === 1) {
val = path;
path = this._currPath
}
var conds = this._conditions[path] || (this._conditions[path] = {});
conds['$' + $conditional] = val;
return this;
};
});
Query.prototype.notEqualTo = Query.prototype.ne;
;['mod', 'near'].forEach( function ($conditional) {
Query.prototype['$' + $conditional] =
Query.prototype[$conditional] = function (path, val) {
if (arguments.length === 1) {
val = path;
path = this._currPath
} else if (arguments.length === 2 && !Array.isArray(val)) {
val = utils.args(arguments);
path = this._currPath;
} else if (arguments.length === 3) {
val = utils.args(arguments, 1);
}
var conds = this._conditions[path] || (this._conditions[path] = {});
conds['$' + $conditional] = val;
return this;
};
});
Query.prototype['$exists'] =
Query.prototype.exists = function (path, val) {
if (arguments.length === 0) {
path = this._currPath
val = true;
} else if (arguments.length === 1) {
if ('boolean' === typeof path) {
val = path;
path = this._currPath;
} else {
val = true;
}
}
var conds = this._conditions[path] || (this._conditions[path] = {});
conds['$exists'] = val;
return this;
};
Query.prototype['$elemMatch'] =
Query.prototype.elemMatch = function (path, criteria) {
var block;
if (path.constructor === Object) {
criteria = path;
path = this._currPath;
} else if ('function' === typeof path) {
block = path;
path = this._currPath;
} else if (criteria.constructor === Object) {
} else if ('function' === typeof criteria) {
block = criteria;
} else {
throw new Error("Argument error");
}
var conds = this._conditions[path] || (this._conditions[path] = {});
if (block) {
criteria = new Query();
block(criteria);
conds['$elemMatch'] = criteria._conditions;
} else {
conds['$elemMatch'] = criteria;
}
return this;
};
;['maxscan'].forEach( function (method) {
Query.prototype[method] = function (v) {
this.options[method] = v;
return this;
};
});
// TODO
Query.prototype.explain = function () {
throw new Error("Unimplemented");
};
// TODO Add being able to skip casting -- e.g., this would be nice for scenarios like
// if you're migrating to usernames from user id numbers:
// query.where('user_id').in([4444, 'brian']);
// TODO "immortal" cursors - (only work on capped collections)
// TODO geoNear command
// To be used idiomatically where Query#box and Query#center
;['wherein', '$wherein'].forEach(function (getter) {
Object.defineProperty(Query.prototype, getter, {
get: function () {
return this;
}
});
});
Query.prototype['$box'] =
Query.prototype.box = function (path, val) {
if (arguments.length === 1) {
val = path;
path = this._currPath;
}
var conds = this._conditions[path] || (this._conditions[path] = {});
conds['$wherein'] = { '$box': [val.ll, val.ur] };
return this;
};
Query.prototype['$center'] =
Query.prototype.center = function (path, val) {
if (arguments.length === 1) {
val = path;
path = this._currPath;
}
var conds = this._conditions[path] || (this._conditions[path] = {});
conds['$wherein'] = { '$center': [val.center, val.radius] };
return this;
};
/**
* Chainable method for specifying which fields
* to include or exclude from the document that is
* returned from MongoDB.
*
* Examples:
* query.fields({a: 1, b: 1, c: 1, _id: 0});
* query.fields('a b c');
*
* @param {Object}
*/
Query.prototype.select =
Query.prototype.fields = function () {
var arg0 = arguments[0];
if (!arg0) return this;
if (arg0.constructor === Object || Array.isArray(arg0)) {
this._applyFields(arg0);
} else if (arguments.length === 1 && typeof arg0 === 'string') {
this._applyFields({only: arg0});
} else {
this._applyFields({only: this._parseOnlyExcludeFields.apply(this, arguments)});
}
return this;
};
/**
* Chainable method for adding the specified fields to the
* object of fields to only include.
*
* Examples:
* query.only('a b c');
* query.only('a', 'b', 'c');
* query.only(['a', 'b', 'c']);
* @param {String|Array} space separated list of fields OR
* an array of field names
* We can also take arguments as the "array" of field names
* @api public
*/
Query.prototype.only = function (fields) {
fields = this._parseOnlyExcludeFields.apply(this, arguments);
this._applyFields({ only: fields });
return this;
};
/**
* Chainable method for adding the specified fields to the
* object of fields to exclude.
*
* Examples:
* query.exclude('a b c');
* query.exclude('a', 'b', 'c');
* query.exclude(['a', 'b', 'c']);
* @param {String|Array} space separated list of fields OR
* an array of field names
* We can also take arguments as the "array" of field names
* @api public
*/
Query.prototype.exclude = function (fields) {
fields = this._parseOnlyExcludeFields.apply(this, arguments);
this._applyFields({ exclude: fields });
return this;
};
Query.prototype['$slice'] =
Query.prototype.slice = function (path, val) {
if (arguments.length === 1) {
val = path;
path = this._currPath
} else if (arguments.length === 2) {
if ('number' === typeof path) {
val = [path, val];
path = this._currPath;
}
} else if (arguments.length === 3) {
val = utils.args(arguments, 1);
}
var myFields = this._fields || (this._fields = {});
myFields[path] = { '$slice': val };
return this;
};
/**
* Private method for interpreting the different ways
* you can pass in fields to both Query.prototype.only
* and Query.prototype.exclude.
*
* @param {String|Array|Object} fields
* @api private
*/
Query.prototype._parseOnlyExcludeFields = function (fields) {
if (1 === arguments.length && 'string' === typeof fields) {
fields = fields.split(' ');
} else if (Array.isArray(fields)) {
// do nothing
} else {
fields = utils.args(arguments);
}
return fields;
};
/**
* Private method for interpreting and applying the different
* ways you can specify which fields you want to include
* or exclude.
*
* Example 1: Include fields 'a', 'b', and 'c' via an Array
* query.fields('a', 'b', 'c');
* query.fields(['a', 'b', 'c']);
*
* Example 2: Include fields via 'only' shortcut
* query.only('a b c');
*
* Example 3: Exclude fields via 'exclude' shortcut
* query.exclude('a b c');
*
* Example 4: Include fields via MongoDB's native format
* query.fields({a: 1, b: 1, c: 1})
*
* Example 5: Exclude fields via MongoDB's native format
* query.fields({a: 0, b: 0, c: 0});
*
* @param {Object|Array} the formatted collection of fields to
* include and/or exclude
* @api private
*/
Query.prototype._applyFields = function (fields) {
var $fields
, pathList;
if (Array.isArray(fields)) {
$fields = fields.reduce(function ($fields, field) {
$fields[field] = 1;
return $fields;
}, {});
} else if (pathList = fields.only || fields.exclude) {
$fields =
this._parseOnlyExcludeFields(pathList)
.reduce(function ($fields, field) {
$fields[field] = fields.only ? 1: 0;
return $fields;
}, {});
} else if (fields.constructor === Object) {
$fields = fields;
} else {
throw new Error("fields is invalid");
}
var myFields = this._fields || (this._fields = {});
for (var k in $fields) myFields[k] = $fields[k];
};
/**
* Sets the sort
*
* Examples:
* query.sort('test', 1)
* query.sort('field', -1)
* query.sort('field', -1, 'test', 1)
*
* @api public
*/
Query.prototype.sort = function () {
var sort = this.options.sort || (this.options.sort = []);
inGroupsOf(2, arguments, function (field, value) {
sort.push([field, value]);
});
return this;
};
Query.prototype.asc = function () {
var sort = this.options.sort || (this.options.sort = []);
for (var i = 0, l = arguments.length; i < l; i++) {
sort.push([arguments[i], 1]);
}
return this;
};
Query.prototype.desc = function () {
var sort = this.options.sort || (this.options.sort = []);
for (var i = 0, l = arguments.length; i < l; i++) {
sort.push([arguments[i], -1]);
}
return this;
};
;['limit', 'skip', 'maxscan', 'snapshot'].forEach( function (method) {
Query.prototype[method] = function (v) {
this.options[method] = v;
return this;
};
});
/**
* Query hints.
*
* Examples:
* new Query().hint({ indexA: 1, indexB: -1})
* new Query().hint("indexA", 1, "indexB", -1)
*
* @param {Object|String} v
* @param {Int} [multi]
* @return {Query}
* @api public
*/
Query.prototype.hint = function (v, multi) {
var hint = this.options.hint || (this.options.hint = {})
, k
if (multi) {
inGroupsOf(2, arguments, function (field, val) {
hint[field] = val;
});
} else if (v.constructor === Object) {
// must keep object keys in order so don't use Object.keys()
for (k in v) {
hint[k] = v[k];
}
}
return this;
};
/**
* Sets slaveOk option
*
* new Query().slaveOk() <== true
* new Query().slaveOk(true)
* new Query().slaveOk(false)
*
* @param {Boolean} v (defaults to true)
*/
Query.prototype.slaveOk = function (v) {
this.options.slaveOk = arguments.length ? !!v : true;
return this;
};
Query.prototype.execFind = function (callback) {
var model = this.model
, options = this._optionsForExec(model)
, self = this
options.fields = this._fields;
var promise = new Promise(callback);
try {
this.cast(model);
} catch (err) {
return promise.error(err);
}
var castQuery = this._conditions;
model.collection.find(castQuery, options, function (err, cursor) {
if (err) return promise.error(err);
cursor.toArray(function (err, docs) {
if (err) return promise.error(err);
var arr = []
, count = docs.length;
if (!count) return promise.complete([]);
for (var i = 0, l = docs.length; i < l; i++) {
arr[i] = new model();
delete arr[i]._doc._id; // Remove the _id, so that pre inits do not
// have an _id
arr[i].init(docs[i], self, function (err) {
if (err) return promise.error(err);
--count || promise.complete(arr);
});
}
});
});
return this;
};
/**
* Steaming cursors.
*
* The `callback` is called repeatedly for each document
* found in the collection as it's streamed. If an error
* occurs streaming stops.
*
* Example:
* query.each(function (err, user) {
* if (err) return res.end("aww, received an error. all done.");
* if (user) {
* res.write(user.name + '\n')
* } else {
* res.end("reached end of cursor. all done.");
* }
* });
*
* A third parameter may also be used in the callback which
* allows you to iterate the cursor manually.
*
* Example:
* query.each(function (err, user, next) {
* if (err) return res.end("aww, received an error. all done.");
* if (user) {
* res.write(user.name + '\n')
* doSomethingAsync(next);
* } else {
* res.end("reached end of cursor. all done.");
* }
* });
*
* @param {Function} callback
* @return {Query}
* @api public
*/
Query.prototype.each = function (callback) {
var model = this.model
, options = this._optionsForExec(model)
, manual = 3 == callback.length
options.fields = this._fields;
try {
this.cast(model);
} catch (err) {
return callback(err);
}
function complete (err, val) {
if (complete.ran) return;
complete.ran = true;
callback(err, val);
}
var self = this;
model.collection.find(this._conditions, options, function (err, cursor) {
if (err) return complete(err);
next();
function next () {
// nextTick is necessary to avoid stack overflows when
// dealing with large result sets.
process.nextTick(function () {
cursor.nextObject(onNextObject);
});
}
function onNextObject (err, doc) {
if (err) return complete(err);
// when doc is null we hit the end of the cursor
if (!doc) return complete(null, null);
var instance = new model;
delete instance._doc._id; // Remove the _id, so that pre inits do not
// have an _id
instance.init(doc, self, function (err) {
if (err) return complete(err);
if (manual) {
callback(null, instance, next);
} else {
callback(null, instance);
next();
}
});
}
});
return this;
}
/**
* Casts the query, sends the findOne command to mongodb.
* Upon receiving the document, we initialize a mongoose
* document based on the returned document from mongodb,
* and then we invoke a callback on our mongoose document.
*
* @param {Function} callback function (err, found)
* @api public
*/
Query.prototype.findOne = function (callback) {
this.op = 'findOne';
var model = this.model;
var options = this._optionsForExec(model);
options.fields = this._fields;
var promise = new Promise(callback);
try {
this.cast(model);
} catch (err) {
return promise.error(err);
}
var castQuery = this._conditions
, self = this
model.collection.findOne(castQuery, options, function (err, doc) {
if (err) return promise.error(err);
if (!doc) return promise.complete(null);
var casted = new model();
delete casted._doc._id; // Remove the _id, so that pre inits do not have
// an _id
casted.init(doc, self, function (err) {
if (err) return promise.error(err);
promise.complete(casted);
});
});
return this;
};
/**
* Casts this._conditions and sends a count
* command to mongodb. Invokes a callback upon
* receiving results
*
* @param {Function} callback fn(err, cardinality)
* @api public
*/
Query.prototype.count = function (callback) {
this.op = 'count';
var model = this.model;
try {
this.cast(model);
} catch (err) {
return callback(err);
}
var castQuery = this._conditions;
model.collection.count(castQuery, callback);
return this;
};
/**
* Casts this._conditions and sends a distinct
* command to mongodb. Invokes a callback upon
* receiving results
*
* @param {Function} callback fn(err, cardinality)
* @api public
*/
Query.prototype.distinct = function (field, callback) {
this.op = 'distinct';
var model = this.model;
try {
this.cast(model);
} catch (err) {
return callback(err);
}
var castQuery = this._conditions;
model.collection.distinct(field, castQuery, callback);
return this;
};
/**
* Casts the query, sends the update command to mongodb.
*
* @param {Function} callback fn(err)
* @api public
*/
Query.prototype.update = function (doc, callback) {
this.op = 'update';
this._updateArg = doc;
var model = this.model
, options = this._optionsForExec(model)
, useSet = model.options['use$SetOnSave']
, castQuery
, castDoc
try {
this.cast(model);
castQuery = this._conditions;
} catch (err) {
return callback(err);
}
try {
castDoc = this._castUpdate(doc);
} catch (err) {
return callback(err);
}
if (Object.keys(castDoc).length) {
model.collection.update(castQuery, castDoc, options, callback);
} else {
process.nextTick(function () {
callback(null);
});
}
return this;
};
/**
* Apply default model values and casting to `obj`.
*
* @param {Object} obj
* @return {Object}
* @api private
*/
Query.prototype._castUpdate = function (obj) {
var schema = this.model.schema
, doc = new this.model // applies our defaults
, paths = Object.keys(obj)
, i = paths.length
, subpaths
, subval
, method
, path
, val
, ii
while (i--) {
path = paths[i];
val = obj[path];
if ('$' !== path[0]) {
doc.set(path, val);
continue;
} else {
if (Object !== val.constructor) {
var msg = 'Invalid atomic update value for ' + path + '. '
+ 'Expected an object, received ' + typeof val;
throw new Error(msg);
}
subpaths = Object.keys(val)
ii = subpaths.length
method = path;
while (ii--) {
subpath = subpaths[ii];
subval = val[subpath];
if (schema.path(subpath)) {
if ('$set' === method) {
doc.set(subpath, subval);
continue;
}
var cur = doc.get(subpath);
if ('$inc' === method && !cur) {
doc.set(subpath, 0);
cur = doc.get(subpath);
}
// apply using model instance methods ($inc, $pull, etc)
if ('function' === typeof cur[method]) {
cur[method](subval);
}
}
}
}
}
var delta = doc._delta();
return delta;
};
/**
* Casts the query, sends the remove command to
* mongodb where the query contents, and then
* invokes a callback upon receiving the command
* result.
*
* @param {Function} callback
* @api public
*/
Query.prototype.remove = function (callback) {
this.op = 'remove';
var model = this.model
, options = this._optionsForExec(model);
try {
this.cast(model);
} catch (err) {
return callback(err);
}
var castQuery = this._conditions;
model.collection.remove(castQuery, options, callback);
return this;
};
/**
* Sets population options.
*/
Query.prototype.populate = function (path, fields) {
this.options.populate[path] = fields || [];
return this;
};
/**
* Exports.
*/
module.exports = Query;