mongoosejs / mquery

Expressive MongoDB query builder
MIT License
478 stars 73 forks source link
javascript mongodb nodejs query-builder

mquery

mquery is a fluent mongodb query builder designed to run in multiple environments.

Build Status NPM version

npm

Features

Use

const mongo = require('mongodb');

const client = new mongo.MongoClient(uri);
await client.connect();
// get a collection
const collection = client.collection('artists');

// pass it to the constructor
await mquery(collection).find({...});

// or pass it to the collection method
const docs = await mquery().find({...}).collection(collection);

// or better yet, create a custom query constructor that has it always set
const Artist = mquery(collection).toConstructor();
const docs = await Artist().find(...).where(...);

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

Helpers

find()

Declares this query a find query. Optionally pass a match clause.

mquery().find()
mquery().find(match)
await mquery().find()
const docs = await mquery().find(match);
assert(Array.isArray(docs));

findOne()

Declares this query a findOne query. Optionally pass a match clause.

mquery().findOne()
mquery().findOne(match)
await mquery().findOne()
const doc = await mquery().findOne(match);
if (doc) {
  // the document may not be found
  console.log(doc);
}

count()

Declares this query a count query. Optionally pass a match clause.

mquery().count()
mquery().count(match)
await mquery().count()
const number = await mquery().count(match);
console.log('we found %d matching documents', number);

findOneAndUpdate()

Declares this query a findAndModify with update query. Optionally pass a match clause, update document, options.

When executed, the first matching document (if found) is modified according to the update document and passed back.

findOneAndUpdate() options

Options are passed to the setOptions() method.

query.findOneAndUpdate()
query.findOneAndUpdate(updateDocument)
query.findOneAndUpdate(match, updateDocument)
query.findOneAndUpdate(match, updateDocument, options)

// the following all execute the command
await query.findOneAndUpdate()
await query.findOneAndUpdate(updateDocument)
await query.findOneAndUpdate(match, updateDocument)
const doc = await await query.findOneAndUpdate(match, updateDocument, options);
if (doc) {
  // the document may not be found
  console.log(doc);
}

findOneAndRemove()

Declares this query a findAndModify with remove query. Alias of findOneAndDelete. Optionally pass a match clause, options.

When executed, the first matching document (if found) is modified according to the update document, removed from the collection and passed as a result.

findOneAndRemove() options

Options are passed to the setOptions() method.

A.where().findOneAndDelete()
A.where().findOneAndRemove()
A.where().findOneAndRemove(match)
A.where().findOneAndRemove(match, options)

// the following all execute the command
await A.where().findOneAndRemove()
await A.where().findOneAndRemove(match)
const doc = await A.where().findOneAndRemove(match, options);
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.

mquery().distinct()
mquery().distinct(match)
mquery().distinct(match, field)
mquery().distinct(field)

// the following all execute the command
await mquery().distinct()
await mquery().distinct(field)
await mquery().distinct(match)
const result = await mquery().distinct(match, field);
console.log(result);

exec()

Executes the query.

const docs = await mquery().findOne().where('route').intersects(polygon).exec()

stream()

Executes the query and returns a stream.

var stream = mquery().find().stream(options);
stream.on('data', cb);
stream.on('close', fn);

Note: this only works with find() operations.

Note: returns the stream object directly from the node-mongodb-native driver. (currently streams1 type stream). Any options will be passed along to the driver method.


all()

Specifies an $all query condition

mquery().where('permission').all(['read', 'write'])

MongoDB documentation

and()

Specifies arguments for an $and condition

mquery().and([{ color: 'green' }, { status: 'ok' }])

MongoDB documentation

box()

Specifies a $box condition

var lowerLeft = [40.73083, -73.99756]
var upperRight= [40.741404,  -73.988135]

mquery().where('location').within().box(lowerLeft, upperRight)

MongoDB Documentation

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);
})

MongoDB Documentation

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);

MongoDB Documentation

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 })

// or
query.where('loc').near().geometry({ type: 'Point', coordinates: [3,5] })

geometry() must come after intersects(), within(), or near().

The object argument must contain type and coordinates properties.

MongoDB Documentation

gt()

Specifies a $gt query condition.

mquery().where('clicks').gt(999)

MongoDB Documentation

gte()

Specifies a $gte query condition.

MongoDB Documentation

mquery().where('clicks').gte(1000)

in()

Specifies an $in query condition.

mquery().where('author_id').in([3, 48901, 761])

MongoDB Documentation

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().

MongoDB Documentation

lt()

Specifies a $lt query condition.

mquery().where('clicks').lt(50)

MongoDB Documentation

lte()

Specifies a $lte query condition.

mquery().where('clicks').lte(49)

MongoDB Documentation

maxDistance()

Specifies a $maxDistance query condition.

mquery().where('location').near({ center: [139, 74.3] }).maxDistance(5)

MongoDB Documentation

mod()

Specifies a $mod condition

mquery().where('count').mod(2, 0)

MongoDB Documentation

ne()

Specifies a $ne query condition.

mquery().where('status').ne('ok')

MongoDB Documentation

nin()

Specifies an $nin query condition.

mquery().where('author_id').nin([3, 48901, 761])

MongoDB Documentation

nor()

Specifies arguments for an $nor condition.

mquery().nor([{ color: 'green' }, { status: 'ok' }])

MongoDB Documentation

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 });

// GeoJSON
query.where('loc').near({ center: { type: 'Point', coordinates: [10, 10] }});
query.where('loc').near({ center: { type: 'Point', coordinates: [10, 10] }, maxDistance: 5, spherical: true });
query.where('loc').near().geometry({ type: 'Point', coordinates: [10, 10] });

// For a $nearSphere condition, pass the `spherical` option.
query.near({ center: [10, 10], maxDistance: 5, spherical: true });

MongoDB Documentation

or()

Specifies arguments for an $or condition.

mquery().or([{ color: 'red' }, { status: 'emergency' }])

MongoDB Documentation

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])

MongoDB Documentation

regex()

Specifies a $regex query condition.

mquery().where('name').regex(/^sixstepsrecords/)

MongoDB Documentation

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().

selected()

Determines if the query has selected any fields.

var query = mquery();
query.selected() // false
query.select('-name');
query.selected() // true

selectedInclusively()

Determines if the query has selected any fields inclusively.

var query = mquery().select('name');
query.selectedInclusively() // true

var query = mquery();
query.selected() // false
query.select('-name');
query.selectedInclusively() // false
query.selectedExclusively() // true

selectedExclusively()

Determines if the query has selected any fields exclusively.

var query = mquery().select('-name');
query.selectedExclusively() // true

var query = mquery();
query.selected() // false
query.select('name');
query.selectedExclusively() // false
query.selectedInclusively() // true

size()

Specifies a $size query condition.

mquery().where('someArray').size(6)

MongoDB Documentation

slice()

Specifies a $slice projection for a path

mquery().where('comments').slice(5)
mquery().where('comments').slice(-5)
mquery().where('comments').slice([-10, 5])

MongoDB Documentation

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().

MongoDB Documentation

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
await mquery()
  .where('age').gte(21).lte(65)
  .where({ 'name': /^vonderful/i })
  .where('friends').slice(10)
  .exec()

$where()

Specifies a $where condition.

Use $where when you need to select documents using a JavaScript expression.

await query.$where('this.comments.length > 10 || this.name.length > 5').exec()

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().

MongoDB documentation

collation()

Specifies the collation option.

query.collation({ locale: "en_US", strength: 1 })

MongoDB documentation

comment()

Specifies the comment option.

query.comment('login query');

Cannot be used with distinct().

MongoDB documentation

hint()

Sets query hints.

mquery().hint({ indexA: 1, indexB: -1 })

Cannot be used with distinct().

MongoDB documentation

j()

Requests acknowledgement that this operation has been persisted to MongoDB's on-disk journal.

This option is only valid for operations that write to the database:

Defaults to the j value if it is specified in writeConcern

mquery().j(true);

limit()

Specifies the limit option.

query.limit(20)

Cannot be used with distinct().

MongoDB documentation

maxTime()

Specifies the maxTimeMS option.

query.maxTime(100)
query.maxTimeMS(100)

MongoDB documentation

skip()

Specifies the skip option.

query.skip(100).limit(20)

Cannot be used with distinct().

MongoDB documentation

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().

MongoDB documentation

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

mquery().setReadPreference('primary') // alias of .read()

Preferences:

Aliases

Preference Tags:

To keep the separation of concerns between mquery and your driver clean, mquery#read() no longer handles specifying a second tags argument as of version 0.5. If you need to specify tags, pass any non-string argument as the first argument. mquery will pass this argument untouched to your collections methods later. For example:

// example of specifying tags using the Node.js driver
var ReadPref = require('mongodb').ReadPreference;
var preference = new ReadPref('secondary', [{ dc:'sf', s: 1 },{ dc:'ma', s: 2 }]);
mquery(...).read(preference).exec();

Read more about how to use read preferences here and here.

readConcern()

Sets the readConcern option for the query.

// local
mquery().readConcern('local')
mquery().readConcern('l')
mquery().r('l')

// available
mquery().readConcern('available')
mquery().readConcern('a')
mquery().r('a')

// majority
mquery().readConcern('majority')
mquery().readConcern('m')
mquery().r('m')

// linearizable
mquery().readConcern('linearizable')
mquery().readConcern('lz')
mquery().r('lz')

// snapshot
mquery().readConcern('snapshot')
mquery().readConcern('s')
mquery().r('s')

Read Concern Level:

Aliases

Read more about how to use read concern here.

writeConcern()

Sets the writeConcern option for the query.

This option is only valid for operations that write to the database:

mquery().writeConcern(0)
mquery().writeConcern(1)
mquery().writeConcern({ w: 1, j: true, wtimeout: 2000 })
mquery().writeConcern('majority')
mquery().writeConcern('m') // same as majority
mquery().writeConcern('tagSetName') // if the tag set is 'm', use .writeConcern({ w: 'm' }) instead
mquery().w(1) // w is alias of writeConcern

Write Concern:

writeConcern({ w: <value>, j: <boolean>, wtimeout: <number> }`)

Can be break down to use the following syntax:

mquery().w(<value>).j(<boolean>).wtimeout(<number>)

Read more about how to use write concern 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)

MongoDB documentation

tailable()

Sets tailable option.

mquery().tailable() <== true
mquery().tailable(true)
mquery().tailable(false)

Cannot be used with distinct().

MongoDB Documentation

wtimeout()

Specifies a time limit, in milliseconds, for the write concern. If w > 1, it is maximum amount of time to wait for this write to propagate through the replica set before this operation fails. The default is 0, which means no timeout.

This option is only valid for operations that write to the database:

Defaults to wtimeout value if it is specified in writeConcern

mquery().wtimeout(2000)
mquery().wTimeout(2000)

Helpers

collection()

Sets the querys collection.

mquery().collection(aCollection)

then()

Executes the query and returns a promise which will be resolved with the query results or rejected if the query responds with an error.

mquery().find(..).then(success, error);

This is very useful when combined with co or koa, which automatically resolve promise-like objects for you.

co(function*(){
  var doc = yield mquery().findOne({ _id: 499 });
  console.log(doc); // { _id: 499, name: 'amazing', .. }
})();

NOTE: The returned promise is a bluebird promise but this is customizable. If you want to use your favorite promise library, simply set mquery.Promise = YourPromiseConstructor. Your Promise must be promises A+ compliant.

merge(object)

Merges other mquery or match condition objects into this one. When an mquery instance is passed, its match conditions, field selection and options are merged.

const drum = mquery({ type: 'drum' }).collection(instruments);
const redDrum = mquery({ color: 'red' }).merge(drum);
const n = await redDrum.count();
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 })

setOptions() options

* denotes a query helper method is also available

setTraceFunction(func)

Set a function to trace this query. Useful for profiling or logging.

function traceFunction (method, queryInfo, query) {
  console.log('starting ' + method + ' query');

  return function (err, result, millis) {
    console.log('finished ' + method + ' query in ' + millis + 'ms');
  };
}

mquery().setTraceFunction(traceFunction).findOne({name: 'Joe'}, cb);

The trace function is passed (method, queryInfo, query)

The trace function should return a callback function which accepts:

NOTE: stream requests are not traced.

mquery.setGlobalTraceFunction(func)

Similar to setTraceFunction() but automatically applied to all queries.

mquery.setTraceFunction(traceFunction);

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!
const n = await greatMovies().count();
console.log('There are %d great movies', n);

const docs = await greatMovies().where({ name: /^Life/ }).select('name').find();
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.

General compatibility

ObjectIds

mquery clones query arguments before passing them to a collection method for execution. This prevents accidental side-affects to the objects you pass. To clone ObjectIds we need to make some assumptions.

First, to check if an object is an ObjectId, we check its constructors name. If it matches either ObjectId or ObjectID we clone it.

To clone ObjectIds, we call its optional clone method. If a clone method does not exist, we fall back to calling new obj.constructor(obj.id). We assume, for compatibility with the Node.js driver, that the ObjectId instance has a public id property and that when creating an ObjectId instance we can pass that id as an argument.

Read Preferences

mquery supports specifying Read Preferences to control from which MongoDB node your query will read. The Read Preferences spec also support specifying tags. To pass tags, some drivers (Node.js driver) require passing a special constructor that handles both the read preference and its tags. If you need to specify tags, pass an instance of your drivers ReadPreference constructor or roll your own. mquery will store whatever you provide and pass later to your collection during execution.

Future goals

Installation

npm install mquery

License

MIT