feat: notes in studysets, synced and merged as cards are, with tags

Server: three synced tables, only additions (migration
20261002191122122-notes): `note` (its studyset, a fractional position),
`note_revision` (append-only, as a card's: parents, author, report,
the payload {schemaVersion, title, body}) and `note_tag` (one row per
assignment, never updated, as `card_tag`). Each indexes `spaceId`, and
everything goes with its space, studyset, note or tag. A migration of
synced tables locks app builds from before it out of syncing (ADR 0002,
rule 3): cheap now, before the first release.

App, `feature/note`:
- `NoteContent` is a title and rich text, merged apart (ADR 0003): a
  title changed on one device and the body on another both survive, a
  body changed on two is a conflict.
- `NoteRepository` creates a note at the end of its studyset, saves an
  edit on the revisions it started from (`saveEdit`, as the card
  composer now does), says when a note was last edited (its newest
  head's UUID v7), counts notes per studyset, and deletes a note's
  revisions blanked first (ADR 0002, rule 2).
- Notes share the person's tags: `setTagsOfNote`, and folding tags of
  one name or undoing a tag's deletion covers notes too.
- The delayed deletion with its Undo moves to `core/async`
  (`DelayedDeletions`): `StudysetDeletions` builds on it and now takes
  a studyset's notes too; `NoteDeletions` deletes a single note.

Tests: content, merge, payload, repository, deletions, tags, the device
migration guard, and sync tests against the server (four).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-02 21:53:56 +02:00
commit f3b56f2c0c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
55 changed files with 25469 additions and 131 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,405 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "note",
"dartName": "Note",
"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": "note_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "note_fk_1",
"columns": [
"studysetId"
],
"referenceTable": "studyset",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "note_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "note_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.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "note_revision",
"dartName": "NoteRevision",
"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": "noteId",
"fieldName": "noteId",
"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": "note_revision_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "note_revision_fk_1",
"columns": [
"noteId"
],
"referenceTable": "note",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "note_revision_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "note_revision_note_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "noteId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "note_tag",
"dartName": "NoteTag",
"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": "noteId",
"fieldName": "noteId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "tagId",
"fieldName": "tagId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "note_tag_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "note_tag_fk_1",
"columns": [
"noteId"
],
"referenceTable": "note",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "note_tag_fk_2",
"columns": [
"tagId"
],
"referenceTable": "tag",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "note_tag_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "note_tag_note_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "noteId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "note_tag_tag_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "tagId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -8,6 +8,7 @@ part '20260930094729362-studysets/migration.dart';
part '20260930122235111-cards/migration.dart';
part '20260930145136285-reviews/migration.dart';
part '20261001064926911-tree-tags-assets/migration.dart';
part '20261002191122122-notes/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -18,6 +19,7 @@ class MigrationRegistry {
_Migration20260930122235111_cards(),
_Migration20260930145136285_reviews(),
_Migration20261001064926911_tree_tags_assets(),
_Migration20261002191122122_notes(),
];
/// List of all client-side database migration versions.

View file

@ -0,0 +1,725 @@
/* 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 '../studyset/studyset.dart' as _ie5k1jc2;
/// A note in a studyset: rich text the person writes and reads, never studied.
/// Its content lives in its revisions (NoteRevision), never in this row: content
/// is append-only (ADR 0001, rule 2), as a card's is. Synced; its space owns it.
abstract class Note
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
Note._({
this.id,
this.spaceId,
required this.studysetId,
this.studyset,
required this.position,
});
factory Note({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue studysetId,
_ie5k1jc2.Studyset? studyset,
required String position,
}) = _NoteImpl;
factory Note.fromJson(Map<String, dynamic> jsonSerialization) {
return Note(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
studysetId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['studysetId'],
),
studyset: jsonSerialization['studyset'] == null
? null
: _itx5j3f5.Protocol().deserialize<_ie5k1jc2.Studyset>(
jsonSerialization['studyset'],
),
position: jsonSerialization['position'] as String,
);
}
static final t = NoteTable();
static const db = NoteRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue studysetId;
/// The studyset the note belongs to; the note goes with it.
_ie5k1jc2.Studyset? studyset;
/// Where the note stands among its studyset's notes: a fractional index, as a
/// card's position; last writer wins.
String position;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [Note]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
Note copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? studysetId,
_ie5k1jc2.Studyset? studyset,
String? position,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'Note',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'studysetId': studysetId.toJson(),
if (studyset != null) 'studyset': studyset?.toJson(),
'position': position,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'Note',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'studysetId': studysetId.toJson(),
if (studyset != null) 'studyset': studyset?.toJsonForProtocol(),
'position': position,
};
}
static NoteInclude include({_ie5k1jc2.StudysetInclude? studyset}) {
return NoteInclude._(studyset: studyset);
}
static NoteIncludeList includeList({
_isd.WhereExpressionBuilder<NoteTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<NoteTable>? orderBy,
_isd.OrderByListBuilder<NoteTable>? orderByList,
NoteInclude? include,
}) {
return NoteIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(Note.t),
orderByList: orderByList?.call(Note.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _NoteImpl extends Note {
_NoteImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue studysetId,
_ie5k1jc2.Studyset? studyset,
required String position,
}) : super._(
id: id,
spaceId: spaceId,
studysetId: studysetId,
studyset: studyset,
position: position,
);
/// Returns a shallow copy of this [Note]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
Note copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? studysetId,
Object? studyset = _Undefined,
String? position,
}) {
return Note(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
studysetId: studysetId ?? this.studysetId,
studyset: studyset is _ie5k1jc2.Studyset?
? studyset
: this.studyset?.copyWith(),
position: position ?? this.position,
);
}
}
class NoteUpdateTable extends _isd.UpdateTable<NoteTable> {
NoteUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> studysetId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.studysetId,
value,
);
_isd.ColumnValue<String, String> position(String value) => _isd.ColumnValue(
table.position,
value,
);
}
class NoteTable extends _isd.Table<_isc.UuidValue?> {
NoteTable({super.tableRelation}) : super(tableName: 'note') {
updateTable = NoteUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
studysetId = _isd.ColumnUuid(
'studysetId',
this,
);
position = _isd.ColumnString(
'position',
this,
);
}
late final NoteUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid studysetId;
/// The studyset the note belongs to; the note goes with it.
_ie5k1jc2.StudysetTable? _studyset;
/// Where the note stands among its studyset's notes: a fractional index, as a
/// card's position; last writer wins.
late final _isd.ColumnString position;
_ie5k1jc2.StudysetTable get studyset {
if (_studyset != null) return _studyset!;
_studyset = _isd.createRelationTable(
relationFieldName: 'studyset',
field: Note.t.studysetId,
foreignField: _ie5k1jc2.Studyset.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_ie5k1jc2.StudysetTable(tableRelation: foreignTableRelation),
);
return _studyset!;
}
@override
List<_isd.Column> get columns => [
id,
spaceId,
studysetId,
position,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'studyset') {
return studyset;
}
return null;
}
}
class NoteInclude extends _isd.IncludeObject {
NoteInclude._({_ie5k1jc2.StudysetInclude? studyset}) {
_studyset = studyset;
}
_ie5k1jc2.StudysetInclude? _studyset;
@override
Map<String, _isd.Include?> get includes => {'studyset': _studyset};
@override
_isd.Table<_isc.UuidValue?> get table => Note.t;
}
class NoteIncludeList extends _isd.IncludeList {
NoteIncludeList._({
_isd.WhereExpressionBuilder<NoteTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(Note.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => Note.t;
}
class NoteRepository {
const NoteRepository._();
final attachRow = const NoteAttachRowRepository._();
/// Returns a list of [Note]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<Note>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<NoteTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<NoteTable>? orderBy,
_isd.OrderByListBuilder<NoteTable>? orderByList,
_isd.Transaction? transaction,
NoteInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<Note>(
where: where?.call(Note.t),
orderBy: orderBy?.call(Note.t),
orderByList: orderByList?.call(Note.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [Note] 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<Note?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<NoteTable>? where,
int? offset,
_isd.OrderByBuilder<NoteTable>? orderBy,
_isd.OrderByListBuilder<NoteTable>? orderByList,
_isd.Transaction? transaction,
NoteInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<Note>(
where: where?.call(Note.t),
orderBy: orderBy?.call(Note.t),
orderByList: orderByList?.call(Note.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [Note] by its [id] or null if no such row exists.
Future<Note?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
NoteInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<Note>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [Note]s in the list and returns the inserted rows.
///
/// The returned [Note]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<Note>> insert(
_isd.DatabaseSession session,
List<Note> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<Note>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [Note] and returns the inserted row.
///
/// The returned [Note] will have its `id` field set.
Future<Note> insertRow(
_isd.DatabaseSession session,
Note row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<Note>(
row,
transaction: transaction,
);
}
/// Upserts all [Note]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 [Note]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<Note>> upsert(
_isd.DatabaseSession session,
List<Note> rows, {
required _isd.ColumnSelections<NoteTable> conflictColumns,
_isd.ColumnSelections<NoteTable>? updateColumns,
_isd.WhereExpressionBuilder<NoteTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<Note>(
rows,
conflictColumns: conflictColumns(Note.t),
updateColumns: updateColumns?.call(Note.t),
updateWhere: updateWhere?.call(Note.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [Note] 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 [Note] will have its `id` field set.
Future<Note?> upsertRow(
_isd.DatabaseSession session,
Note row, {
required _isd.ColumnSelections<NoteTable> conflictColumns,
_isd.ColumnSelections<NoteTable>? updateColumns,
_isd.WhereExpressionBuilder<NoteTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<Note>(
row,
conflictColumns: conflictColumns(Note.t),
updateColumns: updateColumns?.call(Note.t),
updateWhere: updateWhere?.call(Note.t),
transaction: transaction,
);
}
/// Updates all [Note]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<Note>> update(
_isd.DatabaseSession session,
List<Note> rows, {
_isd.ColumnSelections<NoteTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<Note>(
rows,
columns: columns?.call(Note.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [Note]. 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<Note> updateRow(
_isd.DatabaseSession session,
Note row, {
_isd.ColumnSelections<NoteTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<Note>(
row,
columns: columns?.call(Note.t),
transaction: transaction,
);
}
/// Updates a single [Note] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<Note?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<NoteUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<Note>(
id,
columnValues: columnValues(Note.t.updateTable),
transaction: transaction,
);
}
/// Updates all [Note]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<Note>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<NoteUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<NoteTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<NoteTable>? orderBy,
_isd.OrderByListBuilder<NoteTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<Note>(
columnValues: columnValues(Note.t.updateTable),
where: where(Note.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(Note.t),
orderByList: orderByList?.call(Note.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [Note]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<Note>> delete(
_isd.DatabaseSession session,
List<Note> rows, {
_isd.OrderByBuilder<NoteTable>? orderBy,
_isd.OrderByListBuilder<NoteTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<Note>(
rows,
orderBy: orderBy?.call(Note.t),
orderByList: orderByList?.call(Note.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [Note].
Future<Note> deleteRow(
_isd.DatabaseSession session,
Note row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<Note>(
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<Note>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<NoteTable> where,
_isd.OrderByBuilder<NoteTable>? orderBy,
_isd.OrderByListBuilder<NoteTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<Note>(
where: where(Note.t),
orderBy: orderBy?.call(Note.t),
orderByList: orderByList?.call(Note.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<NoteTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<Note>(
where: where?.call(Note.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [Note] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<NoteTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<Note>(
where: where(Note.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class NoteAttachRowRepository {
const NoteAttachRowRepository._();
/// Creates a relation between the given [Note] and [Studyset]
/// by setting the [Note]'s foreign key `studysetId` to refer to the [Studyset].
Future<void> studyset(
_isd.DatabaseSession session,
Note note,
_ie5k1jc2.Studyset studyset, {
_isd.Transaction? transaction,
}) async {
if (note.id == null) {
throw ArgumentError.notNull('note.id');
}
if (studyset.id == null) {
throw ArgumentError.notNull('studyset.id');
}
var $note = note.copyWith(studysetId: studyset.id);
await session.db.updateRow<Note>(
$note,
columns: [Note.t.studysetId],
transaction: transaction,
);
}
}

View file

@ -0,0 +1,874 @@
/* 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 '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 '../note/note.dart' as _iu6jc5sx;
/// One version of a note's content, never changed once written (ADR 0001,
/// rule 2), merged as a card's revisions are (ADR 0003): editing appends a
/// revision whose parents are the versions the edit started from, and a note's
/// current versions are the revisions no other replaces. Deleting a note blanks
/// every revision's payload first (ADR 0002, rule 2).
abstract class NoteRevision
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
NoteRevision._({
this.id,
this.spaceId,
required this.noteId,
this.note,
required this.parents,
required this.authorId,
this.reportId,
this.keyId,
required this.payload,
this.nonce,
});
factory NoteRevision({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue noteId,
_iu6jc5sx.Note? note,
required List<_isc.UuidValue> parents,
required _isc.UuidValue authorId,
_isc.UuidValue? reportId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _NoteRevisionImpl;
factory NoteRevision.fromJson(Map<String, dynamic> jsonSerialization) {
return NoteRevision(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
noteId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['noteId']),
note: jsonSerialization['note'] == null
? null
: _itx5j3f5.Protocol().deserialize<_iu6jc5sx.Note>(
jsonSerialization['note'],
),
parents: _itx5j3f5.Protocol().deserialize<List<_isc.UuidValue>>(
jsonSerialization['parents'],
),
authorId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['authorId'],
),
reportId: jsonSerialization['reportId'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['reportId']),
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = NoteRevisionTable();
static const db = NoteRevisionRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue noteId;
/// The note this is a version of; its revisions go with it.
_iu6jc5sx.Note? note;
/// The revisions this one replaces: none for a note's first, several when it
/// resolves a conflict. Plain ids, since revisions can arrive in any order.
List<_isc.UuidValue> parents;
/// Who wrote it: the AuthUser's id (a synced table can't relate to the
/// server-only AuthUser).
_isc.UuidValue authorId;
/// The report this revision answers, once reports exist; null otherwise.
_isc.UuidValue? reportId;
/// The encryption key's id; always null (ADR 0002, rule 6).
String? keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "title", "body"}.
_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 [NoteRevision]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
NoteRevision copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? noteId,
_iu6jc5sx.Note? note,
List<_isc.UuidValue>? parents,
_isc.UuidValue? authorId,
_isc.UuidValue? reportId,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'NoteRevision',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'noteId': noteId.toJson(),
if (note != null) 'note': note?.toJson(),
'parents': parents.toJson(valueToJson: (v) => v.toJson()),
'authorId': authorId.toJson(),
if (reportId != null) 'reportId': reportId?.toJson(),
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'NoteRevision',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'noteId': noteId.toJson(),
if (note != null) 'note': note?.toJsonForProtocol(),
'parents': parents.toJson(valueToJson: (v) => v.toJson()),
'authorId': authorId.toJson(),
if (reportId != null) 'reportId': reportId?.toJson(),
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static NoteRevisionInclude include({_iu6jc5sx.NoteInclude? note}) {
return NoteRevisionInclude._(note: note);
}
static NoteRevisionIncludeList includeList({
_isd.WhereExpressionBuilder<NoteRevisionTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<NoteRevisionTable>? orderBy,
_isd.OrderByListBuilder<NoteRevisionTable>? orderByList,
NoteRevisionInclude? include,
}) {
return NoteRevisionIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(NoteRevision.t),
orderByList: orderByList?.call(NoteRevision.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _NoteRevisionImpl extends NoteRevision {
_NoteRevisionImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue noteId,
_iu6jc5sx.Note? note,
required List<_isc.UuidValue> parents,
required _isc.UuidValue authorId,
_isc.UuidValue? reportId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
noteId: noteId,
note: note,
parents: parents,
authorId: authorId,
reportId: reportId,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [NoteRevision]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
NoteRevision copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? noteId,
Object? note = _Undefined,
List<_isc.UuidValue>? parents,
_isc.UuidValue? authorId,
Object? reportId = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return NoteRevision(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
noteId: noteId ?? this.noteId,
note: note is _iu6jc5sx.Note? ? note : this.note?.copyWith(),
parents: parents ?? this.parents.map((e0) => e0).toList(),
authorId: authorId ?? this.authorId,
reportId: reportId is _isc.UuidValue? ? reportId : this.reportId,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class NoteRevisionUpdateTable extends _isd.UpdateTable<NoteRevisionTable> {
NoteRevisionUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> noteId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.noteId,
value,
);
_isd.ColumnValue<List<_isc.UuidValue>, List<_isc.UuidValue>> parents(
List<_isc.UuidValue> value,
) => _isd.ColumnValue(
table.parents,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> authorId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.authorId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> reportId(
_isc.UuidValue? value,
) => _isd.ColumnValue(
table.reportId,
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 NoteRevisionTable extends _isd.Table<_isc.UuidValue?> {
NoteRevisionTable({super.tableRelation}) : super(tableName: 'note_revision') {
updateTable = NoteRevisionUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
noteId = _isd.ColumnUuid(
'noteId',
this,
);
parents = _isd.ColumnSerializable<List<_isc.UuidValue>>(
'parents',
this,
);
authorId = _isd.ColumnUuid(
'authorId',
this,
);
reportId = _isd.ColumnUuid(
'reportId',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final NoteRevisionUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid noteId;
/// The note this is a version of; its revisions go with it.
_iu6jc5sx.NoteTable? _note;
/// The revisions this one replaces: none for a note's first, several when it
/// resolves a conflict. Plain ids, since revisions can arrive in any order.
late final _isd.ColumnSerializable<List<_isc.UuidValue>> parents;
/// Who wrote it: the AuthUser's id (a synced table can't relate to the
/// server-only AuthUser).
late final _isd.ColumnUuid authorId;
/// The report this revision answers, once reports exist; null otherwise.
late final _isd.ColumnUuid reportId;
/// The encryption key's id; always null (ADR 0002, rule 6).
late final _isd.ColumnString keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "title", "body"}.
late final _isd.ColumnByteData payload;
/// The encryption nonce; always null, like keyId.
late final _isd.ColumnByteData nonce;
_iu6jc5sx.NoteTable get note {
if (_note != null) return _note!;
_note = _isd.createRelationTable(
relationFieldName: 'note',
field: NoteRevision.t.noteId,
foreignField: _iu6jc5sx.Note.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_iu6jc5sx.NoteTable(tableRelation: foreignTableRelation),
);
return _note!;
}
@override
List<_isd.Column> get columns => [
id,
spaceId,
noteId,
parents,
authorId,
reportId,
keyId,
payload,
nonce,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'note') {
return note;
}
return null;
}
}
class NoteRevisionInclude extends _isd.IncludeObject {
NoteRevisionInclude._({_iu6jc5sx.NoteInclude? note}) {
_note = note;
}
_iu6jc5sx.NoteInclude? _note;
@override
Map<String, _isd.Include?> get includes => {'note': _note};
@override
_isd.Table<_isc.UuidValue?> get table => NoteRevision.t;
}
class NoteRevisionIncludeList extends _isd.IncludeList {
NoteRevisionIncludeList._({
_isd.WhereExpressionBuilder<NoteRevisionTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(NoteRevision.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => NoteRevision.t;
}
class NoteRevisionRepository {
const NoteRevisionRepository._();
final attachRow = const NoteRevisionAttachRowRepository._();
/// Returns a list of [NoteRevision]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<NoteRevision>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<NoteRevisionTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<NoteRevisionTable>? orderBy,
_isd.OrderByListBuilder<NoteRevisionTable>? orderByList,
_isd.Transaction? transaction,
NoteRevisionInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<NoteRevision>(
where: where?.call(NoteRevision.t),
orderBy: orderBy?.call(NoteRevision.t),
orderByList: orderByList?.call(NoteRevision.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [NoteRevision] 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<NoteRevision?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<NoteRevisionTable>? where,
int? offset,
_isd.OrderByBuilder<NoteRevisionTable>? orderBy,
_isd.OrderByListBuilder<NoteRevisionTable>? orderByList,
_isd.Transaction? transaction,
NoteRevisionInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<NoteRevision>(
where: where?.call(NoteRevision.t),
orderBy: orderBy?.call(NoteRevision.t),
orderByList: orderByList?.call(NoteRevision.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [NoteRevision] by its [id] or null if no such row exists.
Future<NoteRevision?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
NoteRevisionInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<NoteRevision>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [NoteRevision]s in the list and returns the inserted rows.
///
/// The returned [NoteRevision]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<NoteRevision>> insert(
_isd.DatabaseSession session,
List<NoteRevision> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<NoteRevision>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [NoteRevision] and returns the inserted row.
///
/// The returned [NoteRevision] will have its `id` field set.
Future<NoteRevision> insertRow(
_isd.DatabaseSession session,
NoteRevision row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<NoteRevision>(
row,
transaction: transaction,
);
}
/// Upserts all [NoteRevision]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 [NoteRevision]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<NoteRevision>> upsert(
_isd.DatabaseSession session,
List<NoteRevision> rows, {
required _isd.ColumnSelections<NoteRevisionTable> conflictColumns,
_isd.ColumnSelections<NoteRevisionTable>? updateColumns,
_isd.WhereExpressionBuilder<NoteRevisionTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<NoteRevision>(
rows,
conflictColumns: conflictColumns(NoteRevision.t),
updateColumns: updateColumns?.call(NoteRevision.t),
updateWhere: updateWhere?.call(NoteRevision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [NoteRevision] 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 [NoteRevision] will have its `id` field set.
Future<NoteRevision?> upsertRow(
_isd.DatabaseSession session,
NoteRevision row, {
required _isd.ColumnSelections<NoteRevisionTable> conflictColumns,
_isd.ColumnSelections<NoteRevisionTable>? updateColumns,
_isd.WhereExpressionBuilder<NoteRevisionTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<NoteRevision>(
row,
conflictColumns: conflictColumns(NoteRevision.t),
updateColumns: updateColumns?.call(NoteRevision.t),
updateWhere: updateWhere?.call(NoteRevision.t),
transaction: transaction,
);
}
/// Updates all [NoteRevision]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<NoteRevision>> update(
_isd.DatabaseSession session,
List<NoteRevision> rows, {
_isd.ColumnSelections<NoteRevisionTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<NoteRevision>(
rows,
columns: columns?.call(NoteRevision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [NoteRevision]. 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<NoteRevision> updateRow(
_isd.DatabaseSession session,
NoteRevision row, {
_isd.ColumnSelections<NoteRevisionTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<NoteRevision>(
row,
columns: columns?.call(NoteRevision.t),
transaction: transaction,
);
}
/// Updates a single [NoteRevision] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<NoteRevision?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<NoteRevisionUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<NoteRevision>(
id,
columnValues: columnValues(NoteRevision.t.updateTable),
transaction: transaction,
);
}
/// Updates all [NoteRevision]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<NoteRevision>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<NoteRevisionUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<NoteRevisionTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<NoteRevisionTable>? orderBy,
_isd.OrderByListBuilder<NoteRevisionTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<NoteRevision>(
columnValues: columnValues(NoteRevision.t.updateTable),
where: where(NoteRevision.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(NoteRevision.t),
orderByList: orderByList?.call(NoteRevision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [NoteRevision]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<NoteRevision>> delete(
_isd.DatabaseSession session,
List<NoteRevision> rows, {
_isd.OrderByBuilder<NoteRevisionTable>? orderBy,
_isd.OrderByListBuilder<NoteRevisionTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<NoteRevision>(
rows,
orderBy: orderBy?.call(NoteRevision.t),
orderByList: orderByList?.call(NoteRevision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [NoteRevision].
Future<NoteRevision> deleteRow(
_isd.DatabaseSession session,
NoteRevision row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<NoteRevision>(
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<NoteRevision>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<NoteRevisionTable> where,
_isd.OrderByBuilder<NoteRevisionTable>? orderBy,
_isd.OrderByListBuilder<NoteRevisionTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<NoteRevision>(
where: where(NoteRevision.t),
orderBy: orderBy?.call(NoteRevision.t),
orderByList: orderByList?.call(NoteRevision.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<NoteRevisionTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<NoteRevision>(
where: where?.call(NoteRevision.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [NoteRevision] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<NoteRevisionTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<NoteRevision>(
where: where(NoteRevision.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class NoteRevisionAttachRowRepository {
const NoteRevisionAttachRowRepository._();
/// Creates a relation between the given [NoteRevision] and [Note]
/// by setting the [NoteRevision]'s foreign key `noteId` to refer to the [Note].
Future<void> note(
_isd.DatabaseSession session,
NoteRevision noteRevision,
_iu6jc5sx.Note note, {
_isd.Transaction? transaction,
}) async {
if (noteRevision.id == null) {
throw ArgumentError.notNull('noteRevision.id');
}
if (note.id == null) {
throw ArgumentError.notNull('note.id');
}
var $noteRevision = noteRevision.copyWith(noteId: note.id);
await session.db.updateRow<NoteRevision>(
$noteRevision,
columns: [NoteRevision.t.noteId],
transaction: transaction,
);
}
}

View file

@ -32,10 +32,13 @@ import 'card/card.dart' as _ipiw1cmx;
import 'card/card_revision.dart' as _ipbyuef1;
import 'common/not_found_exception.dart' as _i8oee73b;
import 'instance/instance_info.dart' as _iskrg2g3;
import 'note/note.dart' as _i9njjhyv;
import 'note/note_revision.dart' as _i6j5dng4;
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/note_tag.dart' as _itkfn57x;
import 'tag/tag.dart' as _ie8t3ar4;
export 'asset/asset.dart';
export 'asset/file_allowance.dart';
@ -48,10 +51,13 @@ export 'card/card.dart';
export 'card/card_revision.dart';
export 'common/not_found_exception.dart';
export 'instance/instance_info.dart';
export 'note/note.dart';
export 'note/note_revision.dart';
export 'review/review_log.dart';
export 'settings/person_settings.dart';
export 'studyset/studyset.dart';
export 'tag/card_tag.dart';
export 'tag/note_tag.dart';
export 'tag/tag.dart';
export 'client.dart';
export 'sync_tables.dart';
@ -479,6 +485,319 @@ class Protocol extends _isd.DatabaseSerializationManager {
],
managed: true,
),
_isd.TableDefinition(
name: 'note',
dartName: 'Note',
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: 'studysetId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'position',
columnType: _isd.ColumnType.text,
isNullable: false,
dartType: 'String',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'note_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: 'note_fk_1',
columns: ['studysetId'],
referenceTable: 'studyset',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'note_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'note_studyset_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'studysetId',
),
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'position',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'note_revision',
dartName: 'NoteRevision',
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: 'noteId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'parents',
columnType: _isd.ColumnType.json,
isNullable: false,
dartType: 'List<UuidValue>',
),
_isd.ColumnDefinition(
name: 'authorId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'reportId',
columnType: _isd.ColumnType.uuid,
isNullable: true,
dartType: 'UuidValue?',
),
_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: 'note_revision_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: 'note_revision_fk_1',
columns: ['noteId'],
referenceTable: 'note',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'note_revision_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'note_revision_note_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'noteId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'note_tag',
dartName: 'NoteTag',
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: 'noteId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'tagId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'note_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: 'note_tag_fk_1',
columns: ['noteId'],
referenceTable: 'note',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
_isd.ForeignKeyDefinition(
constraintName: 'note_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: 'note_tag_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'note_tag_note_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'noteId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'note_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',
@ -921,6 +1240,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _iskrg2g3.InstanceInfo) {
return _iskrg2g3.InstanceInfo.fromJson(data) as T;
}
if (t == _i9njjhyv.Note) {
return _i9njjhyv.Note.fromJson(data) as T;
}
if (t == _i6j5dng4.NoteRevision) {
return _i6j5dng4.NoteRevision.fromJson(data) as T;
}
if (t == _irzjbdqz.ReviewLog) {
return _irzjbdqz.ReviewLog.fromJson(data) as T;
}
@ -933,6 +1258,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _i6jz6nyq.CardTag) {
return _i6jz6nyq.CardTag.fromJson(data) as T;
}
if (t == _itkfn57x.NoteTag) {
return _itkfn57x.NoteTag.fromJson(data) as T;
}
if (t == _ie8t3ar4.Tag) {
return _ie8t3ar4.Tag.fromJson(data) as T;
}
@ -980,6 +1308,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _isc.getType<_iskrg2g3.InstanceInfo?>()) {
return (data != null ? _iskrg2g3.InstanceInfo.fromJson(data) : null) as T;
}
if (t == _isc.getType<_i9njjhyv.Note?>()) {
return (data != null ? _i9njjhyv.Note.fromJson(data) : null) as T;
}
if (t == _isc.getType<_i6j5dng4.NoteRevision?>()) {
return (data != null ? _i6j5dng4.NoteRevision.fromJson(data) : null) as T;
}
if (t == _isc.getType<_irzjbdqz.ReviewLog?>()) {
return (data != null ? _irzjbdqz.ReviewLog.fromJson(data) : null) as T;
}
@ -993,6 +1327,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _isc.getType<_i6jz6nyq.CardTag?>()) {
return (data != null ? _i6jz6nyq.CardTag.fromJson(data) : null) as T;
}
if (t == _isc.getType<_itkfn57x.NoteTag?>()) {
return (data != null ? _itkfn57x.NoteTag.fromJson(data) : null) as T;
}
if (t == _isc.getType<_ie8t3ar4.Tag?>()) {
return (data != null ? _ie8t3ar4.Tag.fromJson(data) : null) as T;
}
@ -1031,10 +1368,13 @@ class Protocol extends _isd.DatabaseSerializationManager {
_ipbyuef1.CardRevision => 'CardRevision',
_i8oee73b.NotFoundException => 'NotFoundException',
_iskrg2g3.InstanceInfo => 'InstanceInfo',
_i9njjhyv.Note => 'Note',
_i6j5dng4.NoteRevision => 'NoteRevision',
_irzjbdqz.ReviewLog => 'ReviewLog',
_irnhx5e5.PersonSettings => 'PersonSettings',
_ikbclmae.Studyset => 'Studyset',
_i6jz6nyq.CardTag => 'CardTag',
_itkfn57x.NoteTag => 'NoteTag',
_ie8t3ar4.Tag => 'Tag',
_ => null,
};
@ -1075,6 +1415,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'NotFoundException';
case _iskrg2g3.InstanceInfo():
return 'InstanceInfo';
case _i9njjhyv.Note():
return 'Note';
case _i6j5dng4.NoteRevision():
return 'NoteRevision';
case _irzjbdqz.ReviewLog():
return 'ReviewLog';
case _irnhx5e5.PersonSettings():
@ -1083,6 +1427,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'Studyset';
case _i6jz6nyq.CardTag():
return 'CardTag';
case _itkfn57x.NoteTag():
return 'NoteTag';
case _ie8t3ar4.Tag():
return 'Tag';
}
@ -1146,6 +1492,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'InstanceInfo') {
return deserialize<_iskrg2g3.InstanceInfo>(data['data']);
}
if (dataClassName == 'Note') {
return deserialize<_i9njjhyv.Note>(data['data']);
}
if (dataClassName == 'NoteRevision') {
return deserialize<_i6j5dng4.NoteRevision>(data['data']);
}
if (dataClassName == 'ReviewLog') {
return deserialize<_irzjbdqz.ReviewLog>(data['data']);
}
@ -1158,6 +1510,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'CardTag') {
return deserialize<_i6jz6nyq.CardTag>(data['data']);
}
if (dataClassName == 'NoteTag') {
return deserialize<_itkfn57x.NoteTag>(data['data']);
}
if (dataClassName == 'Tag') {
return deserialize<_ie8t3ar4.Tag>(data['data']);
}
@ -1218,6 +1573,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
return _ipiw1cmx.Card.t;
case _ipbyuef1.CardRevision:
return _ipbyuef1.CardRevision.t;
case _i9njjhyv.Note:
return _i9njjhyv.Note.t;
case _i6j5dng4.NoteRevision:
return _i6j5dng4.NoteRevision.t;
case _irzjbdqz.ReviewLog:
return _irzjbdqz.ReviewLog.t;
case _irnhx5e5.PersonSettings:
@ -1226,6 +1585,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
return _ikbclmae.Studyset.t;
case _i6jz6nyq.CardTag:
return _i6jz6nyq.CardTag.t;
case _itkfn57x.NoteTag:
return _itkfn57x.NoteTag.t;
case _ie8t3ar4.Tag:
return _ie8t3ar4.Tag.t;
}

View file

@ -14,10 +14,13 @@ 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 'note/note.dart' as _i9njjhyv;
import 'note/note_revision.dart' as _i6j5dng4;
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/note_tag.dart' as _itkfn57x;
import 'tag/tag.dart' as _ie8t3ar4;
/// The tables synchronized between client and server by the
@ -28,6 +31,9 @@ final List<_isd.Table> syncTables = [
_ipiw1cmx.Card.t,
_ipbyuef1.CardRevision.t,
_i6jz6nyq.CardTag.t,
_i9njjhyv.Note.t,
_i6j5dng4.NoteRevision.t,
_itkfn57x.NoteTag.t,
_irnhx5e5.PersonSettings.t,
_irzjbdqz.ReviewLog.t,
_ikbclmae.Studyset.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 '../note/note.dart' as _iu6jc5sx;
import '../tag/tag.dart' as _isqed5gg;
/// A note having a tag, as CardTag is for cards (ADR 0004): one row per
/// assignment, never updated, so concurrent edits merge without losing either;
/// a note with the same tag twice counts once. Notes and cards share the
/// person's tags. Synced; goes with its note and its tag.
abstract class NoteTag
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
NoteTag._({
this.id,
this.spaceId,
required this.noteId,
this.note,
required this.tagId,
this.tag,
});
factory NoteTag({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue noteId,
_iu6jc5sx.Note? note,
required _isc.UuidValue tagId,
_isqed5gg.Tag? tag,
}) = _NoteTagImpl;
factory NoteTag.fromJson(Map<String, dynamic> jsonSerialization) {
return NoteTag(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
noteId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['noteId']),
note: jsonSerialization['note'] == null
? null
: _itx5j3f5.Protocol().deserialize<_iu6jc5sx.Note>(
jsonSerialization['note'],
),
tagId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['tagId']),
tag: jsonSerialization['tag'] == null
? null
: _itx5j3f5.Protocol().deserialize<_isqed5gg.Tag>(
jsonSerialization['tag'],
),
);
}
static final t = NoteTagTable();
static const db = NoteTagRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue noteId;
/// The note tagged.
_iu6jc5sx.Note? note;
_isc.UuidValue tagId;
/// The tag it has.
_isqed5gg.Tag? tag;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [NoteTag]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
NoteTag copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? noteId,
_iu6jc5sx.Note? note,
_isc.UuidValue? tagId,
_isqed5gg.Tag? tag,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'NoteTag',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'noteId': noteId.toJson(),
if (note != null) 'note': note?.toJson(),
'tagId': tagId.toJson(),
if (tag != null) 'tag': tag?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'NoteTag',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'noteId': noteId.toJson(),
if (note != null) 'note': note?.toJsonForProtocol(),
'tagId': tagId.toJson(),
if (tag != null) 'tag': tag?.toJsonForProtocol(),
};
}
static NoteTagInclude include({
_iu6jc5sx.NoteInclude? note,
_isqed5gg.TagInclude? tag,
}) {
return NoteTagInclude._(
note: note,
tag: tag,
);
}
static NoteTagIncludeList includeList({
_isd.WhereExpressionBuilder<NoteTagTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<NoteTagTable>? orderBy,
_isd.OrderByListBuilder<NoteTagTable>? orderByList,
NoteTagInclude? include,
}) {
return NoteTagIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(NoteTag.t),
orderByList: orderByList?.call(NoteTag.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _NoteTagImpl extends NoteTag {
_NoteTagImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue noteId,
_iu6jc5sx.Note? note,
required _isc.UuidValue tagId,
_isqed5gg.Tag? tag,
}) : super._(
id: id,
spaceId: spaceId,
noteId: noteId,
note: note,
tagId: tagId,
tag: tag,
);
/// Returns a shallow copy of this [NoteTag]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
NoteTag copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? noteId,
Object? note = _Undefined,
_isc.UuidValue? tagId,
Object? tag = _Undefined,
}) {
return NoteTag(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
noteId: noteId ?? this.noteId,
note: note is _iu6jc5sx.Note? ? note : this.note?.copyWith(),
tagId: tagId ?? this.tagId,
tag: tag is _isqed5gg.Tag? ? tag : this.tag?.copyWith(),
);
}
}
class NoteTagUpdateTable extends _isd.UpdateTable<NoteTagTable> {
NoteTagUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> noteId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.noteId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> tagId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.tagId,
value,
);
}
class NoteTagTable extends _isd.Table<_isc.UuidValue?> {
NoteTagTable({super.tableRelation}) : super(tableName: 'note_tag') {
updateTable = NoteTagUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
noteId = _isd.ColumnUuid(
'noteId',
this,
);
tagId = _isd.ColumnUuid(
'tagId',
this,
);
}
late final NoteTagUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid noteId;
/// The note tagged.
_iu6jc5sx.NoteTable? _note;
late final _isd.ColumnUuid tagId;
/// The tag it has.
_isqed5gg.TagTable? _tag;
_iu6jc5sx.NoteTable get note {
if (_note != null) return _note!;
_note = _isd.createRelationTable(
relationFieldName: 'note',
field: NoteTag.t.noteId,
foreignField: _iu6jc5sx.Note.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_iu6jc5sx.NoteTable(tableRelation: foreignTableRelation),
);
return _note!;
}
_isqed5gg.TagTable get tag {
if (_tag != null) return _tag!;
_tag = _isd.createRelationTable(
relationFieldName: 'tag',
field: NoteTag.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,
noteId,
tagId,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'note') {
return note;
}
if (relationField == 'tag') {
return tag;
}
return null;
}
}
class NoteTagInclude extends _isd.IncludeObject {
NoteTagInclude._({
_iu6jc5sx.NoteInclude? note,
_isqed5gg.TagInclude? tag,
}) {
_note = note;
_tag = tag;
}
_iu6jc5sx.NoteInclude? _note;
_isqed5gg.TagInclude? _tag;
@override
Map<String, _isd.Include?> get includes => {
'note': _note,
'tag': _tag,
};
@override
_isd.Table<_isc.UuidValue?> get table => NoteTag.t;
}
class NoteTagIncludeList extends _isd.IncludeList {
NoteTagIncludeList._({
_isd.WhereExpressionBuilder<NoteTagTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(NoteTag.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => NoteTag.t;
}
class NoteTagRepository {
const NoteTagRepository._();
final attachRow = const NoteTagAttachRowRepository._();
/// Returns a list of [NoteTag]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<NoteTag>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<NoteTagTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<NoteTagTable>? orderBy,
_isd.OrderByListBuilder<NoteTagTable>? orderByList,
_isd.Transaction? transaction,
NoteTagInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<NoteTag>(
where: where?.call(NoteTag.t),
orderBy: orderBy?.call(NoteTag.t),
orderByList: orderByList?.call(NoteTag.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [NoteTag] 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<NoteTag?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<NoteTagTable>? where,
int? offset,
_isd.OrderByBuilder<NoteTagTable>? orderBy,
_isd.OrderByListBuilder<NoteTagTable>? orderByList,
_isd.Transaction? transaction,
NoteTagInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<NoteTag>(
where: where?.call(NoteTag.t),
orderBy: orderBy?.call(NoteTag.t),
orderByList: orderByList?.call(NoteTag.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [NoteTag] by its [id] or null if no such row exists.
Future<NoteTag?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
NoteTagInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<NoteTag>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [NoteTag]s in the list and returns the inserted rows.
///
/// The returned [NoteTag]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<NoteTag>> insert(
_isd.DatabaseSession session,
List<NoteTag> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<NoteTag>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [NoteTag] and returns the inserted row.
///
/// The returned [NoteTag] will have its `id` field set.
Future<NoteTag> insertRow(
_isd.DatabaseSession session,
NoteTag row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<NoteTag>(
row,
transaction: transaction,
);
}
/// Upserts all [NoteTag]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 [NoteTag]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<NoteTag>> upsert(
_isd.DatabaseSession session,
List<NoteTag> rows, {
required _isd.ColumnSelections<NoteTagTable> conflictColumns,
_isd.ColumnSelections<NoteTagTable>? updateColumns,
_isd.WhereExpressionBuilder<NoteTagTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<NoteTag>(
rows,
conflictColumns: conflictColumns(NoteTag.t),
updateColumns: updateColumns?.call(NoteTag.t),
updateWhere: updateWhere?.call(NoteTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [NoteTag] 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 [NoteTag] will have its `id` field set.
Future<NoteTag?> upsertRow(
_isd.DatabaseSession session,
NoteTag row, {
required _isd.ColumnSelections<NoteTagTable> conflictColumns,
_isd.ColumnSelections<NoteTagTable>? updateColumns,
_isd.WhereExpressionBuilder<NoteTagTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<NoteTag>(
row,
conflictColumns: conflictColumns(NoteTag.t),
updateColumns: updateColumns?.call(NoteTag.t),
updateWhere: updateWhere?.call(NoteTag.t),
transaction: transaction,
);
}
/// Updates all [NoteTag]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<NoteTag>> update(
_isd.DatabaseSession session,
List<NoteTag> rows, {
_isd.ColumnSelections<NoteTagTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<NoteTag>(
rows,
columns: columns?.call(NoteTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [NoteTag]. 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<NoteTag> updateRow(
_isd.DatabaseSession session,
NoteTag row, {
_isd.ColumnSelections<NoteTagTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<NoteTag>(
row,
columns: columns?.call(NoteTag.t),
transaction: transaction,
);
}
/// Updates a single [NoteTag] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<NoteTag?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<NoteTagUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<NoteTag>(
id,
columnValues: columnValues(NoteTag.t.updateTable),
transaction: transaction,
);
}
/// Updates all [NoteTag]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<NoteTag>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<NoteTagUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<NoteTagTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<NoteTagTable>? orderBy,
_isd.OrderByListBuilder<NoteTagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<NoteTag>(
columnValues: columnValues(NoteTag.t.updateTable),
where: where(NoteTag.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(NoteTag.t),
orderByList: orderByList?.call(NoteTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [NoteTag]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<NoteTag>> delete(
_isd.DatabaseSession session,
List<NoteTag> rows, {
_isd.OrderByBuilder<NoteTagTable>? orderBy,
_isd.OrderByListBuilder<NoteTagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<NoteTag>(
rows,
orderBy: orderBy?.call(NoteTag.t),
orderByList: orderByList?.call(NoteTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [NoteTag].
Future<NoteTag> deleteRow(
_isd.DatabaseSession session,
NoteTag row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<NoteTag>(
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<NoteTag>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<NoteTagTable> where,
_isd.OrderByBuilder<NoteTagTable>? orderBy,
_isd.OrderByListBuilder<NoteTagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<NoteTag>(
where: where(NoteTag.t),
orderBy: orderBy?.call(NoteTag.t),
orderByList: orderByList?.call(NoteTag.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<NoteTagTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<NoteTag>(
where: where?.call(NoteTag.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [NoteTag] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<NoteTagTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<NoteTag>(
where: where(NoteTag.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class NoteTagAttachRowRepository {
const NoteTagAttachRowRepository._();
/// Creates a relation between the given [NoteTag] and [Note]
/// by setting the [NoteTag]'s foreign key `noteId` to refer to the [Note].
Future<void> note(
_isd.DatabaseSession session,
NoteTag noteTag,
_iu6jc5sx.Note note, {
_isd.Transaction? transaction,
}) async {
if (noteTag.id == null) {
throw ArgumentError.notNull('noteTag.id');
}
if (note.id == null) {
throw ArgumentError.notNull('note.id');
}
var $noteTag = noteTag.copyWith(noteId: note.id);
await session.db.updateRow<NoteTag>(
$noteTag,
columns: [NoteTag.t.noteId],
transaction: transaction,
);
}
/// Creates a relation between the given [NoteTag] and [Tag]
/// by setting the [NoteTag]'s foreign key `tagId` to refer to the [Tag].
Future<void> tag(
_isd.DatabaseSession session,
NoteTag noteTag,
_isqed5gg.Tag tag, {
_isd.Transaction? transaction,
}) async {
if (noteTag.id == null) {
throw ArgumentError.notNull('noteTag.id');
}
if (tag.id == null) {
throw ArgumentError.notNull('tag.id');
}
var $noteTag = noteTag.copyWith(tagId: tag.id);
await session.db.updateRow<NoteTag>(
$noteTag,
columns: [NoteTag.t.tagId],
transaction: transaction,
);
}
}

View file

@ -0,0 +1,91 @@
import 'dart:async';
import 'dart:developer';
/// Deletions that can be undone for a moment (decided 2026-10-01): what is
/// deleted leaves every list at once ([pending]), and only once the time to
/// undo is over does [_deleteNow] run, since blanking can't be undone (ADR
/// 0002, rule 2).
///
/// It belongs to the person's data, so a deletion outlives the screen it
/// started on, and it closes with the person's data: a deletion still
/// waiting then is dropped, and what it would have deleted stays.
class DelayedDeletions(
/// Deletes an item, given its id, for good.
final Future<void> Function(String id) _deleteNow, {
/// Names the log a failed deletion goes to.
final String _logName = 'Deletions',
/// How long a deletion can be undone: the three seconds the message
/// offering it shows, and a moment for a late tap.
final Duration _undoWindow = const Duration(seconds: 4),
}) {
/// The deletions that can still be undone, by the item's id.
final _waiting = <String, Timer>{};
/// The deletions under way, too late to undo.
final _deleting = <String, Future<void>>{};
final _changes = StreamController<Set<String>>.broadcast();
/// The items whose deletion waits or is under way: lists leave them out.
Set<String> get pending => {..._waiting.keys, ..._deleting.keys};
/// [pending], now and after every change.
Stream<Set<String>> watchPending() => Stream.multi((listener) {
listener.add(pending);
final subscription = _changes.stream.listen(listener.add);
listener.onCancel = subscription.cancel;
});
/// Deletes the item [id] once the time to undo that is over.
void delete(String id) {
if (_deleting.containsKey(id)) {
return;
}
_waiting[id]?.cancel();
_waiting[id] = Timer(_undoWindow, () => _start(id));
_changed();
}
/// Keeps the item [id] after all, while its deletion still waits.
void undo(String id) {
if (_waiting.remove(id) case final timer?) {
timer.cancel();
_changed();
}
}
/// Drops the deletions still waiting, and lets those under way finish.
Future<void> dispose() async {
for (final timer in _waiting.values) {
timer.cancel();
}
_waiting.clear();
await Future.wait(_deleting.values);
await _changes.close();
}
void _start(String id) {
_waiting.remove(id);
_deleting[id] = _guarded(id).whenComplete(() {
_deleting.remove(id);
_changed();
});
}
Future<void> _guarded(String id) async {
try {
await _deleteNow(id);
} on Object catch (error) {
// What is left shows again; deleting it once more retries.
log('Deleting $id failed', name: _logName, error: error);
}
}
void _changed() {
if (!_changes.isClosed) {
_changes.add(pending);
}
}
}

View file

@ -21,6 +21,9 @@ import 'package:halkyon_learn_flutter/src/feature/auth/model/auth_status.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/sync_card_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/sync_note_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/sync_settings_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
@ -153,13 +156,18 @@ Future<PersonDependencies> _openPerson(
final cards = CardRepository(
SyncCardGateway(db, sync, person.authUserId),
);
final notes = NoteRepository(
SyncNoteGateway(db, sync, person.authUserId),
);
final tags = TagRepository(
SyncTagGateway(db, sync),
cardChanges: cards.changes,
noteChanges: notes.changes,
);
final settings = SettingsRepository(SyncSettingsGateway(db, sync));
final reviews = ReviewRepository(SyncReviewGateway(db, sync));
final deletions = StudysetDeletions(studysets, cards);
final deletions = StudysetDeletions(studysets, cards, notes);
final noteDeletions = NoteDeletions(notes);
final assets = AssetRepository(
SyncAssetGateway(db, sync),
ServerpodAssetServerGateway(client),
@ -170,21 +178,25 @@ Future<PersonDependencies> _openPerson(
return PersonDependencies(
studysets: studysets,
cards: cards,
notes: notes,
reviews: reviews,
tags: tags,
settings: settings,
assets: assets,
deletions: deletions,
noteDeletions: noteDeletions,
onDispose: () async {
try {
// Deletions first: those still waiting are dropped, those under way
// finish while their repositories are open.
await deletions.dispose();
await noteDeletions.dispose();
await studysets.dispose();
await tags.dispose();
await settings.dispose();
await reviews.dispose();
await cards.dispose();
await notes.dispose();
await assets.dispose();
await sync.dispose();
await db.close();

View file

@ -1,5 +1,7 @@
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
@ -18,10 +20,13 @@ class PersonDependencies({
/// The person's flashcards.
required final CardRepository cards,
/// The person's notes.
required final NoteRepository notes,
/// The person's reviews.
required final ReviewRepository reviews,
/// The person's tags and which cards have them.
/// The person's tags and which cards and notes have them.
required final TagRepository tags,
/// The person's settings, which follow them to every device.
@ -32,6 +37,9 @@ class PersonDependencies({
/// The studyset deletions that can still be undone.
required final StudysetDeletions deletions,
/// The note deletions that can still be undone.
required final NoteDeletions noteDeletions,
final Future<void> Function()? _onDispose,
}) {
/// Closes what was opened for the person.

View file

@ -0,0 +1,10 @@
import 'package:halkyon_learn_flutter/src/core/async/delayed_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
/// The person's note deletions, which can be undone for a moment (see
/// [DelayedDeletions]). Once the time to undo is over, the note is blanked
/// and deleted.
class NoteDeletions(NoteRepository notes, {super.undoWindow})
extends DelayedDeletions {
this : super(notes.delete, logName: 'Notes');
}

View file

@ -0,0 +1,54 @@
import 'dart:typed_data';
/// A note's row as the device's database stores it.
typedef NoteRow = ({String id, String position});
/// A note wherever it is: its id and its studyset.
typedef NoteStampRow = ({String id, String studysetId});
/// A note revision's row as the device's database stores it, with when it
/// was written (from its UUID v7 id; null for an id that doesn't say).
typedef NoteRevisionRow = ({
String id,
String noteId,
Set<String> parents,
ByteData payload,
DateTime? writtenAt,
});
/// The boundary to the notes in this person's local, synced database. Tests
/// replace it with a fake.
abstract interface class NoteGateway() {
/// The notes of the studyset [studysetId] on this device, unordered.
Future<List<NoteRow>> notesOf(String studysetId);
/// Every note of the person on this device, unordered.
Future<List<NoteStampRow>> allNotes();
/// The revisions of the notes [noteIds], unordered.
Future<List<NoteRevisionRow>> revisionsOf(Set<String> noteIds);
/// Stores a new note with its first revision; returns both ids.
Future<({String noteId, String revisionId})> insertNote({
required String studysetId,
required String position,
required ByteData payload,
});
/// Appends a revision to the note [noteId] that replaces [parents];
/// returns its id.
Future<String> insertRevision({
required String noteId,
required Set<String> parents,
required ByteData payload,
});
/// Blanks the payload of every revision of the note [noteId].
Future<void> blankRevisions(String noteId);
/// Deletes the note [noteId], its revisions and its tags.
Future<void> deleteNote(String noteId);
/// Fires when a sync brought changes from the person's other devices.
Stream<void> get remoteChanges;
}

View file

@ -0,0 +1,33 @@
import 'dart:typed_data';
import 'package:halkyon_learn_flutter/src/core/content/payload.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.dart';
// A note revision's content as its synced row stores it (see
// core/content/payload). Schema 1: the title and the body.
/// The schema this app writes.
const noteSchemaVersion = 1;
/// Encodes a note's content into a revision's payload.
ByteData encodeNotePayload(NoteContent content) =>
encodePayload(noteSchemaVersion, {
'title': content.title,
'body': content.body.toJson(),
});
/// Decodes a note revision's payload.
///
/// Throws a [FormatException] for anything else, including content written
/// by a newer app version in a schema this one doesn't know.
NoteContent decodeNotePayload(ByteData payload) {
final json = decodePayload(payload, newestSchema: noteSchemaVersion);
if (json case {
'title': final String title,
'body': final Map<String, Object?> body,
}) {
return NoteContent(title: title, body: RichContent.fromJson(body));
}
throw const FormatException('Not a note payload.');
}

View file

@ -0,0 +1,193 @@
import 'dart:async';
import 'dart:developer';
import 'package:fractional_indexing_dart/fractional_indexing_dart.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note_merge.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note_revision.dart';
/// The person's notes: content kept as append-only revisions (ADR 0001,
/// rule 2), merged title and body apart when devices edited offline (ADR
/// 0003), ordered by a fractional index.
class NoteRepository(final NoteGateway _gateway) {
this {
_remoteChanges = _gateway.remoteChanges.listen((_) => _changes.add(null));
}
final _changes = StreamController<void>.broadcast();
late final StreamSubscription<void> _remoteChanges;
/// Fires after every change of any note, made on this device or synced
/// from another.
Stream<void> get changes => _changes.stream;
/// The notes of [studysetId] in order, now and after every change, local
/// or synced. A read that fails arrives as an error; the next change reads
/// again.
Stream<List<Note>> watch(String studysetId) => Stream.multi((listener) {
var cancelled = false;
var reading = Future<void>.value();
// One read after another, so an older list never overtakes a newer one.
void read() => reading = reading.then((_) async {
if (cancelled) {
return;
}
try {
final notes = await notesOf(studysetId);
if (!cancelled) {
listener.add(notes);
}
} on Object catch (error, st) {
log('Reading notes failed', name: 'Notes', error: error);
if (!cancelled) {
listener.addError(error, st);
}
}
});
final subscription = _changes.stream.listen((_) => read());
listener.onCancel = () {
cancelled = true;
return subscription.cancel();
};
read();
});
/// The notes of [studysetId] on this device right now, in order, read
/// once; [watch] follows every change.
Future<List<Note>> notesOf(String studysetId) async {
final rows = await _gateway.notesOf(studysetId);
final revisionsByNote = <String, List<NoteRevision>>{};
for (final row in await _gateway.revisionsOf({
for (final note in rows) note.id,
})) {
if (_decode(row) case final revision?) {
(revisionsByNote[row.noteId] ??= []).add(revision);
}
}
return [
for (final row in rows)
if (revisionsByNote[row.id] case final revisions?)
if (mergeNoteRevisions(revisions) case final merged?)
Note(
id: row.id,
position: row.position,
content: merged.content,
heads: merged.heads,
conflicts: merged.conflicts,
editedAt: _newestOf(revisions, merged.heads)?.writtenAt,
),
]..sort(
(a, b) => switch (a.position.compareTo(b.position)) {
0 => a.id.compareTo(b.id),
final order => order,
},
);
}
/// How many notes each studyset holds, by the studyset's id; studysets
/// without notes are left out.
Future<Map<String, int>> countsByStudyset() async {
final counts = <String, int>{};
for (final note in await _gateway.allNotes()) {
counts.update(note.studysetId, (count) => count + 1, ifAbsent: () => 1);
}
return counts;
}
/// Creates a note with [content] at the end of [studysetId]; returns its
/// id and its first revision's, for the next edit to build on.
Future<({String noteId, String revisionId})> create(
String studysetId,
NoteContent content,
) async {
String? last;
for (final note in await _gateway.notesOf(studysetId)) {
if (last == null || note.position.compareTo(last) > 0) {
last = note.position;
}
}
final ids = await _gateway.insertNote(
studysetId: studysetId,
position: FractionalIndexing.generateKeyBetween(last, null),
payload: encodeNotePayload(content.normalized),
);
_changes.add(null);
return ids;
}
/// Stores [content] as the next revision of the note [noteId], replacing
/// [basedOn]: the revisions the edit started from (all current heads to
/// settle a conflict). What another device wrote meanwhile stays a head of
/// its own and merges with this one (ADR 0003), so it is never replaced
/// unseen. Returns the new revision's id, for the next edit to build on.
Future<String> saveEdit(
String noteId,
NoteContent content, {
required Set<String> basedOn,
}) async {
final id = await _gateway.insertRevision(
noteId: noteId,
parents: basedOn,
payload: encodeNotePayload(content.normalized),
);
_changes.add(null);
return id;
}
/// Deletes the note [noteId]. Its revisions are blanked first: the sync
/// engine keeps deleted rows until the whole space is deleted (ADR 0002,
/// rule 2).
Future<void> delete(String noteId) async {
await _gateway.blankRevisions(noteId);
await _gateway.deleteNote(noteId);
_changes.add(null);
}
/// Deletes every note of [studysetId] as [delete] does, before the
/// studyset itself goes.
Future<void> deleteAllOf(String studysetId) async {
for (final note in await _gateway.notesOf(studysetId)) {
await _gateway.blankRevisions(note.id);
await _gateway.deleteNote(note.id);
}
_changes.add(null);
}
/// Stops following remote changes.
Future<void> dispose() async {
await _remoteChanges.cancel();
await _changes.close();
}
/// The newest of [revisions] among [heads] (UUID v7 ids sort by creation),
/// or null.
NoteRevision? _newestOf(List<NoteRevision> revisions, Set<String> heads) {
NoteRevision? newest;
for (final revision in revisions) {
if (heads.contains(revision.id) &&
(newest == null || revision.id.compareTo(newest.id) > 0)) {
newest = revision;
}
}
return newest;
}
/// The revision in [row], or null for one this app can't read: written by
/// a newer app version, or blanked.
NoteRevision? _decode(NoteRevisionRow row) {
try {
return NoteRevision(
id: row.id,
parents: row.parents,
content: decodeNotePayload(row.payload),
writtenAt: row.writtenAt,
);
} on FormatException catch (error) {
log('Skipped note revision ${row.id}: ${error.message}', name: 'Notes');
return null;
}
}
}

View file

@ -0,0 +1,135 @@
import 'dart:typed_data';
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
import 'package:halkyon_learn_flutter/src/core/content/payload.dart';
import 'package:halkyon_learn_flutter/src/core/sync/sync_service.dart';
import 'package:halkyon_learn_flutter/src/core/sync/uuid_time.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_gateway.dart';
import 'package:serverpod_offline_sync_client/offline_sync.dart';
/// [NoteGateway] on the person's local database, which the sync engine keeps
/// in step with their other devices. Revisions record [_authorId] as their
/// author.
class SyncNoteGateway(
final OfflineSyncDatabaseSession _db,
final SyncService _sync,
final String _authorId,
) implements NoteGateway {
@override
Future<List<NoteRow>> notesOf(String studysetId) async => [
for (final note in await protocol.Note.db.find(
_db,
where: (t) =>
t.studysetId.equals(protocol.UuidValue.withValidation(studysetId)),
))
(id: note.id!.uuid, position: note.position),
];
@override
Future<List<NoteStampRow>> allNotes() async => [
for (final note in await protocol.Note.db.find(_db))
(id: note.id!.uuid, studysetId: note.studysetId.uuid),
];
@override
Future<List<NoteRevisionRow>> revisionsOf(Set<String> noteIds) async {
if (noteIds.isEmpty) {
return const [];
}
return [
for (final revision in await protocol.NoteRevision.db.find(
_db,
where: (t) => t.noteId.inSet({
for (final id in noteIds) protocol.UuidValue.withValidation(id),
}),
))
(
id: revision.id!.uuid,
noteId: revision.noteId.uuid,
parents: {for (final parent in revision.parents) parent.uuid},
payload: revision.payload,
writtenAt: uuidV7Time(revision.id!.uuid),
),
];
}
@override
Future<({String noteId, String revisionId})> insertNote({
required String studysetId,
required String position,
required ByteData payload,
}) async {
final note = await protocol.Note.db.insertRow(
_db,
protocol.Note(
studysetId: protocol.UuidValue.withValidation(studysetId),
position: position,
),
);
final revisionId = await insertRevision(
noteId: note.id!.uuid,
parents: const {},
payload: payload,
);
return (noteId: note.id!.uuid, revisionId: revisionId);
}
@override
Future<String> insertRevision({
required String noteId,
required Set<String> parents,
required ByteData payload,
}) async {
final revision = await protocol.NoteRevision.db.insertRow(
_db,
protocol.NoteRevision(
noteId: protocol.UuidValue.withValidation(noteId),
parents: [
for (final parent in parents)
protocol.UuidValue.withValidation(parent),
],
authorId: protocol.UuidValue.withValidation(_authorId),
payload: payload,
),
);
_sync.requestSync();
return revision.id!.uuid;
}
@override
Future<void> blankRevisions(String noteId) async {
final revisions = await protocol.NoteRevision.db.find(
_db,
where: (t) => t.noteId.equals(protocol.UuidValue.withValidation(noteId)),
);
// A column-scoped update, which syncs like any other edit (ADR 0002,
// rule 2).
await protocol.NoteRevision.db.update(
_db,
[
for (final revision in revisions)
revision.copyWith(payload: redactedPayload),
],
columns: (t) => [t.payload],
);
_sync.requestSync();
}
@override
Future<void> deleteNote(String noteId) async {
final id = protocol.UuidValue.withValidation(noteId);
await protocol.NoteTag.db.deleteWhere(
_db,
where: (t) => t.noteId.equals(id),
);
await protocol.NoteRevision.db.deleteWhere(
_db,
where: (t) => t.noteId.equals(id),
);
await protocol.Note.db.deleteWhere(_db, where: (t) => t.id.equals(id));
_sync.requestSync();
}
@override
Stream<void> get remoteChanges => _sync.merges;
}

View file

@ -0,0 +1,40 @@
import 'package:equatable/equatable.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.dart';
/// A note as the app shows it: its revisions merged (`mergeNoteRevisions`).
final class const Note({
/// A UUID v7, made on the device that created it.
required final String id,
/// Where the note stands among its studyset's notes: a fractional index.
required final String position,
/// The current content. For a field in [conflicts], the newest version
/// shows until the person chooses.
required final NoteContent content,
/// The revisions its content is merged from: all current heads.
required final Set<String> heads,
/// The fields changed differently on different devices, with every
/// version, oldest first.
final Map<NoteField, List<Object?>> conflicts = const {},
/// When it was last edited, here or on another device: its newest
/// revision; null when the revision doesn't say.
final DateTime? editedAt,
}) with Equatable {
/// Whether devices changed a field differently and the person has to
/// choose.
bool get hasConflict => conflicts.isNotEmpty;
@override
List<Object?> get props => [
id,
position,
content,
heads,
conflicts,
editedAt,
];
}

View file

@ -0,0 +1,47 @@
import 'package:equatable/equatable.dart';
import 'package:halkyon_learn_flutter/src/core/content/revision_merge.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
/// The parts of a note that merge separately when devices edited it while
/// offline (ADR 0003): a title changed on one device and the body on another
/// both survive.
enum NoteField() {
/// The title.
title,
/// The text.
body,
}
/// What a note holds: a title and rich text.
final class const NoteContent({
/// The title; empty for an untitled note.
final String title = '',
/// The text.
required final RichContent body,
}) with Equatable implements MergeableContent<NoteField, NoteContent> {
/// A note with no title and no text.
factory empty() => NoteContent(body: RichContent.empty());
@override
Object? operator [](NoteField field) => switch (field) {
NoteField.title => title,
NoteField.body => body,
};
@override
NoteContent withField(NoteField field, Object? value) => NoteContent(
title: field == NoteField.title ? value! as String : title,
body: field == NoteField.body ? value! as RichContent : body,
);
/// Whether it holds nothing: no title and no text.
bool get isBlank => title.trim().isEmpty && body.isBlank;
/// This content as it is stored: the title without surrounding spaces.
NoteContent get normalized => NoteContent(title: title.trim(), body: body);
@override
List<Object?> get props => [title, body];
}

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