Skip to content

Commit eef1a66

Browse files
committed
chore(changeset): rest ?limit= parsing narrowing (minor, BREAKING)
Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3d07f06 commit eef1a66

1 file changed

Lines changed: 74 additions & 0 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
'@objectstack/rest': minor
3+
---
4+
5+
fix(rest): four published doors refuse a `?limit=` they cannot read with `400 VALIDATION_FAILED`, instead of substituting, clamping or dropping it and answering `200` (#20061, #20062)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING**: shipped as `minor` under the launch-window convention
10+
(`check-changeset-no-major` refuses `major` until GA). The banner and the ADR-0087
11+
disposition below carry the breaking-ness, not the level.
12+
13+
`GET /data/import/jobs`, `GET /data/:object/export`, `GET /meta/:type/:name/history`
14+
and `GET /search` read `?limit=` with a bare `Number()`. That coercion does not
15+
fail. It invents a value, and the door served it with a `200`:
16+
- `?limit=0` on the import-job history answered the 50-row default against a
17+
declaration of `min(1).max(200)`;
18+
- `?limit=abc` on the export downloaded one row;
19+
- on the metadata history it returned the whole change log;
20+
- on search it removed the overall cap.
21+
22+
Since `@objectstack/client` sends `limit` exactly as the caller wrote it, the door
23+
is the only place such a value can be refused.
24+
25+
Each door now reads the parameter against its own declaration:
26+
- **`GET /data/import/jobs`** reads `limit` and `offset` through
27+
`ListImportJobsRequestSchema` (`limit` `int().min(1).max(200)`, default 50;
28+
`offset` `int().min(0)`, default 0).
29+
- **`GET /meta/:type/:name/history`** reads `limit` through
30+
`HistoryMetaItemRequestSchema.limit` (`z.number()`, so any finite number, as
31+
before).
32+
- **`GET /data/:object/export`** and **`GET /search`** declare no request schema.
33+
There `limit` must be a whole number. Their range handling is unchanged: the
34+
export floor of 1 and cap of 50000, and search's `[1, 100]` clamp.
35+
36+
A value outside the declaration answers `400` with the data surface's existing
37+
envelope, `{ error, code: 'VALIDATION_FAILED', fields }`. `fields[0].field` names
38+
the parameter. `fields[0].code` is the ADR-0114 member for the failed constraint:
39+
`invalid_type` for a value that is not a number (or not a whole one, where one is
40+
required), `min_value` or `max_value` for one outside a declared bound. The
41+
service is never called.
42+
43+
What changes, per door (every row answered `200` before):
44+
45+
| door | request | answered before | answers now |
46+
|:--|:--|:--|:--|
47+
| `GET /data/import/jobs` | `?limit=0`, `?limit=-3` | 50 rows / 1 row | `400`, `min_value` |
48+
| `GET /data/import/jobs` | `?limit=201`, `?limit=500` | 200 rows | `400`, `max_value` |
49+
| `GET /data/import/jobs` | `?limit=abc`, `?limit=1.5`, `?limit=Infinity` | 50 / 1.5 / 200 rows | `400`, `invalid_type` |
50+
| `GET /data/import/jobs` | `?offset=-1`, `?offset=abc`, `?offset=1.5` | offset 0 / 0 / 1.5 | `400`, `min_value` / `invalid_type` |
51+
| `GET /data/:object/export` | `?limit=abc`, `?limit=` (empty) | a one-row export | `400`, `invalid_type` |
52+
| `GET /data/:object/export` | `?limit=1.5`, `?limit=Infinity` | limit 1.5 / capped to 50000 | `400`, `invalid_type` |
53+
| `GET /meta/:type/:name/history` | `?limit=abc`, `?limit=Infinity` | the whole change log | `400`, `invalid_type` |
54+
| `GET /meta/:type/:name/history` | `?limit=` (empty) | zero events | `400`, `invalid_type` |
55+
| `GET /search` | `?limit=abc` | no overall cap | `400`, `invalid_type` |
56+
| `GET /search` | `?limit=1.5`, `?limit=Infinity` | a cap of 1.5 / 100 | `400`, `invalid_type` |
57+
58+
A blank value such as `?limit=%20` is refused on every door.
59+
60+
**Unchanged:**
61+
- An absent `limit` keeps each door's default: 50 jobs, a 10000-row export, the
62+
full history, search's 20.
63+
- An empty `?limit=` on the import-job history and on search still means absent,
64+
as it always did there.
65+
- Every conforming value reaches the service exactly as before, including the
66+
ranges no card here takes a position on: export `?limit=0` still exports one
67+
row, search `?limit=500` is still clamped to 100, and history still forwards
68+
`0` or `1.5` because its declaration admits them.
69+
70+
**Fix for a caller that now gets the `400`:** send `limit` as a whole number, within
71+
the declared range where the door declares one (`1`–`200` for import jobs), or
72+
omit it to get the door's default.
73+
74+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authored moves: no spec key, metadata property or exported symbol is added, removed or renamed, and `packages/spec` is untouched, so `objectstack migrate meta` has nothing to rewrite and no ADR-0087 registry gains a row. What narrows is the set of HTTP query values four REST doors accept, back to what their declarations (`ListImportJobsRequestSchema`, `HistoryMetaItemRequestSchema`) or a whole-number reading already said. A query string is neither authored nor persisted. The other categories are closed on facts: `@objectstack/rest` publishes (not `unpublished`), no ADR-0087 id covers an HTTP query value (not `registered` / `already-registered`), and the change is runtime behaviour, not a TypeScript declaration (not `runtime-interface-only` / `type-surface-only`). -->

0 commit comments

Comments
 (0)