mekanika-query
v0.10.1
Published
Query envelope (Qe) builder and adapter bridge
Downloads
4
Maintainers
Readme
query
query
is an isomorphic interface for working with Qe - Query envelopes.
There are two distinct parts of query
:
- Qe builder for creating valid Qe
- 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 multipleids
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 severalids
.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 returncb( 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)