Query envelope (Qe) builder and adapter bridge


query is an isomorphic interface for working with Qe - Query envelopes.

There are two distinct parts of query:

  1. Qe builder for creating valid Qe
  2. Adapter bridge for passing Qe to adapters and receiving results

An example query:

query( someAdapter )
  .on( 'users' )         // Resource to query 
  .where( 'name' )       // Match conditions 
    .in( ['Tom','Bob'] )
  .and( 'age' )          // also supports `.or()` 
    .gt( 25 )
  .limit( 20 )           // Number of results 
  .offset( 5 )           // Result to skip 
  .done( callback );     // `callback(err, results)` 
  npm install mekanika-query

Build Qe - Query envelopes:

var myq = query().on('villains').find().limit(5);
// -> {do:'find',on:'villains',limit:5} 

Plug into Qe adapters with .useAdapter( adptr ):

var superdb = require('superhero-adapter');
myq.useAdapter( superdb );

Invoke adapter with .done( cb ):

var handler = function (errres) {
  console.log('Returned!', err, res);
myq.done( handler );
// Passes `myq.qe` to `myq.adapter.exec()` 

query chains nicely. So you can do the following:

query( superdb )
  .done( handler );

Go crazy.

Initiate a query:

query() // -> new Query 
// Or with an adapter 
query( myadapter )

Build up your Qe using the fluent interface methods that correspond to the Qe spec:

  • Actions - create(), find(), update(), remove()
  • Target - on()
  • Matching - ids(), match()
  • Return control - select(), populate()
  • Results display - limit(), offset()
  • Custom data - meta()

Qe are stored as query().qe - so you can optionally assign Qe directly without using the fluent interface:

var myq = query();
myq.qe = {on:'villains', do:'find', limit:5};
// Plug into an adapter and execute 
myq.useAdapter( superheroAdapter );
myq.done( cb ); // `cb` receives (err, results) 

The available .do actions are provided as methods.

All parameters are optional (ie. empty action calls will simply set .do to method name). Parameter descriptions follow:

  • body is the data to set in the Qe .body. May be an array or a single object. Arrays of objects will apply multiple
  • ids is either a string/number or an array of strings/numbers to apply the action to. Sets Qe .ids field.
  • cb callback will immediately invoke .done( cb ) if provided, thus executing the query (remember to set an adapter).

Available actions:

  • create( body, cb ) - create new
  • update( ids, body, cb ) - update existing
  • remove( ids, cb ) - delete
  • find( ids, cb ) - fetch

All methods can apply to multiple entities if their first parameter is an array. ie. Create multiple entities by passing an array of objects in body, or update multiple by passing an array of several ids.

Update/find/remove can all also .match on conditions. (See 'match')

Conditions are set using the following pattern:

.where( field ).<operator>( value )

Operators include:

  • .eq( val ) - Equality (exact) match. Alias: .is().
  • .neq( val ) - Not equal to. Alias: not().
  • .in( array ) - Where field value is in the array.
  • .nin( array ) - Where field value is not in the array.
  • .all( array ) - Everything in the list.
  • .any( array ) - Anything in the list.
  • .lt( num ) - Less than number.
  • .gt( num ) - More (greater) than than number.
  • .lte( num ) - Less than or equal to number.
  • .gte( num ) - More (greater) than or equal to number.


query().where( 'name' ).is( 'Mordecai' );
// Match any record with `{name: 'Mordecai'}` 
query().where( 'age' ).gte( 21 );
// Match records where `age` is 21 or higher 

Multiple conditions may be added using either .and()or .or():

// AND chain 
// OR chain 
  .where('type', 'wizard')

To nest match container conditions see the method below.

The fluent .where() methods are actually just delegates for the generalised method for creating MatchContainer objects.

The Qe spec describes match containers as:

{ '$boolOp': [ mo|mc ... ] }

The 'mc' array is made up of match objects (mo) of the form {$field: {$op:$val}}

'mc' objects chain the familiar .where() method and match operator methods. For example:

var mc = 'and' )
// Generates Qe match container: 
{and: [ {power:{gte:50}}, {state:{neq:'scared'}} ]}

Which means, the fluent API expression:


Is identical to:

query().match( mc );

The upshot is nesting is fully supported, if not fluently. To generate a Qe that matches a nested expression as follows:

(power > 30 && type == 'wizard') || type == 'knight'

A few approaches:

// Using 'where' and 'or' to set the base 'mc' 
    // Generate the 'and' sub match container'and')
      .where('type', 'wizard')
// Directly setting .match and passing 'mc' 
  // Generate the top level 'or' match container'or')
    .where('and').where('power').... )
    .where( 'state', 'NY' )

query supports the following update operator methods (with their update object Qe output shown):

  • .inc( field, number ) - {$field: {inc: $number}}
  • .pull( field, values ) - {$field: {pull: $values}}
  • .push( field, values ) - {$field: {push: $values}}

Where field is the field on the matching records to update, number is the number to increment/decrement and values is an array of values to pull or push.

query can delegate execution to an adapter.

Which means, it can pass Qe to adapters and return the results.

To do this, call .done( cb ) on a query that has an adapter set.

query( customAdapter )
  .on( 'users' )
  .done( cb ); // cb( err, results ) 

This passes the Qe for that query, and the callback handler to the adapter. The errors and results from the adapter are then passed back to the handler - cb( err, results)

Specifically, query#done( cb ) delegates to:

query#adapter.exec( query#qe, cb );

Pass an adapter directly to each query:

  var myadapter = require('my-adapter');
  query( myadapter );

This is sugar for the identical call:

  query().useAdapter( myadapter );

See for more details on adapters.

query supports pre and post .done(cb) request processing.

This enables custom modifications of Qe prior to passing to an adapter, and the custom processing of errors and results prior to passing these to .done(cb) callback handlers. Note that middleware:

  • is executed ONLY if an adapter is set
  • can add multiple methods to pre and post
  • executes in the order it is added

Pre-middleware enables you to modify the query prior to adapter execution (and trigger any other actions as needed).

Pre methods are executed before the Qe is handed to its adapter, and are passed fn( qe, next ) with the current Qe as their first parameter, and the chaining method next() provided to step through the queue (enables running asynchronous calls that wait on next in order to progress).

To pass data between pre-hooks, attach to qe.meta.

next() accepts one argument, treated as an error that forces the query to halt and return cb( param ) (error).

Pre hooks must call next() in order to progress the stack:

function preHandler( qenext ) {
  // Example modification of the Qe passed to the adapter 
  qe.on += ':magic_suffix';
  // Go to next hook (if any) 
query().pre( preHandler );
// Adds `preHandler` to the pre-processing queue 

Supports adding multiple middleware methods:

query().pre( fn1 ).pre( fn2 ); // etc 
// OR 
query().pre( [fn1, fn2] );


Post-middleware enables you to modify results from the adapter (and trigger additional actions if needed).

Post middleware hooks are functions that accept (err, results, qe, next) and must pass next() the following params, either:

  • (err, results) OR
  • an (Error) object to throw

Failing to call next() with either (err,res) or Error will cause the query to throw an Error and halt processing.

Posts run after the adapter execution is complete, and are passed the the err and res responses from the adapter, and qe is the latest version of the Qe after pre middleware.

Important note on Exceptions! Post middleware runs in an asynchronous loop, which means if your post middleware generates an exception, it will crash the process and the final query callback will fail to execute (or be caught). You should wrap your middleware methods in a try-catch block and handle errors appropriately.

You may optionally modify the results from the adapter. Simply return (the modified or not) next(err, res) when ready to step to the next hook in the chain.

  function postHandler( errresqenext ) {
    try {
      err = 'My modified error';
      res = 'Custom results!';
      // Call your own external hooks 
      // MUST call `next(err, res)` to step chain 
      // Can pass to further async calls 
      if (hasAsyncStuffToDo) {
        myOrderCriticalEvent( err,res,next );
      // Or just step sync: 
      else next(err, res);
    catch (e) {
      // Note 'return'. NOT 'throw': 
      next(e); // Cause query to throw this Error 
  query().post( postHandler );
  // Adds `postHandler` to post-processing queue 

Also supports adding multiple middleware methods:

query().post( fn1 ).post( fn2 ); // etc 
// OR 
query().post( [fn1, fn2] );

Ensure you have installed the development dependencies:

  npm install

To run the tests:

  npm test

To generate a coverage.html report, run:

npm run coverage

If you find a bug, report it.

Copyright (c) 2013-2015 Mekanika

Released under the Mozilla Public License v2.0 (MPL-2.0)