Repository navigation
feat(audit-trail): record the id a primary key moved from [PRD-1321] - #1946
Open
bexchauveto wants to merge 4 commits into
Open
bexchauveto wants to merge 4 commits into
bexchauveto wants to merge 4 commits into
Conversation
1 new issue
|
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (5)
🤖 Increase coverage with AI coding...🚦 See full report on Qlty Cloud » 🛟 Help
|
bexchauveto
force-pushed
the
feature/prd-1321-agent-nodejs-carry-the-id-a-primary-key-move-came-from
branch
from
October 7, 2026 13:50
05cba83 to
f068dbc
Compare
bexchauveto
changed the base branch from
main
to
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
October 7, 2026 13:50
bexchauveto
added this pull request to stack #1915
October 7, 2026 13:50
bexchauveto
force-pushed
the
feature/prd-1321-agent-nodejs-carry-the-id-a-primary-key-move-came-from
branch
4 times, most recently
from
October 9, 2026 07:06
c19f05f to
fca32f5
Compare
Base automatically changed from
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
to
main
October 9, 2026 12:33
bexchauveto
force-pushed
the
feature/prd-1321-agent-nodejs-carry-the-id-a-primary-key-move-came-from
branch
from
October 9, 2026 12:33
fca32f5 to
a9158cc
Compare
…each side by it An update carries two states but files under one id, so a writable primary key that is also redacted left the previous side with nothing to answer a permission scope: #1909 withheld it rather than judging it by the id the record ended up with. Correct, and lossy — a caller squarely in scope saw nothing. The row now carries `previous_record_id`, written on every confirmed update, and each side is judged against the id it actually had. Written whether or not the key moved, which is where this departs from agent-ruby#394: that table had never shipped, so a null there can only mean the key held still. Here a null has to keep meaning "written before this column existed", or a row from an older agent whose key did move would be judged by the id it moved to — the leak #1909 closed. For the same reason the column arrives as its own migration rather than an edit to 001. A pending update's new side still answers with what it captured: the row is filed under the id the record had before the write, which says nothing about the state it was moving to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The project-level author filter could only offer activity-log authors: the cross-collection route answered no availableUsers. It now carries them on the first page (no before), like the per-record route, through an optional store method, listTimelineUsers. Authors are not detail values, so the admin gate leaves them in. Stacking on the timeline route also exposed previousRecordId on its rows; it is stripped there as on the per-record paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d, and keep it at insert Migration 002 checked for the column, then added it. Without Postgres's advisory lock, a second agent booting at the same moment could add it in between, and the duplicate-column error failed this agent's startup. A failed addColumn now re-reads the table: a column that is there means the work is done, anything else is rethrown. toRow also keeps a previousRecordId given at insert, as the in-memory store does, so a confirm that omits it cannot make the row read as older than the column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The admin gate is gone, so authors need no exemption from it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bexchauveto
force-pushed
the
feature/prd-1321-agent-nodejs-carry-the-id-a-primary-key-move-came-from
branch
from
October 9, 2026 12:43
a9158cc to
0e91320
Compare
This branch has not been deployed
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.

Stacked on #1911 (PRD-1257): its base retargets to
mainonce #1911 merges.Closes PRD-1321. The Ruby twin is agent-ruby#394, already merged.
What was lossy
An
updatecarries two states but is filed under one id. When a primary key is both writable (so the capture records it) and redacted, both sides hold the placeholder, the packed id is the only value left — and it speaks for the side the row was filed under, not the other one.#1909 chose to withhold rather than answer the previous side from the id the record ended up with. That is correct, and it costs a caller squarely in scope the values they should have seen:
/stateloses the most, since it rebuilds from adeleterow carrying the whole writable column set.What this does
previous_record_idon the audit row, written on every confirmed update, andwithhold.tsjudges the previous side against it.Written whether or not the key moved — this is the one place it departs from agent-ruby#394. That table had never shipped, so a null there can only mean the key held still. This one has: a null has to keep meaning written before this column existed, or a row from an older agent whose key did move would be judged by the id it moved to, which is exactly the leak #1909 closed. For the same reason the column arrives as
002-add-previous-record-idrather than an edit to001, which deployed databases have already recorded as applied.Ported from Ruby unchanged:
Tests
442 tests pass across
test/audit-trailandtest/routes/access.Two things the existing tests caught while I wrote this, both worth knowing: an absent column read as known until I normalised
undefinedto null (which released a previous side it should have withheld), and decoding per side rather than per row made a bad id warn twice.Also in this PR: the timeline's authors
Second commit, here because it needs #1911's timeline route.
The project-level author filter could only offer activity-log authors:
GET /forest/_audit-trailanswered nometa.availableUsers. It now does, on the first page only (nobefore), like the per-record route; later pages omit the key rather than send[].listTimelineUsers(optional likelistTimeline): the distinct authors under the timeline's filters and readable collections, ignoring the cursor. The SQL store shares theMAX()/GROUP BYquery withlistDistinctUsers. A store without it serves the rows with noavailableUsers.listTimelinenow carrypreviousRecordIdtoo, so the timeline route strips it like the per-record paths do.scanServedValuesto return{ page, count, authors }and build the body in its caller. I kept that shape and strippreviousRecordIdwhere the caller builds the response.Front reader: ForestAdmin/forestadmin#10033. Ruby contract added to PRD-1258.
Tests: route (first page announces with the filters and collections, later page omits, store without the method omits,
previousRecordIdabsent) and SQL store (filters and collections honoured, empty collections match nothing). 484 tests pass acrosstest/audit-trailandtest/routes/access; lint clean.🤖 Generated with Claude Code
Note
Record previous primary-key id in audit trail and add timeline route
previousRecordIdin instrument.ts, even when the key did not changeprevious_record_idcolumn and a(timestamp, id)index via new migrations in migrations.ts, with retry handling for concurrent startup racesGET /_audit-trailtimeline route in audit-trail-timeline.ts with newest-first paging, shared filters, and author metadata. It is mounted only when the store exposeslistTimelinecanUseAuditTrailTimelineto the capabilities response in capabilities.ts, set from whether the audit-trail store haslistTimelinepreviousRecordIdis internal and stripped from all client responses in both the timeline and history handlers in audit-trail-timeline.ts and audit-trail.ts; existing audit tables get the new column via migration 003Macroscope summarized 0e91320.