Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .changeset/20320-dispatcher-meta-read-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
'@objectstack/rest': minor
'@objectstack/runtime': patch
---

fix(rest, runtime): the runtime dispatcher's `/meta` reads answer what `RestServer`'s answer — one list chain, one `public`-audience predicate, and the `?state=draft` read (#20320)

Clause-②: yes (widening) — `@objectstack/rest`'s root entry gains five value exports (`createMetaListAnswer`, `translateMetaList`, `metaRequestLocale`, `isPublicAudienceRead`, `STORED_VERSION_DOOR_POLICY`) and six type exports (`MetaListAnswer`, `MetaListAnswerSources`, `MetaListRequest`, `MetaListTranslationSources`, `MetaPublicReadRoute`, `MetaRequestHttp`), so its published surface grows; nothing it exported before is removed, renamed or narrowed. `@objectstack/runtime` publishes no new surface and stays a `patch`.

A host that mounts only the `${prefix}/*` catch-all (`createHonoApp`, and any
adapter written on the public `HttpDispatcher` API) serves `/meta` through the
runtime dispatcher. Its reads now give the same answers as `RestServer`'s
`GET /meta/:type` and `GET /meta/:type/:name`. Until now a dispatcher-only host
answered:

- **`GET /meta/app?id=crm`** — every app the caller may see, not `[crm]`. An
`?id=` that matches nothing listed every app instead of an empty list.
- **`GET /meta/view?object=lead`** — every view, not the lead views sorted for
the switcher.
- **`GET /meta/docs`** (the plural spelling) — every doc WITH its body. The
content slim compared the raw segment, so it ran only for `/meta/doc`.
- **any doc list** — each doc with its `translations` map and in no locale.
`RestServer` collapses each doc to the request's locale.
- **every translatable list** (`app`, `view`, `object`, `page`, `dashboard`,
`action`, `dataset`) — untranslated labels, whatever `Accept-Language` or
`?locale=` asked for, and no `Vary: Accept-Language` header.
- **`GET /meta/api`** — every stored `api` declaration, including ones the
endpoint matcher does not serve (their routes answer 404).
- **an anonymous `GET` of a `public` book or doc** (list or item) —
`401 UNAUTHENTICATED`. `RestServer` serves it (ADR-0046 §6.7).
- **`GET /meta/:type/:name?state=draft` from a caller who may read drafts** —
the ACTIVE item. It should be the pending draft, whole for a caller who may
save the app and pruned per caller for everyone else, or `404 NO_DRAFT` when
nothing is pending. A caller who may not read drafts is still answered the
plain read, byte for byte.

**What changed.** The list route's whole post-read chain moved out of
`RestServer` into `createMetaListAnswer` in `@objectstack/rest`, unchanged. That
chain is the `api` served-set face, the per-caller list gate, `?id=`,
`?object=`, the doc locale collapse and content slim, the transport's own
object mask, and the translation. Every exit of the dispatcher's list branch
now hands its answer to that same function. The anonymous gates on both
transports ask one exported predicate, `isPublicAudienceRead`. It admits only
`GET` reads of book and doc, so every other type keeps the anonymous deny, and
the §6.7 audience gate still refuses `org` and `{ permissionSet }` audiences.
The dispatcher's `?state=draft` read runs the exported
`STORED_VERSION_DOOR_POLICY`, the constant `RestServer`'s draft branch runs.

`RestServer`'s own answers are unchanged: the move is a refactor on that side,
and every existing REST test passes unedited.

New exports from `@objectstack/rest`: `createMetaListAnswer`,
`translateMetaList`, `metaRequestLocale`, `isPublicAudienceRead`,
`STORED_VERSION_DOOR_POLICY` and their types. Nothing is removed or renamed.
25 changes: 24 additions & 1 deletion packages/rest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,34 @@ export { refuseRepeatedQueryParams, repeatedQueryParamMessage } from './query-mu
// audience prunes, the app nav filter and the dashboard widget gate — over the
// same ports. The judge answers the pruned ITEMS; each caller rewraps them in
// its own list envelope.
export { createMetaItemReadGate, createMetaListReadGate } from './meta-item-read-gate.js';
//
// [#20320] …and everything else the two transports' `/meta` reads must answer
// alike: the list route's whole post-read chain (`createMetaListAnswer` — the
// `api` served-set face, the list gate, `?id=`, `?object=`, the doc locale
// collapse and slim, the transport's object mask, the translation
// `translateMetaList`), the one locale parse it reads (`metaRequestLocale`),
// the anonymous gates'
// `public`-audience predicate (`isPublicAudienceRead`) and the stored-version
// doors' policy (`STORED_VERSION_DOOR_POLICY`, which `?state=draft` runs).
export {
createMetaItemReadGate,
createMetaListReadGate,
createMetaListAnswer,
isPublicAudienceRead,
metaRequestLocale,
STORED_VERSION_DOOR_POLICY,
translateMetaList,
} from './meta-item-read-gate.js';
export type {
MetaItemReadGateSources,
MetaItemReadRefusal,
MetaItemReadVerdict,
MetaListAnswer,
MetaListAnswerSources,
MetaListRequest,
MetaListTranslationSources,
MetaPublicReadRoute,
MetaReadGateCaller,
MetaReadGatePolicy,
MetaRequestHttp,
} from './meta-item-read-gate.js';
Loading
Loading