mquery
is a fluent mongodb query builder designed to run in multiple environments. As of v0.1, mquery
runs on Node.js
only with support for the MongoDB shell and browser environments planned for upcoming releases.
##Features
- fluent query builder api
- custom base query support
- MongoDB 2.4 geoJSON support
- method + option combinations validation
- node.js driver compatibility
- environment detection
- debug support
- separated collection implementations for maximum flexibility
##Use
require('mongodb').connect(uri, function (err, db) {
if (err) return handleError(err);
// get a collection
var collection = db.collection('artists');
// pass it to the constructor
mquery(collection).find({..}, callback);
// or pass it to the collection method
mquery().find({..}).collection(collection).exec(callback)
// or better yet, create a custom query constructor that has it always set
var Artist = mquery(collection).toConstructor();
Artist().find(..).where(..).exec(callback)
})
mquery
requires a collection object to work with. In the example above we just pass the collection object created using the official MongoDB driver.
##Fluent API
###find()
Declares this query a find query. Optionally pass a match clause and / or callback. If a callback is passed the query is executed.
mquery().find()
mquery().find(match)
mquery().find(callback)
mquery().find(match, function (err, docs) {
assert(Array.isArray(docs));
})
###findOne()
Declares this query a findOne query. Optionally pass a match clause and / or callback. If a callback is passed the query is executed.
mquery().findOne()
mquery().findOne(match)
mquery().findOne(callback)
mquery().findOne(match, function (err, doc) {
if (doc) {
// the document may not be found
console.log(doc);
}
})
###count()
Declares this query a count query. Optionally pass a match clause and / or callback. If a callback is passed the query is executed.
mquery().count()
mquery().count(match)
mquery().count(callback)
mquery().count(match, function (err, number){
console.log('we found %d matching documents', number);
})
###remove()
Declares this query a remove query. Optionally pass a match clause and / or callback. If a callback is passed the query is executed.
mquery().remove()
mquery().remove(match)
mquery().remove(callback)
mquery().remove(match, function (err){})
###update()
Declares this query an update query. Optionally pass an update document, match clause, options or callback. If a callback is passed, the query is executed. To force execution without passing a callback, run update(true)
.
mquery().update()
mquery().update(match, updateDocument)
mquery().update(match, updateDocument, options)
// the following all execute the command
mquery().update(callback)
mquery().update({$set: updateDocument, callback)
mquery().update(match, updateDocument, callback)
mquery().update(match, updateDocument, options, function (err, result){})
mquery().update(true) // executes (unsafe write)
#####the update document
All paths passed that are not $atomic
operations will become $set
ops. For example:
mquery(collection).where({ _id: id }).update({ title: 'words' }, callback)
becomes
collection.update({ _id: id }, { $set: { title: 'words' }}, callback)
This behavior can be overridden using the overwrite
option (see below).
#####options
Options are passed to the setOptions()
method.
- overwrite
Passing an empty object { }
as the update document will result in a no-op unless the overwrite
option is passed. Without the overwrite
option, the update operation will be ignored and the callback executed without sending the command to MongoDB to prevent accidently overwritting documents in the collection.
var q = mquery(collection).where({ _id: id }).setOptions({ overwrite: true });
q.update({ }, callback); // overwrite with an empty doc
The overwrite
option isn't just for empty objects, it also provides a means to override the default $set
conversion and send the update document as is.
// create a base query
var base = mquery({ _id: 108 }).collection(collection).toConstructor();
base().findOne(function (err, doc) {
console.log(doc); // { _id: 108, name: 'cajon' })
base().setOptions({ overwrite: true }).update({ changed: true }, function (err) {
base.findOne(function (err, doc) {
console.log(doc); // { _id: 108, changed: true }) - the doc was overwritten
});
});
})
- multi
Updates only modify a single document by default. To update multiple documents, set the multi
option to true
.
mquery()
.collection(coll)
.update({ name: /^match/ }, { $addToSet: { arr: 4 }}, { multi: true }, callback)
// another way of doing it
mquery({ name: /^match/ })
.collection(coll)
.setOptions({ multi: true })
.update({ $addToSet: { arr: 4 }}, callback)
// update multiple documents with an empty doc
var q = mquery(collection).where({ name: /^match/ });
q.setOptions({ multi: true, overwrite: true })
q.update({ });
q.update(function (err, result) {
console.log(arguments);
});
###findOneAndUpdate()
Declares this query a findAndModify with update query. Optionally pass a match clause, update document, options, or callback. If a callback is passed, the query is executed.
When executed, the first matching document (if found) is modified according to the update document and passed back to the callback.
#####options
Options are passed to the setOptions()
method.
new
: boolean - true to return the modified document rather than the original. defaults to trueupsert
: boolean - creates the object if it doesn't exist. defaults to falsesort
: if multiple docs are found by the match condition, sets the sort order to choose which doc to update
query.findOneAndUpdate()
query.findOneAndUpdate(updateDocument)
query.findOneAndUpdate(match, updateDocument)
query.findOneAndUpdate(match, updateDocument, options)
// the following all execute the command
query.findOneAndUpdate(callback)
query.findOneAndUpdate(updateDocument, callback)
query.findOneAndUpdate(match, updateDocument, callback)
query.findOneAndUpdate(match, updateDocument, options, function (err, doc) {
if (doc) {
// the document may not be found
console.log(doc);
}
})
###findOneAndRemove()
Declares this query a findAndModify with remove query. Optionally pass a match clause, options, or callback. If a callback is passed, the query is executed.
When executed, the first matching document (if found) is modified according to the update document, removed from the collection and passed to the callback.
#####options
Options are passed to the setOptions()
method.
sort
: if multiple docs are found by the condition, sets the sort order to choose which doc to modify and remove
A.where().findOneAndRemove()
A.where().findOneAndRemove(match)
A.where().findOneAndRemove(match, options)
// the following all execute the command
A.where().findOneAndRemove(callback)
A.where().findOneAndRemove(match, callback)
A.where().findOneAndRemove(match, options, function (err, doc) {
if (doc) {
// the document may not be found
console.log(doc);
}
})
###distinct()
Declares this query a distinct query. Optionally pass the distinct field, a match clause or callback. If a callback is passed the query is executed.
mquery().distinct()
mquery().distinct(match)
mquery().distinct(match, field)
mquery().distinct(field)
// the following all execute the command
mquery().distinct(callback)
mquery().distinct(field, callback)
mquery().distinct(match, callback)
mquery().distinct(match, field, function (err, result) {
console.log(result);
})
###exec()
Executes the query.
mquery().findOne().where('route').intersects(polygon).exec(function (err, docs){})
###all()
Specifies an $all
query condition
mquery().where('permission').all(['read', 'write'])
###and()
Specifies arguments for an $and
condition
mquery().and([{ color: 'green' }, { status: 'ok' }])
###box()
Specifies a $box
condition
var lowerLeft = [40.73083, -73.99756]
var upperRight= [40.741404, -73.988135]
mquery().where('location').within().box(lowerLeft, upperRight)
###circle()
Specifies a $center
or $centerSphere
condition.
var area = { center: [50, 50], radius: 10, unique: true }
query.where('loc').within().circle(area)
query.circle('loc', area);
// for spherical calculations
var area = { center: [50, 50], radius: 10, unique: true, spherical: true }
query.where('loc').within().circle(area)
query.circle('loc', area);
###elemMatch()
Specifies an $elemMatch
condition
query.where('comment').elemMatch({ author: 'autobot', votes: {$gte: 5}})
query.elemMatch('comment', function (elem) {
elem.where('author').equals('autobot');
elem.where('votes').gte(5);
})
###equals()
Specifies the complementary comparison value for the path specified with where()
.
mquery().where('age').equals(49);
// is the same as
mquery().where({ 'age': 49 });
###exists()
Specifies an $exists
condition
// { name: { $exists: true }}
mquery().where('name').exists()
mquery().where('name').exists(true)
mquery().exists('name')
// { name: { $exists: false }}
mquery().where('name').exists(false);
mquery().exists('name', false);
###geometry()
Specifies a $geometry
condition
var polyA = [[[ 10, 20 ], [ 10, 40 ], [ 30, 40 ], [ 30, 20 ]]]
query.where('loc').within().geometry({ type: 'Polygon', coordinates: polyA })
// or
var polyB = [[ 0, 0 ], [ 1, 1 ]]
query.where('loc').within().geometry({ type: 'LineString', coordinates: polyB })
// or
var polyC = [ 0, 0 ]
query.where('loc').within().geometry({ type: 'Point', coordinates: polyC })
// or
query.where('loc').intersects().geometry({ type: 'Point', coordinates: polyC })
geometry()
must come after either intersects()
or within()
.
The object
argument must contain type
and coordinates
properties.
- type
String
- coordinates
Array
###gt()
Specifies a $gt
query condition.
mquery().where('clicks').gt(999)
###gte()
Specifies a $gte
query condition.
mquery().where('clicks').gte(1000)
###in()
Specifies an $in
query condition.
mquery().where('author_id').in([3, 48901, 761])
###intersects()
Declares an $geoIntersects
query for geometry()
.
query.where('path').intersects().geometry({
type: 'LineString'
, coordinates: [[180.0, 11.0], [180, 9.0]]
})
// geometry arguments are supported
query.where('path').intersects({
type: 'LineString'
, coordinates: [[180.0, 11.0], [180, 9.0]]
})
Must be used after where()
.
###lt()
Specifies a $lt
query condition.
mquery().where('clicks').lt(50)
###lte()
Specifies a $lte
query condition.
mquery().where('clicks').lte(49)
###maxDistance()
Specifies a $maxDistance
query condition.
mquery().where('location').near({ center: [139, 74.3] }).maxDistance(5)
###mod()
Specifies a $mod
condition
mquery().where('count').mod(2, 0)
###ne()
Specifies a $ne
query condition.
mquery().where('status').ne('ok')
###nin()
Specifies an $nin
query condition.
mquery().where('author_id').nin([3, 48901, 761])
###nor()
Specifies arguments for an $nor
condition.
mquery().nor([{ color: 'green' }, { status: 'ok' }])
###near()
Specifies arguments for a $near
or $nearSphere
condition.
These operators return documents sorted by distance.
####Example
query.where('loc').near({ center: [10, 10] });
query.where('loc').near({ center: [10, 10], maxDistance: 5 });
query.near('loc', { center: [10, 10], maxDistance: 5 });
// For a $nearSphere condition, pass the `spherical` option.
query.near({ center: [10, 10], maxDistance: 5, spherical: true });
###or()
Specifies arguments for an $or
condition.
mquery().or([{ color: 'red' }, { status: 'emergency' }])
###polygon()
Specifies a $polygon
condition
mquery().where('loc').within().polygon([10,20], [13, 25], [7,15])
mquery().polygon('loc', [10,20], [13, 25], [7,15])
###regex()
Specifies a $regex
query condition.
mquery().where('name').regex(/^sixstepsrecords/)
###select()
Specifies which document fields to include or exclude
// 1 means include, 0 means exclude
mquery().select({ name: 1, address: 1, _id: 0 })
// or
mquery().select('name address -_id')
#####String syntax
When passing a string, prefixing a path with -
will flag that path as excluded. When a path does not have the -
prefix, it is included.
// include a and b, exclude c
query.select('a b -c');
// or you may use object notation, useful when
// you have keys already prefixed with a "-"
query.select({a: 1, b: 1, c: 0});
Cannot be used with distinct()
.
###size()
Specifies a $size
query condition.
mquery().where('someArray').size(6)
###slice()
Specifies a $slice
projection for a path
mquery().where('comments').slice(5)
mquery().where('comments').slice(-5)
mquery().where('comments').slice([-10, 5])
###within()
Sets a $geoWithin
or $within
argument for geo-spatial queries.
mquery().within().box()
mquery().within().circle()
mquery().within().geometry()
mquery().where('loc').within({ center: [50,50], radius: 10, unique: true, spherical: true });
mquery().where('loc').within({ box: [[40.73, -73.9], [40.7, -73.988]] });
mquery().where('loc').within({ polygon: [[],[],[],[]] });
mquery().where('loc').within([], [], []) // polygon
mquery().where('loc').within([], []) // box
mquery().where('loc').within({ type: 'LineString', coordinates: [...] }); // geometry
As of mquery 2.0, $geoWithin
is used by default. This impacts you if running MongoDB < 2.4. To alter this behavior, see mquery.use$geoWithin.
Must be used after where()
.
###where()
Specifies a path
for use with chaining
// instead of writing:
mquery().find({age: {$gte: 21, $lte: 65}});
// we can instead write:
mquery().where('age').gte(21).lte(65);
// passing query conditions is permitted too
mquery().find().where({ name: 'vonderful' })
// chaining
mquery()
.where('age').gte(21).lte(65)
.where({ 'name': /^vonderful/i })
.where('friends').slice(10)
.exec(callback)
###$where()
Specifies a $where
condition.
Use $where
when you need to select documents using a JavaScript expression.
query.$where('this.comments.length > 10 || this.name.length > 5').exec(callback)
query.$where(function () {
return this.comments.length > 10 || this.name.length > 5;
})
Only use $where
when you have a condition that cannot be met using other MongoDB operators like $lt
. Be sure to read about all of its caveats before using.
###batchSize()
Specifies the batchSize option.
query.batchSize(100)
Cannot be used with distinct()
.
###comment()
Specifies the comment option.
query.comment('login query');
Cannot be used with distinct()
.
###hint()
Sets query hints.
mquery().hint({ indexA: 1, indexB: -1 })
Cannot be used with distinct()
.
###limit()
Specifies the limit option.
query.limit(20)
Cannot be used with distinct()
.
###maxScan()
Specifies the maxScan option.
query.maxScan(100)
Cannot be used with distinct()
.
###skip()
Specifies the skip option.
query.skip(100).limit(20)
Cannot be used with distinct()
.
###sort()
Sets the query sort order.
If an object is passed, key values allowed are asc
, desc
, ascending
, descending
, 1
, and -1
.
If a string is passed, it must be a space delimited list of path names. The sort order of each path is ascending unless the path name is prefixed with -
which will be treated as descending.
// these are equivalent
query.sort({ field: 'asc', test: -1 });
query.sort('field -test');
Cannot be used with distinct()
.
###read()
Sets the readPreference option for the query.
mquery().read('primary')
mquery().read('p') // same as primary
mquery().read('primaryPreferred')
mquery().read('pp') // same as primaryPreferred
mquery().read('secondary')
mquery().read('s') // same as secondary
mquery().read('secondaryPreferred')
mquery().read('sp') // same as secondaryPreferred
mquery().read('nearest')
mquery().read('n') // same as nearest
// specifying tags
mquery().read('s', [{ dc:'sf', s: 1 },{ dc:'ma', s: 2 }])
#####Preferences:
primary
- (default) Read from primary only. Operations will produce an error if primary is unavailable. Cannot be combined with tags.secondary
- Read from secondary if available, otherwise error.primaryPreferred
- Read from primary if available, otherwise a secondary.secondaryPreferred
- Read from a secondary if available, otherwise read from the primary.nearest
- All operations read from among the nearest candidates, but unlike other modes, this option will include both the primary and all secondaries in the random selection.
Aliases
p
primarypp
primaryPreferreds
secondarysp
secondaryPreferredn
nearest
Read more about how to use read preferrences here and here.
###slaveOk()
Sets the slaveOk option. true
allows reading from secondaries.
deprecated use read() preferences instead if on mongodb >= 2.2
query.slaveOk() // true
query.slaveOk(true)
query.slaveOk(false)
###snapshot()
Specifies this query as a snapshot query.
mquery().snapshot() // true
mquery().snapshot(true)
mquery().snapshot(false)
Cannot be used with distinct()
.
###tailable()
Sets tailable option.
mquery().tailable() <== true
mquery().tailable(true)
mquery().tailable(false)
Cannot be used with distinct()
.
##Helpers
###collection()
Sets the querys collection.
mquery().collection(aCollection)
###merge(object)
Merges other mquery or match condition objects into this one. When an muery instance is passed, its match conditions, field selection and options are merged.
var drum = mquery({ type: 'drum' }).collection(instruments);
var redDrum = mqery({ color: 'red' }).merge(drum);
redDrum.count(function (err, n) {
console.log('there are %d red drums', n);
})
Internally uses mquery.canMerge
to determine validity.
###setOptions(options)
Sets query options.
mquery().setOptions({ collection: coll, limit: 20 })
#####options
- tailable *
- sort *
- limit *
- skip *
- maxScan *
- batchSize *
- comment *
- snapshot *
- hint *
- slaveOk *
- safe: Boolean - passed through to the collection. Setting to
true
is equivalent to{ w: 1 }
- collection: the collection to query against
* denotes a query helper method is also available
###mquery.canMerge(conditions)
Determines if conditions
can be merged using mquery().merge()
.
var query = mquery({ type: 'drum' });
var okToMerge = mquery.canMerge(anObject)
if (okToMerge) {
query.merge(anObject);
}
##mquery.use$geoWithin
MongoDB 2.4 introduced the $geoWithin
operator which replaces and is 100% backward compatible with $within
. As of mquery 0.2, we default to using $geoWithin
for all within()
calls.
If you are running MongoDB < 2.4 this will be problematic. To force mquery
to be backward compatible and always use $within
, set the mquery.use$geoWithin
flag to false
.
mquery.use$geoWithin = false;
##Custom Base Queries
Often times we want custom base queries that encapsulate predefined criteria. With mquery
this is easy. First create the query you want to reuse and call its toConstructor()
method which returns a new subclass of mquery
that retains all options and criteria of the original.
var greatMovies = mquery(movieCollection).where('rating').gte(4.5).toConstructor();
// use it!
greatMovies().count(function (err, n) {
console.log('There are %d great movies', n);
});
greatMovies().where({ name: /^Life/ }).select('name').find(function (err, docs) {
console.log(docs);
});
##Validation
Method and options combinations are checked for validity at runtime to prevent creation of invalid query constructs. For example, a distinct
query does not support specifying options like hint
or field selection. In this case an error will be thrown so you can catch these mistakes in development.
##Debug support
Debug mode is provided through the use of the debug module. To enable:
DEBUG=mquery node yourprogram.js
Read the debug module documentation for more details.
##Future goals
- mongo shell compatibility
- browser compatibility
- mongoose compatibility
$ npm install mquery