Skip to content

feat(rest): preserve the original audit timeline for a historical import (#3493)#3497

Draft
os-zhuang wants to merge 1 commit into
mainfrom
claude/historical-import-timestamps-0hrwxy
Draft

feat(rest): preserve the original audit timeline for a historical import (#3493)#3497
os-zhuang wants to merge 1 commit into
mainfrom
claude/historical-import-timestamps-0hrwxy

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What & why

Closes #3493. Follow-up to #3479/#3483.

treatAsHistorical solved the FSM half of a historical migration — mid-lifecycle rows are no longer rejected by initialStates. But the other half — preserving the original timeline — still didn't hold:

  • Importing a 2020-created / 2021-closed ticket stored updated_at = the import day and updated_by = the importer, not the original values.
  • A writeMode: 'upsert' refresh silently stripped business readonly fields (closed_at, resolved_by).

Result: migrated data all looked "modified today" — "recently modified" sorting, SLA reports, and audit trails came out wrong.

Three layers were force-overwriting the timeline. All three now honor a single new opt-in flag, ExecutionContext.preserveAudit, which the import runner sets alongside skipStateMachine when the request opts into treatAsHistorical.

Changes

  • specExecutionContext.preserveAudit (server-set only, never client-supplied) and DriverOptions.preserveAudit (threaded to the driver's update stamp).
  • objectql — audit hook (plugin.ts): updated_at / updated_by become client-preferred (?? now / ?? userId) under preserveAudit, symmetric with how created_at / created_by already behave on insert.
  • objectql — readonly strip (rule-validator.ts): stripReadonlyFields admits a whitelist — the audit/timestamp family (created_at/created_by/updated_at/updated_by) plus author-declared business readonly fields (closed_at, …). Platform-managed system columns outside that family (organization_id/tenancy, generated columns) stay stripped.
  • driver-sql — the SQL update path keeps a supplied updated_at instead of force-advancing it to now when DriverOptions.preserveAudit is set (fills-only-empty, mirroring the insert stamp). Covers the single-id and rotation update paths.
  • rest — the import runner sets preserveAudit on the write context iff treatAsHistorical.

Design constraints (from the issue)

  • Opt-in. A normal write leaves preserveAudit unset and still auto-stamps updated_at/updated_by and strips readonly exactly as before.
  • Whitelist, not blanket exemption. Deliberately narrower than the isSystem exemption — a historical import reinstates established facts but cannot forge tenancy (organization_id) or system-generated values. owner_id is readonly: false, so the strip never touched it; ownership stays governed by FLS.
  • No security change. Permissions / RLS / field-level security are unaffected — this only changes which audit/readonly values the runtime overwrites, never who may write the record.
  • No new UI. The objectui "Import as historical data" checkbox (objectui#2815) now drives both halves.

Tests

  • rule-validator.test.ts — the preserveAudit whitelist: keeps the audit family + business closed_at, still strips organization_id, and strips everything when the flag is off.
  • plugin.integration.test.ts — end-to-end on the update path: a supplied updated_at/updated_by/closed_at survives under preserveAudit; a normal update still overwrites updated_by and strips closed_at.
  • sql-driver-timestamp-format.test.tsupdate({ preserveAudit }) keeps a supplied updated_at, still stamps now when none is supplied, and a normal update force-advances even when one is supplied (regression).
  • import-runner-historical.test.tstreatAsHistorical now sets both skipStateMachine and preserveAudit; a normal import sets neither.

Full suites green: @objectstack/spec (6850), @objectstack/objectql (1075), @objectstack/driver-sql (287), @objectstack/rest (348). check:docs, check:api-surface, check:spec-changes, check:upgrade-guide, and spec tsc --noEmit all pass; reference docs regenerated. Changeset included.

🤖 Generated with Claude Code


Generated by Claude Code

…ort (#3493)

Follow-up to #3479/#3483. `treatAsHistorical` skipped the state machine but the
platform still rewrote the timeline: an imported row stamped `updated_at` /
`updated_by` to the import instant, and an `upsert` refresh silently stripped
business `readonly` fields (`closed_at`, `resolved_by`). Reports, audit, and
"recently modified" sorting all came out wrong.

Introduce an opt-in `ExecutionContext.preserveAudit` flag (server-set only) that
`treatAsHistorical` sets alongside `skipStateMachine`, and make the three layers
that force-overwrite the timeline respect it:

- objectql audit hook (plugin.ts): `updated_at` / `updated_by` become
  client-preferred (`?? now` / `?? userId`) under preserveAudit, symmetric with
  how `created_at` / `created_by` already behave on insert.
- objectql readonly strip (rule-validator.ts): admits a WHITELIST — the
  audit/timestamp family plus author-declared business `readonly` fields — while
  platform-managed `system` columns outside that family (`organization_id` /
  tenancy, generated columns) stay stripped. A whitelist, not the blanket
  `isSystem` exemption, so it is not a tenancy-forging backdoor.
- driver-sql update: keeps a supplied `updated_at` instead of force-advancing it
  to `now` (`DriverOptions.preserveAudit`).

Fully opt-in: a normal write still auto-stamps and strips exactly as before.
Permissions / RLS / field-level security are unaffected. The objectui "Import as
historical data" checkbox (objectui#2815) now drives both halves — no new UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5rdfBKjkbcoEif4KUV6xE
@vercel

vercel Bot commented Jul 25, 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 25, 2026 4:02am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling size/m labels Jul 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/objectql, @objectstack/driver-sql, @objectstack/rest, @objectstack/spec.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/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 @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/driver-sql, @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql, @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/objectql, @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 @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/driver-sql, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

2 participants