Skip to content

data import undo: 撤销一次「historical」导入无法还原被保留的 readonly / 审计字段 — undo 的 writeCtx 缺 preserveAudit (#3493 follow-up) #3549

Description

@os-zhuang

现象

#3493 / #3497treatAsHistorical 导入通过 preserveAudit 保留原始时间线:client 提供的 updated_at/updated_by 被保留,审计/业务 readonly 字段(closed_atresolved_by …)进白名单、不再被 strip。

导入 undo(逻辑回滚)没有对称地带上 preserveAudit,所以撤销一次历史 upsert 刷新时,恰恰是这个功能保留下来的那些字段无法被如实还原——被撤销的行并没有回到导入前的状态。

机制(确切落点)

packages/rest/src/rest-server.ts:4052(POST /data/import/jobs/:jobId/undo):

const writeCtx = { ...(context ?? {}), skipAutomations: true, skipStateMachine: true };

undo 通过 updateData({ data: u.before, context: writeCtx }) 把每行「被导入触碰的字段」还原成导入前快照 u.before。但 writeCtx 没有 preserveAudit,于是这条 restore-update 走的是普通写路径:

  • u.before 里的业务 readonly 字段(如 closed_at)被 stripReadonlyFields drop(非 system + 无 preserveAudit)→ 该字段的还原被静默丢弃,行保持导入值;
  • 审计 hook 把 updated_at/updated_by 重新 stamp 成撤销当下 / 撤销者,driver 再强制 stamp updated_at = now(feat(rest): preserve the original audit timeline for a historical import (#3493) #3497 的 hook/driver 只在 preserveAudit 下才尊重 client 值)。

结果:一次历史 upsert 把既有行的 closed_at 改成历史值,撤销后这行的 closed_at 仍是导入值(未回滚)。这正是 #3493 在 undo 路径上的镜像。undo 的代码注释本身写着「an undo reinstates an established fact, it does not walk the FSM」——「reinstate established fact」正是 preserveAudit 服务的语义,却没带上。

影响面

窄但真实:仅影响「treatAsHistorical + upsert 刷新既有行 + 之后 undo」。created-only 行的 undo 是 delete,不受影响;非历史导入不触碰 readonly 字段,也不受影响。

候选修法(需拍板)

  1. 对称补齐(最小改动):undo 的 writeCtx 也带 preserveAudit: true(与 skipStateMachine 并列),让 restore-update 如实还原 u.before 里的审计/readonly 字段。一行改动 + 一条测试。

  2. 一个需拍板的语义点(updated_at):「undo 本身算不算一次修改」存在分歧。

    • 若视 undo 为「回滚到导入前」→ 连 updated_at 一起还原(方案 1 直接达成);
    • 若视 undo 为「一次新的写、updated_at 该记撤销时刻」→ updated_at 不还原可接受,但业务 readonly 字段(closed_at)仍应还原(否则 undo 不完整)——这部分正是 preserveAudit 白名单覆盖的、争议最小的部分。

倾向方案 1(最小、对称、符合 undo 的「逻辑回滚」语义);updated_at 的哲学取舍留给维护者定。

复现

  1. treatAsHistorical:true + writeMode:'upsert' 刷新一行既有记录,payload 带一个声明为 readonly 的业务字段(如 closed_at:'2021-03-01T…')→ 被保留写入(feat(rest): preserve the original audit timeline for a historical import (#3493) #3497)。
  2. 对该 import job 调 POST /data/import/jobs/:jobId/undo
  3. 查库:该行 closed_at 仍是导入值,未还原成导入前的值。

参考

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions