store-api
Defines interfaces and helper classes for a 'store'.
See the API for interface declarations.
What is a store?
A store is a source that can be queried for data. This bundle defines the common interface for stores, so data of different store implementations can be accessed and easily integrated in a uniform way.
Historically, the store interface is based on the dojo/store API.
This bundle specifies in a more formally way, what a store is by defining type interfaces for it.
A store can be synchronous (SyncStore) or asynchronous (AsyncStore), We recommend to implement asynchronous stores and therefore, this documentation mainly describes, how an asynchronous store works.
Part of the store interface is the "ComplexQueryLang". This query language can be used by clients to query specific data from stores. An implementor of the store interface has to convert these queries into its own specific query language in order to return the requested data.
ComplexQuery Language
The language is specified here ComplexQueryLang.
Queries of the ComplexQuery Language are represented as JSON-style objects, for example:
// Find all items where item.a == 'test' and item.x == 1;
const query = {
a: "test",
x: 1
};
// Find all items where item.a starts with 'test' and item.x < 1;
const query = {
a: { $eqw: "test*" },
x: { $lt: 1 }
};
// Find all items where item.a starts with 'test' and (item.x < 1 or item.x > 2);
const query = {
$and: [{ a: { $eqw: "test*" } }, { $or: [{ x: { $lt: 1 } }, { x: { $gt: 2 } }] }]
};
For a list of all available operators see the Reference.
ComplexQuery and SpatialQuery classes
The class ComplexQuery is a parser for the ComplexQueryLang. By default the parser does not support spatial operators. To ensure that spatial operators are evaluated, the SpatialQuery class needs to be loaded by a store implementation. The SpatialQuery registers all spatial operators at the ComplexQuery.
Following samples show, how the parser can be used.
import ComplexQuery from "store-api/ComplexQuery";
const parsedComplexQuery = ComplexQuery.parse({
a: 1,
b: 2
});
// Access the Abstract Syntax Tree (AST) of the query
// The AST represents a normalized view of the expressed query
const ast = parsedComplexQuery.ast;
// AST is an object structure like:
const expectedAst = {
// o is the operator
o: "$and",
// c are the child AST nodes
c: [
{
o: "$eq",
// n is the name of the referenced field
n: "a",
// v ist the value of the field
v: 1,
// vt is the value type of the test object
vt: "number"
},
{
o: "$eq",
n: "b",
v: 2,
vt: "number"
}
]
};
The AST can be walked using the walker() method.
const walker = ast.walker();
if(walker.toFirstChild()){
if(walker.current.o==="$suggest"){
...
}
}
A parsed complex query can be tested against existing items. This can be used to evaluate a complex query against data already available inside the browser.
import ComplexQuery from "store-api/ComplexQuery";
const data = [ { a: 1, b:3 }, {{ a: 1, b:2 }, ...}]
const parsedComplexQuery = ComplexQuery.parse({
a: 1,
b: 2
});
// results contain all items with item.a === 1 and item.b === 2;
const results = data.filter((item) => parsedComplexQuery.test(item));
With SpatialQuery, spatial operations can be evaluated:
import SpatialQuery from "store-api/SpatialQuery";
import Extent from "esri/geometry/Extent";
const testGeom = new Extent({xmin,xmax,ymin,ymax});
const parsedComplexQuery = SpatialQuery.parse({
geometry: { $intersects : testGeom }
});
const otherGeom = ...
if(parsedComplexQuery.test({geometry: otherGeom })){
...
}
Usage of an AsyncStore
The following code samples explain common use cases:
const store = ... // create an async store
// First initialize a store by fetching its metadata
store.getMetadata().then((metadata)=>{
// the metadata object provides access to the data structure of the store
// e.g. which fields are available
if(metadata.supportsGeometry){
// this indicates that items of this store provide a 'geometry' field
}
});
NOTE: If a store
supportsGeometry
, the field name for the item's geometry isgeometry
. The values should be instances ofesri/geometry/Geometry
.
For well known keys within metadata
see Metadata.
Every store implementation can extend the metadata with its own specific information.
// Query a store for data with the 'name' field value equal to 'Test'.
// The option 'count' defines that only 5 items should be returned.
// The 'fields' option defines that only ids should be fetched.
store.query({ name: "Test" }, { count: 5, fields: { [store.idProperty]: true } }).then((resultItems) => {
// if a store supports pagination
// it may respond with the full amount of matching items
const fullAmountOfMatchingItems = resultItems.total;
for (const item of resultItems) {
// The store attribute 'idProperty' declares which field
// in the result item is the id of the item
const itemId = store.idProperty
? item[store.idProperty]
: // if it is not available the store should support a `getIdentity`
// method to provide an id of the item
store.getIdentity(item);
}
});
For more information about the query
method and its options, see AsyncStore.query.
// Retrieve a specific item from a store.
store.get(itemId).then((item) => {
// the specific item
});
For more information about the get
method and its options, see AsyncStore.get.
It is possible to cancel a pending query in two different ways:
-
The recommended way is to use an
AbortSignal
.const aborter = new AbortController(); store.query({name: "Test"}, { signal: aborter.signal }) .then((resultItems)=>{ ... }, (e)=>{ if (e.name === "AbortError"){ // aborted } }) // trigger abort aborter.abort();
-
The old deprecated and no longer recommended, but still supported way is to use the
cancel
method on the QueryResult.const queryResult = store.query({name: "Test"}); queryResult.then((resultItems)=>{ ... }, (e)=>{ if (e.name === "AbortError"){ // aborted } }) // trigger abort/cancel (Note: this is optional) queryResult.cancel?.();
Implementation of an AsyncStore
The following code sample shows an in-memory implementation of an AsyncStore:
import ComplexQuery from "store-api/ComplexQuery";
import { sortAndPaginate } from "store-api/utils";
class MyStore {
// id of this store
id = "mystore";
// name of the item field which represents the id
idProperty = "id";
constructor() {
// in-memory sample data
this._data = [
{ id: 1, name: "A" },
{ id: 2, name: "B" },
{ id: 3, name: "C" },
{ id: 4, name: "D" }
];
}
// The get() method retrieves items by id
async get(id) {
return this._data.filter((item) => item.id === id)[0];
}
// The query() method searches for items which match the given complex query expression
async query(query, queryOptions) {
// parse the query with the ComplexQuery parser
const parsedComplexQuery = ComplexQuery.parse(query, queryOptions);
// filter the in-memory items using the 'test' method of the parsed query
const results = this._data.filter((item) => parsedComplexQuery.test(item));
// support for sort and start/count options
return sortAndPaginate(results);
}
// The getMetadata() method provides the store metadata
async getMetadata() {
return {
supportsGeometry: false,
supportsSorting: true,
// declares which fields
// are available in the result items of the store
fields: [
{
// match the idProperty
name: "id",
title: "ID",
type: "number",
identifier: true
},
{
name: "name",
title: "Name",
type: "string"
}
]
};
}
}
Other bundles can consume stores by referencing them via the OSGi System. E.g. this store can be made available to other bundles via such component declaration:
// manifest.json
{
"components": [
{
"name": "MyStore",
// ct.api.Store is the historic 'OSGI' name for the 'store-api/api/Store' interface
"provides": "ct.api.Store",
"properties": {
// declare that this store should be used in the omnisearch/search-ui
"useIn": ["search"]
}
}
]
}
Utilities
The module store-api/utils provides some helpers for sorting and paginating arrays.
The QueryExecution and QueryExecutions helpers are of interest if multiple stores should be queried in parallel, or the whole execution should be observed or displayed in a UI.
Reference of ComplexQuery Language operators
Note: It depends on the specific store implementation, which of the operators are implemented. All listed operators are accepted by the ComplexQuery Interpreter.
Value compare operators
-
$eq
Equal to operator
const query1 = { x: { $eq: 1 } }; // item.x == 1 const query2 = { x: 1 }; // short form, same meaning
-
$gt
Greater than operator
const query = { x: { $gt: 1 } }; // item.x > 1
-
$gte
Greater than or equal to operator
const query = { x: { $gte: 1 } }; // item.x >= 1
-
$lt
Less than operator
const query = { x: { $lt: 1 } }; // item.x < 1
-
$lte
Less than or equal to operator
const query = { x: { $lte: 1 } }; // item.x <= 1
-
$in
In operator. The operator allows to specify an array of possible matches.
const query = { x: { $in : [1,2,3]} // item.x == 1 || item.x == 2 || item.x == 3
-
$exists
Exists operator. The operator is useful to detect non null values (or the other way around).
const query1 = { x: { $exists: true } }; // item.x !== null && item.x !== undefined const query2 = { x: { $exists: false } }; // item.x == null || item.x == undefined
-
$eqw
Equals wildcard operator.
The operator compares the item value against a string pattern: Supported wildcards are:
?
= single char*
= any chars
const query = { x: { $eqw: "B?roheng*" } }; //Expressed with a regex: /B.roheng.*/.test(item.x)
The preceding sample pattern matches for example
BorohengXy4
but notBroheng
. -
$suggest
Suggest operator.
Make a suggestion/guess search. The exact meaning of "suggest" is implementation specific. This kind of query is typically executed by search UI's.
const query = { x: { $suggest: "London" } }; // item.x is a suggestion for "London"?
-
$elemMatch
Element matches operator.
This operator compares array values against a list of other operators and matches if one element of the array matches the other operators.
const query = { attributes: { $elemMatch: [{ name: "a" }] } }; // item.attributes[i].name == "a"
Logical Operators
-
$or
Logical Or operator.
This operator is a logical or. It fulfills if one of the sub operators match.
const query = { $or: [{ x: 1 }, { x: 2 }] }; // item.x == 1 || item.x == 2
The operator evaluates to
false
if[]
is given as operands, i.e. no values will be found. -
$and
Logical And operator.
This operator is a logical and. It fulfills if all of the sub operators match.
const query = { $and: [{ x: 1 }, { y: 2 }] }; // item.x == 1 && item.y == 2
If no operator is specified,
$and
is assumed as default. This means that{x: 1, y:2}
is the same as the preceding sample.The operator evaluates to
true
if[]
is given as operands, i.e. all values will be found. -
$not
Logical Not operator.
It negates the sub operator result value.
const query = {x: { $not: { $gt: 2 } // !(item.x > 2)
Spatial Operators
All spatial operators work with subclasses of esri/geometry/Geometry
.
-
$intersects
Intersects operator.
Operator matches if field
geometry
intersects the given geometry.const query = { geometry: { $intersects: testGeometry } };
-
$contains
Contains operator.
Operator matches if field
geometry
is contained inside the given container geometry.const query = { geometry: { $contains: containerGeometry } };
-
$crosses
Crosses operator.
Operator matches if field
geometry
crosses the other geometry.const query = { geometry: { $crosses: testGeometry } };
-
$envelope-intersects
Envelope intersects operator.
Operator matches if envelope of field
geometry
intersects envelope of other geometry.const query = { geometry: { "$envelope-intersects": testGeometry } };
-
$overlaps
Overlaps operator.
Operator matches if field
geometry
overlaps the other geometry.const query = { geometry: { $overlaps: testGeometry } };
-
$touches
Touches operator.
Operator matches if field
geometry
touches the other geometry (share a common boundary).const query = { geometry: { $touches: testGeometry } };
-
$within
Within operator.
The opposite of
$contains
.Operator matches if field
geometry
contains the other geometry (In other words: the other geometry is within fieldgeometry
).const query = { geometry: { $within: testGeometry } };