OpenEHR REST Query API

Release: v1.0
Status: DEVELOPMENT

Query Requirements

Purpose

This specification describes the service endpoints and datamodels used when querying an openEHR system.

AQL (Archetype Query Language) is the primary query language. The AQL specification is found here: http://www.openehr.org/releases/QUERY/latest/docs/AQL/AQL.html

The endpoints are designed to be extended with other and not yet defined query languages. The returning response should always be the same structure as defined below.

The query response may also be used for other endpoints in the openEHR REST API. I.e. there might be specified filter/query parameter on Composition resources. These operations will return the responsestructure defined here.

Functional requirements

The following is an outline of the functional requirements which this specification is made to solve:

  • Query functions

  • Single EHR queries

  • Population queries

  • Batch queries

  • Stored queries

Query functions

Query functions are operators/functions which tunes and modifies the execution of a specific query. The following types of query functions are identified:

  • Parameters

  • Scope

Parameters within an AQL is define as :parameter_name. These parameters will be substituted by the server into the actual query that will be executed.

Scope is a way to define the set of compositions to be considered when running queries. Examples of this might be:

  • Episode(s) of care

  • Period(s) of care

  • Workflow

Parameters and Scope might be used in combination. One example might be: I want to query all Temperature entries created after last saturday, and which where registered to a specific Episode of care.

Single EHR queries

A common use-case is to execute queries within a specific EHR. For single EHR queries the endpoint and resources will be as much aligned to the RM classes as possible. That’s why the ehr_id is added to the path for these queries.

Population queries

Population queries are queries which are executed on several EHRs in the same request. Examples of use-cases will be:

  • Ward lists

  • Explore correlations between patients in an pandemic situation

  • research, e.g. epidemiology

Batch queries

Most application will present different data points in the same screen. The datapoints will be collected from different archetypes committed in multiple compositions. To supplement this kind of use-cases the endpoint must provide batch query operations.

Batch query is an operation (e.g. request) where the client submits several queries to be executed within the same operation. The correlation_id is used to correlate the queries provided by the client with specific results in the RESULT_SET.

Stored queries

Stored queries are queries which are present on the server pre-request time. The queries will expose mandatory and optional parameters for the clients.

Non- functional requirements

Some of the non-functional requirements will be:

  • Low payload

  • Ease of use

  • Security

  • Synchronous/Asynchronous

Security

  • Audit logging – Be able to tell who executed which queries

  • Authorization – AQL is open ended - how to deal with authorization on data structures when exposing an open AQL endpoint?

Data structures

Request structure

Below is a mostly self-documented (?) AQL request.

{
  "aql": "select o/data[at0002]/events[at0003 and name/value='Any event']/data[at0001]/items[at0004]/value/magnitude as temperature, o/data[at0002]/events[at0003 and name/value='Any event']/data[at0001]/items[at0004]/value/units as unit from EHR[ehr_id/value='554f896d-faca-4513-bddf-664541146308d'] CONTAINS Observation o[openEHR-EHR-OBSERVATION.body_temperature-zn.v1] WHERE o/data[at0002]/events[at0003 and name/value='Any event']/data[at0001]/items[at0004]/value/magnitude > :temperature and o/data[at0002]/events[at0003 and name/value='Any event']/data[at0001]/items[at0.63 and name/value='Symptoms']/value/defining_code/code_string=:chills order by temperature desc fetch 3",
  "aql_parameters": {
    "temperature": 38.5,
    "chills": "at0.64"
  },
  "composition_uids": [ //optional list of composition uids to filter the query on
    "90910cf0-66a0-4382-b1f8-c0f27e81b42d::default::1"
  ],
  "ehr_ids": [ //optional list of ehr ids to filter the query on
    "81433066-c417-4813-9b29-79783e7bed23"
  ]
}

Response structure

Metadata

Field Description
_href URL of the query executed (only for GET endpoint)
_type Defines type of the serialized object
_schema_version The version of the specification defining the serialized object
_created Result creation timestamp (in full ISO8601 format)
_generator Some identifier of the application that generated the result. Useful i.e. for debugging
_executed_aql The actual query (i.e. AQL) that was executed by the server after exploding the parameters. This attribute is not mandatory, but is useful for debugging

Data

Field Description
name Name of a query when registered as a stored query
aql The AQL which was given in the request
columns Columns are defined by the client provided with the given AQL. I.e. select c/uid/value as CidValue, c/context/start_time as StartTime from .... will give two columns. One columns for CidValue and another for StartTime.
columns.name Name of the column. I.e. CidValue or StartTime from the example above, when column alias is not present in the AQL a 0-based column index is used prefixed by a hash sign (i.e. #0, #1…)
columns.path Path from the given AQL of the specified column. I.e. columns CidValue will have path /uid/value
rows Ordered list with results.
rows.row Each row list of cells. One cell for each column. Content of a cell is ANY (i.e. a OBJECT in most programming languages)

ResultSet example

Below is a synthesized response with all attributes.

{
  "meta": {
    "_href": "<the URI for the executed AQL - used only for GET executions >",
    "_type": "RESULTSET",
    "_schema_version": "1.0.0",
    "_created": "2017-08-19T00:25:47.568+02:00",
    "_generator": "DIPS.OpenEhr.ResultSets.Serialization.Json.ResultSetJsonWriter (5.0.0.0)",
    "_executed_aql": "< the executed aql >", /* should only be returned in debug mode */
  },
  "name": "<the name/identifier of the stored query that was executed>",
  "aql": "select e/ehr_id/value, c/context/start_time/value as startTime,  c/uid/value as cid, c/name from EHR e contains  Composition c limit 2",
  "columns": [
    {
      "name": "#0",
      "path": "/ehr_id/value"
    },
    {
      "name": "startTime",
      "path": "/context/start_time/value"
    },
    {
      "name": "cid",
      "path": "/uid/value"
    },
    {
      "name": "#3",
      "path": "/name"
    }
  ],
  "rows": [
    [
      "81433066-c417-4813-9b29-79783e7bed23",
      "2017-02-16T13:50:11.308+01:00",
      "90910cf0-66a0-4382-b1f8-c0f27e81b42d::default::1",
      {
        "_type": "DV_TEXT",
        "value": "Labs"
      }
    ]
  ]
}

Query

The REST API currently support AQL queries. There might be added other query languages at later versions.

Execute Query

The query execution resources roughly adhere to following patterns

  • Ad-hoc queries: /query/aql{?q,dynamic-query-parameters*,offset,fetch} used for executing non-stored AQL-queries, e.g. useful for test and development.

    Examples:

    • GET /query/aql?q=SELECT...
    • POST /query/aql
  • Stored queries

    These can use namespaces in order to separate queries used by different domains (apps), etc. In this case query name can contain a namespace prefix, suggested to be formatted as a reverse internet id (for example org.openehr). Stored queries SHOULD also be versioned in order to allow using several different versions of the same query. SEMVER versions are suggested (major.minor.patch).

    Examples:

    • GET /query/diabetes-patient-overview/1?fetch=10&Hba1c-limit=6 - if there is no {reverse-internet-id}:: pattern in the uri, then use the system’s configured default reverse-internet-id is assumed
    • GET /query/org.example.departmentx.test::diabetes-patient-overview?Hba1c-limit=6 - if no version supplied then the latest version will be used
    • GET /query/diabetes-patient-overview/1.0.2?Hba1c-limit=6&latest-glucose-max=10 - query with exact version number.
    • POST /query/org.example.departmentx.test::diabetes-patient-overview/1.0.2

The {query-parameters*} part of the path is used for dynamic parameters in the GET /query/{qualified-query-name} described below.

TBD - Server developer information: The : character is allowed in the path part of a URL so it does not to be encoded, see https://www.talisman.org/~erlkonig/misc/lunatech^what-every-webdev-must-know-about-url-encoding/#Thereservedcharactersaredifferentforeachpart …on the other hand some clients may happen to encode : to %3A anyway so a server SHOULD allow both :: and %3A%3A for the {double-colon} part described above but store/retrieve them in a single consistent way.

See API-description of /definition/query for more information regarding naming, versioning etc.

openEHR-EHR-id http request header

Http header information regarding all resources under /query:

If the http header openEHR-EHR-id is present in the request it MUST contain a single openEHR ehr_id. Clients SHOULD send the openEHR-EHR-id header if they know that the query concerns only a singe EHR.

The presence of a ehr_id header:

  • MUST be used by the server to provide the variable ehr_id in queries with the value of the header, overriding any static or dynamic parameters named ehr_id

  • SHOULD be used by the server to restrict querying to only the identified EHR

  • MAY be used by the server to route (e.g. in sharded scale-out systems) based on ehr_id without first having to analyze the request body

  • MAY be used by the server to handle ETag and If-None-Match http headers based on the latest known change to the identified EHR or its access-rights (e.g. by basing the ETag value on the latest contribution uid of the identified EHR + access rights) so that query responses can be safely cached (confirmed by http response code 304 Not Modified) and do not need to be re-run until changes have occurred.

The absence of the http header ‘openEHR-EHR-id’ means that the stored query or dynamic request parameters MAY contain a list of several ehr_ids or be unconstrained regarding ehr_id (e.g. population queries).

Paging

The offset and fetch parameters can be used for “paging”, for example GET /query/diabetes-patient-overview?offset=20&fetch=10&Hba1c-limit=6 could get the third “page” of results in a 10-results per page view. Please note that the query REST calls are “stateless” on the serverside, so the server always presents a selection from the “current” response at the time of calling the query resource.

If there have been changes to the data like additions or deletions between for example the time of query for the first (?offset=0&fetch=10) and the time of query for the second (?offset=10&fetch=10) “page” then some of the results may be duplicated or missed unless the requesting client handles detection of this. Query result analysis and row identification and/or a properly implemented ETag usage may assist in such detection.

Ranged retrieval of a stored batch-mode response via GET /query/batch/{id}/{result-id} may also be used as a way of making the server present chunks of a “frozen” view, if the server implementation supports it (many web servers do). See the http headers Range https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35 and Content-Range https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16

GET http://www.openehr.org/api/v1/query/aql?q=&dynamic_parameter=&offset=&fetch=
Requestsexample 1
Headers
openEHR-EHR-id: fc334f45-ffb7-4077-8134-4d3d5e6cb2a1
Responses200
Headers
Content-Type: application/json
ETag: cdbb5db1-e466-4429-a9e5-bf80a54e120b
Body
{
    /* See discussion on result-set in overview */
}

Execute ad-hoc (non-stored) AQL query
GET/query/aql{?q,dynamic_parameter,offset,fetch}

Warning: URIs in practice have a length restriction. Long query definitions q and many long dynamic-query-parameters may add up to reach that limit, thus we recommend using the POST /query/aql instead when such risks are expected.

URI Parameters
HideShow
q
string (required) 

the AQL to be executed

offset
number (optional) 

row number in result-set to start result-set from (0-based)

fetch
number (optional) 

number of rows to fetch

dynamic_parameter
string (optional) 

dynamic query parameters


POST http://www.openehr.org/api/v1/query/aql
Requestsexample 1
Headers
Content-Type: application/json
openEHR-EHR-id: fc334f45-ffb7-4077-8134-4d3d5e6cb2a1
Body
{
    "q": "<the query to be performed>",
    "offset": NUMBER_TO_START_FROM,
    "fetch": NUMBER_TO_FETCH,
    "composition_uids": ["<object_id>"],        // filter on the given compositions uids
    "ehr_ids": ["<ehr_id>"],                    // filter on the given ehr_ids
    "dynamic-query-parameters": {
        "parameter-x-name": "parameter-x-value",
        "parameter-y-name": "parameter-y-value",
    }
}
Responses200
Headers
Content-Type: application/json
ETag: cdbb5db1-e466-4429-a9e5-bf80a54e120b
Body
{
    /* See discussion on result-set in overview */
}

Execute ad-hoc (non-stored) AQL query
POST/query/aql

TBD: Add support for the request format application/x-www-form-urlencoded often used in html forms TBD: Add logging advice?


GET http://www.openehr.org/api/v1/query/qualified-query-name/version?offset=&fetch=&dynamic_query_parameter=
Requestsexample 1
Headers
openEHR-EHR-id: fc334f45-ffb7-4077-8134-4d3d5e6cb2a1
If-None-Match: cdbb5db1-e466-4429-a9e5-bf80a54e120b
Responses200
Headers
Content-Type: application/json
ETag: cdbb5db1-e466-4429-a9e5-bf80a54e120b
Body
{
  /* See discussion on result-set in overview */
}

Execute stored query
GET/query/{qualified-query-name}/{version}{?offset,fetch,dynamic_query_parameter}

Execute a stored query with the supplied qualified-query-name.

All query parameters found except “offset” and “fetch” are treated as dynamic and sent to the query execution engine. For parameters in AQL queries, see http://www.openehr.org/releases/QUERY/latest/docs/AQL/AQL.html#_parameters. For example call:

  • GET /query/org.openehr::compositions?temperature_from=36&temperature_unit=degC

Will pass parameters temperature_from and temperature_unit to the underlying query.

URI Parameters
HideShow
qualified-query-name
string (required) 

qualified query name to be executed. It should be formatted as ::, this way we can separate queries by domain, etc. Example qualified query name: org.openehr::compositions

version
string (required) 

SEMVER style (major.minor.patch) version prefix. If only major or major.minor are used then the latest version with supplied prefix will be used.

offset
string (optional) 

Query response row number to start from (default: 0)

fetch
string (optional) 

Number of query response rows to fetch (from offset). For AQL queries: use wisely if combined with http://www.openehr.org/releases/QUERY/latest/docs/AQL/AQL.html#_top

dynamic_query_parameter
string (optional) 

dynamic query parameters (can appear multiple times)


POST http://www.openehr.org/api/v1/query/qualified-query-name/version
Requestsexample 1
Headers
Content-Type: application/json
openEHR-EHR-id: fc334f45-ffb7-4077-8134-4d3d5e6cb2a1
If-None-Match: cdbb5db1-e466-4429-a9e5-bf80a54e120b
Body
{
    "aql_parameters": {
        "parameter-x-name": "paramater-x-value",
        "parameter-y-name": "parameter-y-value"
    },
    "offset": NUMBER_TO_START_FROM,
    "fetch": NUMBER_TO_FETCH,
    "composition_uids": ["<object_id>"], // filter on the given compositions uids
    "ehr_ids": ["<ehr_id>"] // filter on the given ehr_ids (for muliti-ehr queries)
}
Responses200
Headers
Content-Type: application/json
ETag: cdbb5db1-e466-4429-a9e5-bf80a54e120b
Body
{
  /* See discussion on result-set in overview */
}

Execute stored query
POST/query/{qualified-query-name}/{version}

Execute a stored query with the supplied prefix+id.

If the http header ‘ehr_id’ is present in the request it MUST contain a single openEHR ehr_id. Clients SHOULD send the ehr_id header if they know that the query concerns only a singe EHR. The presence of such a header:

  • MAY be used by the server to restrict querying to only the identified EHR, and

  • it MAY also be used by the server to route (e.g. in sharded scale-out systems) based on ehr_id without first having to analyze the request body,

The absence of the http header ‘ehr_id’ means that the stored query or dynamic request parameters MAY contain a list of several ehr_ids or be unconstrained regarding ehr_id (e.g. population queries).

NOTE: The body of a POST request is usually not logged in the http log by default, so developers need to make sure to log the body content some other way in order to enable auditing of what query parameters have been executed.

URI Parameters
HideShow
qualified-query-name
string (required) 

qualified query name to be executed. It should be formatted as ::, this way we can separate queries by domain, etc. Example qualified query name: org.openehr::compositions

version
string (required) 

SEMVER style (major.minor.patch) version prefix. If only major or major.minor are used then the latest version with supplied prefix will be used.


Generated by aglio on 07 Sep 2017