Query DSL
The Arda Query DSL is a JSON-based language for filtering, sorting, and paginating records across the Arda Cloud API. It applies to both the Item and Kanban Card domains, and to any DataAuthorityEndpoint.
A query is a JSON object with three optional top-level keys:
{ "filter": { }, "sort": { }, "paginate": { }}All three are optional. Omitting filter returns all records. Omitting paginate uses server defaults.
Kotlin DSL Structure
Section titled “Kotlin DSL Structure”The Query DSL is defined by Kotlin data classes and sealed interfaces in Query.kt (cards.arda.common.lib.lang.query).
The top-level object:
data class Query( val filter: Filter = Filter.TRUE, val sort: Sort = Sort.NO_SORT, val paginate: Pagination = Pagination.FIRST_PAGE)Helper properties thisPage, nextPage, and previousPage are GZIPed and Base64 URL encoded string representations of the Query object, suitable for use as page cursors.
Filter
Section titled “Filter”A sealed interface representing a filtering condition:
Filter├── Filter.Literal│ ├── Filter.TRUE — always evaluates to true│ └── Filter.FALSE — always evaluates to false├── Filter.Composite│ ├── Filter.And(clauses: List<Filter>)│ └── Filter.Or(clauses: List<Filter>)├── Filter.Transform│ └── Filter.Not(clause: Filter)└── Filter.Term ├── Filter.UnaryTerm │ ├── Filter.IsNull(locator) │ └── Filter.IsNotNull(locator) ├── Filter.CompareTerm<FIELD> │ ├── Filter.Eq(locator, value) │ ├── Filter.Ne(locator, value) │ ├── Filter.Gt(locator, value) │ ├── Filter.Ge(locator, value) │ ├── Filter.Lt(locator, value) │ ├── Filter.Le(locator, value) │ └── Filter.RegEx(locator, value: String) └── Filter.SetTerm<FIELD> ├── Filter.In(locator, values: List<FIELD>) └── Filter.NotIn(locator, values: List<FIELD>)Locator is a type alias for String, identifying a field/property path (e.g., "fieldName", "nested.object.field").
data class Sort(val entries: List<SortEntry>) { companion object { val NO_SORT = Sort(emptyList()) }}
data class SortEntry(val key: Locator, val direction: SortDirection)enum class SortDirection { ASC, DESC }Pagination
Section titled “Pagination”data class Pagination(val index: Int, val size: Int) { companion object { val FIRST_PAGE = Pagination(0, 20) } val next: Pagination get() = copy(index = index + 1) val previous: Pagination? get() = if (index > 0) copy(index = index - 1) else null val start: Int get() = index * size}JSON Mapping
Section titled “JSON Mapping”Filter JSON Serialization
Section titled “Filter JSON Serialization”| Kotlin Filter | JSON Representation |
|---|---|
Filter.TRUE | true |
Filter.FALSE | false |
Filter.And | { "and": [<clause1>, <clause2>, ...] } |
Filter.Or | { "or": [<clause1>, <clause2>, ...] } |
Filter.Not | { "not": <clause> } |
Filter.IsNull | { "isNull": "fieldName" } |
Filter.IsNotNull | { "isNotNull": "fieldName" } |
Filter.Eq | { "locator": "fieldName", "eq": <value> } |
Filter.Ne | { "locator": "fieldName", "ne": <value> } |
Filter.Gt | { "locator": "fieldName", "gt": <value> } |
Filter.Ge | { "locator": "fieldName", "ge": <value> } |
Filter.Lt | { "locator": "fieldName", "lt": <value> } |
Filter.Le | { "locator": "fieldName", "le": <value> } |
Filter.EqI | { "locator": "fieldName", "eqI": "text" } |
Filter.NeI | { "locator": "fieldName", "neI": "text" } |
Filter.RegEx | { "locator": "fieldName", "regex": "pattern" } |
Filter.In | { "in": { "locator": "fieldName", "values": [...] } } |
Filter.NotIn | { "notIn": { "locator": "fieldName", "values": [...] } } |
Instant values are serialized as ISO 8601 strings (e.g., "2023-10-27T10:30:00Z").
Case-insensitive equality — eqI and neI
Section titled “Case-insensitive equality — eqI and neI”eqI and neI compare text without regard to case, as terms of the algebra rather than as an anchored regex.
The distinction matters because the regex spelling was a trap rather than merely verbose: a pattern makes the caller responsible for escaping, so a user typing C++ or a.b into an equality filter got a pattern instead of a literal unless every call site remembered to escape — and C++ is not a valid pattern at all. It also lost the intent on the wire, so a reader of a stored request could not tell an exact match from a case-folded one.
Three things to know before using them:
- The value must be text. These two terms advertise string alone, where the others accept string, number or boolean. A value of another type is refused at the wire; a locator resolving to a non-text column is refused when the filter is compiled. Enum columns are accepted, since they store the constant’s name.
- Neither term matches an absent value, in either engine.
lower(col) <> 'acme'is unknown rather than true when the column holds nothing, so the row is not returned. This makes case-folded inequality read differently from ordinaryne. - Case is folded on both sides, not by giving the column a case-insensitive type — SQL
lower()on the column, and the value lowered before binding, which is why the value appears already lowercase in the example above. See Bitemporal Indexes for what that means when a query on this gets slow.
Deserialization uses smartParseJsonPrimitive to infer types: "true" → Boolean, "123" → Long, "123.45" → Double, ISO8601 string → Instant.
Sort JSON
Section titled “Sort JSON”{ "entries": [ { "key": "fieldNameA", "direction": "ASC" }, { "key": "fieldNameB", "direction": "DESC" } ]}Pagination JSON
Section titled “Pagination JSON”{ "index": 12, "size": 50 }JSON Examples
Section titled “JSON Examples”Primitive Term Examples
Section titled “Primitive Term Examples”// Boolean comparison{ "locator": "field", "eq": true }
// Number comparison{ "locator": "field", "gt": 42 }
// String comparison{ "locator": "field", "eq": "value" }
// Instant comparison{ "locator": "field", "eq": "2025-08-12T23:41:30.399755Z" }
// IN with numbers{ "in": { "locator": "field", "values": [1, 2, 3] } }
// IN with strings{ "in": { "locator": "field", "values": ["a", "b", "c"] } }
// NOT IN{ "notIn": { "locator": "field", "values": ["x", "y"] } }
// IS NULL{ "isNull": "field" }
// IS NOT NULL{ "isNotNull": "field" }
// REGEX{ "locator": "field", "regex": "^abc.*" }Composite Operator Examples
Section titled “Composite Operator Examples”// AND{ "and": [true, false] }
// OR{ "or": [true, false] }
// NOT{ "not": true }Full Query Example
Section titled “Full Query Example”Active items in a specific facility, sorted by location:
{ "filter": { "and": [ { "locator": "retired", "eq": false }, { "locator": "physical_locator_facility", "eq": "Building A" }, { "notIn": { "locator": "classification_type", "values": ["Deprecated", "Archive"] } }, { "not": { "isNull": "primary_supply_supplier" } } ] }, "sort": { "entries": [ { "key": "physical_locator_location", "direction": "ASC" } ] }, "paginate": { "index": 0, "size": 100 }}Compiling Queries to SQL (QueryCompiler)
Section titled “Compiling Queries to SQL (QueryCompiler)”The QueryCompiler translates Filter and Sort DSL components into Kotlin Exposed SQL operators.
QueryCompiler(private val tbl: EntityTable, private val translator: ExposedLocatorTranslator? = null)When a translator is provided, locator resolution uses the structured translator first (see below), then falls back to the table’s column lookup. Without a translator, locators resolve directly against the EntityTable’s column names.
Key methods:
filter(f: Filter, asOf: TimeCoordinates): Op<Boolean>— converts a Filter to an Exposed SQL condition, at a stated point in timefilterWithoutReadTime(f: Filter): Op<Boolean>— for the two reads that genuinely have no such momentwhere(f: Filter, asOf: TimeCoordinates): Op<Boolean>— combines filter with bitemporal visibilityordering(sort: Sort): List<Pair<Expression<*>, SortOrder>>— converts Sort to Exposed ordering (prepends bitemporal default sort)DecoratePaging(raw: SizedIterable<T>, p: Pagination): SizedIterable<T>— applies limit/offset
FilterExposedVisitor implements FilterVisitor<Op<Boolean>, EntityTable>, using the EntityTable to resolve locators to columns.
Compiling a filter requires a read time
Section titled “Compiling a filter requires a read time”Since 16.0.0, compiling a filter requires the moment it will answer at. This is required rather than optional, and the reason is worth understanding before working around it.
Only quantified locators consult the read time — roles[any].role resolves to a subquery over a child table, which has to be confined to the child rows visible at that instant. But whether a locator is quantified is decided by a string in a request body, not by the code issuing the read. A caller therefore cannot know in advance that it will not need one.
An optional argument could not distinguish a read that has a moment and forgot to pass it from one that genuinely has none — and adapting the call sites immediately found the first kind: a caller-supplied filter on the item fuzzy-lookup path, compiled with nothing, which would have refused any collection-crossing filter a client sent it.
The two reads that genuinely have no read time — those addressing specific record versions by id — say so by calling filterWithoutReadTime. A quantified locator reaching that path is refused rather than answered without one. crossesCollection(locator) lets a caller without a read time check whether it is about to need what it does not have.
A quantified child locator sees only the visible children
Section titled “A quantified child locator sees only the visible children”roles[any].role, supplies[any]… and lines[any]… compile to an EXISTS over the child table, and that subquery is confined the same way the outer query confines the parent: a bitemporal cut-off at the read time, latest-version-per-entity selection, and the soft-delete check.
Without that confinement the filter meant ever held this value, in any version, at any time rather than holds it now — a bitemporal table keeps every version of every entity, live and retired, so the rows a child table contains are not the rows a caller can see.
Two properties of the construction are load-bearing:
- The confinement is applied to the child set before the quantifier, not folded into the caller’s own term. Folded in,
[all]would read “no child is both visible and failing” — vacuously true for an entity whose every child is invisible, so an entity whose children had all been removed would satisfy any “all children are X” filter. - Retired children are excluded unconditionally, even under
includeRetired. That flag widens which entities a read returns; a filter term answers a different question — whether an entity’s collection holds a value now. Letting the flag reach into the subquery would silently change what every collection filter means for administrative reads.
EntityServiceConfiguration and Structured Locator Translators
Section titled “EntityServiceConfiguration and Structured Locator Translators”EntityServiceConfiguration introspects a @Serializable entity payload class and builds an ExposedLocatorTranslator that maps JSON field names (camelCase property paths) to database columns. This allows API clients to use the same field names they see in JSON responses as filter/sort locators.
// Define once per entity typeval myQueryConfig = EntityServiceConfiguration.create(MyEntity.Entity::class) { opaque("settings") // exclude fields that are not filterable (e.g., JSON blobs)}.also { it.freeze() }
// Bind to a table to get a translatorval translator = myQueryConfig.bindToTable(MY_TABLE)The translator is then passed to QueryCompiler or set as the universe’s translator property:
class MyUniverse : AbstractScopedUniverse<...>() { override val translator by lazy { myQueryConfig.bindToTable(persistence.bt) }}Locator Resolution Order
Section titled “Locator Resolution Order”The ExposedLocatorTranslator.resolveColumn() method uses a multi-step resolution strategy:
- Structured path resolution — matches the locator as a JSON property path (e.g.,
identity.email→ theidentity_emailcolumn) - Exact column name match — falls back to
findColumn()which matches the raw SQL column name (e.g.,identity_email) - Case-insensitive and underscore-insensitive match — handles minor naming variations
This ensures backward compatibility: existing filters using snake_case column names continue to work alongside new camelCase JSON field paths.
Child Universe Translator Pattern
Section titled “Child Universe Translator Pattern”For child universes, the translator must be bound to a ChildTable. Due to invariant generics on ExposedLocatorTranslator, an explicit cast with a runtime guard is required:
override val translator by lazy { check(persistence.bt is ChildTable) { "MyChildUniverse requires a ChildTable, got ${persistence.bt::class}" } myChildQueryConfig.bindToTable(persistence.bt as ChildTable)}Locator Reference
Section titled “Locator Reference”Locators can be expressed as either JSON payload field paths (camelCase, matching the serialized field names) or database column names (snake_case). The structured EntityServiceConfiguration translator accepts both forms; a QueryCompiler without a configured translator accepts only raw column names.
| Style | Example | When to use |
|---|---|---|
| JSON field path | identity.email, cardQuantity.amount | Preferred for API clients — matches JSON response structure |
| Database column name | identity_email, card_quantity_amount | Legacy — still supported for backward compatibility |
Note: Universes configured with EntityServiceConfiguration accept both locator styles. Universes without a configured translator accept only raw column names.
Item Entity
Section titled “Item Entity”| Locator | Type | Description |
|---|---|---|
eid | uuid | Unique entity ID |
item_name | varchar(255) | Display name |
internal_sku | varchar(255) | Internal SKU / part number |
classification_type | varchar(255) | Item type (e.g., Raw Material) |
classification_sub_type | varchar(255) | Item subtype |
use_case | varchar(255) | Use case category |
gl_code | varchar(255) | GL code for accounting |
physical_locator_facility | varchar(255) | Facility name |
physical_locator_department | varchar(255) | Department |
physical_locator_location | varchar(255) | Storage location |
physical_locator_sub_location | varchar(255) | Sub-location |
image_url | varchar(8192) | Product image URL |
notes | varchar(8192) | Item notes |
card_notes_default | varchar(8192) | Default notes for auto-created cards |
retired | boolean | Soft-delete flag |
taxable | boolean | Tax-applicable flag |
primary_supply_supplier | varchar(255) | Primary supplier name |
primary_supply_sku | varchar(255) | Supplier part number |
primary_supply_order_method | varchar(255) | Order mechanism (see OrderMethod enum) |
primary_supply_url | varchar(8192) | Supplier URL |
primary_supply_order_quantity_amount | double | Order quantity |
primary_supply_order_quantity_unit | varchar(255) | Order unit (e.g., EA, BOX) |
primary_supply_unit_cost_value | double | Unit cost |
primary_supply_unit_cost_currency | varchar(5) | Currency code (see Currency enum) |
primary_supply_average_lead_time_length | integer | Lead time value |
primary_supply_average_lead_time_time_unit | varchar(10) | Lead time unit (see TimeUnit enum) |
secondary_supply_supplier | varchar(255) | Secondary supplier |
secondary_supply_sku | varchar(255) | Secondary supplier SKU |
secondary_supply_order_method | varchar(255) | Secondary order mechanism |
secondary_supply_url | varchar(8192) | Secondary supplier URL |
secondary_supply_order_quantity_amount | double | Secondary order quantity |
secondary_supply_order_quantity_unit | varchar(255) | Secondary order unit |
secondary_supply_unit_cost_value | double | Secondary unit cost |
secondary_supply_unit_cost_currency | varchar(5) | Secondary currency |
secondary_supply_average_lead_time_length | integer | Secondary lead time |
secondary_supply_average_lead_time_time_unit | varchar(10) | Secondary lead time unit |
default_supply | varchar(255) | Which supply chain is default |
default_supply_eid | uuid | Default supply entity ID |
card_size | varchar(255) | Kanban card size (see CardSize enum) |
label_size | varchar(255) | Label size |
breadcrumb_size | varchar(255) | Breadcrumb label size |
item_color | varchar(255) | Display color (see ItemColor enum) |
min_quantity_amount | double | Minimum quantity threshold |
min_quantity_unit | varchar(255) | Min quantity unit |
description | varchar(8192) | Extended description |
created_by | varchar(255) | Creator identifier |
created_at_effective | timestamp | When the record was effectively created |
created_at_recorded | timestamp | When the record was recorded in the system |
effective_as_of | timestamp | Bitemporal: when the fact was true |
recorded_as_of | timestamp | Bitemporal: when the fact was recorded |
Kanban Card Entity
Section titled “Kanban Card Entity”The fields below are drawn from the KanbanCard.Entity schema. The eid locator is confirmed to work against the database column directly. For other fields, the EntityServiceConfiguration translator maps JSON field paths to DB column names; if no translator is in effect, use the snake_case column name equivalent (e.g., serial_number instead of serialNumber).
| Locator (JSON path) | Type | Description |
|---|---|---|
eid | uuid | Unique card entity ID |
serialNumber | string | Card serial number |
item.eId | uuid | Referenced item entity ID |
item.name | string | Referenced item display name |
cardQuantity.amount | double | Order quantity amount |
cardQuantity.unit | string | Order quantity unit |
locator.facility | string | Physical location: facility |
locator.department | string | Physical location: department |
locator.location | string | Physical location: location |
locator.subLocation | string | Physical location: sub-location |
status | KanbanCardStatus | Current card status (see enum below) |
printStatus | KanbanCardPrintStatus | Current print status (see enum below) |
notes | string | Card-level notes |
retired | boolean | Soft-delete flag (from EntityRecord wrapper) |
Enum Reference
Section titled “Enum Reference”Enum values used in locator comparisons and returned in query results.
KanbanCardStatus
Section titled “KanbanCardStatus”| Value | Description |
|---|---|
AVAILABLE | Card is available / at rest |
REQUESTED | Reorder has been requested |
REQUESTING | Request is being processed |
IN_PROCESS | Order is being processed |
READY | Order is ready |
FULFILLING | Fulfillment in progress |
FULFILLED | Order fulfilled |
IN_USE | Item is in use |
DEPLETED | Item has been fully consumed |
UNKNOWN | Status not determined |
KanbanCardEventType
Section titled “KanbanCardEventType”| Value | Description |
|---|---|
REQUEST | Reorder requested |
ACCEPT | Request accepted |
SHELVE | Card shelved |
START_PROCESSING | Processing started |
COMPLETE_PROCESSING | Processing completed |
FULFILL | Fulfillment action |
RECEIVE | Item received |
USE | Item put into use |
DEPLETE | Item depleted |
WITHDRAW | Card withdrawn |
NONE | No event |
FAILED_ACTION | Action failed |
KanbanCardPrintStatus
Section titled “KanbanCardPrintStatus”NOT_PRINTED | PRINTED | LOST | DEPRECATED | RETIRED | UNKNOWN
OrderMethod
Section titled “OrderMethod”UNKNOWN | PURCHASE_ORDER | EMAIL | PHONE | IN_STORE | ONLINE | RFQ | PRODUCTION | TASK | THIRD_PARTY | OTHER
CardSize / LabelSize / BreadcrumbSize
Section titled “CardSize / LabelSize / BreadcrumbSize”SMALL | MEDIUM | LARGE | X_LARGE
ItemColor
Section titled “ItemColor”BLUE | GREEN | YELLOW | ORANGE | RED | PINK | PURPLE | GRAY
Currency
Section titled “Currency”USD | CAD | EUR | GBP | JPY | AUD | CNY | INR | RUB | BRL | ZAR | MXN | KRW | SGD | HKD | NZD | CHF
TimeUnit
Section titled “TimeUnit”MILLI | SECOND | MINUTE | HOUR | DAY | WEEK | STANDARD_MONTH | LONG_MONTH | STANDARD_FEBRUARY | LEAP_FEBRUARY | STANDARD_YEAR | CALENDAR_YEAR | LEAP_YEAR
SortDirection
Section titled “SortDirection”ASC | DESC
Applicable Endpoints
Section titled “Applicable Endpoints”The Query DSL request body is accepted by POST /query and POST /details endpoints on DataAuthorityEndpoint services. The following endpoints currently support it.
Item API
Section titled “Item API”| Endpoint | Method | Query DSL support |
|---|---|---|
/v1/item/item/query | POST | Full Query DSL (filter, sort, paginate) |
/v1/item/item/details | POST | Full Query DSL — returns hydrated item details |
/v1/item/item/{item-eid}/history | POST | Full Query DSL — scoped to a single item’s history |
Kanban Card API
Section titled “Kanban Card API”| Endpoint | Method | Query DSL support |
|---|---|---|
/v1/kanban/kanban-card/query | POST | Full Query DSL (filter, sort, paginate) |
/v1/kanban/kanban-card/details | POST | Full Query DSL — returns hydrated card details with embedded item |
/v1/kanban/kanban-card/{card-eid}/history | POST | Full Query DSL — scoped to a single card’s history |
All other endpoints on these APIs (single-record GET/PUT/DELETE, bulk-update, upload, lifecycle events) do not accept the Query DSL body. See the API Endpoint Catalog for the full list of endpoints and their OpenAPI specs.
Creating New Visitors
Section titled “Creating New Visitors”The FilterVisitor<R, TDATA> interface allows extending the DSL for new compilation targets.
interface FilterVisitor<R, TDATA: Any> { fun visit(filter: Filter, data: TDATA): Result<R>}Steps to create a new visitor:
- Determine target type
Rand contextual data typeTDATA - Implement
FilterVisitor<R, TDATA> - Handle all
Filtersubtypes in thewhenexpression; recursively callaccepton composite filters - Use
TDATAfor field mappings or configuration - Return
Result.success(value)orResult.failure(error)
The collectAll() extension function is useful for processing lists of Result<R>.
Copyright: © Arda Systems 2025-2026, All rights reserved