feat(core): one versioning rule — every mutation snapshots and bumps version#77
Merged
Merged
Conversation
…version Closes #61. Only content updates participated in versioning before this: associate(), dissociate(), setPermissions(), and delete() passed straight to the adapter with no snapshot, no version bump, no audit trail. That made #59's recoverability promise ("anything a write-holder does, the owner can undo") false for every association or permission change, and left ifVersion-style conflict detection (#48) blind to non-content races. One rule now, no special cases: every mutation of a record — content, associations, permissions, soft delete, undelete — snapshots the prior full state and bumps version. Hard delete is the only exception; it destroys the record and its history outright, so there's nothing to snapshot. - RecordVersion gains optional associations/permissions (core, wire-types, adapter-api's parseVersion/serializeVersion). The SQLite adapter's versions table had no columns for either — added as JSON, same pattern already used for permissions on the live records table. - associate/dissociate/setPermissions/delete(soft)/undelete all fetch the record, snapshot it, mutate, then bump version — with no-op guards so an identical re-associate, a dissociate of something not present, or a deep-equal setPermissions costs nothing (no bump, no snapshot). Association equality matches the SQLite adapter's existing primary key (kind, label, fileId/recordId — mimeType excluded). - restoreVersion() now restores associations when the target snapshot has them, but never permissions — those stay owner/creator territory (setPermissions's existing carve-out), so a content rollback can't silently widen or narrow access as a side effect. - Behavior tightening: associate/dissociate/delete(soft) didn't fetch the record first, so a call against a missing record either silently no-op'd or threw a generic adapter Error depending on which adapter. Snapshotting requires fetching first, so these now consistently throw StackNotFoundError — matching update/setPermissions/undelete/ restoreVersion, which already worked this way. Out of scope, per #61's own comment thread: extending the ifVersion CAS option to the newly-versioned paths. That's #48's feature and it hasn't landed at all yet — there's no ifVersion anywhere in the codebase to extend.
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.
Summary
Closes #61.
Only content updates participated in versioning before this:
associate(),dissociate(),setPermissions(), anddelete()passed straight through to the adapter — no snapshot, no version bump, no audit trail. That made #59's recoverability promise ("anything a write-holder does, the owner can undo") false for every association or permission change, and leftifVersion-style conflict detection (#48) blind to non-content races.One rule now, no special cases: every mutation of a record — content, associations, permissions, soft delete, undelete — snapshots the prior full state and bumps
version. Hard delete is the one exception; it destroys the record and its history outright, so there's nothing to snapshot.What's added
RecordVersiongains optionalassociations/permissions(packages/core/src/types.ts,wire-types,adapter-api'sparseVersion/serializeVersion). The SQLite adapter'sversionstable had no columns for either — added as JSON, the same pattern already used forpermissionson the liverecordstable.associate/dissociate/setPermissions/delete(soft)/undeleteall fetch the record, snapshot it, mutate, then bump version — with no-op guards so an identical re-associate, a dissociate of something not present, or a deep-equalsetPermissionscosts nothing (no bump, no snapshot). Association equality matches the SQLite adapter's existing primary key (kind,label,fileId/recordId—mimeTypeexcluded).restoreVersion()now restoresassociationswhen the target snapshot has them, but neverpermissions— those stay owner/creator territory (setPermissions's existing carve-out), so a content rollback can't silently widen or narrow access as a side effect.associate/dissociate/soft-deletedidn't fetch the record first, so a call against a missing record either silently no-op'd or threw a generic adapterError, depending on which adapter. Snapshotting requires fetching first, so these now consistently throwStackNotFoundError— matchingupdate/setPermissions/undelete/restoreVersion, which already worked this way.docs/spec.md) states the one-rule contract in §Versions and updates the wire endpoint description to match.Out of scope (per #61's own comment thread)
Extending the
ifVersionCAS option to the newly-versioned paths — that's#48's feature and it hasn't landed at all yet (noifVersionanywhere in the codebase to extend).Test plan
packages/core(version monotonicity across mixed mutations, no-op guards, restore-restores-associations, restore-never-restores-permissions,StackNotFoundErrorfor missing records),record-adapter-sqljs(version round-trip of associations/permissions), andadapter-api(wire parsing of the new fields)pnpm -r build— cleanpnpm -r typecheck— clean across all 6 packagespnpm -r lint— cleanpnpm -r test— 371 tests passing workspace-wideprettier --checkon changed files — cleanRefs #61, #59, #48, #47
Generated by Claude Code