feat(api): record who created a document and who last wrote it - #142
Merged
Conversation
FootprintAI/grandturks#1125 asks whether "user A created this document and user B updated it" is answerable. It was not: the metadata carried no writer at all, so a document overwritten by a different principal was indistinguishable from one written once - in content and in metadata alike. TWO FIELDS, NOT ONE, and that is the point. Writing an existing documentId is an UPSERT and there is no Update rpc. With only a creator, B's overwrite of A's document would still report A and B's write would leave no trace anywhere. _createdBy set on insert, never moves pairs with _createdAt _updatedBy rewritten on every write pairs with _updatedAt The asymmetry is enforced where it actually lives: updated_by is in DocumentCURD.Write's OnConflict DoUpdates list and created_by is deliberately NOT, exactly as with the two timestamps. There is a test for that specifically, because if created_by ever joined the list BOTH fields would report the most recent writer, the pair would answer nothing, and every other assertion would still pass. The writer comes from an injected CallerResolver, mirroring the existing project resolver: restcol is a library and does not own an authentication scheme, so only the wrapping service knows how a caller is represented. OPTIONAL, unlike the project resolver. A deployment with no resolver keeps serving and records an EMPTY writer rather than failing writes - attribution is worth having, not worth refusing data over, and this can be adopted without a flag day. Empty is also honest: an unattributed document must stay distinguishable from one genuinely written by a principal named "system", so nothing invents a placeholder. Tested at both the storage and DTO layers. Refs FootprintAI/grandturks#1125 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSzNfcVc1FnDCfk9AF68Eh
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.
FootprintAI/grandturks#1125 asks whether "user A created this document and user B updated it" is answerable. It wasn't: the metadata carried no writer at all, so a document overwritten by a different principal was indistinguishable from one written once — in content and in metadata alike.
Two fields, not one — and that is the point
Writing an existing
documentIdis an upsert, and there is noUpdaterpc. With only a creator, B's overwrite of A's document would still report A, and B's write would leave no trace anywhere._createdBy_createdAt_updatedBy_updatedAtThe asymmetry is enforced where it actually lives:
updated_byis inDocumentCURD.Write'sOnConflictDoUpdateslist andcreated_bydeliberately is not — exactly as with the two timestamps.There's a test for that specifically, because if
created_byever joined that list, both fields would report the most recent writer, the pair would answer nothing, and every other assertion here would still pass. That's the failure mode worth guarding.The resolver is injected, and optional
The writer comes from an injected
CallerResolver, mirroring the existing project resolver: restcol is a library and doesn't own an authentication scheme, so only the wrapping service knows how a caller is represented.Optional, unlike the project resolver. A deployment with no resolver keeps serving and records an empty writer rather than failing writes — attribution is worth having, not worth refusing data over, and this can be adopted without a flag day.
Empty is also honest: an unattributed document must stay distinguishable from one genuinely written by a principal named
"system", so nothing invents a placeholder. Tested at both the storage and DTO layers.Tests
The headline one walks the exact scenario from the issue — Alice creates, Bob overwrites — and asserts the creator survives, the updater moves, the payload really was replaced, and no second row appeared.
Storage tests need postgres and run in CI.
go build ./...and short-mode tests clean. (pkg/models/collections/collections.goshows as unformatted but was already so on main — deliberately not reformatted.)What this does and doesn't finish
This closes the recording half of #1125. The other half is in grandturks: pipeline writes still reach restcol as one shared service account (
storage_sinker.go'sWithSaAuthInfo()), so once this lands they'd all be attributed to that single principal. A per-project identity for the sink is the remaining work, and this is its prerequisite — there was previously nowhere to put the answer.🤖 Generated with Claude Code
https://claude.ai/code/session_01BSzNfcVc1FnDCfk9AF68Eh