Skip to content

feat(import): server-side coercion + write-mode for /data/:object/import - #2505

Merged
os-zhuang merged 7 commits into
mainfrom
claude/data-import
Jul 1, 2026
Merged

feat(import): server-side coercion + write-mode for /data/:object/import#2505
os-zhuang merged 7 commits into
mainfrom
claude/data-import

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

背景 / Problem

列表数据导入需要把电子表格里的原始值写进对象。特殊值(布尔、数字、日期、下拉选项 select、关联 lookup、多选)如果在每个前端各自转换,逻辑会重复且不一致,而且 lookup 名称→id 必须查库,前端做不了。

方案 / Solution

把所有特殊值处理下沉到服务端 POST /api/v1/data/:object/import,前端只发原始值 + 列映射,一条代码路径服务所有客户端(小文件同步、后续大文件异步)。

改动

  • spec (export.zod.ts):新增 ImportRequest / ImportResponse / ImportRowResult / ImportWriteMode / ImportMapping
    • writeMode: insert | update | upsert(后两者需 matchFields)
    • 开关:runAutomations(批量默认关钩子)、trimWhitespacenullValuescreateMissingOptionsskipBlankMatchKeydryRun
  • rest/import-coerce.ts(新)——coerceRow / coerceFieldValue,export-format 的逆运算:
    • boolean / number / date·datetime·time→ISO / select label→code / multi-select 拆分匹配 / lookup name→id(带缓存的 RefResolver,按 displayField→name/email/id 逐个候选查)
    • createMissingOptionstrimWhitespace 开关;逐字段收集错误而非首错即停
  • rest-server /import:归一化 mapping(record 或 entry[])、校验 writeMode↔matchFields、解析字段元数据、findExisting(blank/none/ambiguous 守卫)、逐行 action 报告(created/updated/skipped/failed)、skipAutomations 抑制钩子、超限 413 提示走异步任务
  • client SDK:两个 data 命名空间都加 data.import(object, request)
  • 测试:import-coerce(单元)+ import-integration(upsert/update/dryRun)共 28 个,全绿

未做(后续 P1/P2)

大文件异步 import_job + 流式 + 进度 + 5万上限、导入历史、真正的钩子开关落地(现在 skipAutomations 已透传,等引擎支持);任务级 undo、字段类型自动探测。

验证 / Verification

pnpm --filter @objectstack/rest exec vitest run import-coerce import-integration28 passed

前端配套改动在 objectui PR(依赖本 PR 发布后的 client)。

@vercel

vercelBot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 1, 2026 10:11am

Request Review

@github-actions

github-actionsBot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/client, @objectstack/platform-objects, @objectstack/rest, @objectstack/spec.

95 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/cloud-artifact-api.mdx(via packages/spec)
  • content/docs/concepts/cluster-semantics.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/implementation-status.mdx(via @objectstack/client, @objectstack/rest, @objectstack/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/concepts/packages.mdx(via @objectstack/client, @objectstack/platform-objects, @objectstack/rest, @objectstack/spec)
  • content/docs/concepts/setup-app.mdx(via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/concepts/skills.mdx(via @objectstack/spec)
  • content/docs/concepts/webhook-delivery.mdx(via @objectstack/spec)
  • content/docs/getting-started/architecture.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.mdx(via @objectstack/spec)
  • content/docs/getting-started/core-concepts.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/guides/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/guides/ai-capabilities.mdx(via @objectstack/spec)
  • content/docs/guides/airtable-dashboard-analysis.mdx(via @objectstack/spec)
  • content/docs/guides/analytics-datasets.mdx(via @objectstack/spec)
  • content/docs/guides/api-reference.mdx(via @objectstack/rest, @objectstack/spec)
  • content/docs/guides/authentication.mdx(via @objectstack/client)
  • content/docs/guides/business-logic.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/error-catalog.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/field-type-gallery.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/field-validation-rules.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/protocol-diagram.mdx(via packages/spec)
  • content/docs/guides/cheatsheets/query-cheat-sheet.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/quick-reference.mdx(via @objectstack/spec)
  • content/docs/guides/client-sdk.mdx(via @objectstack/client, @objectstack/spec)
  • content/docs/guides/common-patterns.mdx(via @objectstack/spec)
  • content/docs/guides/contracts/auth-service.mdx(via packages/spec)
  • content/docs/guides/contracts/cache-service.mdx(via packages/spec)
  • content/docs/guides/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/guides/contracts/index.mdx(via @objectstack/spec)
  • content/docs/guides/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/guides/contracts/storage-service.mdx(via packages/spec)
  • content/docs/guides/data-modeling.mdx(via @objectstack/spec)
  • content/docs/guides/driver-configuration.mdx(via @objectstack/spec)
  • content/docs/guides/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/guides/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/guides/external-datasources.mdx(via @objectstack/spec)
  • content/docs/guides/formula.mdx(via @objectstack/spec)
  • content/docs/guides/hook-bodies.mdx(via packages/spec)
  • content/docs/guides/kernel-services.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/dashboard.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/field.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/flow.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/index.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/object.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/validation.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/workflow.mdx(via @objectstack/spec)
  • content/docs/guides/packages.mdx(via @objectstack/client, @objectstack/platform-objects, @objectstack/rest, @objectstack/spec)
  • content/docs/guides/plugin-development.mdx(via @objectstack/spec)
  • content/docs/guides/plugins.mdx(via @objectstack/rest, @objectstack/spec)
  • content/docs/guides/project-scoping.mdx(via @objectstack/client, @objectstack/spec)
  • content/docs/guides/public-forms.mdx(via @objectstack/spec)
  • content/docs/guides/runtime-services/data-service.mdx(via packages/client)
  • content/docs/guides/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/guides/runtime-services/index.mdx(via packages/client, packages/spec)
  • content/docs/guides/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/guides/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/guides/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/guides/security.mdx(via @objectstack/spec)
  • content/docs/guides/seed-data.mdx(via @objectstack/spec)
  • content/docs/guides/skills.mdx(via packages/client, @objectstack/spec)
  • content/docs/guides/solutions/approval-workflow.mdx(via packages/spec)
  • content/docs/guides/solutions/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/guides/standards.mdx(via @objectstack/spec)
  • content/docs/guides/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/guides/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/realtime-protocol.mdx(via @objectstack/client)
  • content/docs/protocol/objectos/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@baozhoutao

Copy link
Copy Markdown
ContributorAuthor

前端配套 PR:objectstack-ai/objectui#2133(向导走服务端 /import + 写入模式 UI)。

…upsert) for /data/:object/import
Move all special-value handling for list import to the server so one code path
serves every client. Callers POST raw spreadsheet values + a column mapping;
the server coerces each cell from the object's field metadata and routes rows
to insert / update / upsert.
- spec: ImportRequest / ImportResponse / ImportRowResult / ImportWriteMode /
ImportMapping schemas (writeMode, matchFields, runAutomations, trimWhitespace,
nullValues, createMissingOptions, skipBlankMatchKey, dryRun)
- rest/import-coerce: coerceRow/coerceFieldValue — boolean, number, date/
datetime/time→ISO, select label→code, multi-select split+match, lookup
name→id via a caching RefResolver; createMissingOptions + trimWhitespace toggles
- rest-server /import: normalize mapping (record | entry[]), validate
writeMode↔matchFields, resolve field metadata, findExisting with blank/none/
ambiguous guards, per-row action report (created/updated/skipped/failed),
hook suppression via skipAutomations, clearer 413 for oversized payloads
- client SDK: data.import(object, request) on both data namespaces
- tests: import-coerce (unit) + import-integration (upsert/update/dryRun) — 28
Accept format:'xlsx' with a base64-encoded workbook (xlsxBase64) and an
optional sheet selector. The server flattens ExcelJS cells to their
human-visible text (formula results, hyperlink text, rich-text runs,
dates -> ISO) so a parsed xlsx yields the same cells a CSV export would,
then feeds them through the existing coercion + upsert pipeline.
- spec: extend ImportRequestSchema format enum with 'xlsx'; add
xlsxBase64 + sheet fields.
- rest: xlsxCellToString + parseXlsxToRows helpers (dynamic exceljs
import so csv/json imports don't pay for it); wire the xlsx branch
into the /import body parser; 400 on malformed workbooks.
- tests: 3 integration cases (coercion parity with csv, named-sheet
selection + format inference, malformed-payload 400).
…ity)
Reference cells (name/email/id) now resolve more safely:
- id fast-path: try an exact id match first, so a pasted record id is
authoritative and never shadowed by a name/label collision.
- ambiguity guard: when the first matching candidate field hits >1 record,
stop and report a new 'reference_ambiguous' error instead of silently
linking whichever row came back first ($top:2 detects the dup).
- structured resolver contract: RefResolver may return { id, ambiguous,
matchedField }; bare string|undefined from legacy resolvers still works
(normalizeRefMatch).
Tests: unit (structured result + ambiguous + not-found) and integration
(duplicate-name ambiguity, id fast-path).
…50k ceiling
Add an asynchronous import path alongside the synchronous POST /import route
for large files (up to 50,000 rows). A create request persists a sys_import_job
row and returns 201 immediately; a background worker streams the batch through
the SAME shared runImport() core the sync route uses, persisting progress and a
capped per-row results report on the job row.
- spec: import-job schemas + ImportJobApiContracts (create/progress/results/
list/cancel); IMPORT_JOB_MAX_ROWS=50k; status enum.
- platform-objects: sys_import_job object (state/counters/results/timestamps),
registered by the REST plugin via the manifest service.
- rest: extract runImport()/prepareImportRequest() so sync + async paths are
byte-identical; 5 async routes + fire-and-forget worker with onProgress
persistence and cooperative cancellation (process-local signal, persisted
status as durable source of truth).
- tests: real engine + protocol integration for create→poll→results→list→
cancel, 50k ceiling (413), and unknown-job 404s. 195/195 rest tests green.
Add createImportJob / getImportJobProgress / getImportJobResults /
listImportJobs / cancelImportJob to the top-level and scoped `data`
namespaces, mirroring the async REST routes. Large payloads are posted once
via createImportJob (returns a jobId immediately); callers poll progress /
results / history and can cancel. Older servers return 404 → rejected promise,
so consumers can feature-detect and fall back to the synchronous `import`.
5 client tests assert route/method/query-string shaping and response
unwrapping.
Add POST /data/import/jobs/:jobId/undo — logically reverses a finished
import: deletes the records it created and restores the fields it updated
to their pre-import values.
- import-runner: capture an ImportUndoLog (created ids + per-updated
before-snapshots of only the written fields) when captureUndo is set.
- sys_import_job: persist undo_log (json) + reverted_at (datetime).
- rest-server: worker captures undo for non-dry-run jobs <= 5000 rows
(IMPORT_JOB_UNDO_MAX_ROWS); undo route deletes/restores with automations
skipped, then stamps reverted_at. undoable/revertedAt surfaced on
progress + summary DTOs.
- spec: undoable/revertedAt on progress+summary; UndoImportJobResponse;
undoImportJob contract.
- client SDK: data.undoImportJob(jobId) on both namespaces + test.
- tests: end-to-end undo (delete created + restore updated), double-undo
409, unknown-job 404.
…ports
The public API surface check flagged 28 non-breaking additions (the
import-job DTOs/contracts + the new UndoImportJobResponse). Regenerate the
committed snapshot so check:api-surface passes.
os-zhuang added a commit that referenced this pull request Jul 16, 2026
…test code (#2557) (#3011)
ADR-0086 was written the day #2557 opened; its full plan has since shipped and
three later ADRs (0090, 0094, 0095) built on it. Flip status Proposed → Accepted
(substantially implemented) and add a code-grounded Re-evaluation (2026-07-16)
addendum:
- Per-decision/phase status table (D1-D7, P0-P2) with file-level landing points:
D3 packageId+provenance, D5 bootstrapDeclaredPermissions, P0 objectui
slice-merge (#2505/#2508), D6/D7 two doors — all implemented.
- Flag the two mechanisms the original text describes that later work overtook:
P0's shared-data-record slice-merge premise -> ADR-0094 pure projection +
metadata overlay; D4's Salesforce-muting subtract layer -> ADR-0005 first-class
overlay (ADR-0094 D5 revised).
- Restate the D1 classification table in current ADR-0090 vocabulary: Position
(not Role, flat/no parent), Profile removed, everyone/guest anchors, renamed
+ time-boxed (ADR-0091) assignment tables, adminScope (ADR-0090 D12).
- Net: no decision reversed; thesis holds. Remaining open items are the ones
already marked deferred/evidence-gated.
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/xltests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@baozhoutao@os-zhuang