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

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

  • spec — ExecutionContext.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.ts — update({ 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.ts — treatAsHistorical 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.

@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 main Jul 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>

This branch was successfully deployed

1 active deployment
Preview — e8797a09 Deployed Jul 25, 2026 by vercel[bot]
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 size/m tests tooling

Projects

None yet

2 participants