Skip to content

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

Merged
os-zhuang merged 1 commit into
mainfrom
claude/historical-import-timestamps-0hrwxy
Jul 27, 2026
Merged

feat(rest): preserve the original audit timeline for a historical import (#3493)#3497
os-zhuang merged 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

vercelBot commented Jul 25, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 25, 2026 4:02am

Request Review

@github-actionsgithub-actionsBot 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.

@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 01:13
@os-zhuang
os-zhuang merged commit 81ce41a into mainJul 27, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/historical-import-timestamps-0hrwxy branch July 27, 2026 01:13
os-zhuang pushed a commit that referenced this pull request Jul 27, 2026
Follow-up hardening for #3497. Each layer of the historical-import audit
preservation is tested (audit hook + readonly whitelist through the real engine
in plugin.integration.test.ts; the driver's updated_at stamp in
sql-driver-timestamp-format.test.ts; the runner wiring in
import-runner-historical.test.ts) — but nothing asserted that the ENGINE threads
`context.preserveAudit` into the DRIVER options, the wire that makes the driver's
stamp reachable from a real write. Add that assertion via the shared
`buildDriverOptions` observable (same pattern as the tenantId forwarding tests),
plus its opt-in negative. Test-only; releases nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5rdfBKjkbcoEif4KUV6xE
os-zhuang added a commit that referenced this pull request Jul 27, 2026
…#3509)
Follow-up to #3497. Documents the audit-timeline half of treatAsHistorical in the state-machine protocol doc (was only the FSM-skip half), and adds an engine.test.ts assertion that buildDriverOptions threads context.preserveAudit into the driver options — the one untested wire that makes the driver's updated_at stamp reachable from a real write. Docs + test only; empty changeset (releases nothing).
os-zhuang added a commit that referenced this pull request Jul 27, 2026
… exemption (#3493) (#3550)
The `field.readonly` describe said "system-context writes (import, seed replay,
migration) are exempt" — but a normal data import is NON-system (its writeCtx is
session-derived, isSystem:false) and still strips readonly fields. The strip is
bypassed only by `isSystem` writes (seed replay, migration) and, since #3497, by
an opt-in "historical" import (`preserveAudit`) that admits a whitelist (the
audit/timestamp family + author-declared business readonly fields). Fix the
describe accordingly and regenerate references/data/field.mdx.
Describe-string + regenerated reference doc only; no schema shape / behavior /
api-surface change. Empty changeset (releases nothing).
Claude-Session: https://claude.ai/code/session_01B5rdfBKjkbcoEif4KUV6xE
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

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude