halkyon/halkyon_learn_server/lib/src/card/card_revision.spy.yaml
Tizian 「ティツィアーン」 8fe1187ad5
feat(server): cards and their append-only revisions, synced
Card belongs to a studyset and carries only its position, a fractional index (last writer wins), so cards can be reordered and bulk imports keep their order. Its content lives in CardRevision rows, which are never updated except to blank them (ADR 0001, rule 2): parents lists the revisions a revision replaces (decided: a list, so a revision with two parents resolves a conflict), authorId the writer, reportId the report it answers once reports exist, and the envelope holds the content. Deciding the list of parents and the position now matters: changing a synced table later locks out older app versions.

The migration (created by CLI) only adds: the two tables, an index on spaceId for each (space deletion cascades into them), one for a studyset's cards in order and one for a card's revisions, and the foreign keys, from the space with ON DELETE CASCADE and from the studyset and the card with ON DELETE CASCADE, DEFERRABLE INITIALLY DEFERRED, since synced rows can arrive in any order. The client migration creates the same tables on each device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 20:38:17 +02:00

33 lines
1.3 KiB
YAML

### One version of a card's content, never changed once written (ADR 0001,
### rule 2): editing appends a revision whose parents are the versions it
### replaces, and a card's current versions are the revisions no other
### replaces. Deleting a card blanks every revision's payload first (ADR 0002,
### rule 2).
class: CardRevision
table: card_revision
database: sync
fields:
### The card this is a version of; its revisions go with it.
card: Card?, relation(onDelete=Cascade, deferred)
### The revisions this one replaces: none for a card's first, several when it
### resolves a conflict (decided 2026-09-30). Plain ids, since revisions can
### arrive in any order.
parents: List<UuidValue>
### Who wrote it: the AuthUser's id (a synced table can't relate to the
### server-only AuthUser).
authorId: UuidValue
### The report this revision answers, once reports exist; null otherwise.
reportId: UuidValue?
### The encryption key's id; always null (ADR 0002, rule 6).
keyId: String?
### The content as UTF-8 JSON: {"schemaVersion", "front", "back"}.
payload: ByteData
### The encryption nonce; always null, like keyId.
nonce: ByteData?
indexes:
# Deleting a space finds its rows through this index (see studyset.spy.yaml).
card_revision_space_idx:
fields: spaceId
# A card's revisions.
card_revision_card_idx:
fields: cardId