Repository navigation
fix(rest)!: a schema-derived export skips the fields declared hidden - #24178
Conversation
A schema-derived export (no ?fields=) took every field of the object, so the hidden __search search companion became an empty "Search Index" column. The default column set now drops every field declared `hidden`; an explicit ?fields= list is answered as before. Claude-Session: https://claude.ai/code/session_01BVm1pAXVXQ3PjC42j1kPFf Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVm1pAXVXQ3PjC42j1kPFf Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVm1pAXVXQ3PjC42j1kPFf Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 18 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 3042aae41dcba8f50dff167ef223d2e5ad77197f && git checkout 3042aae41dcba8f50dff167ef223d2e5ad77197f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cb8756b005a2d77c7c07574dffe4f99b8e91f8d1 4f1363cde882d0edb51c73a775f65a3f6292e071 && git checkout -B drift-repro cb8756b005a2d77c7c07574dffe4f99b8e91f8d1 && git merge --no-ff 4f1363cde882d0edb51c73a775f65a3f6292e071
node scripts/docs-audit/affected-docs.mjs --json cb8756b005a2d77c7c07574dffe4f99b8e91f8d1 |
Contract reviewServed-tier: Narrowing face: the PR body and the changeset both carry ① Derived judgments
② Semver level
③ Boundary flags
Check-runs on the head, as read: 38 in all; 24 Implemented-by: VERDICT: PASS Inputs read at 2026-10-17T15:24Z: card #24143 (body and its 3 comments), PR #24178 (body, 4-file list, net diff against Generated by Claude Code |
席位核对 — ACCEPT ·
|
Fixes #24143
Clause-②: no (narrowing)
BREAKING (a served-set narrowing at a REST door): the default columns of
GET /api/v1/data/:object/exportlose every field declaredhidden. Shipped asminorunder the launch-window convention; the changeset carries the sameClause-②line and one ADR-0087 disposition (not-required (no-migration-prescription)). This narrowing owes oneCONTRACT_REVIEW_TIERreview of record before the queue.What changed
packages/rest/src/data-action-routes.ts, the export handler's schema-derived column set (no?fields=): the column list ismetaMap's names minus every field whose definition hashidden === true.metaMapitself is untouched, so cell formatting and an explicit?fields=that names a hidden field are answered as before. The field definitions are read through the existingtemplateFieldDefs(import-template.ts), which names fields exactly asbuildFieldMetaMapdoes, so no shape normalizer is spelled a third time.fields=, and one sentence of theimport-template.tsmodule header that described the export's default columns, now say what the export does. That header sentence is a comment-only edit outside the dispatched file surface; my change made it false, so it is corrected here rather than left stale.?fields=arm, the FLS narrowing (跟踪:UI 操作按钮与 apiMethods 白名单一致性契约落地(#3026 设计定稿) #3391, security 服务 getReadableFields 查询面(export 列投影的长期正解) #3547) and the headerless no-projection path,format=json(it streams rows as the read returns them and has no column set),?template=true(its own column rule), and everything inpackages/objectqlandpackages/spec.Shared predicate
None exists. No package exports an "is this field hidden" or "shown by default" predicate:
@objectstack/corehas none, and objectql (search-companion.tsisCompanionSourceEligible), spec (field-group-layout.ts,record-surface.ts,search-fields.ts) and rest (form-routes.ts) each spellhidden === trueinline. This change spells it the same way, once, at the export's column selection.Reach, re-measured on
examples/app-showcaseA
pnpm dev -- --freshboot of this worktree, signed in as the seeded admin. The shipped admin sets carry noallowExport, so an environment permission set grantingallowRead+allowExportwas saved and assigned to the admin for the measurement.GET /api/v1/data/showcase_task/export?format=csv, nofields:After the fix,
?fields=title,organization_id,__searchstill answers the headerTitle,Organization,Search Index, withDefault Organizationin the Organization cells and an empty Search Index.Breadth of the narrowing
On the same boot I exported every served object that carries a hidden field: 80 objects, of which 76 answered 200. For each one I compared the header before and after, and counted non-empty cells in each hidden column of the before export. The only labels that left any header are the hidden fields below. No other column moved, and no column appeared.
__search(Search Index)organization_id(Organization)Default Organizationhere)owning_business_unit_id(Owning Business Unit)sys_session.token(Session Token)sys_account.previous_password_hashessys_api_key.key(Hashed Key)No example app (
app-crm,app-showcase,app-todo,app-multi-package,embed-objectql) declares ahiddenfield of its own. The hidden fields are the companion, the two injected scope anchors (organization_id,owning_business_unit_id), and the three platform-object fields above.organization_idis the one column with real data that leaves the default export. A reader who wants it names it in?fields=.Tests
packages/rest/src/export-hidden-columns.test.tsdrives the real export route: a realRestServeron a realObjectStackProtocolImplementationon a realObjectQL, with a better-sqlite3:memory:driver. The companion is declared by its real producer (provisionSearchCompanion), next to an author-declared hidden field that carries a value. The security service answersgetReadableFieldsthe way plugin-security does for an unrestricted caller: every schema field, the companion included.hidden, and the readable projection the route asked for admits both hidden fields.ID,Title; rows unchanged.ID,Title.ID,Title.ID,Title.?fields=title,internal_code,__searchanswersTitle,Internal Code,Search Index, withIC-1/IC-2in the hidden field's cells and an empty companion.Ablation, through
scripts/ablation-replace.mjs(the anchor must hit, and restore is checked against the blob). The subject isdata-action-routes.ts, which the test reaches through relative./rest-serverimports, so it runs fromsrc/and nodist/is involved. Mutation:?.hidden !== true,became?.hidden !== 'ablated-24143',, so the filter keeps every field. The anchor count went from 1 to 0, and the blob went from7fe336246ce5toa690835e24d0. Result: 4 failed, 2 passed. All four default pins went red: under the projection the header gainedInternal CodeandSearch Index, and on the fallback path it gainedInternal Code. The anti-vacuity case and the explicit-?fields=pin stayed green, which shows the explicit arm answers the same with the filter and without it. Restore leg: blob after restore7fe336246ce5equals HEAD, andgit diff HEADis empty.Suite runs (on the merged tree
4f1363cde):pnpm --filter @objectstack/rest test: 292 files passed; 6013 tests passed, 326 skipped.pnpm --filter @objectstack/rest typecheck: exit 0. The test layer compiles, and--listFilesincludes the new file.test:repofor every package whose repo tests can read the changed files (each one scanspackages/or reads rest's sources): rest (5 files, 254 passed / 12 skipped), core (6 files, 84 passed), spec (66 files, 1105 passed), objectql (1 file, 5 passed) and types (1 file, 11 passed). Runtime'stest:reporeads onlyrest-server.tsandmeta-item-read-gate.tsfrom rest, neither of which this diff touches, so it was not run.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon4f1363cde: 63 derived commands, all exit 0. The--ranreconciliation answers 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN.pnpm exec eslint --no-inline-configon the three changed source files: exit 0.Acceptance notes
format=jsonhas no column projection. It writes each row as the read returns it, so a hidden author field's value stays in a JSON export, and an explicit?fields=does not narrow JSON rows either. That was already true before this change.FieldSchema.hiddenis documented as a UI contract that has never governed serialization, and JSON is the serialization door, so JSON is left as is.?template=true) still offers a writable author-declared hidden field as a column (ruling Q2 C on the template). The export and the template now disagree on such a field on purpose: the export answers "what do I see by default", and the template answers "what can I fill".Generated by Claude Code