You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(spec): QuerySchema declares the search contract ADR-0061 actually serves
The first CI run of the #3899 request-schema gate rejected the dogfood
search proof's own wire shape: `{ search: 'retail', searchFields:
['industry'] }` answered 400. The schema was the wrong half — QuerySchema
declared only the structured FullTextSearchSchema form while ADR-0061 D1
("the client sends only the query text"), the engine executor
(search-filter.ts), and the search-conformance ledger all serve the bare
string plus the validated `searchFields` narrowing.
- `search` becomes `string | FullTextSearch` (string is the canonical
Tier-1 spelling; the object form keeps the declared Tier-2 knobs)
- `searchFields` is formally declared (the ADR's own P1 item), noted as
server-intersected — can only narrow, never widen
- rest request-schema gate pins the ADR-0061 wire shape as a positive
case so entry validation can never 400 it again
- regenerated: references docs + authorable-surface (adds
`data/Query:searchFields`); all 8 artifact gates green
spec suite 7145 green; both previously-failing dogfood files pass
locally (showcase-search 4/4, two-factor-lockout 5/5 — the 2FA pair does
not reproduce here and is green on main @ 5d21a48's identical job).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EynH7cngDczRGMkGMudJpC
Copy file name to clipboardExpand all lines: content/docs/references/api/contract.mdx
+3-2Lines changed: 3 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -149,7 +149,8 @@ const result = ApiError.parse(data);
149
149
|**object**|`string`| ✅ | Object name (e.g. account) |
150
150
|**fields**|`string[]`| optional | Fields to retrieve — field names, optionally dotted to reach through a relationship (`owner.name`). Related *records* are selected with `expand`, not from inside this list. |
|**search**|`string \| { query: string; fields?: string[]; fuzzy: boolean; operator: Enum<'and' \| 'or'>; … }`| optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
153
+
|**searchFields**|`string[]`| optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
|**limit**|`number`| optional | Max records to return (LIMIT) |
155
156
|**offset**|`number`| optional | Records to skip (OFFSET) |
@@ -161,7 +162,7 @@ const result = ApiError.parse(data);
161
162
|**having**|`any`| optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
162
163
|**windowFunctions**|`any`| optional |[REMOVED]`query.windowFunctions` was removed in @objectstack/spec 18 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. |
163
164
|**distinct**|`any`| optional |[REMOVED]`query.distinct` was removed in @objectstack/spec 18 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. |
164
-
|**expand**|`Record<string, { object: string; fields?: string[]; where?: any; search?: object; … }>`| optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. |
165
+
|**expand**|`Record<string, { object: string; fields?: string[]; where?: any; search?: string \| { query: string; fields?: string[]; fuzzy: boolean; operator: Enum<'and' \| 'or'>; … }; … }>`| optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. |
Copy file name to clipboardExpand all lines: content/docs/references/data/data-engine.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -548,7 +548,7 @@ QueryAST-aligned query options for IDataEngine.find() operations
548
548
|**top**|`number`| optional ||
549
549
|**cursor**|`any`| optional |[REMOVED]`query.cursor` was removed in @objectstack/spec 18 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
|**distinct**|`any`| optional |[REMOVED]`query.distinct` was removed in @objectstack/spec 18 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. |
Copy file name to clipboardExpand all lines: content/docs/references/data/query.mdx
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -126,7 +126,8 @@ Type: `string`
126
126
|**object**|`string`| ✅ | Object name (e.g. account) |
127
127
|**fields**|`string[]`| optional | Fields to retrieve — field names, optionally dotted to reach through a relationship (`owner.name`). Related *records* are selected with `expand`, not from inside this list. |
|**search**|`string \| { query: string; fields?: string[]; fuzzy: boolean; operator: Enum<'and' \| 'or'>; … }`| optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
130
+
|**searchFields**|`string[]`| optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
0 commit comments