mekanika-query

0.10.1 • Public • Published

query

npm version Code Climate

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

Installation

  npm install mekanika-query

Usage

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 (err, res) {
  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 )
  .find()
  .on('villains')
  .limit(5)
  .done( handler );

Go crazy.

Building Qe - Query envelopes

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

Writing Qe directly

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)

Query .do actions

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

Setting .match conditions

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.

Examples:

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
query()
  .where('type').is('knight')
  .and('power').gt(20)
  .and('state').not('terrified');
 
// OR chain
query()
  .where('type', 'wizard')
  .or('level').gte(75)
  .or('numfollowers').gt(100);

To nest match container conditions see the query.mc() method below.

Nested Matching query.mc()

The fluent .where() methods are actually just delegates for the generalised query.mc() 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 = query.mc( 'and' )
  .where('power').gt(50)
  .where('state').neq('scared');
 
// Generates Qe match container:
{and: [ {power:{gte:50}}, {state:{neq:'scared'}} ]}

Which means, the fluent API expression:

query().where('power').gt(50).where('state').neq('scared');

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'
query()
  .where(
    // Generate the 'and' sub match container
    query.mc('and')
      .where('power').gt(30)
      .where('type', 'wizard')
  )
  .or('type').is('knight');
 
// Directly setting .match and passing 'mc'
query().match(
  // Generate the top level 'or' match container
  query.mc('or')
    .where( query.mc('and').where('power').... )
    .where( 'state', 'NY' )
);

Setting .update operators

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.

The Adapter bridge

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' )
  .find()
  .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 );

Setting an adapter

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 https://github.com/mekanika/adapter for more details on adapters.

Middleware

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

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( qe, next ) {
  // Example modification of the Qe passed to the adapter
  qe.on += ':magic_suffix';
  // Go to next hook (if any)
  next();
}
 
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

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( err, res, qe, next ) {
    try {
      err = 'My modified error';
      res = 'Custom results!';
      // Call your own external hooks
      myCustomEvent();
 
      // 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] );

Tests

Ensure you have installed the development dependencies:

  npm install

To run the tests:

  npm test

Test Coverage

To generate a coverage.html report, run:

npm run coverage

Bugs

If you find a bug, report it.

License

Copyright (c) 2013-2015 Mekanika

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

Package Sidebar

Install

npm i mekanika-query

Weekly Downloads

0

Version

0.10.1

License

MPL-2.0

Last publish

Collaborators

  • cayuu