feat(rest): preserve the original audit timeline for a historical import (#3493)#3497
Draft
os-zhuang wants to merge 1 commit into
Draft
feat(rest): preserve the original audit timeline for a historical import (#3493)#3497os-zhuang wants to merge 1 commit into
os-zhuang wants to merge 1 commit into
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:
|
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