Repository navigation
fix(rest): the by-name read of a view container answers 200 on an artifact boot and translates the views inside it (#22614) - #22687
Conversation
…ntainer The /meta item read handed a defineView container to the single-view translator, which keys the catalog on the view's name. An artifact-shipped container has none, so the artifact boot answered 500; the config boot's registrar stamps one, so that boot served a fabricated label and left every view inside untranslated. The read now classifies the served view document once: a container's views are translated under the names the composer gives them, with their own packaged bases, and land back in their slots. Claude-Session: https://claude.ai/code/session_01HGa2XMKBRUqPrZFxQavBwH Co-authored-by: Claude <noreply@anthropic.com>
…d its changeset Claude-Session: https://claude.ai/code/session_01HGa2XMKBRUqPrZFxQavBwH Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGa2XMKBRUqPrZFxQavBwH Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6519045629b06e0621c86e263c05fcff9fe00480 && git checkout 6519045629b06e0621c86e263c05fcff9fe00480
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 243dd3c6256673dc1a56f9419be03c397129f825 7c71a9e34a9352727cbb09a9ded59deaa6f3bf59 && git checkout -B drift-repro 243dd3c6256673dc1a56f9419be03c397129f825 && git merge --no-ff 7c71a9e34a9352727cbb09a9ded59deaa6f3bf59
node scripts/docs-audit/affected-docs.mjs --json 243dd3c6256673dc1a56f9419be03c397129f825
|
Fixes #22614
Clause-②: no
A
500is a fault, not a refusal. No accepted set, export or error code changes. The by-name read of a view container now answers on every boot, translated.The defect, re-measured on this branch's base (
3d0eeefa4)One single-package project: an object
acme_note, adefineView({ object: 'acme_note', list, listViews: { mine } })container, and azh-CNcatalog for both views. The read isGET /api/v1/meta/view/acme_note, authenticated as the seeded admin.os build, thenos serveof the artifact)500 INTERNAL_ERROR; log:Cannot read properties of undefined (reading 'startsWith')200;list.labelis全部笔记,listViews.mine.labelis我的笔记os serve objectstack.config.ts)200, butlabel: "acme_note"is served (no author wrote it), and both views stayAll Notes/My Notesfor azh-CNreader200; the same container as the artifact boot servesGET /meta/view/acme_note.default,/acme_note.mine200, translatedWhy the two boots served different documents. Both registrars file the container under its object. The config boot's ObjectQL boot loop stamps the derived key onto the item (
packages/objectql/src/engine.ts:7575,toRegister = item.name === itemName ? item : { ...item, name: itemName }). The artifact loader registers the container as shipped, with noname(packages/metadata/src/plugin.ts:1198). The read handed either one to the spec's single-view translator (translateMetadataDocument('view', …)→translateView→viewTranslationKey), which keys the catalog onview.name. With no name it threw. With the stamped name it served the container as ONE view keyed by the object: the name became the label fallback, and no view inside was translated. The config boot's200was the same fault without the crash.The fix:
packages/rest/src/meta-item-read-gate.tsonlytranslateMetaDocument(both transports call it:RestServerand the runtime dispatcher's/metaitem read) classifies aviewdocument once, with the spec's ownisAggregatedViewContainer. A single view goes to the spec translator, unchanged. A container goes totranslateMetaViewContainer, which translates it as a container, throughtranslateViewunder each part's own key rule:list,form, eachlistViews/formViewsentry) is translated as the item the composer expands it into. That isexpandViewContainer, the same expansion both registrars run, so the view keeps its ownname(OBJECT.KEY). Its packaged base is looked up by that qualified name. This is exactly what the by-name read of the expanded view is translated with, so the container read and the expanded read cannot disagree. The pin asserts that parity. The result lands back in the slot the view came from. The slot mapping follows the composer's documented order: alistViewsentry is found by name (it is the first claim on its key, so it is never renamed). The defaultlist/formis the item the composer flagsisDefault, which covers alistthat restates a named list and collapses into it. AformViewsentry is found by position among the named forms, which covers one renamed by a list that took its key.label/descriptionare keyed by the object it binds (objects.OBJECT._views.OBJECT), with the packaged container as the base. That is the key the config boot already read for a container whosenameis its object, and the key the i18n extractor scaffolds for such a container.translateViewfalls back to a view'snamefor a missing label. That fallback is dropped (it is not a translation), which is what removes the config boot'slabel: "acme_note". With no catalog string, the SAME document reference comes back.view.name?.: that would leave a container untranslated in silence (triage6094457162).The binding comes from
deriveViewContainerObject(@objectstack/metadata/view-container, the registry's one spelling, through its import-free leaf entry). It is not a fifth copy of theobject → list.data.object → form.data.object → namechain.packages/specis untouched, and so ispackages/rest/src/rest-server.ts(PR #22628's region): the classification lives in the gate.Packaging:
@objectstack/metadatamoves fromdevDependenciestodependenciesin@objectstack/restWhat imports it at runtime, measured on the rebuilt
packages/rest/dist/index.jsat7c71a9e34(top-level ESMimport … fromlines):@objectstack/metadata/view-container, 2 lines. One is this PR's import inmeta-item-read-gate.ts. The other sits in the bundled../objectql/dist/index.mjssection.@objectstack/metadata/errors, 5 lines, and@objectstack/metadata/view-container-name, 2 lines. These are in the bundled../metadata-protocol/src/protocol.tsand../objectql/dist/index.mjssections.So the published package already loaded
@objectstack/metadataat module load without declaring it. tsup bundles the dev dependency@objectstack/objectql, reached throughrest-server.ts's optionalawait import('@objectstack/objectql'). That bundled code's own external imports of@objectstack/metadata/*are hoisted into rest's top-level imports.packages/objectql/dist/index.mjscarries the same three specifiers (/errors×2,/view-container×1,/view-container-name×1). The manifest graph does not change (the edge existed as a dev edge), andcheck:workspace-manifest-cyclesandcheck:undeclared-dep-importsboth pass.Tests
All at
7c71a9e34(this branch merged withorigin/mainee3ae0360), unless a line says otherwise.packages/rest/src/meta-view-container-i18n.test.ts: 10 passed. It covers both registrar shapes translating alike, CONTROL parity with the expanded by-name reads, nothing invented, the same reference with no applicable string or no locale, a collapsed default list, a renamed default list (default_2), a renamed named form (edit_2) beside the defaultform, bulk-action defs in their slot, and the per-view packaged base looked up by qualified name (an explicit override beats the catalog).packages/cli/test/serve-view-container-read.integration.test.ts(integration tier: it spawnsos serve; tier partition pin 22/22): one fixture, a real config boot plusos buildand a real artifact boot. 6 passed. It covers each boot answering200with both views translated forzh-CNand no fabricatedlabel, authored labels foren, the same container from both boots, and CONTROL expanded reads unchanged on both. Before the fix (restdistbuilt at3d0eeefa4): 4 failed, 2 passed. The artifact boot answered 500, the config boot servedAll Notes, and the two containers differed. The two CONTROL /enconfig cases stayed green.f63e6a1d1. The classification'sif (metaType === 'view')was mutated throughscripts/ablation-replace.mjs(anchor 1 → 0, blobfa91bb79→cf670c38).src: 10 of 10 failed.pnpm --filter @objectstack/rest build, thenablation-dist-preflightreported the marker present in 2 built files. Boot pin: 4 failed, 2 passed.fa91bb79(equal toHEAD),git diff HEADempty. Rebuild, then the--absentpreflight passed: marker absent from all 6 built files, tree clean. Boot pin: 6 passed.pnpm --filter @objectstack/rest typecheck: exit 0 (includingcheck:test-typecheck, 0 debt).@objectstack/restfull suite (vitest run, both projects), atf63e6a1d1: 275 files, 5180 passed, 327 skipped, exit 0. The merge brought nothing underpackages/rest,packages/metadataorpackages/metadata-protocol. The incomingobjectql/engine.tschange is file-field hydration. So at7c71a9e34the pins, the typecheck and the runtime consumers below were re-run, not the full suite.translateMetaDocument(packages/runtime/src/domains/meta-item-read-gate-parity,meta-item-envelope,meta-list-projection-parity,meta-read-org-scope-parity,meta-package-all-fold): 5 files, 793 passed.dispatch-gates --commandsat7c71a9e34derived 76 families. All 76 were run, and every exit code is 0.--ranreconciliation: 76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN. Two families needed a second run:check:dual-build-cjs-loads: the first run reportedPREREQUISITE NOT MET(9 unbuilt dists). The rerun passed (107 require entries across 66 packages load).check:query-options-erasure: its self-test passed, but the first run's 420 s cap cut the ratchet. The rerun passed in 406 s.pnpm lint), at7c71a9e34:eslint --no-inline-config --format json packages/rest/src packages/cli/test/serve-view-container-read.integration.test.tsexited 0. It linted 303 files, ignored 0, with 0 errors and 0 warnings, and all three touched.tsfiles are in the linted set.eslint.config.mjs's ownfilesglobs (packages/**/*.{ts,tsx,mts,cts},**/*.{ts,…}). No file reported as ignored.parserOptionsare{ ecmaVersion, sourceType }, with noproject. Its only cross-file reads are its own two baselines, which this diff does not touch. So no untouched file's verdict can move.Acceptance notes
OBJECT.NAME.KEY,expandUnderOwnName), which the gate does not reproduce. Its views are translated under the composer's names. This is stated in the function's docblock.getMetaItemsfilters containers out of everyviewlist (protocol.ts,isAggregatedViewContainer), so no container reachestranslateMetaList. Measured:GET /meta/view?object=acme_noteanswered200with only the expanded items on both boots, at base.mainyet. PR fix(metadata,runtime): a multi-package config boot registers its unowned top level under manifest.id, as the artifact boot does #22612'sserve-config-boot-residual.integration.test.tsis in flight, so this is a sibling file built on the same helpers.MetadataPlugin.registerUnclaimedTopLevel, which registers residual containers as shipped (noname). It reads only the list, so its read of such a container's by-name door is not covered there. This fix covers that shape too, since the container is classified by shape and not byname. This fixture is single-package (nopackages[]), and fix(metadata,runtime): a multi-package config boot registers its unowned top level under manifest.id, as the artifact boot does #22612's own CONTROL pins a single-package config as unchanged. I checked this by reading its diff, not by a merged run (NOT MEASURED: a boot of this pin on a merge of fix(metadata,runtime): a multi-package config boot registers its unowned top level under manifest.id, as the artifact boot does #22612's head).distbundles its dev dependency@objectstack/objectql(and metadata-protocol sections), about 107k lines. It is reached through thetry { await import('@objectstack/objectql') } catch { … }inrest-server.ts, whose comment says a deployment without the data engine "must degrade rather than fail to load". With the bundle, that import never fails, and a second copy of the objectql module ships inside rest. No wrong answer was reproduced.i18n-extract.ts's top-level view walk sends a container that carries its ownnamedown the single-view branch. It scaffolds_views.NAMEand none of its views' keys, so a named container's_views.default/listViewskeys are never scaffolded. Not exercised through a public door here.Generated by Claude Code