feat(rest): preserve the original audit timeline for a historical import (#3493) - #3497
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 110 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
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).
This was referenced Jul 27, 2026
Merged
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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Closes #3493. Follow-up to #3479/#3483.
treatAsHistoricalsolved the FSM half of a historical migration — mid-lifecycle rows are no longer rejected byinitialStates. But the other half — preserving the original timeline — still didn't hold:updated_at= the import day andupdated_by= the importer, not the original values.writeMode: 'upsert'refresh silently stripped businessreadonlyfields (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 alongsideskipStateMachinewhen the request opts intotreatAsHistorical.Changes
ExecutionContext.preserveAudit(server-set only, never client-supplied) andDriverOptions.preserveAudit(threaded to the driver's update stamp).plugin.ts):updated_at/updated_bybecome client-preferred (?? now/?? userId) underpreserveAudit, symmetric with howcreated_at/created_byalready behave on insert.rule-validator.ts):stripReadonlyFieldsadmits a whitelist — the audit/timestamp family (created_at/created_by/updated_at/updated_by) plus author-declared businessreadonlyfields (closed_at, …). Platform-managedsystemcolumns outside that family (organization_id/tenancy, generated columns) stay stripped.updatepath keeps a suppliedupdated_atinstead of force-advancing it tonowwhenDriverOptions.preserveAuditis set (fills-only-empty, mirroring the insert stamp). Covers the single-id and rotation update paths.preserveAuditon the write context ifftreatAsHistorical.Design constraints (from the issue)
preserveAuditunset and still auto-stampsupdated_at/updated_byand stripsreadonlyexactly as before.isSystemexemption — a historical import reinstates established facts but cannot forge tenancy (organization_id) or system-generated values.owner_idisreadonly: false, so the strip never touched it; ownership stays governed by FLS.Tests
rule-validator.test.ts— thepreserveAuditwhitelist: keeps the audit family + businessclosed_at, still stripsorganization_id, and strips everything when the flag is off.plugin.integration.test.ts— end-to-end on the update path: a suppliedupdated_at/updated_by/closed_atsurvives underpreserveAudit; a normal update still overwritesupdated_byand stripsclosed_at.sql-driver-timestamp-format.test.ts—update({ preserveAudit })keeps a suppliedupdated_at, still stampsnowwhen none is supplied, and a normal update force-advances even when one is supplied (regression).import-runner-historical.test.ts—treatAsHistoricalnow sets bothskipStateMachineandpreserveAudit; 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, andspec tsc --noEmitall pass; reference docs regenerated. Changeset included.🤖 Generated with Claude Code
Generated by Claude Code