Skip to content

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.

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.

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 }
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
}
Kotlin FilterJSON Representation
Filter.TRUEtrue
Filter.FALSEfalse
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.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").

Deserialization uses smartParseJsonPrimitive to infer types: "true" → Boolean, "123" → Long, "123.45" → Double, ISO8601 string → Instant.

{
"entries": [
{ "key": "fieldNameA", "direction": "ASC" },
{ "key": "fieldNameB", "direction": "DESC" }
]
}
{ "index": 12, "size": 50 }
// 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.*" }
// AND
{ "and": [true, false] }
// OR
{ "or": [true, false] }
// NOT
{ "not": true }

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
}
}

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): Op<Boolean> — converts a Filter to an Exposed SQL condition
  • where(f: Filter, asOf: TimeCoordinates): Op<Boolean> — combines filter with bitemporal visibility
  • ordering(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.

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 type
val 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 translator
val 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) }
}

The ExposedLocatorTranslator.resolveColumn() method uses a multi-step resolution strategy:

  1. Structured path resolution — matches the locator as a JSON property path (e.g., identity.email → the identity_email column)
  2. Exact column name match — falls back to findColumn() which matches the raw SQL column name (e.g., identity_email)
  3. 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.

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)
}

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.

StyleExampleWhen to use
JSON field pathidentity.email, cardQuantity.amountPreferred for API clients — matches JSON response structure
Database column nameidentity_email, card_quantity_amountLegacy — still supported for backward compatibility

Note: Universes configured with EntityServiceConfiguration accept both locator styles. Universes without a configured translator accept only raw column names.

LocatorTypeDescription
eiduuidUnique entity ID
item_namevarchar(255)Display name
internal_skuvarchar(255)Internal SKU / part number
classification_typevarchar(255)Item type (e.g., Raw Material)
classification_sub_typevarchar(255)Item subtype
use_casevarchar(255)Use case category
gl_codevarchar(255)GL code for accounting
physical_locator_facilityvarchar(255)Facility name
physical_locator_departmentvarchar(255)Department
physical_locator_locationvarchar(255)Storage location
physical_locator_sub_locationvarchar(255)Sub-location
image_urlvarchar(8192)Product image URL
notesvarchar(8192)Item notes
card_notes_defaultvarchar(8192)Default notes for auto-created cards
retiredbooleanSoft-delete flag
taxablebooleanTax-applicable flag
primary_supply_suppliervarchar(255)Primary supplier name
primary_supply_skuvarchar(255)Supplier part number
primary_supply_order_methodvarchar(255)Order mechanism (see OrderMethod enum)
primary_supply_urlvarchar(8192)Supplier URL
primary_supply_order_quantity_amountdoubleOrder quantity
primary_supply_order_quantity_unitvarchar(255)Order unit (e.g., EA, BOX)
primary_supply_unit_cost_valuedoubleUnit cost
primary_supply_unit_cost_currencyvarchar(5)Currency code (see Currency enum)
primary_supply_average_lead_time_lengthintegerLead time value
primary_supply_average_lead_time_time_unitvarchar(10)Lead time unit (see TimeUnit enum)
secondary_supply_suppliervarchar(255)Secondary supplier
secondary_supply_skuvarchar(255)Secondary supplier SKU
secondary_supply_order_methodvarchar(255)Secondary order mechanism
secondary_supply_urlvarchar(8192)Secondary supplier URL
secondary_supply_order_quantity_amountdoubleSecondary order quantity
secondary_supply_order_quantity_unitvarchar(255)Secondary order unit
secondary_supply_unit_cost_valuedoubleSecondary unit cost
secondary_supply_unit_cost_currencyvarchar(5)Secondary currency
secondary_supply_average_lead_time_lengthintegerSecondary lead time
secondary_supply_average_lead_time_time_unitvarchar(10)Secondary lead time unit
default_supplyvarchar(255)Which supply chain is default
default_supply_eiduuidDefault supply entity ID
card_sizevarchar(255)Kanban card size (see CardSize enum)
label_sizevarchar(255)Label size
breadcrumb_sizevarchar(255)Breadcrumb label size
item_colorvarchar(255)Display color (see ItemColor enum)
min_quantity_amountdoubleMinimum quantity threshold
min_quantity_unitvarchar(255)Min quantity unit
descriptionvarchar(8192)Extended description
created_byvarchar(255)Creator identifier
created_at_effectivetimestampWhen the record was effectively created
created_at_recordedtimestampWhen the record was recorded in the system
effective_as_oftimestampBitemporal: when the fact was true
recorded_as_oftimestampBitemporal: when the fact was recorded

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)TypeDescription
eiduuidUnique card entity ID
serialNumberstringCard serial number
item.eIduuidReferenced item entity ID
item.namestringReferenced item display name
cardQuantity.amountdoubleOrder quantity amount
cardQuantity.unitstringOrder quantity unit
locator.facilitystringPhysical location: facility
locator.departmentstringPhysical location: department
locator.locationstringPhysical location: location
locator.subLocationstringPhysical location: sub-location
statusKanbanCardStatusCurrent card status (see enum below)
printStatusKanbanCardPrintStatusCurrent print status (see enum below)
notesstringCard-level notes
retiredbooleanSoft-delete flag (from EntityRecord wrapper)

Enum values used in locator comparisons and returned in query results.

ValueDescription
AVAILABLECard is available / at rest
REQUESTEDReorder has been requested
REQUESTINGRequest is being processed
IN_PROCESSOrder is being processed
READYOrder is ready
FULFILLINGFulfillment in progress
FULFILLEDOrder fulfilled
IN_USEItem is in use
DEPLETEDItem has been fully consumed
UNKNOWNStatus not determined
ValueDescription
REQUESTReorder requested
ACCEPTRequest accepted
SHELVECard shelved
START_PROCESSINGProcessing started
COMPLETE_PROCESSINGProcessing completed
FULFILLFulfillment action
RECEIVEItem received
USEItem put into use
DEPLETEItem depleted
WITHDRAWCard withdrawn
NONENo event
FAILED_ACTIONAction failed

NOT_PRINTED | PRINTED | LOST | DEPRECATED | RETIRED | UNKNOWN

UNKNOWN | PURCHASE_ORDER | EMAIL | PHONE | IN_STORE | ONLINE | RFQ | PRODUCTION | TASK | THIRD_PARTY | OTHER

SMALL | MEDIUM | LARGE | X_LARGE

BLUE | GREEN | YELLOW | ORANGE | RED | PINK | PURPLE | GRAY

USD | CAD | EUR | GBP | JPY | AUD | CNY | INR | RUB | BRL | ZAR | MXN | KRW | SGD | HKD | NZD | CHF

MILLI | SECOND | MINUTE | HOUR | DAY | WEEK | STANDARD_MONTH | LONG_MONTH | STANDARD_FEBRUARY | LEAP_FEBRUARY | STANDARD_YEAR | CALENDAR_YEAR | LEAP_YEAR

ASC | DESC

The Query DSL request body is accepted by POST /query and POST /details endpoints on DataAuthorityEndpoint services. The following endpoints currently support it.

EndpointMethodQuery DSL support
/v1/item/item/queryPOSTFull Query DSL (filter, sort, paginate)
/v1/item/item/detailsPOSTFull Query DSL — returns hydrated item details
/v1/item/item/{item-eid}/historyPOSTFull Query DSL — scoped to a single item’s history
EndpointMethodQuery DSL support
/v1/kanban/kanban-card/queryPOSTFull Query DSL (filter, sort, paginate)
/v1/kanban/kanban-card/detailsPOSTFull Query DSL — returns hydrated card details with embedded item
/v1/kanban/kanban-card/{card-eid}/historyPOSTFull 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.

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:

  1. Determine target type R and contextual data type TDATA
  2. Implement FilterVisitor<R, TDATA>
  3. Handle all Filter subtypes in the when expression; recursively call accept on composite filters
  4. Use TDATA for field mappings or configuration
  5. Return Result.success(value) or Result.failure(error)

The collectAll() extension function is useful for processing lists of Result<R>.