npm package: @openreply/aql
Description: API Query Language - parser and filter
Latest version: 2.1.2

# AQL Manual

## Description

API Query Language (AQL) is a filtering language designed for use in URIs with JSON data structures.

## Usage

The strings used for filtering, e.g. `'eq(age,25)'` must be URI-encoded since this is designed for usage in the query string of a URI.
Paths are defined as [JSONPath](http://jsonpath.com) to look inside JSON objects.

```javascript
const { filter, Operator } = require('@openreply/aql');

const customOperators = ({
  // adding a new operator
  hasKey: new Operator((evaluateSubexpression, collection, options, key) =>
    collection.filter(item => Object.keys(item).includes(key))),
  // overriding a default operator
  eq: new Operator((evaluateSubexpression, collection, options, path, value) =>
    collection.filter(item =>
      item[path] === value))
});

const collection = [
  {
    name: 'Isom Hamill',
    age: 25,
    phones: [
      { type: 'work', number: '012345' },
      { type: 'home', number: '012345' }
    ],
    address: {
      street: '7264 Kade Alley'
    }
  },
  {
    name: 'Lonzo Dooley',
    age: 30,
    phones: [
      { type: 'mobile', number: '032345' },
      { type: 'home', number: '042346' }
    ]
  },
  {
    name: 'Bertrand Mertz',
    age: 35
  }
]

// Basic filtering
// filter by name
const result1 = filter('eq(name,Isom Hamill)')(collection);
// => [{"name":"Isom Hamill","age":25,"phones":[{"type":"work","number":"012345"},{"type":"home","number":"012345"}],"address":{"street":"7264 Kade Alley"}}]


// Using JSONPath
// filter by street: street must include 'Kade'
const result2 = filter('includes(address.street,Kade)')(collection);
// => [{"name":"Isom Hamill","age":25,"phones":[{"type":"work","number":"012345"},{"type":"home","number":"012345"}],"address":{"street":"7264 Kade Alley"}}]


// Using custom operators
// filter all items that have the key ('phones')
const result3 = filter('hasKey(phones)', customOperators)(collection);
// => [{"name":"Isom Hamill","age":25,"phones":[{"type":"work","number":"012345"},{"type":"home","number":"012345"}],"address":{"street":"7264 Kade Alley"}},
//     {"name":"Lonzo Dooley","age":30,"phones":[{"type":"mobile","number":"032345"},{"type":"home","number":"042346"}]}]


// quantitative operators
const result4 = filter('all(phones.*,phone,startsWith(phone.number,01))')(collection)
// => [{"name":"Isom Hamill","age":25,"phones":[{"type":"work","number":"012345"},{"type":"home","number":"012345"}],"address":{"street":"7264 Kade Alley"}}]

const result5 = filter('any(phones.*,phone,eq(phone.type,home))')(collection)
// => [{"name":"Isom Hamill","age":25,"phones":[{"type":"work","number":"012345"},{"type":"home","number":"012345"}],"address":{"street":"7264 Kade Alley"}},
//     {"name":"Lonzo Dooley","age":30,"phones":[{"type":"mobile","number":"032345"},{"type":"home","number":"042346"}]}]
```

## Operators

These operators are defined by default:

* `eq(path, value)`: The item at `path` must have the value `value`.
* `ne(path, value)`: The item at `path` must have a value different from `value`.
* `in(path, value1, value2, ...)`: The item at `path` must have one of the specified values.
* `out(path, value1, value2, ...)`: The item at `path` must not equal to any the specified values.
* `gt(path, value)`: The item at `path` must be greater than `value`.
* `ge(path, value)`: The item at `path` must be greater than or equal to `value`.
* `lt(path, value)`: The item at `path` must be less than `value`.
* `le(path, value)`: The item at `path` must be less than or equal to `value`.
* `startsWith(path, prefix)`: The item at `path` must have the prefix `prefix`.
* `endsWith(path, suffix)`: The item at `path` must have the suffix `suffix`.
* `includes(path, value)`: The string/number item at `path` must include the infix `value` *OR* the array at `path` must contain the value `value`.
* `and(condition1, condition2, ...)`: All the conditions must hold.
    *Example*: `and(includes(name,o),ge(age,30))` filters items that include an `o` in the name and with age greater than or equal to 30.
* `or(condition1, condition2, ...)`: At least one of the conditions must hold.
    *Example*: `or(eq(phones.0.type,work),eq(phones.0.type,mobile))` filters items that either have a work phone or a mobile phone as the first phones entry.
* `all(path, binding, condition)`: For all of the items `binding` that match `path`, the `condition` must hold *and* there must be at least one item that matches `path`.
    *Example*: `all(phones.*,phone,startsWith(phone.number,01))` filters items that have at least one phone number and all of the existing phone numbers of the item must start with `01`.
* `any(path, binding, condition)`: For at least one of the items `binding` that match `path`, the `condition` must hold.
    *Example*: `any(phones.*,phone,eq(phone.type,home))` filters items that have at least one phone that has the type `home`.

If the actual value at `path` is a number, `value` (or `value*`) is coerced to a number.

## Extensions

New AQL operators can be added by providing a `customOperators` function to the filter function:

```javascript
const { filter, Operator } = require('@openreply/aql');

const customOperators = collection => ({
  // adding a new operator
  hasKey: new Operator((evaluateSubexpression, collection, options, key) =>
    collection.filter((item) => Object.keys(item).includes(key))),
});

const filteredCollection = filter('hasKey(phones)', customOperators)(fullCollection);
```

The `customOperators` function takes the collection as its only argument and returns an object with the operators.
The keys this object define the name of the operator.
If a key is used that is already defined in the default set of operators, the default will be **overridden**.
Each operator takes an `options` object as the first argument. It contains information on how the operator should be modified.
The other arguments of the operator are free and can be defined and used by the operator itself.
The return value of an operator is expected to be an Array that has the filter condition applied.

## Acknowledgement

Special thanks to [persvr for creating the RQL](https://github.com/persvr/rql). This package is heavily insprired by it.
The main difference is that AQL is using [JSONPath](http://jsonpath.com) instead of a custom path language and AQL does not sort nor aggregate.

## License

This project is licensed under the MIT license.