Repository navigation
fix(rest): export lookup cells name the record by the target object's title field - #24980
Conversation
… title field A lookup cell in GET /data/:object/export read the lookup's own displayField and then a fixed key list on the expanded record, never the target object's nameField, so a contact (nameField full_name, no name column) exported as its job title. The route now resolves each reference target's title field once per export with ADR-0079's resolveDisplayField and hands it to the formatter on the field meta; the key list remains only for a target that resolves no title. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…eset Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…port-lookup-name-field
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 19 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 f716c1973339263f8eb32b0afa3f976dc5381590 && git checkout f716c1973339263f8eb32b0afa3f976dc5381590
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f32d795ce511c0644a0bc2b110710f8a181d87bb 321ea1f2be2f926af9fd431a82b389a74d20b3a1 && git checkout -B drift-repro f32d795ce511c0644a0bc2b110710f8a181d87bb && git merge --no-ff 321ea1f2be2f926af9fd431a82b389a74d20b3a1
node scripts/docs-audit/affected-docs.mjs --json f32d795ce511c0644a0bc2b110710f8a181d87bb
|
席位核对 · ACCEPT
Accepted deviations:
The open question, ruled by this seat: option A, land now.
Landing: through the merge queue by this seat once every check on this head reads Generated by Claude Code |
Fixes #24944
Clause-②: no
What changes
GET /api/v1/data/:object/export(Display → Export as CSV / XLSX) wrote a lookup, master_detail, user or tree cell from the lookup field's owndisplayField. Without one, it took the first scalar key of a fixed list (name,title,label,full_name, …) found on the expanded record. It never read the target object'snameField. A HotCRM contact declaresnameField: 'full_name', has nonamecolumn and has a jobtitlecolumn, so it exported as its job title.packages/rest/src/export-format.tswithReferenceTitleFields(metaMap, readSchema). It reads each reference target's schema once per export (deduplicated by target) and stampstitleFieldon that field's meta. It is best-effort per target and never throws: a target whose schema cannot be read keeps the key-list fallback.ExportCellMetais an export-only extension of core'sExportFieldMetawith that one key.ExportFieldMetain@objectstack/coreis not touched, and nothing new is exported from the package index.displayFromRecordreads, in order: the lookup's owndisplayField; else the target'stitleField; else, only when the target resolves no title, the key list; else the record id.packages/rest/src/data-action-routes.tsreadObjectSchema(the samegetMetaItem, thengetObjectSchema, order as before). It serves the export's own object and each target.metaMap = await withReferenceTitleFields(metaMap, readObjectSchema)runs before the$expandis built.The title rule:
resolveDisplayField, nottitleFieldOftitleFieldOf(packages/objectql/src/record-title.ts) is not reachable from@objectstack/restwithout a new runtime dependency.@objectstack/objectqlis only a devDependency here.resolveDisplayField(@objectstack/spec/data,display-name.ts) is reachable through the existing dependency. It is ADR-0079's single resolver, and plugin-audit and plugin-approvals use it to name a related record. Its rungs arenameField, thendisplayNameField, then a derivation. The first two rungs are exactlytitleFieldOf's rule. The formatter receives the resolved field and holds no rule of its own.On every body the protocol serves, the two answer the same. At registration the registry runs
provisionPrimary(schema, { synthesize: false }), which isresolveDisplayField, and stamps the result asnameField. Measured throughprotocol.getMetaItemon a real engine (a throwaway probe, not committed):nameFieldid,subject(text),emailsubjectemailid,label(select)idlabel(the raw option code)title,full_name(text)titletitleamount(number) onlyThey differ only on a served body that carries no pointer but has a title-eligible field. There
resolveDisplayFieldgives ADR-0079's derivation wheretitleFieldOfgives nothing. None of the measured registry bodies is in that state.Consequence: an object that declares no title is still served with a designated one. Its lookup cells now follow that designated title. In the table above,
subjectreplacesemail, and the id replaces a raw select code.Visible changes beyond the reported case
nameField: 'display_title'and has a literalnamecolumn. Its lookup cells change fromnametodisplay_title. That follows the declaration (pinned:AcmebecomesAcme (EMEA)).$expandsub-read is the engine's ownfindon the target, so FLS deletes an unreadable title field from the expanded record. The cell then falls to the record id, not to the key list. The card's mechanism assumption 3 expected the fallback "as it does today". Today's fallback for an unreadablefull_nameis the job title, which is the defect itself. The binding ruling keeps the key list only for an object that declares neither.displayFieldstill wins (pinned as a control).Evidence
All readings are on
packages/rest. They are taken at0fb2a82b06(the fix) and6dabdfbeab(the unit pins and the changeset), on top of base693352e4c.main.src/export-lookup-title-field.test.tsrun against the unmodified route:Tests 5 failed | 2 passed (7). The contact cell read[ '1', 'VP of Engineering' ]where[ '1', 'John Smith' ]was expected. The two controls (the lookup's owndisplayField, and a target served with no pointer) were green.Tests 10 passed (10).withReferenceTitleFields'sresolveDisplayField(schema as never)was replaced byundefinedthroughscripts/ablation-replace.mjs(anchor 1 → 0, blob6fd4e383→dbe37ef0). Result:Tests 5 failed | 2 passed (7), with the first pin red. Restored to6fd4e383032f== HEAD, andgit diff HEADwas empty.} else {was replaced by} {(blob6fd4e383→f6f403cc). Result:Tests 1 failed | 6 passed (7), the FLS pin. Restored hash-equal to HEAD.src/, reached by relative import, so the ablations needed nodist/rebuild.Gates
All readings below are at the final head
321ea1f2be. That is6dabdfbeabwithorigin/main6440cf1633merged in; the merged commit touches only plugin-security and its changeset.pnpm lint(eslint . --no-inline-config): exit 0, no findings printed.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 66 derived commands, plus the 10 families under "Declared WIDE population". All 76 were run with the exit code captured before any pipe, and all 76 exited 0.--ranreconciliation:✓ dispatch-gates --ran: 66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED (a DERIVED zero — all 66 recorded an exit code and none of them is 3).pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2:Test Files 306 passed (306),Tests 6154 passed | 326 skipped (6480).pnpm --filter @objectstack/rest typecheck: exit 0.Acceptance notes
packages/core/src/utils/import-runner.ts,resolveRef) matches a cell againstid, the lookup'sdisplayField, thenname,title,label,full_name,emailandusername. It never reads the target's title field. Measured throughPOST /api/v1/data/xt_case/importon the fixture of this PR:John Smith(a contact named by its formulafull_name) answersreference_not_found. So doWren(a storeddisplayNameFieldthat is not on the list) andAcme (EMEA)(a formuladisplay_title).mainwrote all resolved:VP of EngineeringandChief Procurement Officerbytitle, andAcmebyname.packages/core, which is held off for this card. It is reported to the dispatching seat as a finding, and not fixed here.Generated by Claude Code