Motivation
Orbit.js allows both Queries and LiveQueries (realtime queries) to be issued against the Store. This proposal assumes the use of the Orbit Query Language (oql) for issuing queries.
Examples of store.query() and store.liveQuery()
const result = store.query({oql: oqe('recordsOfType', 'planet')});
result === {
pluto: { type: 'planet', id: 'pluto', attributes: {name: 'Pluto'}
}
const liveQuery = store.liveQuery({oql: oqe('recordsOfType', 'planet')});
liveQuery.subscribe(jsonPatchOperation => {
// operations received for initial results and changes to results for oqe('recordsOfType')
})
It's important to note that both the queries and results are expressed as JSON. In the above example oqe()
is used as a shortcut to produce JSON e.g.
oqe('recordsOfType', 'pluto') === {
op: 'recordsOfType',
args: ['pluto']
}
OQL Operators
Currently the following Object Query Language (OQL) operators exist:
recordsOfType(type) => Set of records
relatedRecord(type, recordId, hasOneRelationship) => Individual record
relatedRecords(type, recordId, hasManyRelationship) => Set of records
filter(setOfRecords, predicateFunction) => Set of records
get(relativePath) => valueExpression (path is relative to the current record for the filter function)
and(predicationFunction, predicationFunction, …predicateFunction) => predicateFunction
or(predicationFunction, predicationFunction, …predicateFunction) => predicateFunction
equal(value, value, …value) => predicateFunction
It's also desireable to add operators similar to the following:
count(setOrArray) => Integer
pluck(arrayOfRecords) => Array of values
distinct(array) => Set of values
orderBy(setOrArrayOfRecords, valueExpression) => orderedArrayOfRecords
These operators may be composed, e.g.
const oql =
oqe('filter',
oqe('recordsOfType', 'moon'),
oqe('equal',
get('attributes/name'),
'Callisto'));
store.query({oql}) === setOfPlanetsAssociatedWithMoons;
While Query results may be any JSON structure, when considering integration between different operators and with orbit.js consumers (e.g. ember-orbit) it's useful to specify some commonly used structures and corresponding JSON Patch operations (for use with LiveQueries). This allows implementations to achieve full interop while requiring a minimum of inputs/outputs to be handled.
# Proposal
## Overview
The following Structures and JSON Patch operations would be recognised as 'first class' meaning that operator implementations and app/libraries that conform to them can guarantee good interoperation.
Structures
- Individual records or values
- Sets of records or values
- Arrays of records or values
JSON Patch operations
| Helper | JSON |
| --- | --- |
| replaceResult(anyStructure)
| {op: 'replace', path: '.', value: anyStructure}
|
| addResult(recordOrValue)
| {op: 'add', path: './recordOrValueHash', value: value}
|
| removeResult(recordOrValue)
| {op: 'remove', path: './recordOrValueHash'}
|
| insertResultAt(index, recordOrValue)
| {'add', path: './${index}', value: recordOrValue}
|
| removeResultAt(index)
| {'remove'}, path: './${index}'
|
Detailed description of supported Structures/JSON Patch operations
## Individual records or values
#### Structure
recordOrValue
JSON Patch operations
| Helper | JSON |
| --- | --- |
| replaceResult(record)
| {op: 'replace', path: '.', value: recordOrValue}
|
Query examples
Record
oqe('relatedRecord', 'moon', 'io', 'planet')
=>
{type: 'planet', id: 'io', attributes: {name: 'Jupiter'}}
Value
oqe('count', oqe('recordsOfType', 'planet'))
=>
8
## Set of records or values
As JSON doesn't support Sets natively it's necessary to add the semantics. These semantics are added by using a JSON object where the records or values are stored against keys which are calculated by determining a hash for the record or value.
Given the two common cases are:
-
A Set of records
-
A Set of values that can be easily converted to Strings
We could perhaps use an implementation like:
Orbit.jsonHash = function(json) {
if(json.type && json.id) return `${json.type}:${json.id}`;
if(!isObject(json)) return JSON.stringify(json);
return Orbit.fallbackJsonHash(json); // JSON.stringify is non deterministic for objects so we can't use it in the general case
}
Orbit.fallbackJsonHash = someSlowerButMoreGenericJsonHashingFunction;
// could perhaps throw a "fallbackJsonHash function required" error by default.
This gives us fast hash values with an extension point so that an orbit.js consumer may provide a function that's fast for their particular data.
Structure
{record1Hash: record1, record2Hash: record2}
{value1Hash: value1, value2Hash: value2}
JSON Patch operations
| Helper | JSON |
| --- | --- |
| replaceResult([array, of, results])
| {op: 'replace', path: '.', value: {record1Hash: record1, record2Hash: record2}}
|
| addResult(record)
| {op: 'add', path: 'recordHash', value: record}
|
| removeResult(record)
| {op: 'remove', path: 'recordHash'}
|
edit: replaceResult([array, of, results])
needs some thought as it requires conversion from an Array to a Set. As replaceResult
is also to replace an Array it's not clear how it would know which structure to return.
edit: As it's a helper we could perhaps pass an ES6 Set in i.e. replaceResult(new Set([1, 2]))
, not the cleanest signature though...
Query examples
Records
oqe('recordsOfType', 'planet')
=>
{
"jupiter": {type: 'planet', id: 'jupiter', attributes: {name: 'Jupiter'}},
"pluto" {type: 'planet', id: 'pluto', attributes: {name: 'Pluto'}}
}
Values
oqe('distinct', oqe('pluck', oqe('recordsOfType', 'planet'), 'attributes/name'))
=>
{Mercury: 'Mercury', Venus: 'Venus', Earth: 'Earth', Mars: 'Mars'}
## Array of records or values
#### Structure
[array, of, records, or, values]
JSON Patch operations
| Helper | JSON |
| --- | --- |
| replaceResult(recordOrValue)
| {'replace', path: '.', value: arrayOfRrecordsOrValues}
|
| insertResultAt(index, recordOrValue)
| {'add', path: './${index}', value: recordOrValue}
|
| removeResultAt(index)
| {'remove'}, path: './${index}'
|
Query examples
oqe('pluck', oqe('recordsOfType', 'planet'), 'attributes/name')
=>
['Mercury', 'Venus', 'Earth', 'Mars']