common-query
A javascript implementation of the MongoDB-style query and update syntax. It also has some useful methods built on top, and provides an extensible framework for adding new query and update operators.
Basic usage
let commonQuery = ;let createQuery = commonQuerycreateQuery;let createUpdate = commonQuerycreateUpdate; let obj = foo: 'bar' abc: 3 ; let query = ;console; // true let update = ;let newObj = update;console; // { foo: 'baz', abc: 5 }console; // false
Note that createQuery(), createUpdate, query.matches(), and query.apply() can all throw errors if the query/update data object is invalid, or if the query/update is applied to an invalid object.
Supported operators
The following mongo query operators are supported by default:
$and
$or
$nor
$exists
$not
$elemMatch
$in
$nin
$all
$size
$text
$regex
$gt
$gte
$lt
$lte
$ne
See the MongoDB docs here for documentation on these operators.
The following extra operators are included:
$wildcard
: A simplified version of$regex
, supporting glob-like expressions using the operators*
and?
.$var
: See the detailed query documentation below. (Not really an operator)
The following mongo update operators are supported by default:
$set
$unset
$inc
$mul
$rename
$min
$max
$addToSet
$push
$pop
See the MongoDB docs here for documentation on these operators.
Query
createQuery() takes an optional options object as the second argument - see the docs directory for a complete list.
A query with variable parameters can be specified using the 'fake' $var
operator. Example:
let queryData = name: 'Henrietta Wilkinson' age: 21 favoriteColor: $var: 'color' ;let query = ;console; // true
During query construction, the $var
objects are replaced with the value specified by the vars option. Note that
any missing $var
substitution will result in an invalid query.
The query object also includes some additional functionality:
// Get the plain object that represents this queryquery; // Get the query factory used to generate this query (see below)query; /* Get a generated function to match a query. This may be more efficient than query.matches(), and should be used if a query is to be used many times. */let func = query;; // true /* Normalize a query, optionally to a provided schema. Validates the query as well. This is done by default in the constructor; pass the skipValidate option to opt out of this behavior. */query; query; /* Ensure that a query is valid; will throw an error if the query is invalid. This is done by default in the constructor; pass the skipValidate option to opt out of this behavior. Note that calling matches() on an invalid query results in undefined behavior. */query; // throws QueryValidationError // Get a list of fields that a query will access during matchingquery; // [ 'field1', 'field2', 'field3' ] // Get a list of fields that a query matches exactly. These are fields that must match a single scalar value.// See the code documentation for details.query; // { exactMatches: [ 'field1', 'field2' ], onlyExactMatches: false } // Get a list of operators used by this queryquery; // [ '$and', '$gt', '$regex' ]
Update
createUpdate() takes an optional options object as the second argument. Some options are:
- allowFullReplace: By default, if an update has no operators, it will be automatically wrapped in a
$set
operation, and updated only the stated fields. If you set allowFullReplace, this will not occur, and such an update will replace the entire object (as is the default MongoDB behavior).
See the docs directory for a complete list.
On top of apply(), the update object also includes some additional functionality:
// Get the plain object that represents this updateupdate; // Get the update factory used to generate this update (see below)update; /* Get a generated function to apply an update. This may be more efficient than update.apply(), and should be used if a query is to be used many times. */let func = update;; // true /* Normalize an update, optionally to a provided schema. Validates the update as well. This is done by default in the constructor; pass the skipValidate option to opt out of this behavior. */update; update; /* Ensure that an update is valid; will throw an error if the update is invalid. This is done by default in the constructor; pass the skipValidate option to opt out of this behavior. Note that calling apply() on an invalid update results in undefined behavior. */update; // throws UpdateValidationError // Get a list of fields that will be updatedupdate; // [ 'field1', 'field2', 'field3' ] // Get a list of operators used by this updateupdate; // [ '$set', '$inc', '$addToSet' ] // Whether or not the field contains update operatorsupdate; // true // Whether the update has no operators, and will thus be a full object replacement// (this is the inverse of update.hasOperators())update; // false
In addition to the instance methods, there are also some notable static methods on the Update
class.
// Create an update from the recursive diff of two objects// See the method's documentation for accepted optionsUpdate; // Results in: "$set": "foo": "boar" "boom": 4 "plork.0.aoeu": true "plork.0.asdf": false "$unset": "baz": true "plork.1": true "$push": "plork": "$slice": 1
Query and update factories
Each query and update is created from an QueryFactory or UpdateFactory, respectively. The createQuery() and createUpdate() functions are shorthand for getting these entities from a default factory. A more complete, equivalent way to create a query or update would be:
let queryFactory = ;let query = queryFactory; let updateFactory = ;let update = updateFactory;
Each factory will load the default set of operators when it is instantiated. Additional custom operators can be added by subclassing QueryOperator or ExprOperator for queries, or UpdateOperator for updates, and registering them to the query factory. An example follows:
ExprOperator { supername || '$horse'; } { return 'horse' 'foal' 'colt' 'pony' !== -1; } let queryFactory = ;queryFactory; let query = queryFactory;console; // true
See the docs directory for full documentation on creating and registering custom operators.
Aggregates
Unimodel aggregates are specified in a common format that has no analog (that I know of) in existing systems. The best way to understand them is by example. Examples in this section use documents representing pets at animal shelters:
animalType: 'dog' animalSubtype: 'Yorkshire Terrier' age: 4 weight: 142 shelterLocation: 'Clifton' dateFound: '2013-03-20T04:13:23Z' name: 'Ruff'
Collection-Wide Statistics
This aggregate type returns statistics on a field across a whole collection (or a subset matched by a query).
// Perform statistics on a field stats: age: count: true avg: true max: true // Return the total number of documents the aggregate is executed across total: true
A result set for this aggregate would look something like:
// There are 400 animals matched by the query total: 400 stats: age: // Of those, 329 have non-null 'age' fields count: 329 // The average age of animals in 5.2382 avg: 52382 // The maximum age of animals is 19.2 max: 192
The different types of stats you can ask for are:
- count - The number of documents that contain a non-null value for the field.
- avg - The average value of the field.
- min - The minimum value of the field.
- max - The maximum value of the field.
- sum - The sum of values of the field.
- stddev - The standard deviation of values of the field.
Not all model types need support all of these types of stats, and model types may add additional stats if they are supported.
The stats object is a mapping between field paths and the statistics to perform. You can also supply more than one stats field in the aggregate:
stats: age: max: true dateFound: min: true
Results might look like this:
stats: age: max: 192 dateFound: min: '2015-04-12T07:22:09Z'
For convenience, stats
can be a single string. In this case, the string is treated as
a field name, and the count
stat is executed on it:
stats: 'animalType'
is converted to:
stats: animalType: count: true
Group by Discrete Values of Field
This type of aggregate will return statistics grouped by discrete values of a field.
// The field to group by is 'animalType' groupBy: field: 'animalType' // Perform statistics within each group on the 'age' field stats: age: avg: true // Return the total number of documents in each group total: true
Results look like:
// The value of the groupBy field (see below for why this is an array) key: 'cat' // Requested statistics for this grouping stats: age: // Average age of cats avg: 72 // There are 18 cats in the database (note that this is outside the field stats blocks) total: 18 key: 'dog' stats: age: avg: 64 total: 12 key: 'bird' stats: age: avg: 21 total: 4
As a shorthand, you can specify the groupBy
field as a single object:
groupBy: field: 'animalType'
or as a string:
groupBy: 'animalType'
Both shorthand forms are converted to:
groupBy: field: 'animalType'
You can also leave off stats
to get only totals:
groupBy: field: 'animalType' total: true
May yield:
key: 'cat' total: 18 key: 'dog' total: 12 key: 'bird' total: 4
Arrays
If the groupBy
field
is an array, each of the array's elements is treated as a separate
entry. This means that, in this case, the counts and totals in the aggregate result won't
equal the total number of documents (documents can be counted multiple times if the array
has multiple elements).
Group by Ranges of a Field Value
This will group by ranges of a numeric or date field.
groupBy: // Numeric/date field to group by field: 'age' ranges: // First group (group 0) is animals less than 1 year old end: 1 // Second group (group 1) is animals 1-3 years old start: 1 end: 3 // Third group (group 2) is animals 3-9 years old start: 3 end: 9 // Fourth group (group 3) is animals more than 9 years old start: 9 // Give total matching for each group // Note that you can also supply stats here as well total: true
Results look like this:
// This is the entry for group number 0 // These indices correspond to the indices in the given ranges array key: 0 // There are 5 animals in this range (less than 1 year old) total: 5 key: 1 total: 8 key: 2 total: 14 key 3 total: 7
These ranges can also be dates if applied to a date field:
groupBy: field: 'dateFound' ranges: end: '2010-01-01T00:00:00Z' start: '2010-01-01T00:00:00Z' end: '2013-01-01T00:00:00Z' start: '2013-01-01T00:00:00Z' total: true
For convenience, a continuous series of non-overlapping ranges can be specified as:
groupBy: field: 'age' ranges: 1 3 9 total: true
Will be converted to:
groupBy: field: 'age' ranges: end: 1 start: 1 end: 3 start: 3 end: 9 start: 9 total: true
The output of this is:
// This key corresponds to the range ENDING at index 0 (ie, all animals less than 1 year old) key: 0 // There are 5 animals in this range (less than 1 year old) total: 5 // Range from 1-3 years key: 1 total: 8 // Range from 3-9 years key: 2 total: 14 // One more result entry than entries in the array // This is for animals more than 9 years old key 3 total: 7
Strings found in start/end properties will also attempt to be parsed into number/date values. For example:
groupBy: field: 'age' ranges: '3' 5 total: true
will be converted to:
groupBy: field: 'age' ranges: end: 3 start: 3 end: 5 start: 5 total: true
and:
groupBy: field: 'dateFound' ranges: '2015-01-01T05:00:00.000Z' 2015 1 1 0 0 0 total: true
will be converted to:
groupBy: field: 'age' ranges: end: '2015-01-01T00:00:00.000Z' start: '2015-01-01T00:00:00.000Z' end: '2015-02-01T00:00:00.000Z' start: '2015-02-01T00:00:00.000Z' total: true
Group by Fixed Sized Intervals
This will group continuous values across fixed intervals.
groupBy: // Segment the numeric field 'age' field: 'age' // Each interval is of length 3 interval: 3 // By default, intervals start at 0 (ie, -3, 0, 3, 6, 9, etc) // This supplies a different offset // When set to 1, the intervals become -2, 1, 4, 7, etc base: 1 total: true
Results look like this:
// The key here is the start value of the interval // Ie, this entry is for the interval -2 through 1 key: -2 total: 5 // This is for the interval 1 through 4 key: 1 total: 4 key: 4 total: 8 ...
These can also be applied to dates. In this case, the interval should be supplied as an ISO 8601 time Duration. For example, an interval of 'P3H15M' is an interval of 15 minutes.
groupBy: field: 'dateFound' interval: 'P8H' // The default base when using time intervals is not defined. // Override bases are specified as an ISO8601 timestamp. base: '2010-01-01T00:00:00Z' total: true
Results in:
// Result keys are ISO timestamps key: '2010-01-01T00:00:00Z' total: 2 key: '2010-01-01T08:00:00Z' total: 1 ...
As a convenience, you string values from base and interval will be converted to proper number/date values. For instance:
groupBy: field: 'age' interval: '3' base: '1' total: true
will be converted to:
groupBy: field: 'age' interval: 3 base: 1 total: true
Group by Time Components
Usually, when you want to group by (for example) month, you don't actually want to use a time interval of 30 days because these won't align with month boundaries. This grouping type allows you to group by time components.
groupBy: // Field to group by field: 'dateFound' // Time component to group into timeComponent: 'year' // The number of time components in each group (optional) timeComponentCount: 1 total: true
The output looks like:
key: '2012-01-01T00:00:00Z' total: 4 key: '2013-01-01T00:00:00Z' total: 7 key: '2014-01-01T00:00:00Z' total: 5 ...
Each of the result keys is an ISO8601 timestamp corresponding to the start of the range represented by that time component.
The timeComponent
field can be one of the following:
year
month
week
day
hour
minute
second
The timeComponentCount
field is optional, and can be used to create longer intervals.
type: 'group' groupBy: field: 'dateFound' timeComponent: 'day' timeComponentCount: 2 total: true
Can result in:
key: '2012-01-01T00:00:00Z' total: 1 key: '2012-01-03T00:00:00Z' total: 2 key: '2012-01-05T00:00:00Z' total: 2 ...
Note that this does NOT represent a duration. The last interval in the range of a time component may be cut short (for example, in months with 31 days, the last interval in the above example would be only a single day instead of 2 days).
The "base" value for a time component is always the first valid point in time for that component.
For year
, the base point in time used is year 1.
Grouping By Multiple Fields
The groupBy
parameter in it's most verbose form is an array of goruping specifiers.
When multiple specifiers are present, a powerset of the results will be producsed.
For example:
groupBy: field: 'animalType' field: 'age' interval: 4 total: true
This groups by age (in intervals of 4) and animalType. The results for this look like:
key: 'dog' 0 total: 2 key: 'dog' 4 total: 3 key: 'dog' 8 total: 2 key: 'cat' 0 total: 5 key: 'cat' 4 total: 3 ...
Restrict aggregation buckets to a subset of keys
You may specify a whitelist of keys to include in the result for each groupBy entry:
groupBy: field: 'animalType' only: 'cat' 'dog' total: true
May yield:
key: 'cat' total: 18 key: 'dog' total: 12