|
| 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