Repository navigation
fix(rest)!: the CSV export writes a text cell in a form its consumer reads as text, and the CSV import reverses it (#25078) - #25130
Conversation
…reads as text; the CSV import reverses it (#25078) The CSV writer marks a text cell its consumer would interpret, by one rule defined once in export-format.ts and keyed on the field's declared type; the CSV import parse removes exactly that mark, so an export to import round trip through the sync, async-job and admin user import doors returns the stored value. xlsx, json and the json import door are unchanged. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…25078) Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…s existing engine double (#25078) Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…not return unchanged (#25078) Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…port-value-context Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…the tenant column needs (#25078) Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check12 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. 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 c8b37c763ff517e00ab20649e4594aec95bec4cd && git checkout c8b37c763ff517e00ab20649e4594aec95bec4cd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e bf8244cb20dcaa155853344a15baad129cef7e0e && git checkout -B drift-repro 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e && git merge --no-ff bf8244cb20dcaa155853344a15baad129cef7e0e
node scripts/docs-audit/affected-docs.mjs --json 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e |
Contract reviewServed-tier: ① Derived judgmentsRead on the net diff of
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT (landing held)
Accepted deviations: the changeset is Carried to the pre-landing push (this PR merges Open with the maintainer: report Landing: held until objectstack-ai/objectui#13315's PR lands and objectstack's Generated by Claude Code |
Fixes #25078
Clause-②: no (narrowing)
Metadata-protocol: none
Blocked-by: objectstack-ai/objectui#13315
The CSV export now writes a text cell its consumer would interpret in a form that consumer reads as text, and the CSV import reverses exactly that. This is this repository's half of the maintainer's choice B on #25078 (comment
6319255545): the rule is defined once, server-side, and both directions read it.This PR stays a draft until two things are on
main: objectstack-ai/objectui#13315's PR, and this repository's.objectui-shabump over that commit. This PR does not move the pin.Contract
Served answers that change
GET /api/v1/data/:object/export?format=csv, on the metadata path and on the raw path (no field metadata). A CSV text cell is served with one apostrophe (U+0027) in front of its content, before the RFC-4180 quoting, when both of these hold:parseCsvToRows, exported from@objectstack/rest, and through it the CSV payload of three doors:POST /api/v1/data/:object/import;POST /api/v1/data/:object/import/jobs;POST /api/v1/auth/admin/import-users.applyMappingToRows, so no target field type is known at that point.Unchanged by construction: phone numbers and signed amounts; every cell that does not begin with one of the set; numeric, boolean, date and datetime cells; the xlsx and json export formats; json and xlsx import payloads.
Stated consequences
trimWhitespace: truethen trims the leading TAB or CR, exactly as it did before this change. WithtrimWhitespace: falsethe value returns exactly. Both cases are pinned incsv-text-cell-round-trip.test.ts.The rule lives in
packages/rest/src/export-format.ts(csvTextCellNeedsMark,markCsvTextCell,unmarkCsvTextCell,isCsvTextColumn). The writer isformatCsvCell/rowsToCsvinrest-server.ts. The reversal is inparseCsvToRowsinimport-prepare.ts.Files
packages/rest/src/export-format.ts: the rule, defined once. The set of non-text types is composed from the spec'sNUMERIC_VALUE_TYPES,BOOLEAN_VALUE_TYPES,CALENDAR_DATE_TYPESandINSTANT_TYPES, not re-listed.packages/rest/src/rest-server.ts:formatCsvCelltakes a text-column flag, androwsToCsvderives that flag per column from the declared type.packages/rest/src/import-prepare.ts:parseCsvToRowsreverses every cell, and reverses header cells before trimming and mapping.packages/rest/src/data-action-routes.ts: comment only. The export route's doc said the raw path streams stored values unchanged, which this change makes false for csv.packages/rest/src/csv-text-cell.test.ts(new);packages/rest/src/csv-text-cell-round-trip.test.ts(new);describeinpackages/plugins/plugin-auth/src/admin-import-users.test.ts, on that file's existing engine double, so the engine-double ledger does not move..changeset/25078-csv-text-cell.md:@objectstack/restpatch. It carries the BREAKING banner and one ADR-0087 marker,not-required (no-migration-prescription).Tests
All runs are on the final head
bf8244cb20.number,currencyandpercentfields holding a negative amount stay numeric and unprefixed;RestServer.@objectstack/restlocal project: 313 files / 6318 tests passed, 326 skipped.@objectstack/plugin-auth: 181 files / 3423 tests passed, 10 skipped.typecheck, the test layer included, exit 0.Ablations. Each leg goes through
scripts/ablation-replace.mjs. The anchor must hit, and the restore is proven by blob equal to HEAD and an emptygit diff HEAD. The whole tree's porcelain is empty after every leg.formatCsvCellnever marks): 20 red of 133.isCsvTextColumnalways true): 12 red.@objectstack/restthroughdist/:ablation-dist-preflightthen reported the marker absent from all 6 built files, and the admin round trip went red, 1 of 1.Gates
All readings are at
bf8244cb20, with each exit code captured before any pipe.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 71 commands. Every one of them exited 0, and so did the 10 families listed under "Declared WIDE population".✓ dispatch-gates --ran: 71 derived famil(ies) accounted for — 71 run, 0 NOT-MEASURED.node scripts/check-changeset-no-major.mjs --base origin/main --eventwith this body: exit 0,LEVEL AXIS ... a non-protocol change at patch.node scripts/check-adr-0087-registration.mjs --baseat the merge base: exit 0.pnpm lintover the whole repository: exit 0.mainatfccbd9d0d5was merged once into this branch. The incoming commits touch no file this PR edits. One of them adds the NOT NULL tenant column, and that is why the round-trip harness callsinstallDefaultOrganization.Acceptance notes
patch, withMetadata-protocol: none. That follows AGENTS.md Post-Task Checklist step 3 as it reads onmainsincef2b6cf230b: a non-protocol change, widening or breaking, shipspatchand keeps its BREAKING banner and its ADR-0087 disposition.cf153eb2e5, the wizard's default path applies the mapping in the browser, and the mapping is keyed by field name.installDefaultOrganization. The async job door'ssys_import_jobwrite needs the Default Organization on the NOT NULL tenant column that landed onmainin5e9b100743. Without that call one test is red, measured.Generated by Claude Code