JSON Query Syntax Reference

The Resource Registry API provides a powerful JSON-based Query-By-Example (QBE) engine. This syntax allows clients to perform deep topological matching, graph traversal, and response projection without writing raw backend SQL.

The engine is universally accessible across all collections via their respective execution endpoints and read-only mirrors:

  • Contexts: QUERY /contexts and QUERY /access/contexts

  • Schema Types: QUERY /types and QUERY /access/types

  • Instances: QUERY /instances and QUERY /access/instances

  • Query Templates: QUERY /query-templates and QUERY /access/query-templates

This document serves as the formal reference manual for constructing these JSON queries.

Core Principles

  • Query-By-Example: The structure of the JSON query mimics the structure of the desired output. You nest objects (e.g., consistsOf, isRelatedTo) exactly as they appear in a standard representation.

  • Mandatory Root type: Every JSON query must explicitly declare the "type" keyword at its root. This instructs the query engine on which collection/class to scan.

    • When querying Contexts, the type is structurally fixed (e.g., "type": "Context").

    • When querying Schema Types, the type is one of the queryable Type-definition categories: "type": "ResourceSchema", "FacetSchema", "IsRelatedToSchema", "ConsistsOfSchema", or "PropertySchema" (not the concrete schema name being defined — e.g. to find the EService schema definition, query {"type": "ResourceSchema", "name": "EService"}, not {"type": "EService"}).

    • When querying Instances, the type is the specific schema name (e.g., "type": "EService" or "type": "HostingNode").

    • When querying Templates, the type is "type": "QueryTemplate".

  • Mandatory Nested type: Because the engine matches the query tree exactly against the database schema, the "type" keyword is currently strictly mandatory at every nested structural level of the query (e.g., inside consistsOf and isRelatedTo arrays, and within their source or target objects). Omitting the type inside a nested relationship causes the query evaluation to fail. (This is unrelated to depth-based name matching within Type-definition queries — see below — which is a distinct mechanism scoped to the Types collection only.)

  • System Keywords (_ Prefix): To avoid namespace collisions with user-defined property fields (Open-World paradigm), all query operators, traversal commands, and projection directives strictly begin with an underscore _ (e.g., _emit, _and, _eq, _source).

Depth-Based Type-Name Matching (Type-Definition Queries Only)

This section applies exclusively to QUERY /types (and its /access mirror), i.e. queries whose root "type" is one of the Type-definition categories (ResourceSchema, FacetSchema, IsRelatedToSchema, ConsistsOfSchema, PropertySchema). It does not apply to Instance, Context, or Query Template queries, where polymorphic instead controls entry-point subclass expansion (see polymorphic); the Types collection deliberately reuses depth instead, for consistency with GET /types/{type-name} within the same collection (see depth).

Type-definition records carry no OrientDB subclass relationship to each other — every registered schema in a category is a flat row in the same shared class, distinguished only by its own name and its declared extendedTypes. Consequently, an equality match against a Type name — the query’s own name field, or the source/target name inside a Relation Type definition — is resolved against the descendant closure of that name up to depth levels (the same Type-hierarchy walk that powers GET /types/{type-name}?depth=N), not just the literal name. depth=0 restricts the match to the exact name only; a positive depth=N includes descendants up to N levels down; depth=-1 includes all descendants recursively. On QUERY /types, depth defaults to -1 (polymorphic-equivalent matching, consistent with the polymorphic=true default on every other QUERY endpoint in this API — see the depth section for the full default-value rationale).

Example — depth-based source/target matching on a Relation Type:

{
  "type": "IsRelatedToSchema",
  "source": "Service",
  "target": "Service"
}

With the default depth=-1 (or an explicit ?depth=-1), this matches Uses even though Uses is declared between two EService instances (a subtype of Service), because EService is a descendant of Service. With an explicit ?depth=0, only Relation Type definitions declared with the literal source and target name Service are matched.

Example — depth-based name matching combined with a structural constraint:

{
  "type": "FacetSchema",
  "name": "Facet",
  "properties": { /* a constraint matching an element with property name "name" */ }
}

Facet is the abstract base schema and declares no properties of its own. With the default depth=-1 (or an explicit ?depth=-1), the name match expands to Facet and every registered descendant (e.g. ContactFacet), and any other constraint in the same query — such as this properties check — is then evaluated independently against each candidate in that expanded set, using the array-matching operators already described under Comparison Operators. Result: every Facet Type definition (base or descendant) that declares a property named name, e.g. ContactFacet. With an explicit ?depth=0, this query returns an empty array instead (the literal Facet record does not have a property named name).

Response Projection (_emit)

By default, a query returns the full representation of the matched instances. To optimize payloads and return only specific fields (GraphQL style), use the _emit block.

The _emit object acts as an alias map: the keys are the actual fields in the database, and the values are the custom aliases you want returned in the final JSON response.

Canonical _emit Precedence Rules

This subsection is normative and serves as the single source of truth for _emit behavior across direct JSON queries and stored query-template execution:

  1. Projection-first behavior: When _emit is present, the response is strictly projection-based (selected fields/aliases) and will not return the full instance representation.

  2. Metadata/context expansion disabled: Structural inclusion parameters such as metadataPolicy and includeContexts are completely ignored and have no effect when _emit is used.

  3. Privacy masking still enforced: If _emit projects sensitive fields like metadata.createdBy or metadata.lastUpdateBy (under any alias), their values are strictly masked as "[REDACTED]" for unprivileged roles (IS-Admin / Ctx-M, IS-Editor, IS-Contrib, IS-Member).

  4. Alias-agnostic enforcement: Privacy masking is applied based on the true source of the projected field, regardless of the custom alias name chosen by the client.

  5. Scope of application: These rules apply uniformly to all query engines (QUERY /{collection}, QUERY /query-templates/{name}, their respective /access mirrors, and the corresponding POST .../query compatibility fallbacks).

Projecting Context Fields

{
  "type": "Context",
  "effectiveState": "SUSPENDED",
  "_emit": {
    "id": "ContextUUID",
    "name": "ContextName",
    "path" : "ContextPath"
  }
}

Projecting Type Schema Fields

{
  "type": "ResourceSchema",
  "_emit": {
    "id": "UUID",
    "name": "Name",
    "description": "Description"
  }
}

Projecting Instance Fields

{
  "type": "EService",
  "_emit": {
    "id": "ServiceID",
    "type": "ElementType"
  }
}

Deep Projection

You can place _emit blocks at any nested level of the query to project fields from related elements into a flat response object.

{
  "type": "EService",
  "consistsOf": [
    {
      "type": "IsIdentifiedBy",
      "target": {
        "type": "SoftwareFacet",
        "_emit": {
          "name": "SoftwareName",
          "version": "SoftwareVersion"
        }
      }
    }
  ]
}

Projecting QueryTemplate Fields

{
  "type": "EService",
  "_emit": {
    "id": "ServiceID",
    "type": "ElementType"
  }
}

Logical Operators

The engine supports explicitly grouping constraints using _and, _or, and _not.

_or (Match ANY condition)

Use _or when you need to match across completely different topological branches or distinct fields (for simple multi-value matching on the same field, prefer the _in operator).

{
  "type": "EService",
  "_or": [
    {
      "consistsOf": [
        { 
          "type": "ConsistsOf",
          "target": { "type": "StateFacet", "value": "down" } 
        }
      ]
    },
    {
      "isRelatedTo": [
        { 
          "type": "Activates", 
          "source": { "type": "HostingNode" } 
        }
      ]
    }
  ]
}

_and (Match ALL conditions)

Normally, providing multiple fields in a JSON object acts as an implicit AND. However, you can use _and for complex groupings.

{
  "type": "EService",
  "_and": {
    "id": "1a50a46d-2210-4a17-bd2f-22450b109264",
    "metadata": { "createdBy": "luca.frosini" }
  }
}

Comparison Operators

By default, providing a scalar value (e.g., "name": "gcat") implies an Exact Match (_eq). For advanced filtering, you can supply an operator object.

| Operator | Description | |———-|————-| | _eq | Exact match (default behavior). | | _ne | Not equal to the specified value. | | _gt | Greater than. | | _gte | Greater than or equal to. | | _lt | Less than. | | _lte | Less than or equal to. | | _between | Value is between two boundaries. | | _is | Evaluates nullity (e.g., _is: "NULL"). | | _like | Simple wildcard matching (e.g., Luk%). | | _containsText | Full-text search match. | | _matches | Regular expression match. | | _isDefined | Evaluates if the field exists (true/false). | | _isNotDefined | Evaluates if the field does not exist. |

Collection / Map Operators

For property fields that contain collections or maps: | Operator | Description | |———-|————-| | _in | Matches any value in the provided array. | | _contains | The target array contains the specified element. | | _containsAll | The target array contains all the specified elements. | | _containsAny | The target array contains at least one of the specified elements. | | _containsKey | The target map contains the specified key. | | _containsValue | The target map contains the specified value. |

Example: Not Equal (_ne)

{
  "type": "StateFacet",
  "metadata": {
    "createdBy": { "_ne": "luca.frosini" }
  }
}

Inverse Traversal (_source)

Standard queries traverse the graph forwards (e.g., from Resource to its ConsistsOf relations to the Facet). However, if your query’s entry point (root node) is a Facet or Relation, you can traverse backwards to find the Resource that owns it by using the _source keyword.

(Note: _source is strictly permitted only on the entry-point object of the JSON query. You cannot use it deep within a forward-traversal chain).

Example: Finding the EService that owns a specific StateFacet

{
  "type": "StateFacet",
  "value": "down",
  "_source": {
    "type": "ConsistsOf",
    "source": {
      "type": "EService",
      "_emit": {
        "id": "ServiceUUID"
      }
    }
  }
}

Query Templates (Variables)

When the JSON query is saved as a QueryTemplate (via PUT /query-templates/{name}), you can insert template variables prefixed with $. These variables are dynamically substituted at execution time based on the JSON body provided to QUERY /query-templates/{name}.

Target Limitation: The Query Template mechanism is structurally restricted to Instances. Therefore, the root "type" in a saved template must always specify a valid Entity or Relation type (e.g., Resource, EService). Query templates cannot be used to save queries targeting Contexts, Type Schemas, or other Query Templates.

{
  "type": "EService",
  "consistsOf": [
    {
      "type": "ConsistsOf",
      "target": {
        "type": "StateFacet",
        "value": "$targetState"
      }
    }
  ]
}

(Execution Payload)

{
  "$targetState": "ready"
}