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), but it is no longer recommended implementing synchronous stores. 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 will 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

Following examples should explain some 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 will provide a 'geometry' field
    }
});

NOTE: If a store supportsGeometry then the field name for the item's geometry is geometry. The values should be instances of esri/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
    // then 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 it's 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:

  1. 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();
    
  2. The 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 example 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 an 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

Logical Operators

Spatial Operators

All spatial operators work with subclasses of esri/geometry/Geometry.