Skip to content

Commit b956cae

Browse files
zhuangjianguoclaude
andcommitted
docs(changeset): org-scoped metadata read door (#9454)
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WKSnqAaoqtW3QX7SSf1Vk
1 parent f4078a6 commit b956cae

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/metadata-core": patch
3+
"@objectstack/metadata-protocol": patch
4+
"@objectstack/rest": patch
5+
---
6+
7+
fix(rest): org-overridable metadata is served back by every `/meta` read door, not just persisted (#9454)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No authorable key is
10+
added, renamed, retired or tombstoned. One new exported predicate in
11+
`metadata-core` (`organizationIdForMetaRead`), one widened optional request
12+
member on an existing protocol method (`getMetaItemCached`'s `organizationId`),
13+
and caller-side threading in `packages/rest`. The accept/reject behaviour of
14+
every write door is unchanged — this card is read-side only. -->
15+
16+
A runtime `PUT` of an org-overridable metadata type — `view`, `dashboard`,
17+
`report`, `translation`, `email_template` — answered **200** with a receipt
18+
reporting `state: 'active'` plus a version and sequence number, **persisted the
19+
row with its `organization_id`**, and was then served back by **nothing**: the
20+
direct `GET` answered 404, the scoped listing was unchanged, the unfiltered
21+
listing was missing it, and the browser rendered an empty view or "Dashboard Not
22+
Found". The platform reported success in the same breath as not delivering the
23+
work, which is declared ≠ enforced in the direction hardest for an author to
24+
notice — the write path says everything worked.
25+
26+
**The write door was correct as-is.** The row really is persisted, so the
27+
receipt is truthful; this was persisted-but-not-served, never a silent write
28+
no-op. **The overlay-resolution layer was correct too**, and type-agnostic:
29+
`getMetaItem` resolves `(orgId ? findOverlay(orgId) : undefined) ??
30+
findOverlay(null)`, `getMetaItems` unions both scopes under org-wins precedence,
31+
and `getMetaItemLayered` even reports `overlayScope`. The defect was that the
32+
REST read doors **never stated the scope**, so every one of them asked for the
33+
env-wide partition and the org partition was never consulted.
34+
35+
**The repair is one registry-derived predicate, threaded at the read doors.**
36+
`organizationIdForMetaRead` joins `organizationIdForMetaWrite` in
37+
`metadata-core`, deriving from the same `allowOrgOverride` registry flag, so
38+
read scope and write scope cannot drift and a registry entry flipping the flag
39+
moves both doors together. It is threaded through the **already-memoised**
40+
`resolveExecCtx`, so no new per-request organization resolution is introduced.
41+
42+
⛔ **Not a bare `ctx?.tenantId` at each site**, and the reason is measurable
43+
rather than stylistic: deployments predating the #6190 ruling hold **phantom
44+
org-scoped rows for types the registry declares non-overridable** (the runtime
45+
used to stamp `organization_id` on every type). Boot hydration deliberately
46+
walks past those rows, so they are dead. A read door naming the org for *every*
47+
type would resolve them again — serving, on the read side, a document that
48+
vanishes at the next restart.
49+
50+
**`getMetaItemCached` gains an `organizationId` member** — it was the only meta
51+
read verb that could not express one, having hard-coded a two-key delegation to
52+
`getMetaItem`. The organization is also folded into its **ETag**. The mechanism
53+
differs from `locale` and the difference is stated rather than glossed: `locale`
54+
is invisible to the hash (the body is translated after the validator runs), so
55+
folding it in was the only way it could vary the validator at all, whereas the
56+
org-resolved document *is* the thing hashed. No cache leak is claimed — the
57+
directive is `private, no-cache` and there is no server-side cache entry keyed by
58+
type+name. It is folded in because that makes scope a **declared** property of
59+
the validator instead of an emergent property of the body.
60+
61+
**Both REST branches are fixed, which is the half-fix this card could easily
62+
have shipped instead.** `view` and `dashboard` share one mechanism but reach it
63+
through two different arms: `view` takes the cached arm (`getMetaItemCached`),
64+
while `dashboard` bypasses the cache via `isDashboardType` and takes the
65+
uncached arm. Both omitted the org, so a fix applied to one arm would have
66+
fixed exactly one type while the receipt kept claiming success for the other.
67+
The scope is now resolved **above** the fork, so the two arms cannot disagree.
68+
69+
The regression proof drives real REST routes against a real protocol over a stub
70+
engine — write-then-read agreement on **one boot**, for all five types, through
71+
both arms. Its most important assertions are the ones that do **not** merely
72+
check the item comes back: an org-less caller and a **second organization** must
73+
each be refused it. An org-blind overlay fallback would satisfy every other
74+
assertion in the file while matching an arbitrary tenant's row.

0 commit comments

Comments
 (0)