Querying data
The contents of a source can be interrogated using a Query
. Orbit comes with a
standard set of query expressions for finding records and related records. These
expressions can be paired with refinements (e.g. filters, sort order, etc.).
Custom query expressions can also be developed, as long as all the sources participating can understand them.
Query expressions​
The QueryExpression
interface requires one member:
op
- a string identifying the type of query operation
The other members of a QueryExpression
are specific to the op
.
The following standard query expressions are defined in @orbit/data
:
interface QueryExpression {
op: string;
}
interface FindRecord extends QueryExpression {
op: 'findRecord';
record: RecordIdentity;
}
interface FindRelatedRecord extends QueryExpression {
op: 'findRelatedRecord';
record: RecordIdentity;
relationship: string;
}
interface FindRelatedRecords extends QueryExpression {
op: 'findRelatedRecords';
record: RecordIdentity;
relationship: string;
}
interface FindRecords extends QueryExpression {
op: 'findRecords';
type?: string;
sort?: SortSpecifier[];
filter?: FilterSpecifier[];
page?: PageSpecifier;
}
Supporting interfaces include:
export type SortOrder = 'ascending' | 'descending';
export interface SortSpecifier {
kind: string;
order: SortOrder;
}
export interface AttributeSortSpecifier extends SortSpecifier {
kind: 'attribute';
attribute: string;
}
export type ComparisonOperator = 'equal' | 'gt' | 'lt' | 'gte' | 'lte';
export interface FilterSpecifier {
op: ComparisonOperator;
kind: string;
}
export interface AttributeFilterSpecifier extends FilterSpecifier {
kind: 'attribute';
attribute: string;
value: any;
}
export interface PageSpecifier {
kind: string;
}
export interface OffsetLimitPageSpecifier extends PageSpecifier {
kind: 'offsetLimit';
offset?: number;
limit?: number;
}
Queries​
The Query
interface has the following members:
id
- a string that uniquely identifies the queryexpression
- aQueryExpression
objectoptions
- an optional object that represents options that can influence how a query is processed
Although queries can be created "manually", you'll probably find it easier to use a builder function that returns a query.
To use a query builder, pass a function into a source's method that expects
a query, such as query
or pull
. A QueryBuilder
that's compatible
with the source should be applied as an argument. You can then use this builder
to create a query expression.
Standard queries​
You can use the standard @orbit/data
query builder as follows:
// Find a single record by identity
store.query(q => q.findRecord({ type: 'planet', id: 'earth' }));
// Find all records by type
store.query(q => q.findRecords('planet'));
// Find a related record in a to-one relationship
store.query(q => q.findRelatedRecord({ type: 'moon', id: 'io' }, 'planet'));
// Find related records in a to-many relationship
store.query(q => q.findRelatedRecords({ type: 'planet', id: 'earth' }, 'moons'));
The base findRecord
query can be enhanced significantly:
// Sort by name
store.query(q => q.findRecords('planet')
.sort('name'));
// Sort by classification, then name (descending)
store.query(q => q.findRecords('planet')
.sort('classification', '-name'));
// Filter by a single attribute
store.query(q => q.findRecords('planet')
.filter({ attribute: 'classification', value: 'terrestrial' })
// Filter by multiple attributes
store.query(q => q.findRecords('planet')
.filter({ attribute: 'classification', value: 'terrestrial' },
{ attribute: 'mass', op: 'gt', value: 987654321 })
// Filter by related records
store.query(q => q.findRecords('moons')
.filter({ relation: 'planet', record: { type: 'planet', id: 'earth' }})
// Filter by multiple related records
store.query(q => q.findRecords('moons')
.filter({ relation: 'planet', records: [{ type: 'planet', id: 'earth' }, { type: 'planet', id: 'jupiter'}]})
// Paginate by offset and limit
store.query(q => q.findRecords('planet')
.page({ offset: 0, limit: 10 }));
// Combine filtering, sorting, and paginating
store.query(q => q.findRecords('planet')
.filter({ attribute: 'classification', value: 'terrestrial' })
.sort('name')
.page({ offset: 0, limit: 10 }));
findRelatedRecords vs findRecords.filter({ relation: ..., record: ... })​
If you're using the default settings for JSONAPISource, findRelatedRecords
and findRecords.filter(...)
produce very different URLs.
const relatedRecordId = { type: 'planet', id: 'earth' };
// This fetches from: /planets/earth/moons
store.query(q => q.findRelatedRecords(relatedRecordId, 'moons'));
// This fetches from: /moons?filter[planet]=earth
store.query(q => q.findRecords('moon')).filter({ relation: 'planet', record: relatedRecordId });
Besides the different urls findRelatedRecords
does not support sorting, filtering, or pagination. If you want that functionality, findRecords
and filter on the relation.
Query options​
Options can be added to queries to provide processing instructions to particular sources and to include metadata about queries.
For example, the following query is given a label
and contains instructions
for the source named remote
:
store.query(q => q.findRecords('contact').sort('lastName', 'firstName'), {
label: 'Find all contacts',
sources: {
remote: {
include: ['phone-numbers']
}
}
});
A label
can be useful for providing an understanding of actions that have been
queued for processing.
The sources: { ${sourceName}: sourceSpecificOptions }
pattern is used to pass
options that only a particular source will understand when processing a query.
In this instance, we're telling a source named remote
(let's say it's a
JSONAPISource
) to include include=phone-numbers
as a query param. This will
result in a server response that includes contacts together with their related
phone numbers.
Querying a store's cache​
Note that store.query
is asynchronous and thus returns results wrapped in a
promise. This may seem strange at first because the store's data is "in memory".
In fact, if you want to just "peek" into the contents of the store's memory,
you can issue the same queries synchronously against the store's Cache
.
For example:
// Results will be returned synchronously by querying the cache
let planets = store.cache.query(q => q.findRecords('planet').sort('name'));
By querying the cache instead of the store, you're not allowing other sources to participate in the fulfillment of the query. If you want to coordinate queries across multiple sources, it's critical to make requests directly on the store.