mekanika / qe

Query envelope (Qe) specification
2 stars 0 forks source link

Qe - Query envelopes


Status: This is a DRAFT specification and work in progress.

Each section (and some subsections) are marked with a Stability code explained below:

  • Experimental Recently introduced. Likely to change or be removed.
  • Unstable Settling but not stable. May change or be removed.
  • Stable Tested and stable. Only minor changes if any.
  • Final Spec ready and unlikely to ever change.

Qe are resource oriented control messages for APIs.

They are descriptions consumed by Qe-aware APIs to instruct actions, generally using a verbs (actions) acting on nouns (resources) approach.

Query envelopes (Qe) seek to:

Useful reference projects:

An example Qe:

/* Update all users outside of California who have 100
or more followers to 'platinum' status, add 25 credits
to their balance, and return only their ids. */

{
  do: 'update',
  on: 'users',
  match: {
    'and': [
      {followers: {gte:100}},
      {state: {nin:['CA']}}
      ]
  },
  body: [
    {status: 'platinum'}
  ],
  update: [
    {credits: {'inc':25}}
  ],
  select: [ 'id' ]
}

For endpoints that predefine their actions and/or targets (.on), Qe may simply encode relevant data, for example:

{
  match: {
    or: [
      {age: {gt:10}},
      {variety: {eq:'Pinot Noir'}}
    ]
  }
}

Conventions

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

Structure Stable

The structure of a Query envelope is described by its fields below, according to:

field: Stability type description

The core action "do verb on noun" block:

Matching resources:

Data block:

Return controls:

Results display:

And custom data:

A Qe SHOULD NOT have any other fields.

The simplest possible Qe is an empty envelope (no-op).

Serialisation

All examples in this document are shown as Javascript primitives.

Qe MAY be serialised as JSON, or any other appropriate structure (eg. YAML):

# Update user @moomoo `friends` number and return only `{id, fullname}` fields
---
do: update
on: users
ids:
  - @moomoo
body:
  -
    friends: 25
select:
  - id
  - fullname

Qe field details

.do Final

Type: String

The do field is a verb that describes the intended process to invoke.

// Create a tag {label:sweet}
{
  do: 'create',
  on: 'tags',
  body: [ {label:'sweet'} ]
}

The following are reserved action types. An API consuming Qe SHOULD handle these defaults:

These action types SHOULD NOT be aliased or have their intended meaning altered.

Qe MAY specify other (custom) action types.

.on Final

Type: String

The .on field points to a unique entity type to act upon, like a table (SQL), a collection (Document stores), a resource (REST). It is almost always a unique reference to some end-point that a .do field will apply to.

Qe MAY omit .on, as some actions might not act .on anything. eg. {do:'self_destruct', meta:{secret:'☃'}}.

Example .on usage:

// Get 25 tweets
{
  do: 'find',
  on: 'tweets',
  limit: 25
}

.ids Stable

Type: Array of strings or numbers

An Array of entity IDs to which the .action SHOULD apply the .body or .update. If .ids are provided, the .do action SHOULD only apply to those ids provided.

If .ids are provided, .match conditions MUST apply only to that subset of ids.

Example .ids usage:

// Remove ids ['554120', '841042']
{
  do: 'remove',
  ids: ['554120', '841042']
}

.match Unstable

Type: match container Object

.match is used to conditionally specify entities that meet matching criteria. If .ids are provided, .match MUST apply only to that subset of ids.

"Match" borrows its structure from MongoDB.

A match container object (mc) is defined as:

{ '$boolOp': [ mo|mc...  ] }

Where:

An mc MUST contain only one $boolOp. mc MAY contain match objects as well as nested mc (for nesting matches).

Match objects take the form:

{ $field: {'$op':$value} }

Where:

Example:

// Match people in CA and NY over 21 or anyone in WA
{
  match: {
    'or': [
      {'and': [
       {age: {gt:21}},
       {state: {in:['CA', 'NY']}}
      ]},
      {state: {eq:'WA'}}
    ]
  }
}

match operators Stable

The current reserved match operators are:

These operators SHOULD NOT be aliased or have their intended meaning altered.

Qe MAY specify alternative custom operators, eg:

// Custom 'within' operator
{match: {
  'or': [
    {location:{'within':['circle', 2100,3000,20]}
    ]
  }
]}

Deep matches Experimental

TODO: Requires testing in real world use cases

$field MAY present a dot notation property (eg. dob.year) to match on complex properties. In this case the match SHOULD apply to the sub-property. For example:

// Match users who:
//  - have address.state in 'CA'
//  - and a car in the array of `cars` < 1970
{
  do: 'find',
  on: 'users',
  match: {
    or: [
      { 'address.state': {in:['CA']} },
      { 'cars.year': {lt:1970} }
    ]
  }
}

Where a field specifying a sub-property match is typed as an Array (eg. the User's cars field above), the match SHOULD apply to all elements in the Array. e.g each car is checked if its .year property is < 1970.

.body Stable

Type: Array of data elements

.bodyis an array containing one or more elements (usually Objects of arbitrary structure). .body MUST always be an Array, even when your data payload is only one object.

Elements in .body SHOULD be treated as sparse objects, and only apply the keys supplied.

// Example update all guitars:
// - set `onSale` field to `true`
// - set `secretKey` to `undefined` (unset)
{
  do: 'update',
  on: 'guitars',
  body: [{onSale: true, secret:undefined}]
}

Qe implementations MAY treat element fields set to undefined as an 'UNSET' command for schema-less stores. Otherwise specify an .update action with an unset operator.

A Qe .do action SHOULD apply to each element in the .body array.

However, when specifying .ids or other .match constraints, the .body field MUST be empty or contain only one element, and the action SHOULD apply the body element to matching .ids.

Note: To perform discrete data transforms (ie. different/conditional changes on differing records), use a dedicated control message (Qe) per transform.

// Example create multiple 'guitars'
{
  do: 'create',
  on: 'guitars',
  body: [
    {label:'Fender Stratocaster', price:450.75},
    {label:'Parker Fly', price:399.00}
  ]
}
// Example specifying a match within `ids` field
// (note ONLY one object in body)
{
  do: 'update',
  on: 'guitars',
  ids: ['12','35','17','332'],
  match: {and:[{price:{eq:260}}]},
  body: [{price: 250.00}]
}

.update Unstable

Do updates need to support 'deep updates' eg: {"users.cars.reviews":{push::"Great!"}}

Type: Array of update objects

The array of update objects SHOULD all be applied to every matching result (provided by .ids and/or .match conditions).

Update object format:

{ '$field': {'$op': $val} }

Where:

Note: Update objects have the same format as match objects

Updates are explicit instructions that inform non-idempotent changes to specific fields in an existing resource. If .update is present, the Qe do action MUST be 'update'.

Note: For idempotent set/unsetstyle operations, simply pass those fields in the .body field of the Qe

An example query with an .update field:

// Clearly describes an append/"add to" operation
{
  do:'update',
  on:'users',
  ids:['123'],
  update: [
    { comments: {push:['13','21']} }
  ]
}

// In HTTP parlance:
// PATCH /users/123
// Content-Type: application/json-patch+json
//
// [
//   {"op":"add","path":"/comments","value":["13","21"]}
// ]

// In Mongo parlance:
// db.users.update(
//   {_id:'123'},
//   {$push:
//     { "comments": {$each: ["13","21"]} }
//   });

Note: If .body is provided and is modifying the same key on a record as the .update field, there exists sufficient knowledge to collapse the non-idempotent update into the idempotent write (ie. combine the update for that key into the .body field).

As such if both .update and .body are acting on the same record field, the Qe SHOULD return an error.

Reserved update operators are: Stable

These operators SHOULD NOT be aliased or have their intended meaning altered.

Qe MAY specify other update operators (that SHOULD be non-idempotent operators). For example:

// Example of custom operator 'multiply'
{score: {multiply:3}}

.select Stable

Type: Array of strings

Field selector acting either as:

To act as a blacklist, strings are prepended with a -. Select SHOULD only act as a whitelist or a blacklist, not both.

If no .select is present, all fields SHOULD be returned.

// Get artists, leave off 'name','bio'
{
  do: 'find',
  on: 'artists',
  select: [ '-name', '-bio' ]
}

.populate Unstable

Type: Object - a hash of keys populate objects

Populates fields that refer to other resources.

The structure of the .populate field:

{ $field: { [key:'$key'] [, query:$subqe] } }

Where:

Populate objects MUST be unique by $field. For example:

{
  populate: {
    'posts':{},
    'tags': {query:{on:'Tgz'}}
  }
}

Populate object $subqe MAY be a blank Qe [], and SHOULD be a "find-style" Qe with the following considerations:

Populate $subqe MAY nest other .populate requests.

Example Qe with populate:

// Find all users, and:
// Populate 'entries' field with no more than 5 posts
// with higher 3 rating, exclude `post.comments`, and
// sub-populate the `post.sites` field
{
  do: 'find',
  on: 'users',
  populate: {
    entries: {
      query: {
        on: 'posts',
        match: { or: [ {rating:{gt:3}} ] },
        select: ['-comments'],
        limit: 5,
        populate: {
          sites:{}
        }
      }
    }
  }
}

.limit Stable

Type: Number

Maximum number of results to return.

Assume no limit if none specified. Qe services MAY restrict results anyway.

// Limit 25. Such limit.
{ limit: 25 }

.offset Unstable

Type: Number or match object Object

.offset enables two methods of modifying the index at which results are returned. When set as a:

Offset SHOULD be used in combination with a "find" style .do action.

Assume no offset if none present.

// For a set of possible records:
['a','b','c']

{offset:0}
// -> ['a','b','c']

{offset:1}
// -> ['b','c']

// As 'startAt' style:
{offset: {id: {eq:'1234'}}}

.sort Unstable

Type: Array of strings

Ordering strings take the form: "[-][$field]" where the first character MAY be a "-" to indicate reverse sorting, and the "$field" MAY be a text string to sort on.

The empty string "" indicates a default sort (usually an ascending list sorted by the default key, usually 'id'). A "-" string would indicate a descending list sorted on the default key.

As such, the following are valid:

// Only specify a direction to sort results on
{ sort: ["-"] }

// Only specify an index to sort on
{ sort: [ "country" ] }

Sub sorting is provided by adding parameters to order against. These parameters SHOULD be unique.

// Descending `age`, and ascending `name` for same age
{
  sort: [
    "-age", "name"
  ]
}

.meta Stable

Type: Object of arbitrary data

Meta data store acts as a catch-all for context specific meta information that may need to be attached to a query object message. Can be used similarly to the 'Header' block in an HTTP request or as the 'store' on a request that propagates through a system. MAY contain arbitrary data.

// Object hash:
{
  do: 'update',
  on: 'guitars',
  ids: ['11523'],
  body: [ {price:50} ],
  meta: {
    _authToken: 'xyzqwerty098'
  }
}

Implementing Qe: adapters

Experimental

  • canX flags vs. enabled = ["$feature1", ...]

The currently proposed granularity is ugly because features often have sub-capabilities (eg. limit: [byNumber, byMatch] and match having multiple operators and "deepMatch" etc.)

Qe consuming interfaces are referred to as Adapters.

See the Qe Adapter repo for a base implementation of this specification.

Services implementing a Qe consuming interface are strongly RECOMMENDED to provide a documented method to return a 'features' object . Much like an HTTP OPTIONS request to a resource, this object describes what Qe constructs are supported by a service.

If a field is not present that SHOULD be interpreted as not supporting a feature. A blank object treats all features as false/not-implemented.

If returning a populated features object it MUST provide a qeVersion string:

Arrays of strings for actions, updates and match operators:

Requirements and restrictions:

Boolean flags indicating support for specific Qe features:

Specific descriptions for custom fields:

An example response:

{
  qeVersion: "0.6",
  required: ["do", "on"],
  restricted: ["populate"],
  actions: ["create","find","update","remove"],
  updateOps: ["pull","push","inc","unset"],
  matchOps: ["eq","neq","in","nin","lt","gt"],
  canPopulate: false,
  canLimit: true,
  canOffsetByNumber: true,
  canOffsetByMatch: true,
  canInclude: true,
  canExclude: true
  meta: {
    _authToken: "A String used for authentication"
  }
}

License

Final

Maintained and released by Mekanika

Mekanika

Query Envelope Specification is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Creative Commons License