feat(server): tree, tags, review modes, settings and assets (ADR 0004)
All checks were successful
CI / check (push) Successful in 4m18s
CI / deploy (push) Has been skipped

The schema of the features planned for the first release, as one migration: every
migration of a synced table locks older app versions out of syncing (ADR 0002, rule 3),
which costs nothing before that release. New card types need no migration; they are the
next version of the revision payload.

- studyset: `parent` (the tree; cycles and orphans show at the top level) and `position`
  among siblings (fractional index; existing studysets at 'a0').
- tag (its name in the payload) and card_tag, one row per assignment, so concurrent
  adds and removes merge.
- review_log.mode: review, reverse (a schedule of its own) or cram (never moves a
  schedule); existing reviews are 'review'.
- person_settings: study goals now, preferences later; random ids, since an id derived
  from the person could be claimed by someone else and break the owner's sync.
- asset: a file's hash, type, size and name; the file goes into Serverpod's private
  cloud storage, served through an owner-checked endpoint (to come).

Migration 20260930211154224-tree-tags-assets only adds: four tables with their space
indexes and foreign keys, three columns with defaults. On devices SQLite rebuilds
studyset and review_log to add foreign keys, which is safe only with foreign keys off,
as Serverpod's runner does; a new test replays every client migration on a database
with a row in each of our synced tables and checks that none is lost (with foreign keys
on, it fails as it should).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-09-30 23:27:08 +02:00
commit 29a2105a55
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
37 changed files with 23962 additions and 12 deletions

View file

@ -126,6 +126,8 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
- **Content people edit is append-only revisions** (ADR 0001 rule 2, ADR 0003; the reference is `CardRevision`): a revision is never updated except to blank it, and `parents` lists the revisions it replaces. A save's parents are all current heads, which is also how a conflict is resolved. `mergeRevisions` (`feature/card/model/card_merge.dart`) merges several heads per side against their newest common ancestor; a side changed differently is a conflict the person settles, never merged silently. Deleting blanks every revision, then deletes.
- **Reviews are an append-only log** (`ReviewLog`: card, rating, `reviewedAt` in UTC, `durationMs`). A card's schedule is never stored: `scheduleOf` (`feature/study/model/card_schedule.dart`) replays its reviews in time order with FSRS-6 (`fsrs`, pinned exactly), and only the final due date is fuzzed, from the card's id and review count, so every device computes the same. The study queue (`study_queue.dart`): due cards earliest first, then new cards in studyset order, 20 per studyset and local day. Deleting a card deletes its reviews.
- **Order is a fractional index** (`fractional_indexing_dart`, rocicorp's algorithm) in its own column, last writer wins. Two devices appending at once can pick the same key, so ties sort by id.
- **Tree, tags, modes, settings and files** (ADR 0004): a studyset's `parent` and sibling `position` form the tree, and studysets in a cycle or without their parent show at the top level; a card's tags are `card_tag` rows, one per assignment, never updated, and a duplicate counts once; `review_log.mode` is `review`, `reverse` (its own schedule) or `cram` (never moves a schedule), and a schedule replays the reviews of one mode; `person_settings` is meant as one row per person, and extra rows fold into the one with the smallest id; an `asset` row stands for a file in private cloud storage, found by its hash. **Never derive a row's id from a person or anything else guessable**: ids are unique across the whole table, so someone could claim it first in their own space and break the owner's sync.
- **Every client migration keeps a device's rows** (`test/core/sync/device_migration_test.dart` replays each on a database with a row in every synced table of ours; SQLite rebuilds tables to add foreign keys, which Serverpod's runner does with foreign keys off). A new synced table needs a row in that test, which fails until it has one.
- **Every synced table indexes `spaceId`** (`indexes: <table>_space_idx: fields: spaceId`, e.g. `studyset.spy.yaml`): deleting a space cascades into every synced table, and Postgres doesn't index foreign keys by itself, so account deletion and pruning would otherwise read whole tables.
- **The client's local migration** (in `halkyon_learn_client/lib/migrations/`, written by the same `create-migration`) creates the device's SQLite schema from the whole definition, so it also contains the server's own tables. Only the synced tables and the bookkeeping ever hold data on a device.
- **Serverpod's version moves together with `serverpod_offline_sync`**, which pins Serverpod exactly: the root `pubspec.yaml` overrides the four core packages to the project's version. Upgrade Serverpod only together with those overrides and the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`), after reading the sync package's changelog.

View file

@ -0,0 +1,86 @@
# ADR 0004: Studyset tree, tags, review modes, settings and assets, in one migration
- **Status:** accepted
- **Date:** 2026-09-30
## Context
The features decided for the first public release need data the synced tables don't
have yet:
- a studyset tree (studysets inside studysets, studied one at a time or as a whole
subtree);
- tags on cards, to study and filter by;
- study modes beyond the schedule: reversed cards (back to front) and cramming;
- study goals on a home dashboard, which should follow a person to every device;
- importing Anki decks with their images and sounds.
Every migration of a synced table locks older app versions out of syncing (ADR 0002,
rule 3). Before the first release that costs nothing; after it, every device has to
update before it syncs again. So all of this comes in one migration now,
`20260930211154224-tree-tags-assets`, and later content fields go into the versioned
payloads instead. New card types (multiple choice, vocabulary, drag-and-drop, map
questions) need no migration at all: they are the next schema version of the card
revision's payload, and the per-side merge of ADR 0003 becomes per field.
## Decision
1. **The tree is a parent and a position.** `studyset.parent` names the studyset it
sits in (null at the top level), and `studyset.position` orders siblings with a
fractional index, as `card.position` does (ADR 0003). A move updates one column, and
the last writer wins. Two devices moving studysets into each other while offline
make a cycle, and a studyset can lose its parent to a deletion on another device;
the app shows the studysets caught either way at the top level until someone moves
them. Existing studysets start at the top level with position `a0`; ties sort by id.
2. **Tags are rows, and so are their assignments.** `tag` holds a name in its payload
(content, like a studyset's title); `card_tag` links a card and a tag, one row per
assignment, never updated. Removing a tag deletes one row, and adding it on another
device at the same time makes another, so concurrent edits merge without losing
either. A card with the same tag twice counts once.
3. **Reviews say how the card was studied.** `review_log.mode` is `review` (front to
back), `reverse` (back to front, with a schedule of its own, as Anki's reverse cards
have) or `cram` (outside the schedule: it counts for streaks and the activity
heatmap, and never moves a schedule). A card's schedule replays only the reviews of
one mode. Existing reviews are `review`.
4. **A person's settings are one synced row.** `person_settings` holds settings in a
versioned payload: study goals now, other preferences later. Two devices creating it
while offline make two rows; the app keeps the one with the smallest id and folds the
other in. Its id is random like every row's: an id derived from the person could be
claimed first by someone else in their own space, and the primary key's uniqueness
would then break the owner's sync.
5. **A file is a synced row plus a stored file.** `asset` holds a file's SHA-256, media
type and size, and its name in the payload. The file itself goes into the server's
private cloud storage (Serverpod's `CloudStorage`, in the database unless configured
otherwise), at a path derived from the space and the hash, and is served only by an
endpoint that checks the owner. Devices cache files by hash, and the hash checks each
download. The same file added twice is stored once; two rows for it, from two devices
offline, count as one. Content refers to a file by the row's id.
## Consequences
- The server migration only adds: four tables, three columns with defaults, indexes,
and foreign keys (deferred where sync inserts rows in any order). The running server
keeps working while it is applied, before the new server replaces it.
- On devices, SQLite can't add a foreign key to a table, so the migration rebuilds
`studyset` and `review_log` (a new table, the rows copied, the old one dropped).
Dropping them with foreign keys enforced would cascade into cards and reviews or fail;
Serverpod's SQLite migration runner switches foreign keys off around migrations.
`device_migration_test.dart` replays every client migration on a database holding a
row in each of our synced tables and checks that none is lost, and fails if a synced
table has no row there.
- Stored files outlive their rows unless deleted with them: deleting a person, or a
file nobody refers to anymore, has to delete the stored file too. That comes with the
file endpoints.
- Media makes the database, and so its backups, grow. The storage can move to S3 or
similar later without changing the rows, since the path is derived from the hash.
## Alternatives considered
- **The tree inside the payload:** no new columns, but a move and a title edit on two
devices would overwrite each other, as both live in one payload.
- **Tags inside the card revisions:** no new tables, but every tag change would write a
revision, and filtering by tag would decode every card.
- **No mode column:** cramming would leave no trace (no streak), and reversed answers
would feed the normal schedule and skew it.
- **Media after the release:** the asset table would then need a migration that locks
older app versions out.

View file

@ -0,0 +1,976 @@
{
"__className__": "serverpod.DatabaseDefinition",
"schemaVersion": 2,
"moduleName": "halkyon_learn",
"tables": [
{
"__className__": "serverpod.TableDefinition",
"name": "asset",
"dartName": "Asset",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "sha256",
"fieldName": "sha256",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "mimeType",
"fieldName": "mimeType",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "byteSize",
"fieldName": "byteSize",
"columnType": 6,
"isNullable": false,
"dartType": "int"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "asset_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "asset_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "asset_hash_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "sha256"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "card",
"dartName": "Card",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "studysetId",
"fieldName": "studysetId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "position",
"fieldName": "position",
"columnType": 0,
"isNullable": false,
"dartType": "String"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_fk_1",
"columns": [
"studysetId"
],
"referenceTable": "studyset",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_studyset_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "studysetId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "position"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "card_revision",
"dartName": "CardRevision",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "cardId",
"fieldName": "cardId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "parents",
"fieldName": "parents",
"columnType": 8,
"isNullable": false,
"dartType": "List<UuidValue>"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "authorId",
"fieldName": "authorId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "reportId",
"fieldName": "reportId",
"columnType": 7,
"isNullable": true,
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_revision_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_revision_fk_1",
"columns": [
"cardId"
],
"referenceTable": "card",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_revision_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_revision_card_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "cardId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "card_tag",
"dartName": "CardTag",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "cardId",
"fieldName": "cardId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "tagId",
"fieldName": "tagId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_tag_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_tag_fk_1",
"columns": [
"cardId"
],
"referenceTable": "card",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_tag_fk_2",
"columns": [
"tagId"
],
"referenceTable": "tag",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_tag_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_tag_card_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "cardId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_tag_tag_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "tagId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "person_settings",
"dartName": "PersonSettings",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "person_settings_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "person_settings_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "review_log",
"dartName": "ReviewLog",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "cardId",
"fieldName": "cardId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "rating",
"fieldName": "rating",
"columnType": 6,
"isNullable": false,
"dartType": "int"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "reviewedAt",
"fieldName": "reviewedAt",
"columnType": 4,
"isNullable": false,
"dartType": "DateTime"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "durationMs",
"fieldName": "durationMs",
"columnType": 6,
"isNullable": false,
"dartType": "int"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "mode",
"fieldName": "mode",
"columnType": 0,
"isNullable": false,
"columnDefault": "'review'",
"dartType": "String"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "review_log_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "review_log_fk_1",
"columns": [
"cardId"
],
"referenceTable": "card",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "review_log_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "review_log_card_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "cardId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "reviewedAt"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "studyset",
"dartName": "Studyset",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "parentId",
"fieldName": "parentId",
"columnType": 7,
"isNullable": true,
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "position",
"fieldName": "position",
"columnType": 0,
"isNullable": false,
"columnDefault": "'a0'",
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "studyset_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "studyset_fk_1",
"columns": [
"parentId"
],
"referenceTable": "studyset",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "studyset_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "studyset_parent_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "parentId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "position"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "tag",
"dartName": "Tag",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "tag_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "tag_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
],
"installedModules": [
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod",
"version": "20260824182259319"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_idp",
"version": "20260924105404509"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_offline_sync",
"version": "20260914143806119"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_core",
"version": "20260924105232991"
}
],
"migrationApiVersion": 1
}

View file

@ -0,0 +1,546 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "asset",
"dartName": "Asset",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "sha256",
"fieldName": "sha256",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "mimeType",
"fieldName": "mimeType",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "byteSize",
"fieldName": "byteSize",
"columnType": 6,
"isNullable": false,
"dartType": "int"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "asset_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "asset_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "asset_hash_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "sha256"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "card_tag",
"dartName": "CardTag",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "cardId",
"fieldName": "cardId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "tagId",
"fieldName": "tagId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_tag_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_tag_fk_1",
"columns": [
"cardId"
],
"referenceTable": "card",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_tag_fk_2",
"columns": [
"tagId"
],
"referenceTable": "tag",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_tag_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_tag_card_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "cardId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_tag_tag_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "tagId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "person_settings",
"dartName": "PersonSettings",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "person_settings_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "person_settings_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "alterTable",
"alterTable": {
"__className__": "serverpod.TableMigration",
"name": "review_log",
"schema": "public",
"addColumns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "mode",
"fieldName": "mode",
"columnType": 0,
"isNullable": false,
"columnDefault": "'review'",
"dartType": "String"
}
],
"deleteColumns": [],
"modifyColumns": [],
"addIndexes": [],
"deleteIndexes": [],
"addForeignKeys": [],
"deleteForeignKeys": [],
"warnings": []
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "alterTable",
"alterTable": {
"__className__": "serverpod.TableMigration",
"name": "studyset",
"schema": "public",
"addColumns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "parentId",
"fieldName": "parentId",
"columnType": 7,
"isNullable": true,
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "position",
"fieldName": "position",
"columnType": 0,
"isNullable": false,
"columnDefault": "'a0'",
"dartType": "String"
}
],
"deleteColumns": [],
"modifyColumns": [],
"addIndexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "studyset_parent_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "parentId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "position"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"deleteIndexes": [],
"addForeignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "studyset_fk_1",
"columns": [
"parentId"
],
"referenceTable": "studyset",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"deleteForeignKeys": [],
"warnings": []
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "tag",
"dartName": "Tag",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "tag_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "tag_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -7,6 +7,7 @@ import 'package:serverpod_database/serverpod_database.dart';
part '20260930094729362-studysets/migration.dart';
part '20260930122235111-cards/migration.dart';
part '20260930145136285-reviews/migration.dart';
part '20260930211154224-tree-tags-assets/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -16,6 +17,7 @@ class MigrationRegistry {
_Migration20260930094729362_studysets(),
_Migration20260930122235111_cards(),
_Migration20260930145136285_reviews(),
_Migration20260930211154224_tree_tags_assets(),
];
/// List of all client-side database migration versions.

View file

@ -0,0 +1,756 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
/// A file in a person's content, an image or a sound (ADR 0004). This row is synced;
/// the file itself is in the server's private cloud storage, reachable only
/// through an endpoint that checks the owner, and each device caches it by its
/// hash. Content refers to it by the row's id.
///
/// A file added twice (the same image in two imported cards) is one file in
/// storage, since its path is its hash; two rows for it may exist when two
/// devices added it while offline, and count as one.
abstract class Asset
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
Asset._({
this.id,
this.spaceId,
required this.sha256,
required this.mimeType,
required this.byteSize,
this.keyId,
required this.payload,
this.nonce,
});
factory Asset({
_isc.UuidValue? id,
int? spaceId,
required String sha256,
required String mimeType,
required int byteSize,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _AssetImpl;
factory Asset.fromJson(Map<String, dynamic> jsonSerialization) {
return Asset(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
sha256: jsonSerialization['sha256'] as String,
mimeType: jsonSerialization['mimeType'] as String,
byteSize: jsonSerialization['byteSize'] as int,
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = AssetTable();
static const db = AssetRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
/// The file's SHA-256, in lowercase hex: its name in storage and in every
/// device's cache, and the check that a download is the file that was added.
String sha256;
/// Its media type, e.g. 'image/png' or 'audio/mpeg'.
String mimeType;
/// Its size in bytes.
int byteSize;
/// The encryption key's id; always null (ADR 0002), as on studysets.
String? keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "fileName"}; the name it came
/// with is content, like a studyset's title.
_idt.ByteData payload;
/// The encryption nonce; always null, like keyId.
_idt.ByteData? nonce;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [Asset]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
Asset copyWith({
_isc.UuidValue? id,
int? spaceId,
String? sha256,
String? mimeType,
int? byteSize,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'Asset',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'sha256': sha256,
'mimeType': mimeType,
'byteSize': byteSize,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'Asset',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'sha256': sha256,
'mimeType': mimeType,
'byteSize': byteSize,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static AssetInclude include() {
return AssetInclude._();
}
static AssetIncludeList includeList({
_isd.WhereExpressionBuilder<AssetTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<AssetTable>? orderBy,
_isd.OrderByListBuilder<AssetTable>? orderByList,
AssetInclude? include,
}) {
return AssetIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _AssetImpl extends Asset {
_AssetImpl({
_isc.UuidValue? id,
int? spaceId,
required String sha256,
required String mimeType,
required int byteSize,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
sha256: sha256,
mimeType: mimeType,
byteSize: byteSize,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [Asset]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
Asset copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
String? sha256,
String? mimeType,
int? byteSize,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return Asset(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
sha256: sha256 ?? this.sha256,
mimeType: mimeType ?? this.mimeType,
byteSize: byteSize ?? this.byteSize,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class AssetUpdateTable extends _isd.UpdateTable<AssetTable> {
AssetUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<String, String> sha256(String value) => _isd.ColumnValue(
table.sha256,
value,
);
_isd.ColumnValue<String, String> mimeType(String value) => _isd.ColumnValue(
table.mimeType,
value,
);
_isd.ColumnValue<int, int> byteSize(int value) => _isd.ColumnValue(
table.byteSize,
value,
);
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
table.keyId,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_isd.ColumnValue(
table.payload,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_isd.ColumnValue(
table.nonce,
value,
);
}
class AssetTable extends _isd.Table<_isc.UuidValue?> {
AssetTable({super.tableRelation}) : super(tableName: 'asset') {
updateTable = AssetUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
sha256 = _isd.ColumnString(
'sha256',
this,
);
mimeType = _isd.ColumnString(
'mimeType',
this,
);
byteSize = _isd.ColumnInt(
'byteSize',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final AssetUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
/// The file's SHA-256, in lowercase hex: its name in storage and in every
/// device's cache, and the check that a download is the file that was added.
late final _isd.ColumnString sha256;
/// Its media type, e.g. 'image/png' or 'audio/mpeg'.
late final _isd.ColumnString mimeType;
/// Its size in bytes.
late final _isd.ColumnInt byteSize;
/// The encryption key's id; always null (ADR 0002), as on studysets.
late final _isd.ColumnString keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "fileName"}; the name it came
/// with is content, like a studyset's title.
late final _isd.ColumnByteData payload;
/// The encryption nonce; always null, like keyId.
late final _isd.ColumnByteData nonce;
@override
List<_isd.Column> get columns => [
id,
spaceId,
sha256,
mimeType,
byteSize,
keyId,
payload,
nonce,
];
}
class AssetInclude extends _isd.IncludeObject {
AssetInclude._();
@override
Map<String, _isd.Include?> get includes => {};
@override
_isd.Table<_isc.UuidValue?> get table => Asset.t;
}
class AssetIncludeList extends _isd.IncludeList {
AssetIncludeList._({
_isd.WhereExpressionBuilder<AssetTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(Asset.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => Asset.t;
}
class AssetRepository {
const AssetRepository._();
/// Returns a list of [Asset]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<Asset>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<AssetTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<AssetTable>? orderBy,
_isd.OrderByListBuilder<AssetTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<Asset>(
where: where?.call(Asset.t),
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [Asset] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<Asset?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<AssetTable>? where,
int? offset,
_isd.OrderByBuilder<AssetTable>? orderBy,
_isd.OrderByListBuilder<AssetTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<Asset>(
where: where?.call(Asset.t),
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [Asset] by its [id] or null if no such row exists.
Future<Asset?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<Asset>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [Asset]s in the list and returns the inserted rows.
///
/// The returned [Asset]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> insert(
_isd.DatabaseSession session,
List<Asset> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<Asset>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [Asset] and returns the inserted row.
///
/// The returned [Asset] will have its `id` field set.
Future<Asset> insertRow(
_isd.DatabaseSession session,
Asset row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<Asset>(
row,
transaction: transaction,
);
}
/// Upserts all [Asset]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [Asset]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> upsert(
_isd.DatabaseSession session,
List<Asset> rows, {
required _isd.ColumnSelections<AssetTable> conflictColumns,
_isd.ColumnSelections<AssetTable>? updateColumns,
_isd.WhereExpressionBuilder<AssetTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<Asset>(
rows,
conflictColumns: conflictColumns(Asset.t),
updateColumns: updateColumns?.call(Asset.t),
updateWhere: updateWhere?.call(Asset.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [Asset] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [Asset] will have its `id` field set.
Future<Asset?> upsertRow(
_isd.DatabaseSession session,
Asset row, {
required _isd.ColumnSelections<AssetTable> conflictColumns,
_isd.ColumnSelections<AssetTable>? updateColumns,
_isd.WhereExpressionBuilder<AssetTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<Asset>(
row,
conflictColumns: conflictColumns(Asset.t),
updateColumns: updateColumns?.call(Asset.t),
updateWhere: updateWhere?.call(Asset.t),
transaction: transaction,
);
}
/// Updates all [Asset]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> update(
_isd.DatabaseSession session,
List<Asset> rows, {
_isd.ColumnSelections<AssetTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<Asset>(
rows,
columns: columns?.call(Asset.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [Asset]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<Asset> updateRow(
_isd.DatabaseSession session,
Asset row, {
_isd.ColumnSelections<AssetTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<Asset>(
row,
columns: columns?.call(Asset.t),
transaction: transaction,
);
}
/// Updates a single [Asset] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<Asset?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<AssetUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<Asset>(
id,
columnValues: columnValues(Asset.t.updateTable),
transaction: transaction,
);
}
/// Updates all [Asset]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<AssetUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<AssetTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<AssetTable>? orderBy,
_isd.OrderByListBuilder<AssetTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<Asset>(
columnValues: columnValues(Asset.t.updateTable),
where: where(Asset.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [Asset]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> delete(
_isd.DatabaseSession session,
List<Asset> rows, {
_isd.OrderByBuilder<AssetTable>? orderBy,
_isd.OrderByListBuilder<AssetTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<Asset>(
rows,
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [Asset].
Future<Asset> deleteRow(
_isd.DatabaseSession session,
Asset row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<Asset>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<AssetTable> where,
_isd.OrderByBuilder<AssetTable>? orderBy,
_isd.OrderByListBuilder<AssetTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<Asset>(
where: where(Asset.t),
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<AssetTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<Asset>(
where: where?.call(Asset.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [Asset] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<AssetTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<Asset>(
where: where(Asset.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -21,6 +21,7 @@ import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart'
as _ipulbpi2;
import 'asset/asset.dart' as _ipb7vkxr;
import 'auth/device.dart' as _i6c5ny7n;
import 'auth/github_sign_in_refusal.dart' as _ir1gd9p5;
import 'auth/github_sign_in_refused_exception.dart' as _i0xfcu5n;
@ -29,7 +30,11 @@ import 'card/card_revision.dart' as _ipbyuef1;
import 'common/not_found_exception.dart' as _i8oee73b;
import 'instance/instance_info.dart' as _iskrg2g3;
import 'review/review_log.dart' as _irzjbdqz;
import 'settings/person_settings.dart' as _irnhx5e5;
import 'studyset/studyset.dart' as _ikbclmae;
import 'tag/card_tag.dart' as _i6jz6nyq;
import 'tag/tag.dart' as _ie8t3ar4;
export 'asset/asset.dart';
export 'auth/device.dart';
export 'auth/github_sign_in_refusal.dart';
export 'auth/github_sign_in_refused_exception.dart';
@ -38,7 +43,10 @@ export 'card/card_revision.dart';
export 'common/not_found_exception.dart';
export 'instance/instance_info.dart';
export 'review/review_log.dart';
export 'settings/person_settings.dart';
export 'studyset/studyset.dart';
export 'tag/card_tag.dart';
export 'tag/tag.dart';
export 'client.dart';
export 'sync_tables.dart';
@ -50,6 +58,108 @@ class Protocol extends _isd.DatabaseSerializationManager {
static final Protocol _instance = Protocol._().._registerHostProtocols();
static List<_isd.TableDefinition> get targetTableDefinitions => [
_isd.TableDefinition(
name: 'asset',
dartName: 'Asset',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'sha256',
columnType: _isd.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isd.ColumnDefinition(
name: 'mimeType',
columnType: _isd.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isd.ColumnDefinition(
name: 'byteSize',
columnType: _isd.ColumnType.bigint,
isNullable: false,
dartType: 'int',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isd.ColumnDefinition(
name: 'payload',
columnType: _isd.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isd.ColumnDefinition(
name: 'nonce',
columnType: _isd.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'asset_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'asset_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'asset_hash_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'sha256',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'card',
dartName: 'Card',
@ -254,6 +364,182 @@ class Protocol extends _isd.DatabaseSerializationManager {
],
managed: true,
),
_isd.TableDefinition(
name: 'card_tag',
dartName: 'CardTag',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'cardId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'tagId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'card_tag_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
_isd.ForeignKeyDefinition(
constraintName: 'card_tag_fk_1',
columns: ['cardId'],
referenceTable: 'card',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
_isd.ForeignKeyDefinition(
constraintName: 'card_tag_fk_2',
columns: ['tagId'],
referenceTable: 'tag',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'card_tag_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'card_tag_card_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'cardId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'card_tag_tag_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'tagId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'person_settings',
dartName: 'PersonSettings',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isd.ColumnDefinition(
name: 'payload',
columnType: _isd.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isd.ColumnDefinition(
name: 'nonce',
columnType: _isd.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'person_settings_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'person_settings_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'review_log',
dartName: 'ReviewLog',
@ -297,6 +583,13 @@ class Protocol extends _isd.DatabaseSerializationManager {
isNullable: false,
dartType: 'int',
),
_isd.ColumnDefinition(
name: 'mode',
columnType: _isd.ColumnType.text,
isNullable: false,
dartType: 'String',
columnDefault: '\'review\'',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
@ -374,6 +667,19 @@ class Protocol extends _isd.DatabaseSerializationManager {
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'parentId',
columnType: _isd.ColumnType.uuid,
isNullable: true,
dartType: 'UuidValue?',
),
_isd.ColumnDefinition(
name: 'position',
columnType: _isd.ColumnType.text,
isNullable: false,
dartType: 'String',
columnDefault: '\'a0\'',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
@ -404,6 +710,17 @@ class Protocol extends _isd.DatabaseSerializationManager {
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
_isd.ForeignKeyDefinition(
constraintName: 'studyset_fk_1',
columns: ['parentId'],
referenceTable: 'studyset',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isd.IndexDefinition(
@ -419,6 +736,90 @@ class Protocol extends _isd.DatabaseSerializationManager {
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'studyset_parent_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'parentId',
),
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'position',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'tag',
dartName: 'Tag',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isd.ColumnDefinition(
name: 'payload',
columnType: _isd.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isd.ColumnDefinition(
name: 'nonce',
columnType: _isd.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'tag_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'tag_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
@ -463,6 +864,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
}
if (t == _ipb7vkxr.Asset) {
return _ipb7vkxr.Asset.fromJson(data) as T;
}
if (t == _i6c5ny7n.Device) {
return _i6c5ny7n.Device.fromJson(data) as T;
}
@ -487,9 +891,21 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _irzjbdqz.ReviewLog) {
return _irzjbdqz.ReviewLog.fromJson(data) as T;
}
if (t == _irnhx5e5.PersonSettings) {
return _irnhx5e5.PersonSettings.fromJson(data) as T;
}
if (t == _ikbclmae.Studyset) {
return _ikbclmae.Studyset.fromJson(data) as T;
}
if (t == _i6jz6nyq.CardTag) {
return _i6jz6nyq.CardTag.fromJson(data) as T;
}
if (t == _ie8t3ar4.Tag) {
return _ie8t3ar4.Tag.fromJson(data) as T;
}
if (t == _isc.getType<_ipb7vkxr.Asset?>()) {
return (data != null ? _ipb7vkxr.Asset.fromJson(data) : null) as T;
}
if (t == _isc.getType<_i6c5ny7n.Device?>()) {
return (data != null ? _i6c5ny7n.Device.fromJson(data) : null) as T;
}
@ -521,9 +937,19 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _isc.getType<_irzjbdqz.ReviewLog?>()) {
return (data != null ? _irzjbdqz.ReviewLog.fromJson(data) : null) as T;
}
if (t == _isc.getType<_irnhx5e5.PersonSettings?>()) {
return (data != null ? _irnhx5e5.PersonSettings.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_ikbclmae.Studyset?>()) {
return (data != null ? _ikbclmae.Studyset.fromJson(data) : null) as T;
}
if (t == _isc.getType<_i6jz6nyq.CardTag?>()) {
return (data != null ? _i6jz6nyq.CardTag.fromJson(data) : null) as T;
}
if (t == _isc.getType<_ie8t3ar4.Tag?>()) {
return (data != null ? _ie8t3ar4.Tag.fromJson(data) : null) as T;
}
if (t == List<_isc.UuidValue>) {
return (data as List).map((e) => deserialize<_isc.UuidValue>(e)).toList()
as T;
@ -548,6 +974,7 @@ class Protocol extends _isd.DatabaseSerializationManager {
static String? getClassNameForType(Type type) {
return switch (type) {
_ipb7vkxr.Asset => 'Asset',
_i6c5ny7n.Device => 'Device',
_ir1gd9p5.GitHubSignInRefusal => 'GitHubSignInRefusal',
_i0xfcu5n.GitHubSignInRefusedException => 'GitHubSignInRefusedException',
@ -556,7 +983,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
_i8oee73b.NotFoundException => 'NotFoundException',
_iskrg2g3.InstanceInfo => 'InstanceInfo',
_irzjbdqz.ReviewLog => 'ReviewLog',
_irnhx5e5.PersonSettings => 'PersonSettings',
_ikbclmae.Studyset => 'Studyset',
_i6jz6nyq.CardTag => 'CardTag',
_ie8t3ar4.Tag => 'Tag',
_ => null,
};
}
@ -574,6 +1004,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
switch (data) {
case _ipb7vkxr.Asset():
return 'Asset';
case _i6c5ny7n.Device():
return 'Device';
case _ir1gd9p5.GitHubSignInRefusal():
@ -590,8 +1022,14 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'InstanceInfo';
case _irzjbdqz.ReviewLog():
return 'ReviewLog';
case _irnhx5e5.PersonSettings():
return 'PersonSettings';
case _ikbclmae.Studyset():
return 'Studyset';
case _i6jz6nyq.CardTag():
return 'CardTag';
case _ie8t3ar4.Tag():
return 'Tag';
}
className = _iaic.Protocol().getClassNameForObject(data);
if (className != null) {
@ -620,6 +1058,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName is! String) {
return super.deserializeByClassName(data);
}
if (dataClassName == 'Asset') {
return deserialize<_ipb7vkxr.Asset>(data['data']);
}
if (dataClassName == 'Device') {
return deserialize<_i6c5ny7n.Device>(data['data']);
}
@ -644,9 +1085,18 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'ReviewLog') {
return deserialize<_irzjbdqz.ReviewLog>(data['data']);
}
if (dataClassName == 'PersonSettings') {
return deserialize<_irnhx5e5.PersonSettings>(data['data']);
}
if (dataClassName == 'Studyset') {
return deserialize<_ikbclmae.Studyset>(data['data']);
}
if (dataClassName == 'CardTag') {
return deserialize<_i6jz6nyq.CardTag>(data['data']);
}
if (dataClassName == 'Tag') {
return deserialize<_ie8t3ar4.Tag>(data['data']);
}
if (dataClassName.startsWith('serverpod_auth_idp.')) {
data['className'] = dataClassName.substring(19);
return _iaic.Protocol().deserializeByClassName(data);
@ -698,14 +1148,22 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
}
switch (t) {
case _ipb7vkxr.Asset:
return _ipb7vkxr.Asset.t;
case _ipiw1cmx.Card:
return _ipiw1cmx.Card.t;
case _ipbyuef1.CardRevision:
return _ipbyuef1.CardRevision.t;
case _irzjbdqz.ReviewLog:
return _irzjbdqz.ReviewLog.t;
case _irnhx5e5.PersonSettings:
return _irnhx5e5.PersonSettings.t;
case _ikbclmae.Studyset:
return _ikbclmae.Studyset.t;
case _i6jz6nyq.CardTag:
return _i6jz6nyq.CardTag.t;
case _ie8t3ar4.Tag:
return _ie8t3ar4.Tag.t;
}
return null;
}

View file

@ -30,7 +30,8 @@ abstract class ReviewLog
required this.rating,
required this.reviewedAt,
required this.durationMs,
});
String? mode,
}) : mode = mode ?? 'review';
factory ReviewLog({
_isc.UuidValue? id,
@ -40,6 +41,7 @@ abstract class ReviewLog
required int rating,
required DateTime reviewedAt,
required int durationMs,
String? mode,
}) = _ReviewLogImpl;
factory ReviewLog.fromJson(Map<String, dynamic> jsonSerialization) {
@ -59,6 +61,7 @@ abstract class ReviewLog
jsonSerialization['reviewedAt'],
),
durationMs: jsonSerialization['durationMs'] as int,
mode: jsonSerialization['mode'] as String?,
);
}
@ -88,6 +91,12 @@ abstract class ReviewLog
/// milliseconds.
int durationMs;
/// How the card was studied (ADR 0004): 'review' (front to back), 'reverse'
/// (back to front, with a schedule of its own, as Anki's reverse cards have),
/// or 'cram' (outside the schedule: counts for streaks and the activity
/// heatmap, never moves a schedule). Reviews from before modes are 'review'.
String mode;
@override
_isd.Table<_isc.UuidValue?> get table => t;
@ -102,6 +111,7 @@ abstract class ReviewLog
int? rating,
DateTime? reviewedAt,
int? durationMs,
String? mode,
});
@override
Map<String, dynamic> toJson() {
@ -114,6 +124,7 @@ abstract class ReviewLog
'rating': rating,
'reviewedAt': reviewedAt.toJson(),
'durationMs': durationMs,
'mode': mode,
};
}
@ -128,6 +139,7 @@ abstract class ReviewLog
'rating': rating,
'reviewedAt': reviewedAt.toJson(),
'durationMs': durationMs,
'mode': mode,
};
}
@ -170,6 +182,7 @@ class _ReviewLogImpl extends ReviewLog {
required int rating,
required DateTime reviewedAt,
required int durationMs,
String? mode,
}) : super._(
id: id,
spaceId: spaceId,
@ -178,6 +191,7 @@ class _ReviewLogImpl extends ReviewLog {
rating: rating,
reviewedAt: reviewedAt,
durationMs: durationMs,
mode: mode,
);
/// Returns a shallow copy of this [ReviewLog]
@ -192,6 +206,7 @@ class _ReviewLogImpl extends ReviewLog {
int? rating,
DateTime? reviewedAt,
int? durationMs,
String? mode,
}) {
return ReviewLog(
id: id is _isc.UuidValue? ? id : this.id,
@ -201,6 +216,7 @@ class _ReviewLogImpl extends ReviewLog {
rating: rating ?? this.rating,
reviewedAt: reviewedAt ?? this.reviewedAt,
durationMs: durationMs ?? this.durationMs,
mode: mode ?? this.mode,
);
}
}
@ -235,6 +251,11 @@ class ReviewLogUpdateTable extends _isd.UpdateTable<ReviewLogTable> {
table.durationMs,
value,
);
_isd.ColumnValue<String, String> mode(String value) => _isd.ColumnValue(
table.mode,
value,
);
}
class ReviewLogTable extends _isd.Table<_isc.UuidValue?> {
@ -260,6 +281,11 @@ class ReviewLogTable extends _isd.Table<_isc.UuidValue?> {
'durationMs',
this,
);
mode = _isd.ColumnString(
'mode',
this,
hasDefault: true,
);
}
late final ReviewLogUpdateTable updateTable;
@ -283,6 +309,12 @@ class ReviewLogTable extends _isd.Table<_isc.UuidValue?> {
/// milliseconds.
late final _isd.ColumnInt durationMs;
/// How the card was studied (ADR 0004): 'review' (front to back), 'reverse'
/// (back to front, with a schedule of its own, as Anki's reverse cards have),
/// or 'cram' (outside the schedule: counts for streaks and the activity
/// heatmap, never moves a schedule). Reviews from before modes are 'review'.
late final _isd.ColumnString mode;
_i4926i81.CardTable get card {
if (_card != null) return _card!;
_card = _isd.createRelationTable(
@ -304,6 +336,7 @@ class ReviewLogTable extends _isd.Table<_isc.UuidValue?> {
rating,
reviewedAt,
durationMs,
mode,
];
@override

View file

@ -0,0 +1,677 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
/// A person's settings that follow them to every device (ADR 0004): study goals
/// now, other preferences later. The content lives in the payload, so a new
/// setting changes the payload's schema version, not this synced table.
///
/// Meant as one row per person. Two devices creating it while offline make two;
/// the app then keeps the one with the smallest id, folding the other's settings
/// in. Its id is random like every other row's: an id derived from the person
/// could be claimed first by someone else, and would then break their sync.
abstract class PersonSettings
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
PersonSettings._({
this.id,
this.spaceId,
this.keyId,
required this.payload,
this.nonce,
});
factory PersonSettings({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _PersonSettingsImpl;
factory PersonSettings.fromJson(Map<String, dynamic> jsonSerialization) {
return PersonSettings(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = PersonSettingsTable();
static const db = PersonSettingsRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
/// The encryption key's id; always null (ADR 0002), as on studysets.
String? keyId;
/// The content as UTF-8 JSON: {"schemaVersion", …}.
_idt.ByteData payload;
/// The encryption nonce; always null, like keyId.
_idt.ByteData? nonce;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [PersonSettings]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
PersonSettings copyWith({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'PersonSettings',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'PersonSettings',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static PersonSettingsInclude include() {
return PersonSettingsInclude._();
}
static PersonSettingsIncludeList includeList({
_isd.WhereExpressionBuilder<PersonSettingsTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<PersonSettingsTable>? orderBy,
_isd.OrderByListBuilder<PersonSettingsTable>? orderByList,
PersonSettingsInclude? include,
}) {
return PersonSettingsIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(PersonSettings.t),
orderByList: orderByList?.call(PersonSettings.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _PersonSettingsImpl extends PersonSettings {
_PersonSettingsImpl({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [PersonSettings]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
PersonSettings copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return PersonSettings(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class PersonSettingsUpdateTable extends _isd.UpdateTable<PersonSettingsTable> {
PersonSettingsUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
table.keyId,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_isd.ColumnValue(
table.payload,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_isd.ColumnValue(
table.nonce,
value,
);
}
class PersonSettingsTable extends _isd.Table<_isc.UuidValue?> {
PersonSettingsTable({super.tableRelation})
: super(tableName: 'person_settings') {
updateTable = PersonSettingsUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final PersonSettingsUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
/// The encryption key's id; always null (ADR 0002), as on studysets.
late final _isd.ColumnString keyId;
/// The content as UTF-8 JSON: {"schemaVersion", …}.
late final _isd.ColumnByteData payload;
/// The encryption nonce; always null, like keyId.
late final _isd.ColumnByteData nonce;
@override
List<_isd.Column> get columns => [
id,
spaceId,
keyId,
payload,
nonce,
];
}
class PersonSettingsInclude extends _isd.IncludeObject {
PersonSettingsInclude._();
@override
Map<String, _isd.Include?> get includes => {};
@override
_isd.Table<_isc.UuidValue?> get table => PersonSettings.t;
}
class PersonSettingsIncludeList extends _isd.IncludeList {
PersonSettingsIncludeList._({
_isd.WhereExpressionBuilder<PersonSettingsTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(PersonSettings.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => PersonSettings.t;
}
class PersonSettingsRepository {
const PersonSettingsRepository._();
/// Returns a list of [PersonSettings]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<PersonSettings>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<PersonSettingsTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<PersonSettingsTable>? orderBy,
_isd.OrderByListBuilder<PersonSettingsTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<PersonSettings>(
where: where?.call(PersonSettings.t),
orderBy: orderBy?.call(PersonSettings.t),
orderByList: orderByList?.call(PersonSettings.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [PersonSettings] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<PersonSettings?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<PersonSettingsTable>? where,
int? offset,
_isd.OrderByBuilder<PersonSettingsTable>? orderBy,
_isd.OrderByListBuilder<PersonSettingsTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<PersonSettings>(
where: where?.call(PersonSettings.t),
orderBy: orderBy?.call(PersonSettings.t),
orderByList: orderByList?.call(PersonSettings.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [PersonSettings] by its [id] or null if no such row exists.
Future<PersonSettings?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<PersonSettings>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [PersonSettings]s in the list and returns the inserted rows.
///
/// The returned [PersonSettings]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PersonSettings>> insert(
_isd.DatabaseSession session,
List<PersonSettings> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<PersonSettings>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [PersonSettings] and returns the inserted row.
///
/// The returned [PersonSettings] will have its `id` field set.
Future<PersonSettings> insertRow(
_isd.DatabaseSession session,
PersonSettings row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<PersonSettings>(
row,
transaction: transaction,
);
}
/// Upserts all [PersonSettings]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [PersonSettings]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PersonSettings>> upsert(
_isd.DatabaseSession session,
List<PersonSettings> rows, {
required _isd.ColumnSelections<PersonSettingsTable> conflictColumns,
_isd.ColumnSelections<PersonSettingsTable>? updateColumns,
_isd.WhereExpressionBuilder<PersonSettingsTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<PersonSettings>(
rows,
conflictColumns: conflictColumns(PersonSettings.t),
updateColumns: updateColumns?.call(PersonSettings.t),
updateWhere: updateWhere?.call(PersonSettings.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [PersonSettings] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [PersonSettings] will have its `id` field set.
Future<PersonSettings?> upsertRow(
_isd.DatabaseSession session,
PersonSettings row, {
required _isd.ColumnSelections<PersonSettingsTable> conflictColumns,
_isd.ColumnSelections<PersonSettingsTable>? updateColumns,
_isd.WhereExpressionBuilder<PersonSettingsTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<PersonSettings>(
row,
conflictColumns: conflictColumns(PersonSettings.t),
updateColumns: updateColumns?.call(PersonSettings.t),
updateWhere: updateWhere?.call(PersonSettings.t),
transaction: transaction,
);
}
/// Updates all [PersonSettings]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PersonSettings>> update(
_isd.DatabaseSession session,
List<PersonSettings> rows, {
_isd.ColumnSelections<PersonSettingsTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<PersonSettings>(
rows,
columns: columns?.call(PersonSettings.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [PersonSettings]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<PersonSettings> updateRow(
_isd.DatabaseSession session,
PersonSettings row, {
_isd.ColumnSelections<PersonSettingsTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<PersonSettings>(
row,
columns: columns?.call(PersonSettings.t),
transaction: transaction,
);
}
/// Updates a single [PersonSettings] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<PersonSettings?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<PersonSettingsUpdateTable>
columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<PersonSettings>(
id,
columnValues: columnValues(PersonSettings.t.updateTable),
transaction: transaction,
);
}
/// Updates all [PersonSettings]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PersonSettings>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<PersonSettingsUpdateTable>
columnValues,
required _isd.WhereExpressionBuilder<PersonSettingsTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<PersonSettingsTable>? orderBy,
_isd.OrderByListBuilder<PersonSettingsTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<PersonSettings>(
columnValues: columnValues(PersonSettings.t.updateTable),
where: where(PersonSettings.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(PersonSettings.t),
orderByList: orderByList?.call(PersonSettings.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [PersonSettings]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PersonSettings>> delete(
_isd.DatabaseSession session,
List<PersonSettings> rows, {
_isd.OrderByBuilder<PersonSettingsTable>? orderBy,
_isd.OrderByListBuilder<PersonSettingsTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<PersonSettings>(
rows,
orderBy: orderBy?.call(PersonSettings.t),
orderByList: orderByList?.call(PersonSettings.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [PersonSettings].
Future<PersonSettings> deleteRow(
_isd.DatabaseSession session,
PersonSettings row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<PersonSettings>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PersonSettings>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<PersonSettingsTable> where,
_isd.OrderByBuilder<PersonSettingsTable>? orderBy,
_isd.OrderByListBuilder<PersonSettingsTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<PersonSettings>(
where: where(PersonSettings.t),
orderBy: orderBy?.call(PersonSettings.t),
orderByList: orderByList?.call(PersonSettings.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<PersonSettingsTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<PersonSettings>(
where: where?.call(PersonSettings.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [PersonSettings] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<PersonSettingsTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<PersonSettings>(
where: where(PersonSettings.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -8,11 +8,14 @@
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: dead_code, unnecessary_null_comparison
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:halkyon_learn_client/src/protocol/protocol.dart' as _itx5j3f5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import '../studyset/studyset.dart' as _ie5k1jc2;
/// A studyset: the cards, notes and paths that belong together; what people study and
/// share. Synced between a person's devices; its space owns it, so it has no owner column.
@ -25,14 +28,20 @@ abstract class Studyset
Studyset._({
this.id,
this.spaceId,
this.parentId,
this.parent,
String? position,
this.keyId,
required this.payload,
this.nonce,
});
}) : position = position ?? 'a0';
factory Studyset({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? parentId,
_ie5k1jc2.Studyset? parent,
String? position,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
@ -44,6 +53,15 @@ abstract class Studyset
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
parentId: jsonSerialization['parentId'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['parentId']),
parent: jsonSerialization['parent'] == null
? null
: _itx5j3f5.Protocol().deserialize<_ie5k1jc2.Studyset>(
jsonSerialization['parent'],
),
position: jsonSerialization['position'] as String?,
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
@ -64,6 +82,19 @@ abstract class Studyset
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue? parentId;
/// The studyset this one sits in; null at the top level (ADR 0004). A move is
/// an update of this column, and the last writer wins. Two devices moving
/// studysets into each other make a cycle, and a studyset whose parent is gone
/// has none: the app shows both at the top level until someone moves them.
_ie5k1jc2.Studyset? parent;
/// Where the studyset stands among its siblings: a fractional index, like a
/// card's (ADR 0003). Studysets from before the tree all start at 'a0'; ties
/// sort by id.
String position;
/// The encryption key's id; always null, since content isn't end-to-end encrypted
/// (ADR 0002). Kept so opt-in encryption of single studysets stays possible.
String? keyId;
@ -83,6 +114,9 @@ abstract class Studyset
Studyset copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? parentId,
_ie5k1jc2.Studyset? parent,
String? position,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
@ -93,6 +127,9 @@ abstract class Studyset
'__className__': 'Studyset',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
if (parentId != null) 'parentId': parentId?.toJson(),
if (parent != null) 'parent': parent?.toJson(),
'position': position,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
@ -105,14 +142,17 @@ abstract class Studyset
'__className__': 'Studyset',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
if (parentId != null) 'parentId': parentId?.toJson(),
if (parent != null) 'parent': parent?.toJsonForProtocol(),
'position': position,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static StudysetInclude include() {
return StudysetInclude._();
static StudysetInclude include({_ie5k1jc2.StudysetInclude? parent}) {
return StudysetInclude._(parent: parent);
}
static StudysetIncludeList includeList({
@ -145,12 +185,18 @@ class _StudysetImpl extends Studyset {
_StudysetImpl({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? parentId,
_ie5k1jc2.Studyset? parent,
String? position,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
parentId: parentId,
parent: parent,
position: position,
keyId: keyId,
payload: payload,
nonce: nonce,
@ -163,6 +209,9 @@ class _StudysetImpl extends Studyset {
Studyset copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
Object? parentId = _Undefined,
Object? parent = _Undefined,
String? position,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
@ -170,6 +219,9 @@ class _StudysetImpl extends Studyset {
return Studyset(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
parentId: parentId is _isc.UuidValue? ? parentId : this.parentId,
parent: parent is _ie5k1jc2.Studyset? ? parent : this.parent?.copyWith(),
position: position ?? this.position,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
@ -185,6 +237,18 @@ class StudysetUpdateTable extends _isd.UpdateTable<StudysetTable> {
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> parentId(
_isc.UuidValue? value,
) => _isd.ColumnValue(
table.parentId,
value,
);
_isd.ColumnValue<String, String> position(String value) => _isd.ColumnValue(
table.position,
value,
);
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
table.keyId,
value,
@ -210,6 +274,15 @@ class StudysetTable extends _isd.Table<_isc.UuidValue?> {
'spaceId',
this,
);
parentId = _isd.ColumnUuid(
'parentId',
this,
);
position = _isd.ColumnString(
'position',
this,
hasDefault: true,
);
keyId = _isd.ColumnString(
'keyId',
this,
@ -229,6 +302,19 @@ class StudysetTable extends _isd.Table<_isc.UuidValue?> {
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid parentId;
/// The studyset this one sits in; null at the top level (ADR 0004). A move is
/// an update of this column, and the last writer wins. Two devices moving
/// studysets into each other make a cycle, and a studyset whose parent is gone
/// has none: the app shows both at the top level until someone moves them.
_ie5k1jc2.StudysetTable? _parent;
/// Where the studyset stands among its siblings: a fractional index, like a
/// card's (ADR 0003). Studysets from before the tree all start at 'a0'; ties
/// sort by id.
late final _isd.ColumnString position;
/// The encryption key's id; always null, since content isn't end-to-end encrypted
/// (ADR 0002). Kept so opt-in encryption of single studysets stays possible.
late final _isd.ColumnString keyId;
@ -239,21 +325,48 @@ class StudysetTable extends _isd.Table<_isc.UuidValue?> {
/// The encryption nonce; always null, like keyId.
late final _isd.ColumnByteData nonce;
_ie5k1jc2.StudysetTable get parent {
if (_parent != null) return _parent!;
_parent = _isd.createRelationTable(
relationFieldName: 'parent',
field: Studyset.t.parentId,
foreignField: _ie5k1jc2.Studyset.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_ie5k1jc2.StudysetTable(tableRelation: foreignTableRelation),
);
return _parent!;
}
@override
List<_isd.Column> get columns => [
id,
spaceId,
parentId,
position,
keyId,
payload,
nonce,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'parent') {
return parent;
}
return null;
}
}
class StudysetInclude extends _isd.IncludeObject {
StudysetInclude._();
StudysetInclude._({_ie5k1jc2.StudysetInclude? parent}) {
_parent = parent;
}
_ie5k1jc2.StudysetInclude? _parent;
@override
Map<String, _isd.Include?> get includes => {};
Map<String, _isd.Include?> get includes => {'parent': _parent};
@override
_isd.Table<_isc.UuidValue?> get table => Studyset.t;
@ -281,6 +394,10 @@ class StudysetIncludeList extends _isd.IncludeList {
class StudysetRepository {
const StudysetRepository._();
final attachRow = const StudysetAttachRowRepository._();
final detachRow = const StudysetDetachRowRepository._();
/// Returns a list of [Studyset]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
@ -311,6 +428,7 @@ class StudysetRepository {
_isd.OrderByBuilder<StudysetTable>? orderBy,
_isd.OrderByListBuilder<StudysetTable>? orderByList,
_isd.Transaction? transaction,
StudysetInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
@ -321,6 +439,7 @@ class StudysetRepository {
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
@ -350,6 +469,7 @@ class StudysetRepository {
_isd.OrderByBuilder<StudysetTable>? orderBy,
_isd.OrderByListBuilder<StudysetTable>? orderByList,
_isd.Transaction? transaction,
StudysetInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
@ -359,6 +479,7 @@ class StudysetRepository {
orderByList: orderByList?.call(Studyset.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
@ -369,12 +490,14 @@ class StudysetRepository {
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
StudysetInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<Studyset>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
@ -672,3 +795,56 @@ class StudysetRepository {
);
}
}
class StudysetAttachRowRepository {
const StudysetAttachRowRepository._();
/// Creates a relation between the given [Studyset] and [Studyset]
/// by setting the [Studyset]'s foreign key `parentId` to refer to the [Studyset].
Future<void> parent(
_isd.DatabaseSession session,
Studyset studyset,
_ie5k1jc2.Studyset parent, {
_isd.Transaction? transaction,
}) async {
if (studyset.id == null) {
throw ArgumentError.notNull('studyset.id');
}
if (parent.id == null) {
throw ArgumentError.notNull('parent.id');
}
var $studyset = studyset.copyWith(parentId: parent.id);
await session.db.updateRow<Studyset>(
$studyset,
columns: [Studyset.t.parentId],
transaction: transaction,
);
}
}
class StudysetDetachRowRepository {
const StudysetDetachRowRepository._();
/// Detaches the relation between this [Studyset] and the [Studyset] set in `parent`
/// by setting the [Studyset]'s foreign key `parentId` to `null`.
///
/// This removes the association between the two models without deleting
/// the related record.
Future<void> parent(
_isd.DatabaseSession session,
Studyset studyset, {
_isd.Transaction? transaction,
}) async {
if (studyset.id == null) {
throw ArgumentError.notNull('studyset.id');
}
var $studyset = studyset.copyWith(parentId: null);
await session.db.updateRow<Studyset>(
$studyset,
columns: [Studyset.t.parentId],
transaction: transaction,
);
}
}

View file

@ -11,17 +11,25 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import 'asset/asset.dart' as _ipb7vkxr;
import 'card/card.dart' as _ipiw1cmx;
import 'card/card_revision.dart' as _ipbyuef1;
import 'review/review_log.dart' as _irzjbdqz;
import 'settings/person_settings.dart' as _irnhx5e5;
import 'studyset/studyset.dart' as _ikbclmae;
import 'tag/card_tag.dart' as _i6jz6nyq;
import 'tag/tag.dart' as _ie8t3ar4;
/// The tables synchronized between client and server by the
/// `serverpod_offline_sync` package, including the ones owned by modules and
/// shared packages.
final List<_isd.Table> syncTables = [
_ipb7vkxr.Asset.t,
_ipiw1cmx.Card.t,
_ipbyuef1.CardRevision.t,
_i6jz6nyq.CardTag.t,
_irnhx5e5.PersonSettings.t,
_irzjbdqz.ReviewLog.t,
_ikbclmae.Studyset.t,
_ie8t3ar4.Tag.t,
];

View file

@ -0,0 +1,795 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: dead_code, unnecessary_null_comparison
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_client/src/protocol/protocol.dart' as _itx5j3f5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import '../card/card.dart' as _i4926i81;
import '../tag/tag.dart' as _isqed5gg;
/// A card having a tag (ADR 0004): one row per assignment, never updated. Removing
/// the tag deletes the row, and adding it on another device at the same time
/// makes a new row, so concurrent edits merge without losing either; a card
/// with the same tag twice counts once. Synced; goes with its card and its tag.
abstract class CardTag
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
CardTag._({
this.id,
this.spaceId,
required this.cardId,
this.card,
required this.tagId,
this.tag,
});
factory CardTag({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue cardId,
_i4926i81.Card? card,
required _isc.UuidValue tagId,
_isqed5gg.Tag? tag,
}) = _CardTagImpl;
factory CardTag.fromJson(Map<String, dynamic> jsonSerialization) {
return CardTag(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
cardId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['cardId']),
card: jsonSerialization['card'] == null
? null
: _itx5j3f5.Protocol().deserialize<_i4926i81.Card>(
jsonSerialization['card'],
),
tagId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['tagId']),
tag: jsonSerialization['tag'] == null
? null
: _itx5j3f5.Protocol().deserialize<_isqed5gg.Tag>(
jsonSerialization['tag'],
),
);
}
static final t = CardTagTable();
static const db = CardTagRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue cardId;
/// The card tagged.
_i4926i81.Card? card;
_isc.UuidValue tagId;
/// The tag it has.
_isqed5gg.Tag? tag;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [CardTag]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
CardTag copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? cardId,
_i4926i81.Card? card,
_isc.UuidValue? tagId,
_isqed5gg.Tag? tag,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'CardTag',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'cardId': cardId.toJson(),
if (card != null) 'card': card?.toJson(),
'tagId': tagId.toJson(),
if (tag != null) 'tag': tag?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'CardTag',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'cardId': cardId.toJson(),
if (card != null) 'card': card?.toJsonForProtocol(),
'tagId': tagId.toJson(),
if (tag != null) 'tag': tag?.toJsonForProtocol(),
};
}
static CardTagInclude include({
_i4926i81.CardInclude? card,
_isqed5gg.TagInclude? tag,
}) {
return CardTagInclude._(
card: card,
tag: tag,
);
}
static CardTagIncludeList includeList({
_isd.WhereExpressionBuilder<CardTagTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<CardTagTable>? orderBy,
_isd.OrderByListBuilder<CardTagTable>? orderByList,
CardTagInclude? include,
}) {
return CardTagIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(CardTag.t),
orderByList: orderByList?.call(CardTag.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _CardTagImpl extends CardTag {
_CardTagImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue cardId,
_i4926i81.Card? card,
required _isc.UuidValue tagId,
_isqed5gg.Tag? tag,
}) : super._(
id: id,
spaceId: spaceId,
cardId: cardId,
card: card,
tagId: tagId,
tag: tag,
);
/// Returns a shallow copy of this [CardTag]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
CardTag copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? cardId,
Object? card = _Undefined,
_isc.UuidValue? tagId,
Object? tag = _Undefined,
}) {
return CardTag(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
cardId: cardId ?? this.cardId,
card: card is _i4926i81.Card? ? card : this.card?.copyWith(),
tagId: tagId ?? this.tagId,
tag: tag is _isqed5gg.Tag? ? tag : this.tag?.copyWith(),
);
}
}
class CardTagUpdateTable extends _isd.UpdateTable<CardTagTable> {
CardTagUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> cardId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.cardId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> tagId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.tagId,
value,
);
}
class CardTagTable extends _isd.Table<_isc.UuidValue?> {
CardTagTable({super.tableRelation}) : super(tableName: 'card_tag') {
updateTable = CardTagUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
cardId = _isd.ColumnUuid(
'cardId',
this,
);
tagId = _isd.ColumnUuid(
'tagId',
this,
);
}
late final CardTagUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid cardId;
/// The card tagged.
_i4926i81.CardTable? _card;
late final _isd.ColumnUuid tagId;
/// The tag it has.
_isqed5gg.TagTable? _tag;
_i4926i81.CardTable get card {
if (_card != null) return _card!;
_card = _isd.createRelationTable(
relationFieldName: 'card',
field: CardTag.t.cardId,
foreignField: _i4926i81.Card.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_i4926i81.CardTable(tableRelation: foreignTableRelation),
);
return _card!;
}
_isqed5gg.TagTable get tag {
if (_tag != null) return _tag!;
_tag = _isd.createRelationTable(
relationFieldName: 'tag',
field: CardTag.t.tagId,
foreignField: _isqed5gg.Tag.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_isqed5gg.TagTable(tableRelation: foreignTableRelation),
);
return _tag!;
}
@override
List<_isd.Column> get columns => [
id,
spaceId,
cardId,
tagId,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'card') {
return card;
}
if (relationField == 'tag') {
return tag;
}
return null;
}
}
class CardTagInclude extends _isd.IncludeObject {
CardTagInclude._({
_i4926i81.CardInclude? card,
_isqed5gg.TagInclude? tag,
}) {
_card = card;
_tag = tag;
}
_i4926i81.CardInclude? _card;
_isqed5gg.TagInclude? _tag;
@override
Map<String, _isd.Include?> get includes => {
'card': _card,
'tag': _tag,
};
@override
_isd.Table<_isc.UuidValue?> get table => CardTag.t;
}
class CardTagIncludeList extends _isd.IncludeList {
CardTagIncludeList._({
_isd.WhereExpressionBuilder<CardTagTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(CardTag.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => CardTag.t;
}
class CardTagRepository {
const CardTagRepository._();
final attachRow = const CardTagAttachRowRepository._();
/// Returns a list of [CardTag]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<CardTag>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<CardTagTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<CardTagTable>? orderBy,
_isd.OrderByListBuilder<CardTagTable>? orderByList,
_isd.Transaction? transaction,
CardTagInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<CardTag>(
where: where?.call(CardTag.t),
orderBy: orderBy?.call(CardTag.t),
orderByList: orderByList?.call(CardTag.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [CardTag] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<CardTag?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<CardTagTable>? where,
int? offset,
_isd.OrderByBuilder<CardTagTable>? orderBy,
_isd.OrderByListBuilder<CardTagTable>? orderByList,
_isd.Transaction? transaction,
CardTagInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<CardTag>(
where: where?.call(CardTag.t),
orderBy: orderBy?.call(CardTag.t),
orderByList: orderByList?.call(CardTag.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [CardTag] by its [id] or null if no such row exists.
Future<CardTag?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
CardTagInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<CardTag>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [CardTag]s in the list and returns the inserted rows.
///
/// The returned [CardTag]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<CardTag>> insert(
_isd.DatabaseSession session,
List<CardTag> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<CardTag>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [CardTag] and returns the inserted row.
///
/// The returned [CardTag] will have its `id` field set.
Future<CardTag> insertRow(
_isd.DatabaseSession session,
CardTag row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<CardTag>(
row,
transaction: transaction,
);
}
/// Upserts all [CardTag]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [CardTag]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<CardTag>> upsert(
_isd.DatabaseSession session,
List<CardTag> rows, {
required _isd.ColumnSelections<CardTagTable> conflictColumns,
_isd.ColumnSelections<CardTagTable>? updateColumns,
_isd.WhereExpressionBuilder<CardTagTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<CardTag>(
rows,
conflictColumns: conflictColumns(CardTag.t),
updateColumns: updateColumns?.call(CardTag.t),
updateWhere: updateWhere?.call(CardTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [CardTag] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [CardTag] will have its `id` field set.
Future<CardTag?> upsertRow(
_isd.DatabaseSession session,
CardTag row, {
required _isd.ColumnSelections<CardTagTable> conflictColumns,
_isd.ColumnSelections<CardTagTable>? updateColumns,
_isd.WhereExpressionBuilder<CardTagTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<CardTag>(
row,
conflictColumns: conflictColumns(CardTag.t),
updateColumns: updateColumns?.call(CardTag.t),
updateWhere: updateWhere?.call(CardTag.t),
transaction: transaction,
);
}
/// Updates all [CardTag]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<CardTag>> update(
_isd.DatabaseSession session,
List<CardTag> rows, {
_isd.ColumnSelections<CardTagTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<CardTag>(
rows,
columns: columns?.call(CardTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [CardTag]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<CardTag> updateRow(
_isd.DatabaseSession session,
CardTag row, {
_isd.ColumnSelections<CardTagTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<CardTag>(
row,
columns: columns?.call(CardTag.t),
transaction: transaction,
);
}
/// Updates a single [CardTag] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<CardTag?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<CardTagUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<CardTag>(
id,
columnValues: columnValues(CardTag.t.updateTable),
transaction: transaction,
);
}
/// Updates all [CardTag]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<CardTag>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<CardTagUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<CardTagTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<CardTagTable>? orderBy,
_isd.OrderByListBuilder<CardTagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<CardTag>(
columnValues: columnValues(CardTag.t.updateTable),
where: where(CardTag.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(CardTag.t),
orderByList: orderByList?.call(CardTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [CardTag]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<CardTag>> delete(
_isd.DatabaseSession session,
List<CardTag> rows, {
_isd.OrderByBuilder<CardTagTable>? orderBy,
_isd.OrderByListBuilder<CardTagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<CardTag>(
rows,
orderBy: orderBy?.call(CardTag.t),
orderByList: orderByList?.call(CardTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [CardTag].
Future<CardTag> deleteRow(
_isd.DatabaseSession session,
CardTag row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<CardTag>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<CardTag>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<CardTagTable> where,
_isd.OrderByBuilder<CardTagTable>? orderBy,
_isd.OrderByListBuilder<CardTagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<CardTag>(
where: where(CardTag.t),
orderBy: orderBy?.call(CardTag.t),
orderByList: orderByList?.call(CardTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<CardTagTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<CardTag>(
where: where?.call(CardTag.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [CardTag] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<CardTagTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<CardTag>(
where: where(CardTag.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class CardTagAttachRowRepository {
const CardTagAttachRowRepository._();
/// Creates a relation between the given [CardTag] and [Card]
/// by setting the [CardTag]'s foreign key `cardId` to refer to the [Card].
Future<void> card(
_isd.DatabaseSession session,
CardTag cardTag,
_i4926i81.Card card, {
_isd.Transaction? transaction,
}) async {
if (cardTag.id == null) {
throw ArgumentError.notNull('cardTag.id');
}
if (card.id == null) {
throw ArgumentError.notNull('card.id');
}
var $cardTag = cardTag.copyWith(cardId: card.id);
await session.db.updateRow<CardTag>(
$cardTag,
columns: [CardTag.t.cardId],
transaction: transaction,
);
}
/// Creates a relation between the given [CardTag] and [Tag]
/// by setting the [CardTag]'s foreign key `tagId` to refer to the [Tag].
Future<void> tag(
_isd.DatabaseSession session,
CardTag cardTag,
_isqed5gg.Tag tag, {
_isd.Transaction? transaction,
}) async {
if (cardTag.id == null) {
throw ArgumentError.notNull('cardTag.id');
}
if (tag.id == null) {
throw ArgumentError.notNull('tag.id');
}
var $cardTag = cardTag.copyWith(tagId: tag.id);
await session.db.updateRow<CardTag>(
$cardTag,
columns: [CardTag.t.tagId],
transaction: transaction,
);
}
}

View file

@ -0,0 +1,669 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
/// A tag a person gives cards (ADR 0004): a name to study and filter by. Synced; its
/// space owns it. The name is content, so it lives in the payload, like a
/// studyset's title.
abstract class Tag
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
Tag._({
this.id,
this.spaceId,
this.keyId,
required this.payload,
this.nonce,
});
factory Tag({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _TagImpl;
factory Tag.fromJson(Map<String, dynamic> jsonSerialization) {
return Tag(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = TagTable();
static const db = TagRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
/// The encryption key's id; always null (ADR 0002), as on studysets.
String? keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "name"}.
_idt.ByteData payload;
/// The encryption nonce; always null, like keyId.
_idt.ByteData? nonce;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [Tag]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
Tag copyWith({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'Tag',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'Tag',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static TagInclude include() {
return TagInclude._();
}
static TagIncludeList includeList({
_isd.WhereExpressionBuilder<TagTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<TagTable>? orderBy,
_isd.OrderByListBuilder<TagTable>? orderByList,
TagInclude? include,
}) {
return TagIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(Tag.t),
orderByList: orderByList?.call(Tag.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _TagImpl extends Tag {
_TagImpl({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [Tag]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
Tag copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return Tag(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class TagUpdateTable extends _isd.UpdateTable<TagTable> {
TagUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
table.keyId,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_isd.ColumnValue(
table.payload,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_isd.ColumnValue(
table.nonce,
value,
);
}
class TagTable extends _isd.Table<_isc.UuidValue?> {
TagTable({super.tableRelation}) : super(tableName: 'tag') {
updateTable = TagUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final TagUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
/// The encryption key's id; always null (ADR 0002), as on studysets.
late final _isd.ColumnString keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "name"}.
late final _isd.ColumnByteData payload;
/// The encryption nonce; always null, like keyId.
late final _isd.ColumnByteData nonce;
@override
List<_isd.Column> get columns => [
id,
spaceId,
keyId,
payload,
nonce,
];
}
class TagInclude extends _isd.IncludeObject {
TagInclude._();
@override
Map<String, _isd.Include?> get includes => {};
@override
_isd.Table<_isc.UuidValue?> get table => Tag.t;
}
class TagIncludeList extends _isd.IncludeList {
TagIncludeList._({
_isd.WhereExpressionBuilder<TagTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(Tag.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => Tag.t;
}
class TagRepository {
const TagRepository._();
/// Returns a list of [Tag]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<Tag>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<TagTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<TagTable>? orderBy,
_isd.OrderByListBuilder<TagTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<Tag>(
where: where?.call(Tag.t),
orderBy: orderBy?.call(Tag.t),
orderByList: orderByList?.call(Tag.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [Tag] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<Tag?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<TagTable>? where,
int? offset,
_isd.OrderByBuilder<TagTable>? orderBy,
_isd.OrderByListBuilder<TagTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<Tag>(
where: where?.call(Tag.t),
orderBy: orderBy?.call(Tag.t),
orderByList: orderByList?.call(Tag.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [Tag] by its [id] or null if no such row exists.
Future<Tag?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<Tag>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [Tag]s in the list and returns the inserted rows.
///
/// The returned [Tag]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Tag>> insert(
_isd.DatabaseSession session,
List<Tag> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<Tag>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [Tag] and returns the inserted row.
///
/// The returned [Tag] will have its `id` field set.
Future<Tag> insertRow(
_isd.DatabaseSession session,
Tag row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<Tag>(
row,
transaction: transaction,
);
}
/// Upserts all [Tag]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [Tag]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Tag>> upsert(
_isd.DatabaseSession session,
List<Tag> rows, {
required _isd.ColumnSelections<TagTable> conflictColumns,
_isd.ColumnSelections<TagTable>? updateColumns,
_isd.WhereExpressionBuilder<TagTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<Tag>(
rows,
conflictColumns: conflictColumns(Tag.t),
updateColumns: updateColumns?.call(Tag.t),
updateWhere: updateWhere?.call(Tag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [Tag] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [Tag] will have its `id` field set.
Future<Tag?> upsertRow(
_isd.DatabaseSession session,
Tag row, {
required _isd.ColumnSelections<TagTable> conflictColumns,
_isd.ColumnSelections<TagTable>? updateColumns,
_isd.WhereExpressionBuilder<TagTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<Tag>(
row,
conflictColumns: conflictColumns(Tag.t),
updateColumns: updateColumns?.call(Tag.t),
updateWhere: updateWhere?.call(Tag.t),
transaction: transaction,
);
}
/// Updates all [Tag]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Tag>> update(
_isd.DatabaseSession session,
List<Tag> rows, {
_isd.ColumnSelections<TagTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<Tag>(
rows,
columns: columns?.call(Tag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [Tag]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<Tag> updateRow(
_isd.DatabaseSession session,
Tag row, {
_isd.ColumnSelections<TagTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<Tag>(
row,
columns: columns?.call(Tag.t),
transaction: transaction,
);
}
/// Updates a single [Tag] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<Tag?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<TagUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<Tag>(
id,
columnValues: columnValues(Tag.t.updateTable),
transaction: transaction,
);
}
/// Updates all [Tag]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Tag>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<TagUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<TagTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<TagTable>? orderBy,
_isd.OrderByListBuilder<TagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<Tag>(
columnValues: columnValues(Tag.t.updateTable),
where: where(Tag.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(Tag.t),
orderByList: orderByList?.call(Tag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [Tag]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Tag>> delete(
_isd.DatabaseSession session,
List<Tag> rows, {
_isd.OrderByBuilder<TagTable>? orderBy,
_isd.OrderByListBuilder<TagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<Tag>(
rows,
orderBy: orderBy?.call(Tag.t),
orderByList: orderByList?.call(Tag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [Tag].
Future<Tag> deleteRow(
_isd.DatabaseSession session,
Tag row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<Tag>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Tag>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<TagTable> where,
_isd.OrderByBuilder<TagTable>? orderBy,
_isd.OrderByListBuilder<TagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<Tag>(
where: where(Tag.t),
orderBy: orderBy?.call(Tag.t),
orderByList: orderByList?.call(Tag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<TagTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<Tag>(
where: where?.call(Tag.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [Tag] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<TagTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<Tag>(
where: where(Tag.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -69,6 +69,8 @@ dev_dependencies:
# The sync tests (test/sync/) sign in like the app and read the server's database.
postgres: ^3.5.16
serverpod_auth_core_client: 4.0.3
# The device-migration test builds local databases like the sync engine does.
sqlite3: ^3.6.0
yaml: ^3.1.4
dependency_overrides:

View file

@ -0,0 +1,127 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_client/migrations/migration_registry.dart';
import 'package:sqlite3/sqlite3.dart';
/// Every device keeps its content through every upgrade of its local database:
/// each client migration, applied to a database at the version before it that
/// holds a row in every synced table, loses none of them and breaks no foreign
/// key.
///
/// SQLite can't add a foreign key to a table, so a migration may rebuild one
/// (create a copy, move the rows, drop the original). Dropping it with foreign
/// keys enforced would delete its children through their cascades, or fail.
/// Serverpod's SQLite migration runner switches foreign keys off around
/// migrations for that reason (`sqlite_migration_runner.dart`), and so does
/// this test, which applies the SQL the same way.
void main() {
final migrations = MigrationRegistry.migrations;
for (var i = 1; i < migrations.length; i++) {
final from = migrations[i - 1];
final to = migrations[i];
test('upgrading a device from ${from.version} to ${to.version} keeps every '
'row', () {
final db = sqlite3.openInMemory();
addTearDown(db.close);
db
..execute('PRAGMA foreign_keys = ON')
..execute(from.definitionSql);
final rows = _fillSyncedTables(db);
db
..execute('PRAGMA foreign_keys = OFF')
..execute(to.migrationSql)
..execute('PRAGMA foreign_keys = ON');
expect({for (final table in rows.keys) table: _count(db, table)}, rows);
expect(db.select('PRAGMA foreign_key_check'), isEmpty);
});
}
}
final _random = Random(1);
Uint8List _id() =>
Uint8List.fromList(List.generate(16, (_) => _random.nextInt(256)));
/// Puts one row into each of our synced tables the database has, and returns how
/// many rows each then holds. A synced table this doesn't know fails the test:
/// it needs a row here, so its upgrades are checked too.
Map<String, int> _fillSyncedTables(Database db) {
final studyset = _id();
final card = _id();
final tag = _id();
final rows = <String, (String, List<Object?>)>{
'studyset': (
"INSERT INTO studyset (id, spaceId, payload) VALUES (?, 1, x'00')",
[studyset],
),
'card': (
'INSERT INTO card (id, spaceId, studysetId, position) '
"VALUES (?, 1, ?, 'a0')",
[card, studyset],
),
'card_revision': (
'INSERT INTO card_revision (id, spaceId, cardId, parents, authorId, '
"payload) VALUES (?, 1, ?, '[]', ?, x'00')",
[_id(), card, _id()],
),
'review_log': (
'INSERT INTO review_log (id, spaceId, cardId, rating, reviewedAt, '
'durationMs) VALUES (?, 1, ?, 3, 0, 1000)',
[_id(), card],
),
'tag': (
"INSERT INTO tag (id, spaceId, payload) VALUES (?, 1, x'00')",
[tag],
),
'card_tag': (
'INSERT INTO card_tag (id, spaceId, cardId, tagId) VALUES (?, 1, ?, ?)',
[_id(), card, tag],
),
'person_settings': (
"INSERT INTO person_settings (id, spaceId, payload) VALUES (?, 1, x'00')",
[_id()],
),
'asset': (
'INSERT INTO asset (id, spaceId, sha256, mimeType, byteSize, payload) '
"VALUES (?, 1, 'ab', 'image/png', 1, x'00')",
[_id()],
),
};
final synced = {
for (final table in db.select(
"SELECT name FROM sqlite_master WHERE type = 'table'",
))
// Ours: synced (a spaceId), not the sync engine's own bookkeeping, which
// only the engine's migrations change.
if (!RegExp('^(crdt|offline_sync)_').hasMatch(table['name'] as String) &&
db
.select('PRAGMA table_info("${table['name']}")')
.any((column) => column['name'] == 'spaceId'))
table['name'] as String,
};
expect(
synced.difference(rows.keys.toSet()),
isEmpty,
reason: 'every synced table needs a row in this test',
);
db.execute(
'INSERT INTO offline_sync_spaces (id, uuidSpaceId) VALUES (1, ?)',
[_id()],
);
for (final MapEntry(key: table, value: (sql, parameters)) in rows.entries) {
if (synced.contains(table)) {
db.execute(sql, parameters);
}
}
return {for (final table in synced) table: _count(db, table)};
}
int _count(Database db, String table) =>
db.select('SELECT count(*) AS n FROM "$table"').single['n'] as int;

View file

@ -0,0 +1,33 @@
### A file in a person's content, an image or a sound (ADR 0004). This row is synced;
### the file itself is in the server's private cloud storage, reachable only
### through an endpoint that checks the owner, and each device caches it by its
### hash. Content refers to it by the row's id.
###
### A file added twice (the same image in two imported cards) is one file in
### storage, since its path is its hash; two rows for it may exist when two
### devices added it while offline, and count as one.
class: Asset
table: asset
database: sync
fields:
### The file's SHA-256, in lowercase hex: its name in storage and in every
### device's cache, and the check that a download is the file that was added.
sha256: String
### Its media type, e.g. 'image/png' or 'audio/mpeg'.
mimeType: String
### Its size in bytes.
byteSize: int
### The encryption key's id; always null (ADR 0002), as on studysets.
keyId: String?
### The content as UTF-8 JSON: {"schemaVersion", "fileName"}; the name it came
### with is content, like a studyset's title.
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).
asset_space_idx:
fields: spaceId
# A space's file with a given hash, to add a file only once.
asset_hash_idx:
fields: spaceId, sha256

View file

@ -0,0 +1,753 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod/serverpod.dart' as _is;
/// A file in a person's content, an image or a sound (ADR 0004). This row is synced;
/// the file itself is in the server's private cloud storage, reachable only
/// through an endpoint that checks the owner, and each device caches it by its
/// hash. Content refers to it by the row's id.
///
/// A file added twice (the same image in two imported cards) is one file in
/// storage, since its path is its hash; two rows for it may exist when two
/// devices added it while offline, and count as one.
abstract class Asset
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
Asset._({
this.id,
this.spaceId,
required this.sha256,
required this.mimeType,
required this.byteSize,
this.keyId,
required this.payload,
this.nonce,
});
factory Asset({
_is.UuidValue? id,
int? spaceId,
required String sha256,
required String mimeType,
required int byteSize,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _AssetImpl;
factory Asset.fromJson(Map<String, dynamic> jsonSerialization) {
return Asset(
id: jsonSerialization['id'] == null
? null
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
sha256: jsonSerialization['sha256'] as String,
mimeType: jsonSerialization['mimeType'] as String,
byteSize: jsonSerialization['byteSize'] as int,
keyId: jsonSerialization['keyId'] as String?,
payload: _is.ByteDataJsonExtension.fromJson(jsonSerialization['payload']),
nonce: jsonSerialization['nonce'] == null
? null
: _is.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = AssetTable();
static const db = AssetRepository._();
@override
_is.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
/// The file's SHA-256, in lowercase hex: its name in storage and in every
/// device's cache, and the check that a download is the file that was added.
String sha256;
/// Its media type, e.g. 'image/png' or 'audio/mpeg'.
String mimeType;
/// Its size in bytes.
int byteSize;
/// The encryption key's id; always null (ADR 0002), as on studysets.
String? keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "fileName"}; the name it came
/// with is content, like a studyset's title.
_idt.ByteData payload;
/// The encryption nonce; always null, like keyId.
_idt.ByteData? nonce;
@override
_is.Table<_is.UuidValue?> get table => t;
/// Returns a shallow copy of this [Asset]
/// with some or all fields replaced by the given arguments.
@_is.useResult
Asset copyWith({
_is.UuidValue? id,
int? spaceId,
String? sha256,
String? mimeType,
int? byteSize,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'Asset',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'sha256': sha256,
'mimeType': mimeType,
'byteSize': byteSize,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'Asset',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'sha256': sha256,
'mimeType': mimeType,
'byteSize': byteSize,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static AssetInclude include() {
return AssetInclude._();
}
static AssetIncludeList includeList({
_is.WhereExpressionBuilder<AssetTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<AssetTable>? orderBy,
_is.OrderByListBuilder<AssetTable>? orderByList,
AssetInclude? include,
}) {
return AssetIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
include: include,
);
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _AssetImpl extends Asset {
_AssetImpl({
_is.UuidValue? id,
int? spaceId,
required String sha256,
required String mimeType,
required int byteSize,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
sha256: sha256,
mimeType: mimeType,
byteSize: byteSize,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [Asset]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
Asset copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
String? sha256,
String? mimeType,
int? byteSize,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return Asset(
id: id is _is.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
sha256: sha256 ?? this.sha256,
mimeType: mimeType ?? this.mimeType,
byteSize: byteSize ?? this.byteSize,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class AssetUpdateTable extends _is.UpdateTable<AssetTable> {
AssetUpdateTable(super.table);
_is.ColumnValue<int, int> spaceId(int? value) => _is.ColumnValue(
table.spaceId,
value,
);
_is.ColumnValue<String, String> sha256(String value) => _is.ColumnValue(
table.sha256,
value,
);
_is.ColumnValue<String, String> mimeType(String value) => _is.ColumnValue(
table.mimeType,
value,
);
_is.ColumnValue<int, int> byteSize(int value) => _is.ColumnValue(
table.byteSize,
value,
);
_is.ColumnValue<String, String> keyId(String? value) => _is.ColumnValue(
table.keyId,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_is.ColumnValue(
table.payload,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_is.ColumnValue(
table.nonce,
value,
);
}
class AssetTable extends _is.Table<_is.UuidValue?> {
AssetTable({super.tableRelation}) : super(tableName: 'asset') {
updateTable = AssetUpdateTable(this);
spaceId = _is.ColumnInt(
'spaceId',
this,
);
sha256 = _is.ColumnString(
'sha256',
this,
);
mimeType = _is.ColumnString(
'mimeType',
this,
);
byteSize = _is.ColumnInt(
'byteSize',
this,
);
keyId = _is.ColumnString(
'keyId',
this,
);
payload = _is.ColumnByteData(
'payload',
this,
);
nonce = _is.ColumnByteData(
'nonce',
this,
);
}
late final AssetUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _is.ColumnInt spaceId;
/// The file's SHA-256, in lowercase hex: its name in storage and in every
/// device's cache, and the check that a download is the file that was added.
late final _is.ColumnString sha256;
/// Its media type, e.g. 'image/png' or 'audio/mpeg'.
late final _is.ColumnString mimeType;
/// Its size in bytes.
late final _is.ColumnInt byteSize;
/// The encryption key's id; always null (ADR 0002), as on studysets.
late final _is.ColumnString keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "fileName"}; the name it came
/// with is content, like a studyset's title.
late final _is.ColumnByteData payload;
/// The encryption nonce; always null, like keyId.
late final _is.ColumnByteData nonce;
@override
List<_is.Column> get columns => [
id,
spaceId,
sha256,
mimeType,
byteSize,
keyId,
payload,
nonce,
];
}
class AssetInclude extends _is.IncludeObject {
AssetInclude._();
@override
Map<String, _is.Include?> get includes => {};
@override
_is.Table<_is.UuidValue?> get table => Asset.t;
}
class AssetIncludeList extends _is.IncludeList {
AssetIncludeList._({
_is.WhereExpressionBuilder<AssetTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(Asset.t);
}
@override
Map<String, _is.Include?> get includes => include?.includes ?? {};
@override
_is.Table<_is.UuidValue?> get table => Asset.t;
}
class AssetRepository {
const AssetRepository._();
/// Returns a list of [Asset]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<Asset>> find(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<AssetTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<AssetTable>? orderBy,
_is.OrderByListBuilder<AssetTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.find<Asset>(
where: where?.call(Asset.t),
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [Asset] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<Asset?> findFirstRow(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<AssetTable>? where,
int? offset,
_is.OrderByBuilder<AssetTable>? orderBy,
_is.OrderByListBuilder<AssetTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<Asset>(
where: where?.call(Asset.t),
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [Asset] by its [id] or null if no such row exists.
Future<Asset?> findById(
_is.DatabaseSession session,
_is.UuidValue id, {
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findById<Asset>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [Asset]s in the list and returns the inserted rows.
///
/// The returned [Asset]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> insert(
_is.DatabaseSession session,
List<Asset> rows, {
_is.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<Asset>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [Asset] and returns the inserted row.
///
/// The returned [Asset] will have its `id` field set.
Future<Asset> insertRow(
_is.DatabaseSession session,
Asset row, {
_is.Transaction? transaction,
}) async {
return session.db.insertRow<Asset>(
row,
transaction: transaction,
);
}
/// Upserts all [Asset]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [Asset]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> upsert(
_is.DatabaseSession session,
List<Asset> rows, {
required _is.ColumnSelections<AssetTable> conflictColumns,
_is.ColumnSelections<AssetTable>? updateColumns,
_is.WhereExpressionBuilder<AssetTable>? updateWhere,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<Asset>(
rows,
conflictColumns: conflictColumns(Asset.t),
updateColumns: updateColumns?.call(Asset.t),
updateWhere: updateWhere?.call(Asset.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [Asset] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [Asset] will have its `id` field set.
Future<Asset?> upsertRow(
_is.DatabaseSession session,
Asset row, {
required _is.ColumnSelections<AssetTable> conflictColumns,
_is.ColumnSelections<AssetTable>? updateColumns,
_is.WhereExpressionBuilder<AssetTable>? updateWhere,
_is.Transaction? transaction,
}) async {
return session.db.upsertRow<Asset>(
row,
conflictColumns: conflictColumns(Asset.t),
updateColumns: updateColumns?.call(Asset.t),
updateWhere: updateWhere?.call(Asset.t),
transaction: transaction,
);
}
/// Updates all [Asset]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> update(
_is.DatabaseSession session,
List<Asset> rows, {
_is.ColumnSelections<AssetTable>? columns,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<Asset>(
rows,
columns: columns?.call(Asset.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [Asset]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<Asset> updateRow(
_is.DatabaseSession session,
Asset row, {
_is.ColumnSelections<AssetTable>? columns,
_is.Transaction? transaction,
}) async {
return session.db.updateRow<Asset>(
row,
columns: columns?.call(Asset.t),
transaction: transaction,
);
}
/// Updates a single [Asset] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<Asset?> updateById(
_is.DatabaseSession session,
_is.UuidValue id, {
required _is.ColumnValueListBuilder<AssetUpdateTable> columnValues,
_is.Transaction? transaction,
}) async {
return session.db.updateById<Asset>(
id,
columnValues: columnValues(Asset.t.updateTable),
transaction: transaction,
);
}
/// Updates all [Asset]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> updateWhere(
_is.DatabaseSession session, {
required _is.ColumnValueListBuilder<AssetUpdateTable> columnValues,
required _is.WhereExpressionBuilder<AssetTable> where,
int? limit,
int? offset,
_is.OrderByBuilder<AssetTable>? orderBy,
_is.OrderByListBuilder<AssetTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<Asset>(
columnValues: columnValues(Asset.t.updateTable),
where: where(Asset.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [Asset]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> delete(
_is.DatabaseSession session,
List<Asset> rows, {
_is.OrderByBuilder<AssetTable>? orderBy,
_is.OrderByListBuilder<AssetTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<Asset>(
rows,
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [Asset].
Future<Asset> deleteRow(
_is.DatabaseSession session,
Asset row, {
_is.Transaction? transaction,
}) async {
return session.db.deleteRow<Asset>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<Asset>> deleteWhere(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<AssetTable> where,
_is.OrderByBuilder<AssetTable>? orderBy,
_is.OrderByListBuilder<AssetTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<Asset>(
where: where(Asset.t),
orderBy: orderBy?.call(Asset.t),
orderByList: orderByList?.call(Asset.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<AssetTable>? where,
int? limit,
_is.Transaction? transaction,
}) async {
return session.db.count<Asset>(
where: where?.call(Asset.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [Asset] rows matching the [where] expression.
Future<void> lockRows(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<AssetTable> where,
required _is.LockMode lockMode,
required _is.Transaction transaction,
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
}) async {
return session.db.lockRows<Asset>(
where: where(Asset.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -21,6 +21,7 @@ import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
as _iais;
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart'
as _izehhkf5;
import 'asset/asset.dart' as _ipb7vkxr;
import 'auth/device.dart' as _i6c5ny7n;
import 'auth/github_sign_in_refusal.dart' as _ir1gd9p5;
import 'auth/github_sign_in_refused_exception.dart' as _i0xfcu5n;
@ -29,7 +30,11 @@ import 'card/card_revision.dart' as _ipbyuef1;
import 'common/not_found_exception.dart' as _i8oee73b;
import 'instance/instance_info.dart' as _iskrg2g3;
import 'review/review_log.dart' as _irzjbdqz;
import 'settings/person_settings.dart' as _irnhx5e5;
import 'studyset/studyset.dart' as _ikbclmae;
import 'tag/card_tag.dart' as _i6jz6nyq;
import 'tag/tag.dart' as _ie8t3ar4;
export 'asset/asset.dart';
export 'auth/device.dart';
export 'auth/github_sign_in_refusal.dart';
export 'auth/github_sign_in_refused_exception.dart';
@ -38,7 +43,10 @@ export 'card/card_revision.dart';
export 'common/not_found_exception.dart';
export 'instance/instance_info.dart';
export 'review/review_log.dart';
export 'settings/person_settings.dart';
export 'studyset/studyset.dart';
export 'tag/card_tag.dart';
export 'tag/tag.dart';
export 'sync_tables.dart';
class Protocol extends _is.DatabaseSerializationManager {
@ -49,6 +57,108 @@ class Protocol extends _is.DatabaseSerializationManager {
static final Protocol _instance = Protocol._().._registerHostProtocols();
static List<_isp.TableDefinition> get targetTableDefinitions => [
_isp.TableDefinition(
name: 'asset',
dartName: 'Asset',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isp.ColumnDefinition(
name: 'spaceId',
columnType: _isp.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isp.ColumnDefinition(
name: 'sha256',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'mimeType',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'byteSize',
columnType: _isp.ColumnType.bigint,
isNullable: false,
dartType: 'int',
),
_isp.ColumnDefinition(
name: 'keyId',
columnType: _isp.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isp.ColumnDefinition(
name: 'payload',
columnType: _isp.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isp.ColumnDefinition(
name: 'nonce',
columnType: _isp.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
constraintName: 'asset_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isp.IndexDefinition(
indexName: 'asset_space_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isp.IndexDefinition(
indexName: 'asset_hash_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'spaceId',
),
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'sha256',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isp.TableDefinition(
name: 'card',
dartName: 'Card',
@ -253,6 +363,182 @@ class Protocol extends _is.DatabaseSerializationManager {
],
managed: true,
),
_isp.TableDefinition(
name: 'card_tag',
dartName: 'CardTag',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isp.ColumnDefinition(
name: 'spaceId',
columnType: _isp.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isp.ColumnDefinition(
name: 'cardId',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isp.ColumnDefinition(
name: 'tagId',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
constraintName: 'card_tag_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
_isp.ForeignKeyDefinition(
constraintName: 'card_tag_fk_1',
columns: ['cardId'],
referenceTable: 'card',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isp.DeferrableConstraint.initiallyDeferred,
),
_isp.ForeignKeyDefinition(
constraintName: 'card_tag_fk_2',
columns: ['tagId'],
referenceTable: 'tag',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isp.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isp.IndexDefinition(
indexName: 'card_tag_space_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isp.IndexDefinition(
indexName: 'card_tag_card_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'cardId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isp.IndexDefinition(
indexName: 'card_tag_tag_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'tagId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isp.TableDefinition(
name: 'person_settings',
dartName: 'PersonSettings',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isp.ColumnDefinition(
name: 'spaceId',
columnType: _isp.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isp.ColumnDefinition(
name: 'keyId',
columnType: _isp.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isp.ColumnDefinition(
name: 'payload',
columnType: _isp.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isp.ColumnDefinition(
name: 'nonce',
columnType: _isp.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
constraintName: 'person_settings_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isp.IndexDefinition(
indexName: 'person_settings_space_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isp.TableDefinition(
name: 'review_log',
dartName: 'ReviewLog',
@ -296,6 +582,13 @@ class Protocol extends _is.DatabaseSerializationManager {
isNullable: false,
dartType: 'int',
),
_isp.ColumnDefinition(
name: 'mode',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
columnDefault: '\'review\'',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
@ -373,6 +666,19 @@ class Protocol extends _is.DatabaseSerializationManager {
isNullable: true,
dartType: 'int?',
),
_isp.ColumnDefinition(
name: 'parentId',
columnType: _isp.ColumnType.uuid,
isNullable: true,
dartType: 'UuidValue?',
),
_isp.ColumnDefinition(
name: 'position',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
columnDefault: '\'a0\'',
),
_isp.ColumnDefinition(
name: 'keyId',
columnType: _isp.ColumnType.text,
@ -403,6 +709,17 @@ class Protocol extends _is.DatabaseSerializationManager {
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
_isp.ForeignKeyDefinition(
constraintName: 'studyset_fk_1',
columns: ['parentId'],
referenceTable: 'studyset',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isp.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isp.IndexDefinition(
@ -418,6 +735,90 @@ class Protocol extends _is.DatabaseSerializationManager {
isUnique: false,
isPrimary: false,
),
_isp.IndexDefinition(
indexName: 'studyset_parent_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'parentId',
),
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'position',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isp.TableDefinition(
name: 'tag',
dartName: 'Tag',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isp.ColumnDefinition(
name: 'spaceId',
columnType: _isp.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isp.ColumnDefinition(
name: 'keyId',
columnType: _isp.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isp.ColumnDefinition(
name: 'payload',
columnType: _isp.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isp.ColumnDefinition(
name: 'nonce',
columnType: _isp.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
constraintName: 'tag_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isp.IndexDefinition(
indexName: 'tag_space_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
@ -454,6 +855,9 @@ class Protocol extends _is.DatabaseSerializationManager {
}
}
if (t == _ipb7vkxr.Asset) {
return _ipb7vkxr.Asset.fromJson(data) as T;
}
if (t == _i6c5ny7n.Device) {
return _i6c5ny7n.Device.fromJson(data) as T;
}
@ -478,9 +882,21 @@ class Protocol extends _is.DatabaseSerializationManager {
if (t == _irzjbdqz.ReviewLog) {
return _irzjbdqz.ReviewLog.fromJson(data) as T;
}
if (t == _irnhx5e5.PersonSettings) {
return _irnhx5e5.PersonSettings.fromJson(data) as T;
}
if (t == _ikbclmae.Studyset) {
return _ikbclmae.Studyset.fromJson(data) as T;
}
if (t == _i6jz6nyq.CardTag) {
return _i6jz6nyq.CardTag.fromJson(data) as T;
}
if (t == _ie8t3ar4.Tag) {
return _ie8t3ar4.Tag.fromJson(data) as T;
}
if (t == _is.getType<_ipb7vkxr.Asset?>()) {
return (data != null ? _ipb7vkxr.Asset.fromJson(data) : null) as T;
}
if (t == _is.getType<_i6c5ny7n.Device?>()) {
return (data != null ? _i6c5ny7n.Device.fromJson(data) : null) as T;
}
@ -512,9 +928,19 @@ class Protocol extends _is.DatabaseSerializationManager {
if (t == _is.getType<_irzjbdqz.ReviewLog?>()) {
return (data != null ? _irzjbdqz.ReviewLog.fromJson(data) : null) as T;
}
if (t == _is.getType<_irnhx5e5.PersonSettings?>()) {
return (data != null ? _irnhx5e5.PersonSettings.fromJson(data) : null)
as T;
}
if (t == _is.getType<_ikbclmae.Studyset?>()) {
return (data != null ? _ikbclmae.Studyset.fromJson(data) : null) as T;
}
if (t == _is.getType<_i6jz6nyq.CardTag?>()) {
return (data != null ? _i6jz6nyq.CardTag.fromJson(data) : null) as T;
}
if (t == _is.getType<_ie8t3ar4.Tag?>()) {
return (data != null ? _ie8t3ar4.Tag.fromJson(data) : null) as T;
}
if (t == List<_is.UuidValue>) {
return (data as List).map((e) => deserialize<_is.UuidValue>(e)).toList()
as T;
@ -542,6 +968,7 @@ class Protocol extends _is.DatabaseSerializationManager {
static String? getClassNameForType(Type type) {
return switch (type) {
_ipb7vkxr.Asset => 'Asset',
_i6c5ny7n.Device => 'Device',
_ir1gd9p5.GitHubSignInRefusal => 'GitHubSignInRefusal',
_i0xfcu5n.GitHubSignInRefusedException => 'GitHubSignInRefusedException',
@ -550,7 +977,10 @@ class Protocol extends _is.DatabaseSerializationManager {
_i8oee73b.NotFoundException => 'NotFoundException',
_iskrg2g3.InstanceInfo => 'InstanceInfo',
_irzjbdqz.ReviewLog => 'ReviewLog',
_irnhx5e5.PersonSettings => 'PersonSettings',
_ikbclmae.Studyset => 'Studyset',
_i6jz6nyq.CardTag => 'CardTag',
_ie8t3ar4.Tag => 'Tag',
_ => null,
};
}
@ -568,6 +998,8 @@ class Protocol extends _is.DatabaseSerializationManager {
}
switch (data) {
case _ipb7vkxr.Asset():
return 'Asset';
case _i6c5ny7n.Device():
return 'Device';
case _ir1gd9p5.GitHubSignInRefusal():
@ -584,8 +1016,14 @@ class Protocol extends _is.DatabaseSerializationManager {
return 'InstanceInfo';
case _irzjbdqz.ReviewLog():
return 'ReviewLog';
case _irnhx5e5.PersonSettings():
return 'PersonSettings';
case _ikbclmae.Studyset():
return 'Studyset';
case _i6jz6nyq.CardTag():
return 'CardTag';
case _ie8t3ar4.Tag():
return 'Tag';
}
className = _iais.Protocol().getClassNameForObject(data);
if (className != null) {
@ -618,6 +1056,9 @@ class Protocol extends _is.DatabaseSerializationManager {
if (dataClassName is! String) {
return super.deserializeByClassName(data);
}
if (dataClassName == 'Asset') {
return deserialize<_ipb7vkxr.Asset>(data['data']);
}
if (dataClassName == 'Device') {
return deserialize<_i6c5ny7n.Device>(data['data']);
}
@ -642,9 +1083,18 @@ class Protocol extends _is.DatabaseSerializationManager {
if (dataClassName == 'ReviewLog') {
return deserialize<_irzjbdqz.ReviewLog>(data['data']);
}
if (dataClassName == 'PersonSettings') {
return deserialize<_irnhx5e5.PersonSettings>(data['data']);
}
if (dataClassName == 'Studyset') {
return deserialize<_ikbclmae.Studyset>(data['data']);
}
if (dataClassName == 'CardTag') {
return deserialize<_i6jz6nyq.CardTag>(data['data']);
}
if (dataClassName == 'Tag') {
return deserialize<_ie8t3ar4.Tag>(data['data']);
}
if (dataClassName.startsWith('serverpod_auth_idp.')) {
data['className'] = dataClassName.substring(19);
return _iais.Protocol().deserializeByClassName(data);
@ -697,14 +1147,22 @@ class Protocol extends _is.DatabaseSerializationManager {
}
}
switch (t) {
case _ipb7vkxr.Asset:
return _ipb7vkxr.Asset.t;
case _ipiw1cmx.Card:
return _ipiw1cmx.Card.t;
case _ipbyuef1.CardRevision:
return _ipbyuef1.CardRevision.t;
case _irzjbdqz.ReviewLog:
return _irzjbdqz.ReviewLog.t;
case _irnhx5e5.PersonSettings:
return _irnhx5e5.PersonSettings.t;
case _ikbclmae.Studyset:
return _ikbclmae.Studyset.t;
case _i6jz6nyq.CardTag:
return _i6jz6nyq.CardTag.t;
case _ie8t3ar4.Tag:
return _ie8t3ar4.Tag.t;
}
return null;
}

Some files were not shown because too many files have changed in this diff Show more