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
State SaveReportInput's requirements at the reports.save door (#12421)
* fix(client,rest): state SaveReportInput's requirements at the reports.save door
`IReportService.saveReport` takes a `SaveReportInput`, on which `name`,
`object` and `query` are all required. Nothing on the path said so: the SDK
method declared its parameter `any` and the route forwarded `req.body ?? {}`
unchecked, so the requirement held only as far as each reports implementation
chose to re-derive it privately.
- `client.reports.save` now takes `SaveReportInput` instead of `any`.
- `POST /api/v1/reports` refuses a body missing any of the three required keys,
and a `query` that is not a `ReportQuery` envelope, with 400 /
VALIDATION_FAILED — ordered after the existing 501 for an unmounted service.
The query-less literal that `client.test.ts` had been constructing invisibly is
preserved verbatim and becomes a `@ts-expect-error` pin asserting the refusal.
The REST pass-through test is re-driven through a door-valid body so it keeps
pinning the service-raised VALIDATION_FAILED mapping instead of going vacuous.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
* refactor(rest): raise the reports door refusal through handleValidation
The door check wrote its own 400 body. That put a second VALIDATION_FAILED
construction site on one route — so the same refusal could reach a client in
two different envelopes depending on whether the door or the service raised it
— and added two non-conforming bodies to the `check:route-envelope` ratchet,
which only ticks down (stringError 46 vs 44, siblingCode 71 vs 69).
It now throws `VALIDATION_FAILED: …` from inside the existing try, so the
route's single `handleValidation` builds the body exactly as it already did for
a service-raised refusal. No new response body; the ratchet is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(client,rest): state `SaveReportInput`'s requirements at the `reports.save` door (#11926)
7
+
8
+
**BREAKING** accept-set narrowing on `POST /api/v1/reports` and on the
9
+
`client.reports.save` parameter type, shipped as `minor` under the repo's
10
+
launch-window convention for breaking changes.
11
+
12
+
`IReportService.saveReport` takes a `SaveReportInput`, on which `name`, `object`
13
+
and `query` are all required. Nothing on the path said so. The SDK method
14
+
declared its parameter `any`, and the route forwarded `req.body ?? {}` straight
15
+
through, so the requirement held only as far as each reports implementation
16
+
chose to re-derive it privately — the bundled `@objectstack/plugin-reports` does
17
+
re-derive all three, but a third-party implementation need not, and a caller
18
+
could not tell which one it was talking to. This is the ADR-0078
19
+
declared-but-unenforced shape arriving at an authoring surface: the producer
20
+
accepted off-spec input and handed it to a service that requires more.
21
+
22
+
Both halves now state the contract:
23
+
24
+
-**`client.reports.save(report)`** takes `SaveReportInput` instead of `any`.
25
+
Omitting `query` (or `name`, or `object`) is now a compile error at the call
26
+
site rather than a surprise from whichever implementation is mounted. The SDK
27
+
remains a transport and adds no runtime validation — it is not a second
28
+
validator.
29
+
-**`POST /api/v1/reports`** refuses a body missing any of the three required
30
+
keys, and a `query` that is not a `ReportQuery` envelope (a scalar or an
31
+
array), with `400` / `VALIDATION_FAILED` — the same envelope the route
32
+
already produced for a service-raised validation error (ADR-0112). A
33
+
JavaScript or `curl` caller that never sees the TypeScript type is refused
34
+
too. The refusal is ordered **after** the existing `501` for an unmounted
35
+
reports service: "no reports service on this deployment" is a deployment fact
36
+
and outranks anything about the body. An empty `query: {}` stays legal —
37
+
every field on `ReportQuery` is optional — and is pinned as such.
38
+
39
+
**Migration.** A caller that omitted `query` was already relying on
40
+
implementation-specific behaviour; supply the `ReportQuery` envelope the report
41
+
should run (`{}` for "no filters"). Callers already sending a complete
42
+
definition are unaffected, and the bundled reports implementation already
43
+
refused all three omissions, so no deployment running it changes behaviour —
44
+
only the layer that produces the refusal moves, from the service to the door.
45
+
46
+
<!-- adr-0087: not-required (no-migration-prescription) A validity narrowing over keys that already exist and are already required by the service contract: no key is removed, renamed or re-shaped, so there is no tombstone and nothing mechanical for `objectstack migrate meta` to rewrite. `SaveReportInput` is a cross-package TS contract with no spec schema and no stored-metadata form, so no authored artifact at rest carries the affected shape. What a query-less report definition was *meant* to query is authoring intent no migration entry can supply on an upgrader's behalf; the 400 at the door is the channel that reaches the author, naming the missing keys. Mirrors the disposition of the #11519 and #11842 accept-set narrowings. -->
0 commit comments