|
| 1 | +--- |
| 2 | +'@objectstack/spec': minor |
| 3 | +--- |
| 4 | + |
| 5 | +feat(spec): the `/packages` doors declare the query parameters they execute, and stop declaring the two they never did (#17667) |
| 6 | + |
| 7 | +`GET /api/v1/packages` diverged from its own declared request contract in BOTH |
| 8 | +directions, on the same door, with the same `200`. This aligns the declaration |
| 9 | +with the reads, per the maintainer-approved ruling of 2026-09-13 (decision batch |
| 10 | +#126 item 1, route 2 of three). |
| 11 | + |
| 12 | +**BREAKING** — `limit` and `cursor` no longer parse on |
| 13 | +`ListInstalledPackagesRequestSchema`, and `limit`'s `.default(50)` is gone with |
| 14 | +them. Both were declared here and read by nothing: the serving door filters on |
| 15 | +`status` / `type` and then returns every remaining row, so no page was ever |
| 16 | +withheld and no continuation token was ever minted. The response half's |
| 17 | +`nextCursor` has never been emitted, so a caller looping "until the cursor runs |
| 18 | +out" re-read the first and only page forever, with no error and no `400`. |
| 19 | + |
| 20 | +``` |
| 21 | +FROM ListInstalledPackagesRequestSchema.parse({}) |
| 22 | + -> { limit: 50 } // a cap the server has never applied |
| 23 | + ListInstalledPackagesRequestSchema.parse({ limit: 1, cursor: 'x' }) |
| 24 | + -> { limit: 1, cursor: 'x' } // both dropped on the wire, 200, every row |
| 25 | +
|
| 26 | +TO ListInstalledPackagesRequestSchema.parse({}) |
| 27 | + -> {} // no window is declared, because none exists |
| 28 | + ListInstalledPackagesRequestSchema.parse({ limit: 1 }) |
| 29 | + -> throws: '`limit` / `cursor` were removed from GET /api/v1/packages in |
| 30 | + @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) …' |
| 31 | +``` |
| 32 | + |
| 33 | +**Read the removed default, not just the removed key.** `limit` carried |
| 34 | +`.default(50)`, so a reader of the published schema — an SDK, codegen, an AI |
| 35 | +client — was entitled to believe an unparameterised list is capped at 50 rows. |
| 36 | +It has never been capped at all. Nothing parses a query string through this |
| 37 | +schema, so that default has never been stamped onto anything; there is nothing |
| 38 | +to send instead and nothing to restore. **A client that sized a buffer or a |
| 39 | +page control to the declared 50 should size it to the installed set instead** — |
| 40 | +which is a bounded table of tens of rows, which is also why paging was removed |
| 41 | +rather than implemented. |
| 42 | + |
| 43 | +Both keys are `retiredKey()` tombstones rather than deletions: the schema is not |
| 44 | +`.strict()`, so a bare deletion would have made Zod silently strip whatever a |
| 45 | +generated client kept sending — a clean parse and a parameter that never takes |
| 46 | +effect, which is this defect re-created one layer down (ADR-0104). Writing |
| 47 | +either key is now a `tsc` error and a parse error carrying the prescription. |
| 48 | + |
| 49 | +**The other direction, and nothing on the wire changes for it.** Three query |
| 50 | +parameters the doors already executed were declared by no request schema, so |
| 51 | +they were invisible to anything generated from the contract: |
| 52 | + |
| 53 | +| door | parameter | now declared on | |
| 54 | +|---|---|---| |
| 55 | +| `GET /api/v1/packages` | `type` — exact match against `manifest.type` | `ListInstalledPackagesRequestSchema` | |
| 56 | +| `GET /api/v1/packages/:id` | `version` — exact installed-version scope; `latest` reads the installed row | `GetInstalledPackageRequestSchema` | |
| 57 | +| `DELETE /api/v1/packages/:id` | `keepData` — keep object tables, remove metadata only | `UninstallPackageApiRequestSchema` | |
| 58 | + |
| 59 | +No accept set moves: the doors served all three before and serve them |
| 60 | +identically now. `overwrite`, the fourth parameter the ruling named, was already |
| 61 | +declared on `PackageInstallRequestSchema` and needed nothing. |
| 62 | + |
| 63 | +**`hasMore` stays the constant `false` it already was, and is now true by |
| 64 | +construction rather than by coincidence**: with no `limit` and no `cursor` to |
| 65 | +ask with, nothing can request a page, so there is never a next one to announce. |
| 66 | + |
| 67 | +Clause-②: yes |
| 68 | + |
| 69 | +<!-- adr-0087: registered packages-list-pagination-retired --> |
0 commit comments