Skip to content

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

Open
baozhoutao wants to merge 6 commits into
mainfrom
claude/data-import
Open

feat(import): server-side coercion + write-mode for /data/:object/import#2505
baozhoutao wants to merge 6 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

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 1, 2026 7:28am

Request Review

@github-actions

github-actions Bot 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
Contributor Author

前端配套 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file size/xl tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant