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
{{ message }}
Repository navigation
Commit ffd5d56
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: docs/features/api-contract.md
+30-4Lines changed: 30 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -146,7 +146,7 @@ or use the TS-only `?search` extension, which IS case-insensitive.
146
146
147
147
### TS-only filter extensions (not part of the cross-port contract)
148
148
149
-
The TypeScript runtime parser ships six filter behaviors beyond the nine
149
+
The TypeScript runtime parser ships seven filter behaviors beyond the nine
150
150
operators. They are **NOT part of the cross-port REST contract** — the other
151
151
ports (Java, Kotlin, Python, C#) do not implement them, and a relying adopter
152
152
must not assume them on a non-TS backend. They are deliberately deferred until
@@ -160,6 +160,7 @@ added cross-port later as a purely additive, non-breaking change):
160
160
| leading-wildcard gating | a `like` pattern starting with `%` → HTTP 400 (`filter.leading_wildcard_disallowed`) |
161
161
| filter nesting-depth cap | rejects deeply-nested `or`/`and` (tied to the combinators) |
162
162
| bare filterable-field parameter |`?priority=low` where `priority` is in the allowlist → HTTP 400 `{ "error": "filter.bare_field", "field": "priority", "expected": "filter[priority][eq]=low" }` instead of silently returning every row. Any other unknown parameter (a cache-buster, a tracking tag) is still ignored, and the reserved list parameters (`filter`, `sort`, `limit`, `offset`, `search`, `withCount`) are never claimed |
163
+
| filter-value format check | a comparison value (`eq`/`ne`/`gt`/`gte`/`lt`/`lte`, and every element of an `in` list) that cannot be the field's type → HTTP 400 `{ "error": "invalid_filter_value", "field": "publishedOn", "op": "gte", "expected": "date (YYYY-MM-DD)" }` instead of reaching SQL, where SQLite compared the text and silently returned `[]` and Postgres failed the cast. Checked per field: `field.date` (a real calendar day), `field.time` (`HH:MM[:SS[.fff]]`), `field.timestamp` (a date, optionally with a time and a `Z`/offset), `field.uuid` (`8-4-4-4-12` hex), `field.enum` (a declared member — the response adds `allowed`), numbers (an empty value is not `0`) and booleans. The generated `<Entity>FilterAllowlist` carries the `format` / `enumValues` this needs; an allowlist generated before them still has a temporal value checked against all three temporal formats and an enum checked against the Drizzle column's own members. The envelope is the cross-port one; what is TS-only is refusing a malformed comparison value — the other ports pass it through to the database and only the `isNull` value is corpus-gated |
163
164
164
165
**Leading-wildcard gating is fail-closed with no metadata opt-in.** The
165
166
generated `<Entity>FilterAllowlist` hardcodes `leadingWildcard: false` on every
@@ -178,6 +179,29 @@ safety limit, not a feature — TS enforces it, the other ports currently do not
178
179
Unifying that cap cross-port is the one item here worth doing regardless of
179
180
feature demand (it is a consistency/safety divergence, not a capability).
180
181
182
+
### TS-only error responses (not part of the cross-port contract)
183
+
184
+
The TypeScript mount helpers (`@metaobjectsdev/runtime-ts/drizzle-fastify`,
185
+
`/fastify` and `/hono`) pin two responses the contract leaves open — HTTP 5xx is
186
+
implementation-defined below, and no corpus scenario sends a malformed body. Both
187
+
use the contract's `{ "error": "<code>" }` envelope, and both are scoped to the
188
+
routes the helpers mount: an adopter's own routes, and a Fastify `setErrorHandler`
189
+
or Hono `onError` the adopter installed, answer exactly as they did before.
190
+
191
+
| Response | When |
192
+
|---|---|
193
+
| malformed JSON body | a `POST`/`PATCH`/`PUT` body that does not parse as JSON (an empty body sent as `application/json` included) → HTTP 400 `{ "error": "invalid_json" }`. Before, Fastify answered its own `{ "statusCode": 400, "code": "FST_ERR_CTP_INVALID_JSON_BODY", … }` and Hono a Zod `validation` error about a missing object |
194
+
| unexpected server error | anything that is not a filter, validation, not-found or constraint answer — a query against a column the database no longer has, a driver failure → HTTP 500 `{ "error": "internal" }`, the code the cross-port reference servers already use. The body names no SQL, table, column or bound parameter; the full error goes to the server log (`console.error`). Before, Fastify's default handler echoed the driver message, which for Drizzle is the query text and its parameter values |
195
+
196
+
How each framework scopes it: on Fastify, the helpers pass a **route-level**
197
+
`errorHandler` in the options of each route they register (Fastify applies it to
198
+
that route only). A deliberate 4xx raised on such a route — an auth `preHandler`'s
199
+
401, schema validation, 413, 415 — is rethrown to the enclosing scope's handler
200
+
untouched, and an `errorHandler` you pass in `routeOptions` replaces the helpers'
201
+
own. Hono has no per-route handler (`app.onError` is app-wide), so the helpers wrap
202
+
each handler they register instead; an `HTTPException` is rethrown to your
203
+
`onError`.
204
+
181
205
### Sort + pagination
182
206
183
207
-`sort=<field>:asc|desc` — single sort key (multi-sort not in the
@@ -300,7 +324,8 @@ Non-2xx responses MUST return:
300
324
- HTTP 400 — validation and filter/sort-parser errors.
301
325
- HTTP 404 — `{"error": "not_found"}`.
302
326
- HTTP 409 — a declared constraint conflicting with existing state (a uniqueness or referential violation); a constraint rejecting the request's own value stays a 400.
303
-
- HTTP 5xx — implementation-defined.
327
+
- HTTP 5xx — implementation-defined (the TS mount helpers answer
328
+
`{"error": "internal"}` and nothing more — see "TS-only error responses").
304
329
305
330
#### Filter and sort errors name the field
306
331
@@ -491,9 +516,10 @@ codegen status" above and "Verified by" below, and
491
516
492
517
What's still genuinely open:
493
518
494
-
- The six **TS-only filter extensions** (`?search=`, `filter[or]` /
519
+
- The seven **TS-only filter extensions** (`?search=`, `filter[or]` /
495
520
`filter[and]` nesting, leading-wildcard gating, the nesting-depth cap,
496
-
the `in`-list size cap, and the bare filterable-field 400) — see "TS-only filter extensions" above.
521
+
the `in`-list size cap, the bare filterable-field 400, and the
522
+
filter-value format check) — see "TS-only filter extensions" above.
497
523
None touch the metamodel vocabulary, so any of them can be promoted
498
524
cross-port later as a purely additive, non-breaking change if real
0 commit comments