fix(data-objectstack): MetadataClient.get() honors its unwrapped-body contract — the field permission matrix lives (#4271) - #4545
Merged
Conversation
… contract (#4271) `GET /meta/:type/:name` answers the spec-declared envelope `{ type, name, item, ...protection fields }` — one shape for published and draft reads alike since objectstack#5563. `get()` handed that envelope back while its docblock declared it returned the unwrapped item content, so every consumer reading `obj.fields` read `undefined`. A census of all `get()` call sites found ZERO deliberate envelope readers and nine consumers reading the body directly, all broken identically: the field half of the permission matrix ("No fields registered for this object." for every object), RLS CEL field lint/autocomplete, the dataset inspectors and preview hooks, the report drill-down fallback, the record-page seed, and the Field Designer — which also wrote the envelope back over the object body. Nothing caught it because the repo's test doubles were written against the docblock, answering a bare `{ fields }` body. Fixed at the producer: `get()` unwraps once at the client boundary, so all nine call sites are repaired without being touched. Detection is by presence of the three keys `GetMetaItemResponseSchema` declares, never guessed from payload contents; non-envelope responses pass through unchanged and 404 still reads as null. `getDraft()` keeps returning the envelope its docblock declares and ~11 `.item` readers depend on — the two now share one private transport and differ only in whether they unwrap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
PM step-7 复核 — ACCEPT (session_017Qqyix2QcnpUC9XeYVDzx3)
Auto-merge armed (squash) — landing verified per the merge-queue discipline. Generated by Claude Code Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4271
The disagreement
GET /api/v1/meta/:type/:nameanswers the spec-declared envelope{ type, name, item, …protection fields }— one shape, for published and draft reads alike, since objectstack#5563 collapsed the read to it.MetadataClient.get()handed that envelope straight back while its own docblock declared it returned "the unwrapped item content". Every consumer readingobj.fieldstherefore readundefined.Verified on current
origin/mainbefore implementing — premise holds, at both ends:metadata-client.tsreturned(await res.json()) as Twith no unwrapping, under a docblock promising the body.packages/rest/src/index.tsin the framework records the other half — "GET /meta/:type/:nameanswers exactly one body shape now — the spec-declared{ type, name, item, … }envelope … reading.itemis unconditionally correct."GetMetaItemResponseSchemadeclarestype: string,name: string,item: unknownplus the ADR-0008 protection carriers.Census — the ruling's first step
Every
get()call site, classified. Zero deliberate envelope readers; nine consumers reading the body directly, all broken identically:PermissionMatrixEditor.ensureFieldsobj?.fieldsPermissionMatrixEditor.loadObjectFieldsobj?.fieldspreviews/useObjectFieldsobj.fieldspreviews/useDatasetCatalogtoCatalogEntry(doc)inspectors/useDatasetFields×2normalizeObject(doc)ReportViewdef?.object/def?.dimensionsmetadata-admin/anchorsbuildDefaultPageSchema(objectDef)ResourceEditPage:615obj.fieldsplugin-designer/MetadataFieldsPageraw.fields, then saves{ …raw, fields }backruntime-metadata-persistence.readRuntimeDraftunwrapDraftBodydata-objectstack updateViewunwrapViewDraftThe
useDatasetCatalogcase is the nastiest: an envelope carries a realname, so it produced an entry that looked valid and had zero dimensions and measures.No class-(a) compensator exists, which is what makes the contract fix clean — every broken site is repaired without being edited. That includes
ResourceEditPage.tsx, which #4306 owns: the mutual exclusion was checked and is not triggered, because nothing there needed touching.Why nothing caught this: the repo's test doubles were written against the docblock. They answer a bare
{ fields }body, so the suite exercised the documented contract while production ran the other one.The fix
At the producer, once, at the client boundary. Detection is by presence of the three keys the spec declares — never guessed from payload contents:
typeandname(a view is{ name, type: 'grid', … }); with noitemkey it is the body and is left whole;itemproperty of its own but no envelope identity is likewise untouched;Non-envelope responses pass through byte-for-byte and 404 still reads as
null.getDraft()is unchanged and keeps returning the envelope — its docblock declares it and roughly eleven call sites read.itemoff it (StudioDesignSurface,ResourceEditPage,PackageOwdOverviewPanel,ObjectHooksPanel,PermissionMatrixEditor). The asymmetry is now real rather than aspirational: both methods share one private transport and differ only in whether they unwrap. RoutinggetDraftthrough the unwrappingget()would have silently emptied all eleven — and would have required editing two files #4306 owns.Red-first, with the direction predicted in writing
Predicted before running, then run against unfixed code. 8 red / 7 green, exactly as predicted except one noted below.
Green after the fix: 42/42. Reverse-verified by removing the fix via patch file and
git checkout(nevergit stash) — the same 8 return red — then restored with a sha256-verified byte-identical restore.A10—getDraft()returns the raw envelope — is green before and after, deliberately. It is the guard on the design: it goes red the momentgetDraftis left delegating to the unwrappingget().One prediction was wrong, reported rather than papered over. I predicted A12 (URL construction) green-before; it went red, because I had folded an unwrap assertion into that case alongside the URL assertions. The URL half was green throughout — only the bundled unwrap assertion moved. A second authoring error was mine too: B1b initially asserted
readablestarts unchecked, when the real default posture is readable-but-not-editable; the case now toggleseditable, which is the half with somewhere to travel.The app-shell test drives a real
MetadataClientover a fetch answering the real server shape — nothing in it mocksget(). That is the gap the existing doubles left open.Verification
data-objectstackfull suitefind()/probeAppAccess(#4495/#4521) unmovedapp-shell+plugin-designerpermission-slice, CEL family — all greenapp-shellruns--noEmitandtsconfig.test.json)...@object-ui/data-objectstack, i.e. consumers)origin/mainbaselinecheck-control-bytes,check:phantom-deps,changeset:checkOne downstream package first failed on a missing
@object-ui/app-shelldeclaration — a missing-artifact effect of building only the dependency closure, not the packages themselves. It is green after building both; not a type error from this change..d.tsdiff, measured both waysClean rebuild with
dist/andtsconfig.tsbuildinfocleared between builds.data-objectstack— documentation plus one addedprivate readItemResponse;. Public signatures unchanged:getandgetDraftkeep(type, name, options?) =Promise< T | null >.app-shell— zero diff across all 415 declaration files.Changesets
@object-ui/data-objectstackminor — published behavior moves for existing callers, the #4495find()resolve-to-reject grading. No signature changed, so nothing needs a code edit to keep compiling.Deviation, declared: the ruling anticipated an
app-shellpatch changeset "per its own .d.ts diff". Measured, that diff is empty andapp-shellhas no source change at all — only test additions — so noapp-shellchangeset was written. It is in the fixed group withdata-objectstackand releases alongside it regardless.Notes
loadObjectFieldslives inPermissionMatrixEditor.tsx, not in a separatecelAuthoringmodule as the dispatch brief guessed.celAuthoring.tsexists but only consumes the field list. A location refinement, not a premise inversion.data-objectstack/src/index.tsclaiming "a published read is the bare body" is corrected, since it would have misled the next reader in exactly the way this defect was born.Generated by Claude Code