Chapter 16 · Volume II — The ADL Domain Language

Query Parameters

16.1 Overview

Every ADL list endpoint (GET /api/v1/{domain}/{resources}) automatically supports a standard set of query parameters for pagination, sorting, filtering, search, relationship loading, scope selection, field selection, distinct values, and counting. No configuration is required — these capabilities are generated from the resource’s field and relationship definitions.

The URL syntax and response format for each parameter are specified in §4.7. This chapter covers the domain author’s perspective: what is automatic, what requires configuration, how parameters interact with other features, and what performance implications to consider.

Automatic vs Configured

ParameterAutomaticConfiguration Required
Pagination (page, limit, offset)None
Sorting (sort)None — all stored fields are sortable
Filtering (filter[field])None — all stored fields are filterable
Search (search)features.search: true
Include (include)None — all declared relationships are includable
Scopes (scope)PartialAuto-generated for soft delete; custom scopes require declaration
Field selection (fields)None
DistinctNone
CountNone
Bulk operationsNone

16.2 Pagination

All list endpoints are paginated. The default page size is 100 records. The maximum page size is 1000.

Page-Based

GET /api/v1/blog/posts?page=2&limit=25

Returns totalCount in the response, enabling page number controls:

{
  "data": {
    "items": [ ... ],
    "count": 25,
    "totalCount": 142
  }
}

Offset-Based

GET /api/v1/blog/posts?offset=50&limit=25

Does NOT return totalCount (avoiding the cost of a COUNT query). Use for infinite scroll and “load more” patterns.

Default Sort

When no sort parameter is specified, records are ordered by createdAt ascending. This ensures stable pagination — records do not shift between pages as new records are created.


16.3 Sorting

Any stored field (regular or materialized computed) can be used as a sort key. Virtual computed fields cannot be sorted because they have no database column.

GET /api/v1/orders/orders?sort=-orderTotal,createdAt

Multi-Field Sort

Comma-separated field names apply in order. The first field is the primary sort; subsequent fields break ties:

GET /api/v1/blog/posts?sort=status,-publishedAt

Sort Direction

SyntaxDirection
sort=createdAtAscending
sort=-createdAtDescending
sort=-price,namePrice descending, then name ascending

Unsortable Fields

Field CategorySortable?Reason
Regular stored fieldsDatabase column
Materialized computedDatabase column
Aggregation computedDatabase column
Virtual computedNo database column
JSON/component/repeaterCannot ORDER BY JSON
Relationship fieldsNot a column on this table

Sorting by an unsortable field is silently ignored.


16.4 Filtering

Any stored field can be filtered. Filters are combined with AND.

Filter Operators

OperatorSyntaxSQLDescription
Equalsfilter[status]=published= 'published'Exact match (default)
Equalsfilter[status][eq]=published= 'published'Explicit equals
Not equalsfilter[status][ne]=draft!= 'draft'Exclude a value
Greater thanfilter[price][gt]=100> 100Numeric/date comparison
Greater or equalfilter[createdAt][gte]=2026-01-01>= '2026-01-01'Inclusive lower bound
Less thanfilter[price][lt]=50< 50Numeric/date comparison
Less or equalfilter[age][lte]=65<= 65Inclusive upper bound
Pattern matchfilter[title][like]=%deploy%LIKE '%deploy%'SQL LIKE with wildcards
In listfilter[status][in]=draft,reviewIN ('draft','review')Match any value

Filtering and Permissions

Field-level read permissions affect filtering. If a user does not have read access to a field, filtering by that field is silently ignored. This prevents information leakage through filter-based probing (e.g., discovering salary ranges by filtering on a field the user cannot read).

Filtering on Non-Existent Fields

Filtering on a field that does not exist is silently ignored. This enables forward-compatible clients that may send parameters the server has not yet defined.


Full-text search requires features.search: true on the resource (§15.5).

GET /api/v1/blog/posts?search=kubernetes

The search index covers all fields of type string, text, and markdown. The term is matched across all indexed fields simultaneously. Search can be combined with filters, sorting, and pagination.

If features.search is not enabled and ?search= is included, the parameter is silently ignored.


16.6 Relationship Loading

The ?include= parameter eager-loads related records. All declared relationships are includable without configuration.

GET /api/v1/blog/posts/post-1?include=author,tags,comments

List vs Get

EndpointBehavior
GET /:idIncluded relationships populated on the single record. No performance concern.
GET / (list)Each record in the list has included relationships populated. Generates additional queries per record per relationship. Use sparingly on large lists.

Nested Include

Nested includes (a relationship’s relationship) are NOT supported. Load nested data with separate requests.

Include and Permissions

Included relationships respect the related resource’s list permission. If the user lacks permission, the included relationship returns an empty array (not 403).


16.7 Scopes

Named query filters applied via ?scope=name.

When softDelete: true, three scopes are auto-generated: active (default), deleted, all. Custom scopes are declared in the scopes: block (§20).

Scopes and filters combine with AND — the scope filter applies first, then additional filters narrow the result.


16.8 Field Selection

GET /api/v1/blog/posts?fields=id,title,status,createdAt

Limits which fields appear in the response. Rules:

  • id is always included regardless of selection.
  • Virtual computed fields can be included — evaluated for selected fields only.
  • Included relationship data is NOT affected by field selection.
  • Fields the user lacks read permission for are excluded regardless.
  • Hidden fields (hidden: true) can be included if requestable: true.

16.9 Distinct Values

GET /api/v1/blog/posts/distinct?field=status

Returns unique values for a specified field. Only stored fields support distinct. The query respects the current scope and filters. Values are returned in ascending order. Field-level read permissions apply.


16.10 Count

GET /api/v1/blog/posts/count?filter[status]=published

Returns the number of matching records. Respects filters, search, and scopes. Does not use pagination, sorting, include, or field selection parameters.


16.11 Bulk Operations

Bulk endpoints accept arrays of records for batch processing. Always available — no feature flag required.

Bulk Create

POST /api/v1/blog/posts/bulk
[
  { "title": "Post 1", "body": "Content 1" },
  { "title": "Post 2", "body": "Content 2" },
  { "title": "Post 3", "body": "Content 3" }
]

Response (201):

{
  "data": {
    "created": 3,
    "failed": 0,
    "items": [ ... ],
    "errors": []
  }
}

Each record is processed through the full create pipeline: validation, hooks, computed fields, audit. Records that fail validation are reported; remaining records are still processed.

Bulk Update

PATCH /api/v1/blog/posts/bulk
[
  { "id": "uuid-1", "status": "published" },
  { "id": "uuid-2", "status": "published" }
]

Each object MUST include the id field. Records are updated individually through the full update pipeline.

Bulk Delete

DELETE /api/v1/blog/posts/bulk
{ "ids": ["uuid-1", "uuid-2", "uuid-3"] }

For soft-deletable resources, sets deletedAt. For others, permanently removes records.

Limits and Behavior

  • Maximum 100 records per bulk request. Exceeding returns 422 BULK_LIMIT_EXCEEDED.
  • Each record is subject to the same RBAC checks as an individual operation.
  • Records are processed independently — a failure on one record does not roll back others.
  • If atomic all-or-nothing behavior is required, use a Lua hook or workflow.

16.12 Performance Considerations

N+1 Queries from Include

?include= on list endpoints generates additional queries per record per relationship. For 100 posts with ?include=author,tags: 1 + 100 + 100 = 201 queries.

Mitigations:

  • Use ?include= sparingly on list endpoints; reserve for detail views.
  • Use ?fields= to reduce payload size.
  • Use caching: true on related reference data resources.
  • A future version MAY implement batch loading to eliminate N+1.

Filter Performance

Filtering on unindexed fields requires full table scans. Declare indexes (§19) for frequently-filtered fields:

indexes:
  - fields: [status]
  - fields: [authorId, status]
  - fields: [createdAt]

Search Performance

SQLite uses LIKE '%term%' (full scan regardless of indexes). PostgreSQL uses tsvector with GIN index (efficient full-text search). For large datasets with search as a core feature, PostgreSQL is strongly recommended.

Count Performance

SELECT COUNT(*) on large tables without indexes can be slow. totalCount in page-based pagination uses the same query. For read-heavy resources, caching: true caches both list results and counts.


Spatial filters (v0.3.1): on PostgreSQL + PostGIS, geometry fields accept filter[<geom>][bbox]=minLon,minLat,maxLon,maxLat, [intersects]=<GeoJSON>, and [within]=<GeoJSON>, plus ?simplify=<tolerance>. See Chapter 41 — GIS & Spatial Data.

End of Chapter 16.

Next: Chapter 17 — API Configuration