feat: learning paths in studysets, synced, with progress and tags

Server: five synced tables, only additions (migration
20261002211346701-learning-paths):
- `learning_path`: its studyset, a fractional position, and its own
  content in the payload {title, secondaryTitle, description}, the last
  writer winning, as a studyset's;
- `learning_path_step`: its path, a position, and its kind ('info',
  'singleChoice', 'multipleChoice', 'dragAndDrop', 'fillInTheBlank'),
  fixed once created;
- `learning_path_step_revision`: a step's content as append-only
  revisions, as a card's: {title, body, exercise, rightFeedback,
  wrongFeedback, sources};
- `learning_path_answer`: an append-only log (right, wrong, continued,
  reset), as reviews are, so answers from two devices never conflict;
- `learning_path_tag`: one row per assignment, as `card_tag`.

App, `feature/learning_path`: the five exercises and their JSON, grading
that marks only the learner's own wrong picks, progress worked out from
the log (a step is done once answered right since its last fresh start),
`LearningPathRepository` (steps saved on what they started from, merged
per field) and `PathProgressRepository` (answers, resets with Undo, the
times steps were done). Paths share the person's tags; deleting a path
or a step can be undone for a moment, and a studyset's deletion takes
its paths. Tests: models, codecs, repositories, 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 23:31:20 +02:00
commit 91f6376b93
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
69 changed files with 34455 additions and 26 deletions

View file

@ -0,0 +1,679 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "learning_path",
"dartName": "LearningPath",
"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"
},
{
"__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": "learning_path_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "learning_path_fk_1",
"columns": [
"studysetId"
],
"referenceTable": "studyset",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "learning_path_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "learning_path_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": "learning_path_answer",
"dartName": "LearningPathAnswer",
"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": "stepId",
"fieldName": "stepId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "answeredAt",
"fieldName": "answeredAt",
"columnType": 4,
"isNullable": false,
"dartType": "DateTime"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "outcome",
"fieldName": "outcome",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "answer",
"fieldName": "answer",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "learning_path_answer_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "learning_path_answer_fk_1",
"columns": [
"stepId"
],
"referenceTable": "learning_path_step",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "learning_path_answer_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "learning_path_answer_step_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "stepId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "answeredAt"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "learning_path_step",
"dartName": "LearningPathStep",
"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": "pathId",
"fieldName": "pathId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "position",
"fieldName": "position",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "kind",
"fieldName": "kind",
"columnType": 0,
"isNullable": false,
"dartType": "String"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "learning_path_step_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "learning_path_step_fk_1",
"columns": [
"pathId"
],
"referenceTable": "learning_path",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "learning_path_step_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "learning_path_step_path_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "pathId"
},
{
"__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": "learning_path_step_revision",
"dartName": "LearningPathStepRevision",
"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": "stepId",
"fieldName": "stepId",
"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": "learning_path_step_revision_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "learning_path_step_revision_fk_1",
"columns": [
"stepId"
],
"referenceTable": "learning_path_step",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "learning_path_step_revision_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "learning_path_step_revision_step_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "stepId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "learning_path_tag",
"dartName": "LearningPathTag",
"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": "pathId",
"fieldName": "pathId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "tagId",
"fieldName": "tagId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "learning_path_tag_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "learning_path_tag_fk_1",
"columns": [
"pathId"
],
"referenceTable": "learning_path",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "learning_path_tag_fk_2",
"columns": [
"tagId"
],
"referenceTable": "tag",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "learning_path_tag_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "learning_path_tag_path_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "pathId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "learning_path_tag_tag_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "tagId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -9,6 +9,7 @@ part '20260930122235111-cards/migration.dart';
part '20260930145136285-reviews/migration.dart';
part '20261001064926911-tree-tags-assets/migration.dart';
part '20261002191122122-notes/migration.dart';
part '20261002211346701-learning-paths/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -20,6 +21,7 @@ class MigrationRegistry {
_Migration20260930145136285_reviews(),
_Migration20261001064926911_tree_tags_assets(),
_Migration20261002191122122_notes(),
_Migration20261002211346701_learning_paths(),
];
/// List of all client-side database migration versions.

View file

@ -0,0 +1,815 @@
/* 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 '../studyset/studyset.dart' as _ie5k1jc2;
/// A learning path in a studyset: one lesson, an ordered sequence of steps the
/// person works through. Synced; its space owns it.
///
/// Its own content (title, secondary title, description) lives in the payload,
/// last writer wins, as a studyset's: rarely edited on two devices at once. The
/// steps' content is revisions of their own (LearningPathStepRevision).
abstract class LearningPath
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
LearningPath._({
this.id,
this.spaceId,
required this.studysetId,
this.studyset,
required this.position,
this.keyId,
required this.payload,
this.nonce,
});
factory LearningPath({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue studysetId,
_ie5k1jc2.Studyset? studyset,
required String position,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _LearningPathImpl;
factory LearningPath.fromJson(Map<String, dynamic> jsonSerialization) {
return LearningPath(
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,
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = LearningPathTable();
static const db = LearningPathRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue studysetId;
/// The studyset the path belongs to; the path goes with it.
_ie5k1jc2.Studyset? studyset;
/// Where the path stands among its studyset's paths: a fractional index, as a
/// card's position; last writer wins.
String position;
/// The encryption key's id; always null (ADR 0002, rule 6).
String? keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "title", "secondaryTitle",
/// "description"}; the app's learning_path_payload.dart describes each schema.
_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 [LearningPath]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
LearningPath copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? studysetId,
_ie5k1jc2.Studyset? studyset,
String? position,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'LearningPath',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'studysetId': studysetId.toJson(),
if (studyset != null) 'studyset': studyset?.toJson(),
'position': position,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'LearningPath',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'studysetId': studysetId.toJson(),
if (studyset != null) 'studyset': studyset?.toJsonForProtocol(),
'position': position,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static LearningPathInclude include({_ie5k1jc2.StudysetInclude? studyset}) {
return LearningPathInclude._(studyset: studyset);
}
static LearningPathIncludeList includeList({
_isd.WhereExpressionBuilder<LearningPathTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTable>? orderByList,
LearningPathInclude? include,
}) {
return LearningPathIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(LearningPath.t),
orderByList: orderByList?.call(LearningPath.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _LearningPathImpl extends LearningPath {
_LearningPathImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue studysetId,
_ie5k1jc2.Studyset? studyset,
required String position,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
studysetId: studysetId,
studyset: studyset,
position: position,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [LearningPath]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
LearningPath copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? studysetId,
Object? studyset = _Undefined,
String? position,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return LearningPath(
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,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class LearningPathUpdateTable extends _isd.UpdateTable<LearningPathTable> {
LearningPathUpdateTable(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,
);
_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 LearningPathTable extends _isd.Table<_isc.UuidValue?> {
LearningPathTable({super.tableRelation}) : super(tableName: 'learning_path') {
updateTable = LearningPathUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
studysetId = _isd.ColumnUuid(
'studysetId',
this,
);
position = _isd.ColumnString(
'position',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final LearningPathUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid studysetId;
/// The studyset the path belongs to; the path goes with it.
_ie5k1jc2.StudysetTable? _studyset;
/// Where the path stands among its studyset's paths: a fractional index, as a
/// card's position; last writer wins.
late final _isd.ColumnString position;
/// The encryption key's id; always null (ADR 0002, rule 6).
late final _isd.ColumnString keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "title", "secondaryTitle",
/// "description"}; the app's learning_path_payload.dart describes each schema.
late final _isd.ColumnByteData payload;
/// The encryption nonce; always null, like keyId.
late final _isd.ColumnByteData nonce;
_ie5k1jc2.StudysetTable get studyset {
if (_studyset != null) return _studyset!;
_studyset = _isd.createRelationTable(
relationFieldName: 'studyset',
field: LearningPath.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,
keyId,
payload,
nonce,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'studyset') {
return studyset;
}
return null;
}
}
class LearningPathInclude extends _isd.IncludeObject {
LearningPathInclude._({_ie5k1jc2.StudysetInclude? studyset}) {
_studyset = studyset;
}
_ie5k1jc2.StudysetInclude? _studyset;
@override
Map<String, _isd.Include?> get includes => {'studyset': _studyset};
@override
_isd.Table<_isc.UuidValue?> get table => LearningPath.t;
}
class LearningPathIncludeList extends _isd.IncludeList {
LearningPathIncludeList._({
_isd.WhereExpressionBuilder<LearningPathTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(LearningPath.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => LearningPath.t;
}
class LearningPathRepository {
const LearningPathRepository._();
final attachRow = const LearningPathAttachRowRepository._();
/// Returns a list of [LearningPath]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<LearningPath>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<LearningPathTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTable>? orderByList,
_isd.Transaction? transaction,
LearningPathInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<LearningPath>(
where: where?.call(LearningPath.t),
orderBy: orderBy?.call(LearningPath.t),
orderByList: orderByList?.call(LearningPath.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [LearningPath] 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<LearningPath?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<LearningPathTable>? where,
int? offset,
_isd.OrderByBuilder<LearningPathTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTable>? orderByList,
_isd.Transaction? transaction,
LearningPathInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<LearningPath>(
where: where?.call(LearningPath.t),
orderBy: orderBy?.call(LearningPath.t),
orderByList: orderByList?.call(LearningPath.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [LearningPath] by its [id] or null if no such row exists.
Future<LearningPath?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
LearningPathInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<LearningPath>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [LearningPath]s in the list and returns the inserted rows.
///
/// The returned [LearningPath]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<LearningPath>> insert(
_isd.DatabaseSession session,
List<LearningPath> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<LearningPath>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [LearningPath] and returns the inserted row.
///
/// The returned [LearningPath] will have its `id` field set.
Future<LearningPath> insertRow(
_isd.DatabaseSession session,
LearningPath row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<LearningPath>(
row,
transaction: transaction,
);
}
/// Upserts all [LearningPath]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 [LearningPath]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<LearningPath>> upsert(
_isd.DatabaseSession session,
List<LearningPath> rows, {
required _isd.ColumnSelections<LearningPathTable> conflictColumns,
_isd.ColumnSelections<LearningPathTable>? updateColumns,
_isd.WhereExpressionBuilder<LearningPathTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<LearningPath>(
rows,
conflictColumns: conflictColumns(LearningPath.t),
updateColumns: updateColumns?.call(LearningPath.t),
updateWhere: updateWhere?.call(LearningPath.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [LearningPath] 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 [LearningPath] will have its `id` field set.
Future<LearningPath?> upsertRow(
_isd.DatabaseSession session,
LearningPath row, {
required _isd.ColumnSelections<LearningPathTable> conflictColumns,
_isd.ColumnSelections<LearningPathTable>? updateColumns,
_isd.WhereExpressionBuilder<LearningPathTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<LearningPath>(
row,
conflictColumns: conflictColumns(LearningPath.t),
updateColumns: updateColumns?.call(LearningPath.t),
updateWhere: updateWhere?.call(LearningPath.t),
transaction: transaction,
);
}
/// Updates all [LearningPath]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<LearningPath>> update(
_isd.DatabaseSession session,
List<LearningPath> rows, {
_isd.ColumnSelections<LearningPathTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<LearningPath>(
rows,
columns: columns?.call(LearningPath.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [LearningPath]. 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<LearningPath> updateRow(
_isd.DatabaseSession session,
LearningPath row, {
_isd.ColumnSelections<LearningPathTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<LearningPath>(
row,
columns: columns?.call(LearningPath.t),
transaction: transaction,
);
}
/// Updates a single [LearningPath] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<LearningPath?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<LearningPathUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<LearningPath>(
id,
columnValues: columnValues(LearningPath.t.updateTable),
transaction: transaction,
);
}
/// Updates all [LearningPath]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<LearningPath>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<LearningPathUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<LearningPathTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<LearningPath>(
columnValues: columnValues(LearningPath.t.updateTable),
where: where(LearningPath.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(LearningPath.t),
orderByList: orderByList?.call(LearningPath.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [LearningPath]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<LearningPath>> delete(
_isd.DatabaseSession session,
List<LearningPath> rows, {
_isd.OrderByBuilder<LearningPathTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<LearningPath>(
rows,
orderBy: orderBy?.call(LearningPath.t),
orderByList: orderByList?.call(LearningPath.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [LearningPath].
Future<LearningPath> deleteRow(
_isd.DatabaseSession session,
LearningPath row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<LearningPath>(
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<LearningPath>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<LearningPathTable> where,
_isd.OrderByBuilder<LearningPathTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<LearningPath>(
where: where(LearningPath.t),
orderBy: orderBy?.call(LearningPath.t),
orderByList: orderByList?.call(LearningPath.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<LearningPathTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<LearningPath>(
where: where?.call(LearningPath.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [LearningPath] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<LearningPathTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<LearningPath>(
where: where(LearningPath.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class LearningPathAttachRowRepository {
const LearningPathAttachRowRepository._();
/// Creates a relation between the given [LearningPath] and [Studyset]
/// by setting the [LearningPath]'s foreign key `studysetId` to refer to the [Studyset].
Future<void> studyset(
_isd.DatabaseSession session,
LearningPath learningPath,
_ie5k1jc2.Studyset studyset, {
_isd.Transaction? transaction,
}) async {
if (learningPath.id == null) {
throw ArgumentError.notNull('learningPath.id');
}
if (studyset.id == null) {
throw ArgumentError.notNull('studyset.id');
}
var $learningPath = learningPath.copyWith(studysetId: studyset.id);
await session.db.updateRow<LearningPath>(
$learningPath,
columns: [LearningPath.t.studysetId],
transaction: transaction,
);
}
}

View file

@ -0,0 +1,789 @@
/* 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 '../learning_path/learning_path_step.dart' as _i0qbf905;
/// One answer to a step of a learning path, never changed once written: where a
/// person stands in a path isn't stored but derived from these, as a card's
/// schedule is from its reviews, so answers from different devices never
/// conflict (decided 2026-10-02). Synced; the answer goes with its step.
abstract class LearningPathAnswer
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
LearningPathAnswer._({
this.id,
this.spaceId,
required this.stepId,
this.step,
required this.answeredAt,
required this.outcome,
this.answer,
});
factory LearningPathAnswer({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue stepId,
_i0qbf905.LearningPathStep? step,
required DateTime answeredAt,
required String outcome,
String? answer,
}) = _LearningPathAnswerImpl;
factory LearningPathAnswer.fromJson(Map<String, dynamic> jsonSerialization) {
return LearningPathAnswer(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
stepId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['stepId']),
step: jsonSerialization['step'] == null
? null
: _itx5j3f5.Protocol().deserialize<_i0qbf905.LearningPathStep>(
jsonSerialization['step'],
),
answeredAt: _isc.DateTimeJsonExtension.fromJson(
jsonSerialization['answeredAt'],
),
outcome: jsonSerialization['outcome'] as String,
answer: jsonSerialization['answer'] as String?,
);
}
static final t = LearningPathAnswerTable();
static const db = LearningPathAnswerRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue stepId;
/// The step answered.
_i0qbf905.LearningPathStep? step;
/// When, in UTC.
DateTime answeredAt;
/// 'right' or 'wrong' for an exercise; 'continued' for an info step passed;
/// 'reset' for no answer but a fresh start of the step: its progress replays
/// only what follows. A step is done once answered right (an info step,
/// continued) since its last fresh start.
String outcome;
/// What was answered, as JSON in the step's exercise schema (the choices made,
/// the pairing, the words placed); null for 'continued' and 'reset'.
String? answer;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [LearningPathAnswer]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
LearningPathAnswer copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? stepId,
_i0qbf905.LearningPathStep? step,
DateTime? answeredAt,
String? outcome,
String? answer,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'LearningPathAnswer',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'stepId': stepId.toJson(),
if (step != null) 'step': step?.toJson(),
'answeredAt': answeredAt.toJson(),
'outcome': outcome,
if (answer != null) 'answer': answer,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'LearningPathAnswer',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'stepId': stepId.toJson(),
if (step != null) 'step': step?.toJsonForProtocol(),
'answeredAt': answeredAt.toJson(),
'outcome': outcome,
if (answer != null) 'answer': answer,
};
}
static LearningPathAnswerInclude include({
_i0qbf905.LearningPathStepInclude? step,
}) {
return LearningPathAnswerInclude._(step: step);
}
static LearningPathAnswerIncludeList includeList({
_isd.WhereExpressionBuilder<LearningPathAnswerTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathAnswerTable>? orderBy,
_isd.OrderByListBuilder<LearningPathAnswerTable>? orderByList,
LearningPathAnswerInclude? include,
}) {
return LearningPathAnswerIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(LearningPathAnswer.t),
orderByList: orderByList?.call(LearningPathAnswer.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _LearningPathAnswerImpl extends LearningPathAnswer {
_LearningPathAnswerImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue stepId,
_i0qbf905.LearningPathStep? step,
required DateTime answeredAt,
required String outcome,
String? answer,
}) : super._(
id: id,
spaceId: spaceId,
stepId: stepId,
step: step,
answeredAt: answeredAt,
outcome: outcome,
answer: answer,
);
/// Returns a shallow copy of this [LearningPathAnswer]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
LearningPathAnswer copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? stepId,
Object? step = _Undefined,
DateTime? answeredAt,
String? outcome,
Object? answer = _Undefined,
}) {
return LearningPathAnswer(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
stepId: stepId ?? this.stepId,
step: step is _i0qbf905.LearningPathStep? ? step : this.step?.copyWith(),
answeredAt: answeredAt ?? this.answeredAt,
outcome: outcome ?? this.outcome,
answer: answer is String? ? answer : this.answer,
);
}
}
class LearningPathAnswerUpdateTable
extends _isd.UpdateTable<LearningPathAnswerTable> {
LearningPathAnswerUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> stepId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.stepId,
value,
);
_isd.ColumnValue<DateTime, DateTime> answeredAt(DateTime value) =>
_isd.ColumnValue(
table.answeredAt,
value,
);
_isd.ColumnValue<String, String> outcome(String value) => _isd.ColumnValue(
table.outcome,
value,
);
_isd.ColumnValue<String, String> answer(String? value) => _isd.ColumnValue(
table.answer,
value,
);
}
class LearningPathAnswerTable extends _isd.Table<_isc.UuidValue?> {
LearningPathAnswerTable({super.tableRelation})
: super(tableName: 'learning_path_answer') {
updateTable = LearningPathAnswerUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
stepId = _isd.ColumnUuid(
'stepId',
this,
);
answeredAt = _isd.ColumnDateTime(
'answeredAt',
this,
);
outcome = _isd.ColumnString(
'outcome',
this,
);
answer = _isd.ColumnString(
'answer',
this,
);
}
late final LearningPathAnswerUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid stepId;
/// The step answered.
_i0qbf905.LearningPathStepTable? _step;
/// When, in UTC.
late final _isd.ColumnDateTime answeredAt;
/// 'right' or 'wrong' for an exercise; 'continued' for an info step passed;
/// 'reset' for no answer but a fresh start of the step: its progress replays
/// only what follows. A step is done once answered right (an info step,
/// continued) since its last fresh start.
late final _isd.ColumnString outcome;
/// What was answered, as JSON in the step's exercise schema (the choices made,
/// the pairing, the words placed); null for 'continued' and 'reset'.
late final _isd.ColumnString answer;
_i0qbf905.LearningPathStepTable get step {
if (_step != null) return _step!;
_step = _isd.createRelationTable(
relationFieldName: 'step',
field: LearningPathAnswer.t.stepId,
foreignField: _i0qbf905.LearningPathStep.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_i0qbf905.LearningPathStepTable(tableRelation: foreignTableRelation),
);
return _step!;
}
@override
List<_isd.Column> get columns => [
id,
spaceId,
stepId,
answeredAt,
outcome,
answer,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'step') {
return step;
}
return null;
}
}
class LearningPathAnswerInclude extends _isd.IncludeObject {
LearningPathAnswerInclude._({_i0qbf905.LearningPathStepInclude? step}) {
_step = step;
}
_i0qbf905.LearningPathStepInclude? _step;
@override
Map<String, _isd.Include?> get includes => {'step': _step};
@override
_isd.Table<_isc.UuidValue?> get table => LearningPathAnswer.t;
}
class LearningPathAnswerIncludeList extends _isd.IncludeList {
LearningPathAnswerIncludeList._({
_isd.WhereExpressionBuilder<LearningPathAnswerTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(LearningPathAnswer.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => LearningPathAnswer.t;
}
class LearningPathAnswerRepository {
const LearningPathAnswerRepository._();
final attachRow = const LearningPathAnswerAttachRowRepository._();
/// Returns a list of [LearningPathAnswer]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<LearningPathAnswer>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<LearningPathAnswerTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathAnswerTable>? orderBy,
_isd.OrderByListBuilder<LearningPathAnswerTable>? orderByList,
_isd.Transaction? transaction,
LearningPathAnswerInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<LearningPathAnswer>(
where: where?.call(LearningPathAnswer.t),
orderBy: orderBy?.call(LearningPathAnswer.t),
orderByList: orderByList?.call(LearningPathAnswer.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [LearningPathAnswer] 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<LearningPathAnswer?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<LearningPathAnswerTable>? where,
int? offset,
_isd.OrderByBuilder<LearningPathAnswerTable>? orderBy,
_isd.OrderByListBuilder<LearningPathAnswerTable>? orderByList,
_isd.Transaction? transaction,
LearningPathAnswerInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<LearningPathAnswer>(
where: where?.call(LearningPathAnswer.t),
orderBy: orderBy?.call(LearningPathAnswer.t),
orderByList: orderByList?.call(LearningPathAnswer.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [LearningPathAnswer] by its [id] or null if no such row exists.
Future<LearningPathAnswer?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
LearningPathAnswerInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<LearningPathAnswer>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [LearningPathAnswer]s in the list and returns the inserted rows.
///
/// The returned [LearningPathAnswer]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<LearningPathAnswer>> insert(
_isd.DatabaseSession session,
List<LearningPathAnswer> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<LearningPathAnswer>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [LearningPathAnswer] and returns the inserted row.
///
/// The returned [LearningPathAnswer] will have its `id` field set.
Future<LearningPathAnswer> insertRow(
_isd.DatabaseSession session,
LearningPathAnswer row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<LearningPathAnswer>(
row,
transaction: transaction,
);
}
/// Upserts all [LearningPathAnswer]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 [LearningPathAnswer]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<LearningPathAnswer>> upsert(
_isd.DatabaseSession session,
List<LearningPathAnswer> rows, {
required _isd.ColumnSelections<LearningPathAnswerTable> conflictColumns,
_isd.ColumnSelections<LearningPathAnswerTable>? updateColumns,
_isd.WhereExpressionBuilder<LearningPathAnswerTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<LearningPathAnswer>(
rows,
conflictColumns: conflictColumns(LearningPathAnswer.t),
updateColumns: updateColumns?.call(LearningPathAnswer.t),
updateWhere: updateWhere?.call(LearningPathAnswer.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [LearningPathAnswer] 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 [LearningPathAnswer] will have its `id` field set.
Future<LearningPathAnswer?> upsertRow(
_isd.DatabaseSession session,
LearningPathAnswer row, {
required _isd.ColumnSelections<LearningPathAnswerTable> conflictColumns,
_isd.ColumnSelections<LearningPathAnswerTable>? updateColumns,
_isd.WhereExpressionBuilder<LearningPathAnswerTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<LearningPathAnswer>(
row,
conflictColumns: conflictColumns(LearningPathAnswer.t),
updateColumns: updateColumns?.call(LearningPathAnswer.t),
updateWhere: updateWhere?.call(LearningPathAnswer.t),
transaction: transaction,
);
}
/// Updates all [LearningPathAnswer]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<LearningPathAnswer>> update(
_isd.DatabaseSession session,
List<LearningPathAnswer> rows, {
_isd.ColumnSelections<LearningPathAnswerTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<LearningPathAnswer>(
rows,
columns: columns?.call(LearningPathAnswer.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [LearningPathAnswer]. 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<LearningPathAnswer> updateRow(
_isd.DatabaseSession session,
LearningPathAnswer row, {
_isd.ColumnSelections<LearningPathAnswerTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<LearningPathAnswer>(
row,
columns: columns?.call(LearningPathAnswer.t),
transaction: transaction,
);
}
/// Updates a single [LearningPathAnswer] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<LearningPathAnswer?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<LearningPathAnswerUpdateTable>
columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<LearningPathAnswer>(
id,
columnValues: columnValues(LearningPathAnswer.t.updateTable),
transaction: transaction,
);
}
/// Updates all [LearningPathAnswer]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<LearningPathAnswer>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<LearningPathAnswerUpdateTable>
columnValues,
required _isd.WhereExpressionBuilder<LearningPathAnswerTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathAnswerTable>? orderBy,
_isd.OrderByListBuilder<LearningPathAnswerTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<LearningPathAnswer>(
columnValues: columnValues(LearningPathAnswer.t.updateTable),
where: where(LearningPathAnswer.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(LearningPathAnswer.t),
orderByList: orderByList?.call(LearningPathAnswer.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [LearningPathAnswer]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<LearningPathAnswer>> delete(
_isd.DatabaseSession session,
List<LearningPathAnswer> rows, {
_isd.OrderByBuilder<LearningPathAnswerTable>? orderBy,
_isd.OrderByListBuilder<LearningPathAnswerTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<LearningPathAnswer>(
rows,
orderBy: orderBy?.call(LearningPathAnswer.t),
orderByList: orderByList?.call(LearningPathAnswer.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [LearningPathAnswer].
Future<LearningPathAnswer> deleteRow(
_isd.DatabaseSession session,
LearningPathAnswer row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<LearningPathAnswer>(
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<LearningPathAnswer>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<LearningPathAnswerTable> where,
_isd.OrderByBuilder<LearningPathAnswerTable>? orderBy,
_isd.OrderByListBuilder<LearningPathAnswerTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<LearningPathAnswer>(
where: where(LearningPathAnswer.t),
orderBy: orderBy?.call(LearningPathAnswer.t),
orderByList: orderByList?.call(LearningPathAnswer.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<LearningPathAnswerTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<LearningPathAnswer>(
where: where?.call(LearningPathAnswer.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [LearningPathAnswer] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<LearningPathAnswerTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<LearningPathAnswer>(
where: where(LearningPathAnswer.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class LearningPathAnswerAttachRowRepository {
const LearningPathAnswerAttachRowRepository._();
/// Creates a relation between the given [LearningPathAnswer] and [LearningPathStep]
/// by setting the [LearningPathAnswer]'s foreign key `stepId` to refer to the [LearningPathStep].
Future<void> step(
_isd.DatabaseSession session,
LearningPathAnswer learningPathAnswer,
_i0qbf905.LearningPathStep step, {
_isd.Transaction? transaction,
}) async {
if (learningPathAnswer.id == null) {
throw ArgumentError.notNull('learningPathAnswer.id');
}
if (step.id == null) {
throw ArgumentError.notNull('step.id');
}
var $learningPathAnswer = learningPathAnswer.copyWith(stepId: step.id);
await session.db.updateRow<LearningPathAnswer>(
$learningPathAnswer,
columns: [LearningPathAnswer.t.stepId],
transaction: transaction,
);
}
}

View file

@ -0,0 +1,756 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: 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 '../learning_path/learning_path.dart' as _ixd0fbzl;
/// A step of a learning path: something to read, or an exercise to answer. Its
/// content lives in its revisions (LearningPathStepRevision), never in this row:
/// content is append-only (ADR 0001, rule 2), as a card's is. Synced; its space
/// owns it.
abstract class LearningPathStep
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
LearningPathStep._({
this.id,
this.spaceId,
required this.pathId,
this.path,
required this.position,
required this.kind,
});
factory LearningPathStep({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue pathId,
_ixd0fbzl.LearningPath? path,
required String position,
required String kind,
}) = _LearningPathStepImpl;
factory LearningPathStep.fromJson(Map<String, dynamic> jsonSerialization) {
return LearningPathStep(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
pathId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['pathId']),
path: jsonSerialization['path'] == null
? null
: _itx5j3f5.Protocol().deserialize<_ixd0fbzl.LearningPath>(
jsonSerialization['path'],
),
position: jsonSerialization['position'] as String,
kind: jsonSerialization['kind'] as String,
);
}
static final t = LearningPathStepTable();
static const db = LearningPathStepRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue pathId;
/// The path the step belongs to; the step goes with it.
_ixd0fbzl.LearningPath? path;
/// Where the step stands in its path: a fractional index; last writer wins.
String position;
/// What kind of step it is: 'info', 'singleChoice', 'multipleChoice',
/// 'dragAndDrop' or 'fillInTheBlank' (decided 2026-10-02). Fixed once created,
/// so a column rather than content; a kind this app doesn't know is skipped.
String kind;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [LearningPathStep]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
LearningPathStep copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? pathId,
_ixd0fbzl.LearningPath? path,
String? position,
String? kind,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'LearningPathStep',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'pathId': pathId.toJson(),
if (path != null) 'path': path?.toJson(),
'position': position,
'kind': kind,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'LearningPathStep',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'pathId': pathId.toJson(),
if (path != null) 'path': path?.toJsonForProtocol(),
'position': position,
'kind': kind,
};
}
static LearningPathStepInclude include({
_ixd0fbzl.LearningPathInclude? path,
}) {
return LearningPathStepInclude._(path: path);
}
static LearningPathStepIncludeList includeList({
_isd.WhereExpressionBuilder<LearningPathStepTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathStepTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepTable>? orderByList,
LearningPathStepInclude? include,
}) {
return LearningPathStepIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(LearningPathStep.t),
orderByList: orderByList?.call(LearningPathStep.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _LearningPathStepImpl extends LearningPathStep {
_LearningPathStepImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue pathId,
_ixd0fbzl.LearningPath? path,
required String position,
required String kind,
}) : super._(
id: id,
spaceId: spaceId,
pathId: pathId,
path: path,
position: position,
kind: kind,
);
/// Returns a shallow copy of this [LearningPathStep]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
LearningPathStep copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? pathId,
Object? path = _Undefined,
String? position,
String? kind,
}) {
return LearningPathStep(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
pathId: pathId ?? this.pathId,
path: path is _ixd0fbzl.LearningPath? ? path : this.path?.copyWith(),
position: position ?? this.position,
kind: kind ?? this.kind,
);
}
}
class LearningPathStepUpdateTable
extends _isd.UpdateTable<LearningPathStepTable> {
LearningPathStepUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> pathId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.pathId,
value,
);
_isd.ColumnValue<String, String> position(String value) => _isd.ColumnValue(
table.position,
value,
);
_isd.ColumnValue<String, String> kind(String value) => _isd.ColumnValue(
table.kind,
value,
);
}
class LearningPathStepTable extends _isd.Table<_isc.UuidValue?> {
LearningPathStepTable({super.tableRelation})
: super(tableName: 'learning_path_step') {
updateTable = LearningPathStepUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
pathId = _isd.ColumnUuid(
'pathId',
this,
);
position = _isd.ColumnString(
'position',
this,
);
kind = _isd.ColumnString(
'kind',
this,
);
}
late final LearningPathStepUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid pathId;
/// The path the step belongs to; the step goes with it.
_ixd0fbzl.LearningPathTable? _path;
/// Where the step stands in its path: a fractional index; last writer wins.
late final _isd.ColumnString position;
/// What kind of step it is: 'info', 'singleChoice', 'multipleChoice',
/// 'dragAndDrop' or 'fillInTheBlank' (decided 2026-10-02). Fixed once created,
/// so a column rather than content; a kind this app doesn't know is skipped.
late final _isd.ColumnString kind;
_ixd0fbzl.LearningPathTable get path {
if (_path != null) return _path!;
_path = _isd.createRelationTable(
relationFieldName: 'path',
field: LearningPathStep.t.pathId,
foreignField: _ixd0fbzl.LearningPath.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_ixd0fbzl.LearningPathTable(tableRelation: foreignTableRelation),
);
return _path!;
}
@override
List<_isd.Column> get columns => [
id,
spaceId,
pathId,
position,
kind,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'path') {
return path;
}
return null;
}
}
class LearningPathStepInclude extends _isd.IncludeObject {
LearningPathStepInclude._({_ixd0fbzl.LearningPathInclude? path}) {
_path = path;
}
_ixd0fbzl.LearningPathInclude? _path;
@override
Map<String, _isd.Include?> get includes => {'path': _path};
@override
_isd.Table<_isc.UuidValue?> get table => LearningPathStep.t;
}
class LearningPathStepIncludeList extends _isd.IncludeList {
LearningPathStepIncludeList._({
_isd.WhereExpressionBuilder<LearningPathStepTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(LearningPathStep.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => LearningPathStep.t;
}
class LearningPathStepRepository {
const LearningPathStepRepository._();
final attachRow = const LearningPathStepAttachRowRepository._();
/// Returns a list of [LearningPathStep]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<LearningPathStep>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<LearningPathStepTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathStepTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepTable>? orderByList,
_isd.Transaction? transaction,
LearningPathStepInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<LearningPathStep>(
where: where?.call(LearningPathStep.t),
orderBy: orderBy?.call(LearningPathStep.t),
orderByList: orderByList?.call(LearningPathStep.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [LearningPathStep] 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<LearningPathStep?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<LearningPathStepTable>? where,
int? offset,
_isd.OrderByBuilder<LearningPathStepTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepTable>? orderByList,
_isd.Transaction? transaction,
LearningPathStepInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<LearningPathStep>(
where: where?.call(LearningPathStep.t),
orderBy: orderBy?.call(LearningPathStep.t),
orderByList: orderByList?.call(LearningPathStep.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [LearningPathStep] by its [id] or null if no such row exists.
Future<LearningPathStep?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
LearningPathStepInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<LearningPathStep>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [LearningPathStep]s in the list and returns the inserted rows.
///
/// The returned [LearningPathStep]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<LearningPathStep>> insert(
_isd.DatabaseSession session,
List<LearningPathStep> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<LearningPathStep>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [LearningPathStep] and returns the inserted row.
///
/// The returned [LearningPathStep] will have its `id` field set.
Future<LearningPathStep> insertRow(
_isd.DatabaseSession session,
LearningPathStep row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<LearningPathStep>(
row,
transaction: transaction,
);
}
/// Upserts all [LearningPathStep]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 [LearningPathStep]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<LearningPathStep>> upsert(
_isd.DatabaseSession session,
List<LearningPathStep> rows, {
required _isd.ColumnSelections<LearningPathStepTable> conflictColumns,
_isd.ColumnSelections<LearningPathStepTable>? updateColumns,
_isd.WhereExpressionBuilder<LearningPathStepTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<LearningPathStep>(
rows,
conflictColumns: conflictColumns(LearningPathStep.t),
updateColumns: updateColumns?.call(LearningPathStep.t),
updateWhere: updateWhere?.call(LearningPathStep.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [LearningPathStep] 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 [LearningPathStep] will have its `id` field set.
Future<LearningPathStep?> upsertRow(
_isd.DatabaseSession session,
LearningPathStep row, {
required _isd.ColumnSelections<LearningPathStepTable> conflictColumns,
_isd.ColumnSelections<LearningPathStepTable>? updateColumns,
_isd.WhereExpressionBuilder<LearningPathStepTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<LearningPathStep>(
row,
conflictColumns: conflictColumns(LearningPathStep.t),
updateColumns: updateColumns?.call(LearningPathStep.t),
updateWhere: updateWhere?.call(LearningPathStep.t),
transaction: transaction,
);
}
/// Updates all [LearningPathStep]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<LearningPathStep>> update(
_isd.DatabaseSession session,
List<LearningPathStep> rows, {
_isd.ColumnSelections<LearningPathStepTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<LearningPathStep>(
rows,
columns: columns?.call(LearningPathStep.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [LearningPathStep]. 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<LearningPathStep> updateRow(
_isd.DatabaseSession session,
LearningPathStep row, {
_isd.ColumnSelections<LearningPathStepTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<LearningPathStep>(
row,
columns: columns?.call(LearningPathStep.t),
transaction: transaction,
);
}
/// Updates a single [LearningPathStep] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<LearningPathStep?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<LearningPathStepUpdateTable>
columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<LearningPathStep>(
id,
columnValues: columnValues(LearningPathStep.t.updateTable),
transaction: transaction,
);
}
/// Updates all [LearningPathStep]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<LearningPathStep>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<LearningPathStepUpdateTable>
columnValues,
required _isd.WhereExpressionBuilder<LearningPathStepTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathStepTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<LearningPathStep>(
columnValues: columnValues(LearningPathStep.t.updateTable),
where: where(LearningPathStep.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(LearningPathStep.t),
orderByList: orderByList?.call(LearningPathStep.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [LearningPathStep]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<LearningPathStep>> delete(
_isd.DatabaseSession session,
List<LearningPathStep> rows, {
_isd.OrderByBuilder<LearningPathStepTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<LearningPathStep>(
rows,
orderBy: orderBy?.call(LearningPathStep.t),
orderByList: orderByList?.call(LearningPathStep.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [LearningPathStep].
Future<LearningPathStep> deleteRow(
_isd.DatabaseSession session,
LearningPathStep row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<LearningPathStep>(
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<LearningPathStep>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<LearningPathStepTable> where,
_isd.OrderByBuilder<LearningPathStepTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<LearningPathStep>(
where: where(LearningPathStep.t),
orderBy: orderBy?.call(LearningPathStep.t),
orderByList: orderByList?.call(LearningPathStep.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<LearningPathStepTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<LearningPathStep>(
where: where?.call(LearningPathStep.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [LearningPathStep] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<LearningPathStepTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<LearningPathStep>(
where: where(LearningPathStep.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class LearningPathStepAttachRowRepository {
const LearningPathStepAttachRowRepository._();
/// Creates a relation between the given [LearningPathStep] and [LearningPath]
/// by setting the [LearningPathStep]'s foreign key `pathId` to refer to the [LearningPath].
Future<void> path(
_isd.DatabaseSession session,
LearningPathStep learningPathStep,
_ixd0fbzl.LearningPath path, {
_isd.Transaction? transaction,
}) async {
if (learningPathStep.id == null) {
throw ArgumentError.notNull('learningPathStep.id');
}
if (path.id == null) {
throw ArgumentError.notNull('path.id');
}
var $learningPathStep = learningPathStep.copyWith(pathId: path.id);
await session.db.updateRow<LearningPathStep>(
$learningPathStep,
columns: [LearningPathStep.t.pathId],
transaction: transaction,
);
}
}

View file

@ -0,0 +1,889 @@
/* 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 '../learning_path/learning_path_step.dart' as _i0qbf905;
/// One version of a step'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. Deleting a step
/// blanks every revision's payload first (ADR 0002, rule 2).
abstract class LearningPathStepRevision
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
LearningPathStepRevision._({
this.id,
this.spaceId,
required this.stepId,
this.step,
required this.parents,
required this.authorId,
this.reportId,
this.keyId,
required this.payload,
this.nonce,
});
factory LearningPathStepRevision({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue stepId,
_i0qbf905.LearningPathStep? step,
required List<_isc.UuidValue> parents,
required _isc.UuidValue authorId,
_isc.UuidValue? reportId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _LearningPathStepRevisionImpl;
factory LearningPathStepRevision.fromJson(
Map<String, dynamic> jsonSerialization,
) {
return LearningPathStepRevision(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
stepId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['stepId']),
step: jsonSerialization['step'] == null
? null
: _itx5j3f5.Protocol().deserialize<_i0qbf905.LearningPathStep>(
jsonSerialization['step'],
),
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 = LearningPathStepRevisionTable();
static const db = LearningPathStepRevisionRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue stepId;
/// The step this is a version of; its revisions go with it.
_i0qbf905.LearningPathStep? step;
/// The revisions this one replaces: none for a step'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", "exercise",
/// "rightFeedback", "wrongFeedback", "sources"}; the app's
/// learning_path_step_payload.dart describes each schema.
_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 [LearningPathStepRevision]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
LearningPathStepRevision copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? stepId,
_i0qbf905.LearningPathStep? step,
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__': 'LearningPathStepRevision',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'stepId': stepId.toJson(),
if (step != null) 'step': step?.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__': 'LearningPathStepRevision',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'stepId': stepId.toJson(),
if (step != null) 'step': step?.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 LearningPathStepRevisionInclude include({
_i0qbf905.LearningPathStepInclude? step,
}) {
return LearningPathStepRevisionInclude._(step: step);
}
static LearningPathStepRevisionIncludeList includeList({
_isd.WhereExpressionBuilder<LearningPathStepRevisionTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathStepRevisionTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepRevisionTable>? orderByList,
LearningPathStepRevisionInclude? include,
}) {
return LearningPathStepRevisionIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(LearningPathStepRevision.t),
orderByList: orderByList?.call(LearningPathStepRevision.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _LearningPathStepRevisionImpl extends LearningPathStepRevision {
_LearningPathStepRevisionImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue stepId,
_i0qbf905.LearningPathStep? step,
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,
stepId: stepId,
step: step,
parents: parents,
authorId: authorId,
reportId: reportId,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [LearningPathStepRevision]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
LearningPathStepRevision copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? stepId,
Object? step = _Undefined,
List<_isc.UuidValue>? parents,
_isc.UuidValue? authorId,
Object? reportId = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return LearningPathStepRevision(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
stepId: stepId ?? this.stepId,
step: step is _i0qbf905.LearningPathStep? ? step : this.step?.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 LearningPathStepRevisionUpdateTable
extends _isd.UpdateTable<LearningPathStepRevisionTable> {
LearningPathStepRevisionUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> stepId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.stepId,
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 LearningPathStepRevisionTable extends _isd.Table<_isc.UuidValue?> {
LearningPathStepRevisionTable({super.tableRelation})
: super(tableName: 'learning_path_step_revision') {
updateTable = LearningPathStepRevisionUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
stepId = _isd.ColumnUuid(
'stepId',
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 LearningPathStepRevisionUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid stepId;
/// The step this is a version of; its revisions go with it.
_i0qbf905.LearningPathStepTable? _step;
/// The revisions this one replaces: none for a step'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", "exercise",
/// "rightFeedback", "wrongFeedback", "sources"}; the app's
/// learning_path_step_payload.dart describes each schema.
late final _isd.ColumnByteData payload;
/// The encryption nonce; always null, like keyId.
late final _isd.ColumnByteData nonce;
_i0qbf905.LearningPathStepTable get step {
if (_step != null) return _step!;
_step = _isd.createRelationTable(
relationFieldName: 'step',
field: LearningPathStepRevision.t.stepId,
foreignField: _i0qbf905.LearningPathStep.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_i0qbf905.LearningPathStepTable(tableRelation: foreignTableRelation),
);
return _step!;
}
@override
List<_isd.Column> get columns => [
id,
spaceId,
stepId,
parents,
authorId,
reportId,
keyId,
payload,
nonce,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'step') {
return step;
}
return null;
}
}
class LearningPathStepRevisionInclude extends _isd.IncludeObject {
LearningPathStepRevisionInclude._({_i0qbf905.LearningPathStepInclude? step}) {
_step = step;
}
_i0qbf905.LearningPathStepInclude? _step;
@override
Map<String, _isd.Include?> get includes => {'step': _step};
@override
_isd.Table<_isc.UuidValue?> get table => LearningPathStepRevision.t;
}
class LearningPathStepRevisionIncludeList extends _isd.IncludeList {
LearningPathStepRevisionIncludeList._({
_isd.WhereExpressionBuilder<LearningPathStepRevisionTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(LearningPathStepRevision.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => LearningPathStepRevision.t;
}
class LearningPathStepRevisionRepository {
const LearningPathStepRevisionRepository._();
final attachRow = const LearningPathStepRevisionAttachRowRepository._();
/// Returns a list of [LearningPathStepRevision]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<LearningPathStepRevision>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<LearningPathStepRevisionTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathStepRevisionTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepRevisionTable>? orderByList,
_isd.Transaction? transaction,
LearningPathStepRevisionInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<LearningPathStepRevision>(
where: where?.call(LearningPathStepRevision.t),
orderBy: orderBy?.call(LearningPathStepRevision.t),
orderByList: orderByList?.call(LearningPathStepRevision.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [LearningPathStepRevision] 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<LearningPathStepRevision?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<LearningPathStepRevisionTable>? where,
int? offset,
_isd.OrderByBuilder<LearningPathStepRevisionTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepRevisionTable>? orderByList,
_isd.Transaction? transaction,
LearningPathStepRevisionInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<LearningPathStepRevision>(
where: where?.call(LearningPathStepRevision.t),
orderBy: orderBy?.call(LearningPathStepRevision.t),
orderByList: orderByList?.call(LearningPathStepRevision.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [LearningPathStepRevision] by its [id] or null if no such row exists.
Future<LearningPathStepRevision?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
LearningPathStepRevisionInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<LearningPathStepRevision>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [LearningPathStepRevision]s in the list and returns the inserted rows.
///
/// The returned [LearningPathStepRevision]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<LearningPathStepRevision>> insert(
_isd.DatabaseSession session,
List<LearningPathStepRevision> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<LearningPathStepRevision>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [LearningPathStepRevision] and returns the inserted row.
///
/// The returned [LearningPathStepRevision] will have its `id` field set.
Future<LearningPathStepRevision> insertRow(
_isd.DatabaseSession session,
LearningPathStepRevision row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<LearningPathStepRevision>(
row,
transaction: transaction,
);
}
/// Upserts all [LearningPathStepRevision]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 [LearningPathStepRevision]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<LearningPathStepRevision>> upsert(
_isd.DatabaseSession session,
List<LearningPathStepRevision> rows, {
required _isd.ColumnSelections<LearningPathStepRevisionTable>
conflictColumns,
_isd.ColumnSelections<LearningPathStepRevisionTable>? updateColumns,
_isd.WhereExpressionBuilder<LearningPathStepRevisionTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<LearningPathStepRevision>(
rows,
conflictColumns: conflictColumns(LearningPathStepRevision.t),
updateColumns: updateColumns?.call(LearningPathStepRevision.t),
updateWhere: updateWhere?.call(LearningPathStepRevision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [LearningPathStepRevision] 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 [LearningPathStepRevision] will have its `id` field set.
Future<LearningPathStepRevision?> upsertRow(
_isd.DatabaseSession session,
LearningPathStepRevision row, {
required _isd.ColumnSelections<LearningPathStepRevisionTable>
conflictColumns,
_isd.ColumnSelections<LearningPathStepRevisionTable>? updateColumns,
_isd.WhereExpressionBuilder<LearningPathStepRevisionTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<LearningPathStepRevision>(
row,
conflictColumns: conflictColumns(LearningPathStepRevision.t),
updateColumns: updateColumns?.call(LearningPathStepRevision.t),
updateWhere: updateWhere?.call(LearningPathStepRevision.t),
transaction: transaction,
);
}
/// Updates all [LearningPathStepRevision]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<LearningPathStepRevision>> update(
_isd.DatabaseSession session,
List<LearningPathStepRevision> rows, {
_isd.ColumnSelections<LearningPathStepRevisionTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<LearningPathStepRevision>(
rows,
columns: columns?.call(LearningPathStepRevision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [LearningPathStepRevision]. 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<LearningPathStepRevision> updateRow(
_isd.DatabaseSession session,
LearningPathStepRevision row, {
_isd.ColumnSelections<LearningPathStepRevisionTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<LearningPathStepRevision>(
row,
columns: columns?.call(LearningPathStepRevision.t),
transaction: transaction,
);
}
/// Updates a single [LearningPathStepRevision] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<LearningPathStepRevision?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<LearningPathStepRevisionUpdateTable>
columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<LearningPathStepRevision>(
id,
columnValues: columnValues(LearningPathStepRevision.t.updateTable),
transaction: transaction,
);
}
/// Updates all [LearningPathStepRevision]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<LearningPathStepRevision>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<LearningPathStepRevisionUpdateTable>
columnValues,
required _isd.WhereExpressionBuilder<LearningPathStepRevisionTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathStepRevisionTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepRevisionTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<LearningPathStepRevision>(
columnValues: columnValues(LearningPathStepRevision.t.updateTable),
where: where(LearningPathStepRevision.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(LearningPathStepRevision.t),
orderByList: orderByList?.call(LearningPathStepRevision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [LearningPathStepRevision]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<LearningPathStepRevision>> delete(
_isd.DatabaseSession session,
List<LearningPathStepRevision> rows, {
_isd.OrderByBuilder<LearningPathStepRevisionTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepRevisionTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<LearningPathStepRevision>(
rows,
orderBy: orderBy?.call(LearningPathStepRevision.t),
orderByList: orderByList?.call(LearningPathStepRevision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [LearningPathStepRevision].
Future<LearningPathStepRevision> deleteRow(
_isd.DatabaseSession session,
LearningPathStepRevision row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<LearningPathStepRevision>(
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<LearningPathStepRevision>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<LearningPathStepRevisionTable> where,
_isd.OrderByBuilder<LearningPathStepRevisionTable>? orderBy,
_isd.OrderByListBuilder<LearningPathStepRevisionTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<LearningPathStepRevision>(
where: where(LearningPathStepRevision.t),
orderBy: orderBy?.call(LearningPathStepRevision.t),
orderByList: orderByList?.call(LearningPathStepRevision.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<LearningPathStepRevisionTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<LearningPathStepRevision>(
where: where?.call(LearningPathStepRevision.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [LearningPathStepRevision] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<LearningPathStepRevisionTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<LearningPathStepRevision>(
where: where(LearningPathStepRevision.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class LearningPathStepRevisionAttachRowRepository {
const LearningPathStepRevisionAttachRowRepository._();
/// Creates a relation between the given [LearningPathStepRevision] and [LearningPathStep]
/// by setting the [LearningPathStepRevision]'s foreign key `stepId` to refer to the [LearningPathStep].
Future<void> step(
_isd.DatabaseSession session,
LearningPathStepRevision learningPathStepRevision,
_i0qbf905.LearningPathStep step, {
_isd.Transaction? transaction,
}) async {
if (learningPathStepRevision.id == null) {
throw ArgumentError.notNull('learningPathStepRevision.id');
}
if (step.id == null) {
throw ArgumentError.notNull('step.id');
}
var $learningPathStepRevision = learningPathStepRevision.copyWith(
stepId: step.id,
);
await session.db.updateRow<LearningPathStepRevision>(
$learningPathStepRevision,
columns: [LearningPathStepRevision.t.stepId],
transaction: transaction,
);
}
}

View file

@ -32,12 +32,17 @@ 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 'learning_path/learning_path.dart' as _ivt5m868;
import 'learning_path/learning_path_answer.dart' as _iy9sv3bs;
import 'learning_path/learning_path_step.dart' as _irnpk9dc;
import 'learning_path/learning_path_step_revision.dart' as _i59qfu1l;
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/learning_path_tag.dart' as _i31v2f8r;
import 'tag/note_tag.dart' as _itkfn57x;
import 'tag/tag.dart' as _ie8t3ar4;
export 'asset/asset.dart';
@ -51,12 +56,17 @@ export 'card/card.dart';
export 'card/card_revision.dart';
export 'common/not_found_exception.dart';
export 'instance/instance_info.dart';
export 'learning_path/learning_path.dart';
export 'learning_path/learning_path_answer.dart';
export 'learning_path/learning_path_step.dart';
export 'learning_path/learning_path_step_revision.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/learning_path_tag.dart';
export 'tag/note_tag.dart';
export 'tag/tag.dart';
export 'client.dart';
@ -485,6 +495,533 @@ class Protocol extends _isd.DatabaseSerializationManager {
],
managed: true,
),
_isd.TableDefinition(
name: 'learning_path',
dartName: 'LearningPath',
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',
),
_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: 'learning_path_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: 'learning_path_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: 'learning_path_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'learning_path_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: 'learning_path_answer',
dartName: 'LearningPathAnswer',
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: 'stepId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'answeredAt',
columnType: _isd.ColumnType.timestampWithoutTimeZone,
isNullable: false,
dartType: 'DateTime',
),
_isd.ColumnDefinition(
name: 'outcome',
columnType: _isd.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isd.ColumnDefinition(
name: 'answer',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'learning_path_answer_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: 'learning_path_answer_fk_1',
columns: ['stepId'],
referenceTable: 'learning_path_step',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'learning_path_answer_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'learning_path_answer_step_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'stepId',
),
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'answeredAt',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'learning_path_step',
dartName: 'LearningPathStep',
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: 'pathId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'position',
columnType: _isd.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isd.ColumnDefinition(
name: 'kind',
columnType: _isd.ColumnType.text,
isNullable: false,
dartType: 'String',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'learning_path_step_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: 'learning_path_step_fk_1',
columns: ['pathId'],
referenceTable: 'learning_path',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'learning_path_step_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'learning_path_step_path_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'pathId',
),
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'position',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'learning_path_step_revision',
dartName: 'LearningPathStepRevision',
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: 'stepId',
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: 'learning_path_step_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: 'learning_path_step_revision_fk_1',
columns: ['stepId'],
referenceTable: 'learning_path_step',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'learning_path_step_revision_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'learning_path_step_revision_step_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'stepId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'learning_path_tag',
dartName: 'LearningPathTag',
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: 'pathId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'tagId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'learning_path_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: 'learning_path_tag_fk_1',
columns: ['pathId'],
referenceTable: 'learning_path',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
_isd.ForeignKeyDefinition(
constraintName: 'learning_path_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: 'learning_path_tag_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'learning_path_tag_path_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'pathId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'learning_path_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: 'note',
dartName: 'Note',
@ -1240,6 +1777,18 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _iskrg2g3.InstanceInfo) {
return _iskrg2g3.InstanceInfo.fromJson(data) as T;
}
if (t == _ivt5m868.LearningPath) {
return _ivt5m868.LearningPath.fromJson(data) as T;
}
if (t == _iy9sv3bs.LearningPathAnswer) {
return _iy9sv3bs.LearningPathAnswer.fromJson(data) as T;
}
if (t == _irnpk9dc.LearningPathStep) {
return _irnpk9dc.LearningPathStep.fromJson(data) as T;
}
if (t == _i59qfu1l.LearningPathStepRevision) {
return _i59qfu1l.LearningPathStepRevision.fromJson(data) as T;
}
if (t == _i9njjhyv.Note) {
return _i9njjhyv.Note.fromJson(data) as T;
}
@ -1258,6 +1807,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _i6jz6nyq.CardTag) {
return _i6jz6nyq.CardTag.fromJson(data) as T;
}
if (t == _i31v2f8r.LearningPathTag) {
return _i31v2f8r.LearningPathTag.fromJson(data) as T;
}
if (t == _itkfn57x.NoteTag) {
return _itkfn57x.NoteTag.fromJson(data) as T;
}
@ -1308,6 +1860,23 @@ 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<_ivt5m868.LearningPath?>()) {
return (data != null ? _ivt5m868.LearningPath.fromJson(data) : null) as T;
}
if (t == _isc.getType<_iy9sv3bs.LearningPathAnswer?>()) {
return (data != null ? _iy9sv3bs.LearningPathAnswer.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_irnpk9dc.LearningPathStep?>()) {
return (data != null ? _irnpk9dc.LearningPathStep.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_i59qfu1l.LearningPathStepRevision?>()) {
return (data != null
? _i59qfu1l.LearningPathStepRevision.fromJson(data)
: null)
as T;
}
if (t == _isc.getType<_i9njjhyv.Note?>()) {
return (data != null ? _i9njjhyv.Note.fromJson(data) : null) as T;
}
@ -1327,6 +1896,10 @@ 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<_i31v2f8r.LearningPathTag?>()) {
return (data != null ? _i31v2f8r.LearningPathTag.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_itkfn57x.NoteTag?>()) {
return (data != null ? _itkfn57x.NoteTag.fromJson(data) : null) as T;
}
@ -1368,12 +1941,17 @@ class Protocol extends _isd.DatabaseSerializationManager {
_ipbyuef1.CardRevision => 'CardRevision',
_i8oee73b.NotFoundException => 'NotFoundException',
_iskrg2g3.InstanceInfo => 'InstanceInfo',
_ivt5m868.LearningPath => 'LearningPath',
_iy9sv3bs.LearningPathAnswer => 'LearningPathAnswer',
_irnpk9dc.LearningPathStep => 'LearningPathStep',
_i59qfu1l.LearningPathStepRevision => 'LearningPathStepRevision',
_i9njjhyv.Note => 'Note',
_i6j5dng4.NoteRevision => 'NoteRevision',
_irzjbdqz.ReviewLog => 'ReviewLog',
_irnhx5e5.PersonSettings => 'PersonSettings',
_ikbclmae.Studyset => 'Studyset',
_i6jz6nyq.CardTag => 'CardTag',
_i31v2f8r.LearningPathTag => 'LearningPathTag',
_itkfn57x.NoteTag => 'NoteTag',
_ie8t3ar4.Tag => 'Tag',
_ => null,
@ -1415,6 +1993,14 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'NotFoundException';
case _iskrg2g3.InstanceInfo():
return 'InstanceInfo';
case _ivt5m868.LearningPath():
return 'LearningPath';
case _iy9sv3bs.LearningPathAnswer():
return 'LearningPathAnswer';
case _irnpk9dc.LearningPathStep():
return 'LearningPathStep';
case _i59qfu1l.LearningPathStepRevision():
return 'LearningPathStepRevision';
case _i9njjhyv.Note():
return 'Note';
case _i6j5dng4.NoteRevision():
@ -1427,6 +2013,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'Studyset';
case _i6jz6nyq.CardTag():
return 'CardTag';
case _i31v2f8r.LearningPathTag():
return 'LearningPathTag';
case _itkfn57x.NoteTag():
return 'NoteTag';
case _ie8t3ar4.Tag():
@ -1492,6 +2080,18 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'InstanceInfo') {
return deserialize<_iskrg2g3.InstanceInfo>(data['data']);
}
if (dataClassName == 'LearningPath') {
return deserialize<_ivt5m868.LearningPath>(data['data']);
}
if (dataClassName == 'LearningPathAnswer') {
return deserialize<_iy9sv3bs.LearningPathAnswer>(data['data']);
}
if (dataClassName == 'LearningPathStep') {
return deserialize<_irnpk9dc.LearningPathStep>(data['data']);
}
if (dataClassName == 'LearningPathStepRevision') {
return deserialize<_i59qfu1l.LearningPathStepRevision>(data['data']);
}
if (dataClassName == 'Note') {
return deserialize<_i9njjhyv.Note>(data['data']);
}
@ -1510,6 +2110,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'CardTag') {
return deserialize<_i6jz6nyq.CardTag>(data['data']);
}
if (dataClassName == 'LearningPathTag') {
return deserialize<_i31v2f8r.LearningPathTag>(data['data']);
}
if (dataClassName == 'NoteTag') {
return deserialize<_itkfn57x.NoteTag>(data['data']);
}
@ -1573,6 +2176,14 @@ class Protocol extends _isd.DatabaseSerializationManager {
return _ipiw1cmx.Card.t;
case _ipbyuef1.CardRevision:
return _ipbyuef1.CardRevision.t;
case _ivt5m868.LearningPath:
return _ivt5m868.LearningPath.t;
case _iy9sv3bs.LearningPathAnswer:
return _iy9sv3bs.LearningPathAnswer.t;
case _irnpk9dc.LearningPathStep:
return _irnpk9dc.LearningPathStep.t;
case _i59qfu1l.LearningPathStepRevision:
return _i59qfu1l.LearningPathStepRevision.t;
case _i9njjhyv.Note:
return _i9njjhyv.Note.t;
case _i6j5dng4.NoteRevision:
@ -1585,6 +2196,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
return _ikbclmae.Studyset.t;
case _i6jz6nyq.CardTag:
return _i6jz6nyq.CardTag.t;
case _i31v2f8r.LearningPathTag:
return _i31v2f8r.LearningPathTag.t;
case _itkfn57x.NoteTag:
return _itkfn57x.NoteTag.t;
case _ie8t3ar4.Tag:

View file

@ -14,12 +14,17 @@ 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 'learning_path/learning_path.dart' as _ivt5m868;
import 'learning_path/learning_path_answer.dart' as _iy9sv3bs;
import 'learning_path/learning_path_step.dart' as _irnpk9dc;
import 'learning_path/learning_path_step_revision.dart' as _i59qfu1l;
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/learning_path_tag.dart' as _i31v2f8r;
import 'tag/note_tag.dart' as _itkfn57x;
import 'tag/tag.dart' as _ie8t3ar4;
@ -31,6 +36,11 @@ final List<_isd.Table> syncTables = [
_ipiw1cmx.Card.t,
_ipbyuef1.CardRevision.t,
_i6jz6nyq.CardTag.t,
_ivt5m868.LearningPath.t,
_iy9sv3bs.LearningPathAnswer.t,
_irnpk9dc.LearningPathStep.t,
_i59qfu1l.LearningPathStepRevision.t,
_i31v2f8r.LearningPathTag.t,
_i9njjhyv.Note.t,
_i6j5dng4.NoteRevision.t,
_itkfn57x.NoteTag.t,

View file

@ -0,0 +1,799 @@
/* 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 '../learning_path/learning_path.dart' as _ixd0fbzl;
import '../tag/tag.dart' as _isqed5gg;
/// A learning path having a tag, as CardTag is for cards (ADR 0004): one row per
/// assignment, never updated, so concurrent edits merge without losing either; a
/// path with the same tag twice counts once. Paths share the person's tags with
/// cards and notes. Synced; goes with its path and its tag.
abstract class LearningPathTag
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
LearningPathTag._({
this.id,
this.spaceId,
required this.pathId,
this.path,
required this.tagId,
this.tag,
});
factory LearningPathTag({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue pathId,
_ixd0fbzl.LearningPath? path,
required _isc.UuidValue tagId,
_isqed5gg.Tag? tag,
}) = _LearningPathTagImpl;
factory LearningPathTag.fromJson(Map<String, dynamic> jsonSerialization) {
return LearningPathTag(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
pathId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['pathId']),
path: jsonSerialization['path'] == null
? null
: _itx5j3f5.Protocol().deserialize<_ixd0fbzl.LearningPath>(
jsonSerialization['path'],
),
tagId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['tagId']),
tag: jsonSerialization['tag'] == null
? null
: _itx5j3f5.Protocol().deserialize<_isqed5gg.Tag>(
jsonSerialization['tag'],
),
);
}
static final t = LearningPathTagTable();
static const db = LearningPathTagRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue pathId;
/// The path tagged.
_ixd0fbzl.LearningPath? path;
_isc.UuidValue tagId;
/// The tag it has.
_isqed5gg.Tag? tag;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [LearningPathTag]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
LearningPathTag copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? pathId,
_ixd0fbzl.LearningPath? path,
_isc.UuidValue? tagId,
_isqed5gg.Tag? tag,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'LearningPathTag',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'pathId': pathId.toJson(),
if (path != null) 'path': path?.toJson(),
'tagId': tagId.toJson(),
if (tag != null) 'tag': tag?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'LearningPathTag',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'pathId': pathId.toJson(),
if (path != null) 'path': path?.toJsonForProtocol(),
'tagId': tagId.toJson(),
if (tag != null) 'tag': tag?.toJsonForProtocol(),
};
}
static LearningPathTagInclude include({
_ixd0fbzl.LearningPathInclude? path,
_isqed5gg.TagInclude? tag,
}) {
return LearningPathTagInclude._(
path: path,
tag: tag,
);
}
static LearningPathTagIncludeList includeList({
_isd.WhereExpressionBuilder<LearningPathTagTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathTagTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTagTable>? orderByList,
LearningPathTagInclude? include,
}) {
return LearningPathTagIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(LearningPathTag.t),
orderByList: orderByList?.call(LearningPathTag.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _LearningPathTagImpl extends LearningPathTag {
_LearningPathTagImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue pathId,
_ixd0fbzl.LearningPath? path,
required _isc.UuidValue tagId,
_isqed5gg.Tag? tag,
}) : super._(
id: id,
spaceId: spaceId,
pathId: pathId,
path: path,
tagId: tagId,
tag: tag,
);
/// Returns a shallow copy of this [LearningPathTag]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
LearningPathTag copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? pathId,
Object? path = _Undefined,
_isc.UuidValue? tagId,
Object? tag = _Undefined,
}) {
return LearningPathTag(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
pathId: pathId ?? this.pathId,
path: path is _ixd0fbzl.LearningPath? ? path : this.path?.copyWith(),
tagId: tagId ?? this.tagId,
tag: tag is _isqed5gg.Tag? ? tag : this.tag?.copyWith(),
);
}
}
class LearningPathTagUpdateTable
extends _isd.UpdateTable<LearningPathTagTable> {
LearningPathTagUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> pathId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.pathId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> tagId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.tagId,
value,
);
}
class LearningPathTagTable extends _isd.Table<_isc.UuidValue?> {
LearningPathTagTable({super.tableRelation})
: super(tableName: 'learning_path_tag') {
updateTable = LearningPathTagUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
pathId = _isd.ColumnUuid(
'pathId',
this,
);
tagId = _isd.ColumnUuid(
'tagId',
this,
);
}
late final LearningPathTagUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid pathId;
/// The path tagged.
_ixd0fbzl.LearningPathTable? _path;
late final _isd.ColumnUuid tagId;
/// The tag it has.
_isqed5gg.TagTable? _tag;
_ixd0fbzl.LearningPathTable get path {
if (_path != null) return _path!;
_path = _isd.createRelationTable(
relationFieldName: 'path',
field: LearningPathTag.t.pathId,
foreignField: _ixd0fbzl.LearningPath.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_ixd0fbzl.LearningPathTable(tableRelation: foreignTableRelation),
);
return _path!;
}
_isqed5gg.TagTable get tag {
if (_tag != null) return _tag!;
_tag = _isd.createRelationTable(
relationFieldName: 'tag',
field: LearningPathTag.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,
pathId,
tagId,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'path') {
return path;
}
if (relationField == 'tag') {
return tag;
}
return null;
}
}
class LearningPathTagInclude extends _isd.IncludeObject {
LearningPathTagInclude._({
_ixd0fbzl.LearningPathInclude? path,
_isqed5gg.TagInclude? tag,
}) {
_path = path;
_tag = tag;
}
_ixd0fbzl.LearningPathInclude? _path;
_isqed5gg.TagInclude? _tag;
@override
Map<String, _isd.Include?> get includes => {
'path': _path,
'tag': _tag,
};
@override
_isd.Table<_isc.UuidValue?> get table => LearningPathTag.t;
}
class LearningPathTagIncludeList extends _isd.IncludeList {
LearningPathTagIncludeList._({
_isd.WhereExpressionBuilder<LearningPathTagTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(LearningPathTag.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => LearningPathTag.t;
}
class LearningPathTagRepository {
const LearningPathTagRepository._();
final attachRow = const LearningPathTagAttachRowRepository._();
/// Returns a list of [LearningPathTag]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<LearningPathTag>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<LearningPathTagTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathTagTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTagTable>? orderByList,
_isd.Transaction? transaction,
LearningPathTagInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<LearningPathTag>(
where: where?.call(LearningPathTag.t),
orderBy: orderBy?.call(LearningPathTag.t),
orderByList: orderByList?.call(LearningPathTag.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [LearningPathTag] 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<LearningPathTag?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<LearningPathTagTable>? where,
int? offset,
_isd.OrderByBuilder<LearningPathTagTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTagTable>? orderByList,
_isd.Transaction? transaction,
LearningPathTagInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<LearningPathTag>(
where: where?.call(LearningPathTag.t),
orderBy: orderBy?.call(LearningPathTag.t),
orderByList: orderByList?.call(LearningPathTag.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [LearningPathTag] by its [id] or null if no such row exists.
Future<LearningPathTag?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
LearningPathTagInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<LearningPathTag>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [LearningPathTag]s in the list and returns the inserted rows.
///
/// The returned [LearningPathTag]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<LearningPathTag>> insert(
_isd.DatabaseSession session,
List<LearningPathTag> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<LearningPathTag>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [LearningPathTag] and returns the inserted row.
///
/// The returned [LearningPathTag] will have its `id` field set.
Future<LearningPathTag> insertRow(
_isd.DatabaseSession session,
LearningPathTag row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<LearningPathTag>(
row,
transaction: transaction,
);
}
/// Upserts all [LearningPathTag]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 [LearningPathTag]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<LearningPathTag>> upsert(
_isd.DatabaseSession session,
List<LearningPathTag> rows, {
required _isd.ColumnSelections<LearningPathTagTable> conflictColumns,
_isd.ColumnSelections<LearningPathTagTable>? updateColumns,
_isd.WhereExpressionBuilder<LearningPathTagTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<LearningPathTag>(
rows,
conflictColumns: conflictColumns(LearningPathTag.t),
updateColumns: updateColumns?.call(LearningPathTag.t),
updateWhere: updateWhere?.call(LearningPathTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [LearningPathTag] 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 [LearningPathTag] will have its `id` field set.
Future<LearningPathTag?> upsertRow(
_isd.DatabaseSession session,
LearningPathTag row, {
required _isd.ColumnSelections<LearningPathTagTable> conflictColumns,
_isd.ColumnSelections<LearningPathTagTable>? updateColumns,
_isd.WhereExpressionBuilder<LearningPathTagTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<LearningPathTag>(
row,
conflictColumns: conflictColumns(LearningPathTag.t),
updateColumns: updateColumns?.call(LearningPathTag.t),
updateWhere: updateWhere?.call(LearningPathTag.t),
transaction: transaction,
);
}
/// Updates all [LearningPathTag]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<LearningPathTag>> update(
_isd.DatabaseSession session,
List<LearningPathTag> rows, {
_isd.ColumnSelections<LearningPathTagTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<LearningPathTag>(
rows,
columns: columns?.call(LearningPathTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [LearningPathTag]. 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<LearningPathTag> updateRow(
_isd.DatabaseSession session,
LearningPathTag row, {
_isd.ColumnSelections<LearningPathTagTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<LearningPathTag>(
row,
columns: columns?.call(LearningPathTag.t),
transaction: transaction,
);
}
/// Updates a single [LearningPathTag] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<LearningPathTag?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<LearningPathTagUpdateTable>
columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<LearningPathTag>(
id,
columnValues: columnValues(LearningPathTag.t.updateTable),
transaction: transaction,
);
}
/// Updates all [LearningPathTag]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<LearningPathTag>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<LearningPathTagUpdateTable>
columnValues,
required _isd.WhereExpressionBuilder<LearningPathTagTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<LearningPathTagTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<LearningPathTag>(
columnValues: columnValues(LearningPathTag.t.updateTable),
where: where(LearningPathTag.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(LearningPathTag.t),
orderByList: orderByList?.call(LearningPathTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [LearningPathTag]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<LearningPathTag>> delete(
_isd.DatabaseSession session,
List<LearningPathTag> rows, {
_isd.OrderByBuilder<LearningPathTagTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<LearningPathTag>(
rows,
orderBy: orderBy?.call(LearningPathTag.t),
orderByList: orderByList?.call(LearningPathTag.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [LearningPathTag].
Future<LearningPathTag> deleteRow(
_isd.DatabaseSession session,
LearningPathTag row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<LearningPathTag>(
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<LearningPathTag>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<LearningPathTagTable> where,
_isd.OrderByBuilder<LearningPathTagTable>? orderBy,
_isd.OrderByListBuilder<LearningPathTagTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<LearningPathTag>(
where: where(LearningPathTag.t),
orderBy: orderBy?.call(LearningPathTag.t),
orderByList: orderByList?.call(LearningPathTag.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<LearningPathTagTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<LearningPathTag>(
where: where?.call(LearningPathTag.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [LearningPathTag] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<LearningPathTagTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<LearningPathTag>(
where: where(LearningPathTag.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class LearningPathTagAttachRowRepository {
const LearningPathTagAttachRowRepository._();
/// Creates a relation between the given [LearningPathTag] and [LearningPath]
/// by setting the [LearningPathTag]'s foreign key `pathId` to refer to the [LearningPath].
Future<void> path(
_isd.DatabaseSession session,
LearningPathTag learningPathTag,
_ixd0fbzl.LearningPath path, {
_isd.Transaction? transaction,
}) async {
if (learningPathTag.id == null) {
throw ArgumentError.notNull('learningPathTag.id');
}
if (path.id == null) {
throw ArgumentError.notNull('path.id');
}
var $learningPathTag = learningPathTag.copyWith(pathId: path.id);
await session.db.updateRow<LearningPathTag>(
$learningPathTag,
columns: [LearningPathTag.t.pathId],
transaction: transaction,
);
}
/// Creates a relation between the given [LearningPathTag] and [Tag]
/// by setting the [LearningPathTag]'s foreign key `tagId` to refer to the [Tag].
Future<void> tag(
_isd.DatabaseSession session,
LearningPathTag learningPathTag,
_isqed5gg.Tag tag, {
_isd.Transaction? transaction,
}) async {
if (learningPathTag.id == null) {
throw ArgumentError.notNull('learningPathTag.id');
}
if (tag.id == null) {
throw ArgumentError.notNull('tag.id');
}
var $learningPathTag = learningPathTag.copyWith(tagId: tag.id);
await session.db.updateRow<LearningPathTag>(
$learningPathTag,
columns: [LearningPathTag.t.tagId],
transaction: transaction,
);
}
}

View file

@ -21,6 +21,11 @@ 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/learning_path/data/learning_path_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_learning_path_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_path_answer_gateway.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';
@ -159,15 +164,22 @@ Future<PersonDependencies> _openPerson(
final notes = NoteRepository(
SyncNoteGateway(db, sync, person.authUserId),
);
final paths = LearningPathRepository(
SyncLearningPathGateway(db, sync, person.authUserId),
);
final pathProgress = PathProgressRepository(SyncPathAnswerGateway(db, sync));
final tags = TagRepository(
SyncTagGateway(db, sync),
cardChanges: cards.changes,
noteChanges: notes.changes,
pathChanges: paths.changes,
);
final settings = SettingsRepository(SyncSettingsGateway(db, sync));
final reviews = ReviewRepository(SyncReviewGateway(db, sync));
final deletions = StudysetDeletions(studysets, cards, notes);
final deletions = StudysetDeletions(studysets, cards, notes, paths);
final noteDeletions = NoteDeletions(notes);
final pathDeletions = LearningPathDeletions(paths);
final stepDeletions = StepDeletions(paths);
final assets = AssetRepository(
SyncAssetGateway(db, sync),
ServerpodAssetServerGateway(client),
@ -179,24 +191,32 @@ Future<PersonDependencies> _openPerson(
studysets: studysets,
cards: cards,
notes: notes,
paths: paths,
pathProgress: pathProgress,
reviews: reviews,
tags: tags,
settings: settings,
assets: assets,
deletions: deletions,
noteDeletions: noteDeletions,
pathDeletions: pathDeletions,
stepDeletions: stepDeletions,
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 pathDeletions.dispose();
await stepDeletions.dispose();
await studysets.dispose();
await tags.dispose();
await settings.dispose();
await reviews.dispose();
await cards.dispose();
await notes.dispose();
await paths.dispose();
await pathProgress.dispose();
await assets.dispose();
await sync.dispose();
await db.close();

View file

@ -1,5 +1,8 @@
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/learning_path/data/learning_path_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_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';
@ -23,10 +26,16 @@ class PersonDependencies({
/// The person's notes.
required final NoteRepository notes,
/// The person's learning paths and their steps.
required final LearningPathRepository paths,
/// Where the person stands in their learning paths.
required final PathProgressRepository pathProgress,
/// The person's reviews.
required final ReviewRepository reviews,
/// The person's tags and which cards and notes have them.
/// The person's tags and which cards, notes and paths have them.
required final TagRepository tags,
/// The person's settings, which follow them to every device.
@ -40,6 +49,12 @@ class PersonDependencies({
/// The note deletions that can still be undone.
required final NoteDeletions noteDeletions,
/// The learning path deletions that can still be undone.
required final LearningPathDeletions pathDeletions,
/// The deletions of paths' steps that can still be undone.
required final StepDeletions stepDeletions,
final Future<void> Function()? _onDispose,
}) {
/// Closes what was opened for the person.

View file

@ -0,0 +1,18 @@
import 'package:halkyon_learn_flutter/src/core/async/delayed_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
/// The person's deletions of learning paths, which can be undone for a
/// moment (see [DelayedDeletions]); once the time to undo is over, the path
/// is blanked and deleted with its steps.
class LearningPathDeletions(LearningPathRepository paths, {super.undoWindow})
extends DelayedDeletions {
this : super(paths.deletePath, logName: 'Paths');
}
/// The person's deletions of steps of learning paths, which can be undone
/// for a moment (see [DelayedDeletions]); once the time to undo is over, the
/// step is blanked and deleted with its answers.
class StepDeletions(LearningPathRepository paths, {super.undoWindow})
extends DelayedDeletions {
this : super(paths.deleteStep, logName: 'Paths');
}

View file

@ -0,0 +1,79 @@
import 'dart:typed_data';
/// A path's row as the device's database stores it.
typedef PathRow = ({
String id,
String studysetId,
String position,
ByteData payload,
});
/// A step's row as the device's database stores it.
typedef StepRow = ({String id, String pathId, String position, String kind});
/// A step revision's row as the device's database stores it.
typedef StepRevisionRow = ({
String id,
String stepId,
Set<String> parents,
ByteData payload,
});
/// The boundary to the learning paths and their steps in this person's local,
/// synced database. Tests replace it with a fake.
abstract interface class LearningPathGateway() {
/// The paths of the studyset [studysetId], unordered; every path's without.
Future<List<PathRow>> paths({String? studysetId});
/// Stores a new path; returns its id.
Future<String> insertPath({
required String studysetId,
required String position,
required ByteData payload,
});
/// Replaces the path [id]'s payload; the only column that changes.
Future<void> updatePathPayload(String id, ByteData payload);
/// Moves the path [id] to [position]; the only column that changes.
Future<void> updatePathPosition(String id, String position);
/// Deletes the path [id] and its tags; its steps go first, through
/// [deleteStep].
Future<void> deletePath(String id);
/// The steps of the paths [pathIds], unordered.
Future<List<StepRow>> steps(Set<String> pathIds);
/// The revisions of the steps [stepIds], unordered.
Future<List<StepRevisionRow>> revisions(Set<String> stepIds);
/// Stores a new step with its first revision; returns both ids.
Future<({String stepId, String revisionId})> insertStep({
required String pathId,
required String position,
required String kind,
required ByteData payload,
});
/// Appends a revision to the step [stepId] that replaces [parents];
/// returns its id.
Future<String> insertRevision({
required String stepId,
required Set<String> parents,
required ByteData payload,
});
/// Moves the step [id] to [position]; the only column that changes.
Future<void> updateStepPosition(String id, String position);
/// Blanks the payload of every revision of the step [stepId], and of the
/// path [pathId] when given.
Future<void> blank({String? stepId, String? pathId});
/// Deletes the step [id], its revisions and its answers.
Future<void> deleteStep(String id);
/// Fires when a sync brought changes from the person's other devices.
Stream<void> get remoteChanges;
}

View file

@ -0,0 +1,43 @@
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/learning_path/model/learning_path.dart';
// A learning path's content as its synced row stores it (see
// core/content/payload). Schema 1: the title, the secondary title (left out
// when none) and the description.
/// The schema this app writes.
const learningPathSchemaVersion = 1;
/// Encodes a path's content into its payload.
ByteData encodeLearningPathPayload(LearningPathContent content) =>
encodePayload(learningPathSchemaVersion, {
'title': content.title,
'secondaryTitle': ?content.secondaryTitle,
'description': content.description.toJson(),
});
/// Decodes a path's payload.
///
/// Throws a [FormatException] for anything else, including content written
/// by a newer app version in a schema this one doesn't know.
LearningPathContent decodeLearningPathPayload(ByteData payload) {
final json = decodePayload(payload, newestSchema: learningPathSchemaVersion);
if (json case {
'title': final String title,
'description': final Map<String, Object?> description,
}) {
return LearningPathContent(
title: title,
secondaryTitle: switch (json['secondaryTitle']) {
null => null,
final String secondary => secondary,
_ => throw const FormatException('A malformed secondary title.'),
},
description: RichContent.fromJson(description),
);
}
throw const FormatException('Not a learning path payload.');
}

View file

@ -0,0 +1,307 @@
import 'dart:async';
import 'dart:developer';
import 'package:fractional_indexing_dart/fractional_indexing_dart.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_step_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/exercise.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path_step.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/step_content.dart';
/// The person's learning paths and their steps (decided 2026-10-02): a
/// path's own content in its row, the last writer winning, as a studyset's;
/// a step's content as append-only revisions merged per field (ADR 0003), as
/// a card's; both ordered by a fractional index.
class LearningPathRepository(final LearningPathGateway _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 path or step, made on this device or
/// synced from another.
Stream<void> get changes => _changes.stream;
/// The paths of [studysetId] in order, now and after every change.
Stream<List<LearningPath>> watchPaths(String studysetId) =>
_watch(() => pathsOf(studysetId), 'paths');
/// The paths of [studysetId] on this device right now, in order.
Future<List<LearningPath>> pathsOf(String studysetId) async => [
for (final row in await _gateway.paths(studysetId: studysetId))
?_decodePath(row),
]..sort((a, b) => _byPosition(a.position, a.id, b.position, b.id));
/// How many paths each studyset holds, by the studyset's id; studysets
/// without paths are left out.
Future<Map<String, int>> countsByStudyset() async {
final counts = <String, int>{};
for (final path in await _gateway.paths()) {
counts.update(path.studysetId, (count) => count + 1, ifAbsent: () => 1);
}
return counts;
}
/// Creates a path with [content] at the end of [studysetId]; returns its
/// id.
Future<String> createPath(
String studysetId,
LearningPathContent content,
) async {
final id = await _gateway.insertPath(
studysetId: studysetId,
position: FractionalIndexing.generateKeyBetween(
_last([
for (final row in await _gateway.paths(studysetId: studysetId))
row.position,
]),
null,
),
payload: encodeLearningPathPayload(content.normalized),
);
_changes.add(null);
return id;
}
/// Replaces the path [id]'s content; the last writer wins.
Future<void> updatePath(String id, LearningPathContent content) async {
await _gateway.updatePathPayload(
id,
encodeLearningPathPayload(content.normalized),
);
_changes.add(null);
}
/// Moves the path [id] to [toIndex] among the paths of [studysetId].
Future<void> movePath(
String studysetId,
String id, {
required int toIndex,
}) async {
final others = [
for (final path in await pathsOf(studysetId))
if (path.id != id) (id: path.id, position: path.position),
];
await _gateway.updatePathPosition(id, _between(others, toIndex));
_changes.add(null);
}
/// Deletes the path [id] with its steps, each blanked first (ADR 0002,
/// rule 2), then the path's own content.
Future<void> deletePath(String id) async {
await _deletePathNow(id);
_changes.add(null);
}
/// Deletes every path of [studysetId] as [deletePath] does, before the
/// studyset itself goes.
Future<void> deleteAllOf(String studysetId) async {
for (final path in await _gateway.paths(studysetId: studysetId)) {
await _deletePathNow(path.id);
}
_changes.add(null);
}
/// The steps of [pathId] in order, now and after every change.
Stream<List<LearningPathStep>> watchSteps(String pathId) =>
_watch(() => stepsOf(pathId), 'steps');
/// The steps of [pathId] on this device right now, in order; steps of a
/// kind this app doesn't know, or that it can't read, are left out.
Future<List<LearningPathStep>> stepsOf(String pathId) async {
final rows = await _gateway.steps({pathId});
final kinds = {for (final row in rows) row.id: ?StepKind.of(row.kind)};
final revisionsByStep = <String, List<StepRevision>>{};
for (final row in await _gateway.revisions(kinds.keys.toSet())) {
if (_decodeRevision(row, kinds[row.stepId]!) case final revision?) {
(revisionsByStep[row.stepId] ??= []).add(revision);
}
}
return [
for (final row in rows)
if (kinds[row.id] case final kind?)
if (mergeStepRevisions(revisionsByStep[row.id] ?? const [])
case final merged?)
LearningPathStep(
id: row.id,
position: row.position,
kind: kind,
content: merged.content,
heads: merged.heads,
conflicts: merged.conflicts,
),
]..sort((a, b) => _byPosition(a.position, a.id, b.position, b.id));
}
/// Creates a step of [kind] with [content] at the end of [pathId]; returns
/// its id and its first revision's, for the next edit to build on.
Future<({String stepId, String revisionId})> createStep(
String pathId,
StepKind kind,
StepContent content,
) async {
final ids = await _gateway.insertStep(
pathId: pathId,
position: FractionalIndexing.generateKeyBetween(
_last([
for (final row in await _gateway.steps({pathId})) row.position,
]),
null,
),
kind: kind.value,
payload: encodeStepPayload(content.normalized),
);
_changes.add(null);
return ids;
}
/// Stores [content] as the next revision of the step [stepId], replacing
/// [basedOn]: the revisions the edit started from (all current heads to
/// settle a conflict), so another device's edit merges with it (ADR 0003).
/// Returns the new revision's id.
Future<String> saveStep(
String stepId,
StepContent content, {
required Set<String> basedOn,
}) async {
final id = await _gateway.insertRevision(
stepId: stepId,
parents: basedOn,
payload: encodeStepPayload(content.normalized),
);
_changes.add(null);
return id;
}
/// Moves the step [id] to [toIndex] among the steps of [pathId].
Future<void> moveStep(
String pathId,
String id, {
required int toIndex,
}) async {
final others = [
for (final row in await _gateway.steps({pathId}))
if (row.id != id) (id: row.id, position: row.position),
]..sort((a, b) => _byPosition(a.position, a.id, b.position, b.id));
await _gateway.updateStepPosition(id, _between(others, toIndex));
_changes.add(null);
}
/// Deletes the step [id], its revisions blanked first (ADR 0002, rule 2).
Future<void> deleteStep(String id) async {
await _gateway.blank(stepId: id);
await _gateway.deleteStep(id);
_changes.add(null);
}
/// Stops following remote changes.
Future<void> dispose() async {
await _remoteChanges.cancel();
await _changes.close();
}
Future<void> _deletePathNow(String id) async {
for (final step in await _gateway.steps({id})) {
await _gateway.blank(stepId: step.id);
await _gateway.deleteStep(step.id);
}
await _gateway.blank(pathId: id);
await _gateway.deletePath(id);
}
/// [read] now and after every change, one read after another, so an older
/// list never overtakes a newer one. A failed read arrives as an error;
/// the next change reads again.
Stream<T> _watch<T>(Future<T> Function() read, String what) =>
Stream.multi((listener) {
var cancelled = false;
var reading = Future<void>.value();
void next() => reading = reading.then((_) async {
if (cancelled) {
return;
}
try {
final value = await read();
if (!cancelled) {
listener.add(value);
}
} on Object catch (error, st) {
log('Reading $what failed', name: 'Paths', error: error);
if (!cancelled) {
listener.addError(error, st);
}
}
});
final subscription = _changes.stream.listen((_) => next());
listener.onCancel = () {
cancelled = true;
return subscription.cancel();
};
next();
});
LearningPath? _decodePath(PathRow row) {
try {
return LearningPath(
id: row.id,
studysetId: row.studysetId,
position: row.position,
content: decodeLearningPathPayload(row.payload),
);
} on FormatException catch (error) {
log('Skipped path ${row.id}: ${error.message}', name: 'Paths');
return null;
}
}
StepRevision? _decodeRevision(StepRevisionRow row, StepKind kind) {
try {
return StepRevision(
id: row.id,
parents: row.parents,
content: decodeStepPayload(row.payload, kind),
);
} on FormatException catch (error) {
log('Skipped step revision ${row.id}: ${error.message}', name: 'Paths');
return null;
}
}
}
/// The greatest of [positions], or null for none.
String? _last(Iterable<String> positions) {
String? last;
for (final position in positions) {
if (last == null || position.compareTo(last) > 0) {
last = position;
}
}
return last;
}
/// A position at [index] among [others], in order. Two devices appending
/// at once can pick the same position; the key then goes after every item
/// sharing the lower one.
String _between(List<({String id, String position})> others, int index) {
final at = index.clamp(0, others.length);
final lower = at > 0 ? others[at - 1].position : null;
String? upper;
for (final other in others.skip(at)) {
if (lower == null || other.position.compareTo(lower) > 0) {
upper = other.position;
break;
}
}
return FractionalIndexing.generateKeyBetween(lower, upper);
}
/// By position; ties by id, which sorts by creation.
int _byPosition(String a, String aId, String b, String bId) =>
switch (a.compareTo(b)) {
0 => aId.compareTo(bId),
final order => order,
};

View file

@ -0,0 +1,221 @@
import 'dart:convert';
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/learning_path/model/answer.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/exercise.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/step_content.dart';
// A step revision's content as its synced row stores it (see
// core/content/payload). Schema 1: {title, body, exercise, rightFeedback,
// wrongFeedback, sources}. The exercise's JSON is its kind's, which the step's
// row holds:
//
// - single choice: {"options": ["…"], "correct": 0}
// - multiple choice: {"options": ["…"], "correct": [0, 2]}
// - drag and drop: {"targets": ["…"], "items": [{"text": "…", "target": 0}]}
// - fill in the blank: {"passage": "… ___ …", "blanks": ["…"]}
//
// An info step has none. A source is {"authors", "year", "title", "url"}, each
// left out when empty but the title.
/// The schema this app writes.
const stepSchemaVersion = 1;
/// Encodes a step's content into a revision's payload.
ByteData encodeStepPayload(StepContent content) =>
encodePayload(stepSchemaVersion, {
'title': content.title,
'body': content.body.toJson(),
'exercise': ?switch (content.exercise) {
null => null,
final exercise => exerciseToJson(exercise),
},
'rightFeedback': content.rightFeedback.toJson(),
'wrongFeedback': content.wrongFeedback.toJson(),
'sources': [
for (final source in content.sources)
{
if (source.authors.isNotEmpty) 'authors': source.authors,
if (source.year.isNotEmpty) 'year': source.year,
'title': source.title,
if (source.url.isNotEmpty) 'url': source.url,
},
],
});
/// Decodes a step revision's payload, its exercise as one of [kind]'s.
///
/// Throws a [FormatException] for anything else, including content written
/// by a newer app version in a schema this one doesn't know.
StepContent decodeStepPayload(ByteData payload, StepKind kind) {
final json = decodePayload(payload, newestSchema: stepSchemaVersion);
if (json case {
'title': final String title,
'body': final Map<String, Object?> body,
'rightFeedback': final Map<String, Object?> rightFeedback,
'wrongFeedback': final Map<String, Object?> wrongFeedback,
'sources': final List<Object?> sources,
}) {
return StepContent(
title: title,
body: RichContent.fromJson(body),
exercise: switch (json['exercise']) {
null when !kind.isExercise => null,
final Object exercise when kind.isExercise => exerciseFromJson(
kind,
exercise,
),
_ => throw FormatException('A $kind step with a malformed exercise.'),
},
rightFeedback: RichContent.fromJson(rightFeedback),
wrongFeedback: RichContent.fromJson(wrongFeedback),
sources: [for (final source in sources) _source(source)],
);
}
throw const FormatException('Not a learning path step payload.');
}
/// [exercise] as its kind's JSON.
Map<String, Object?> exerciseToJson(Exercise exercise) => switch (exercise) {
SingleChoice(:final options, :final correct) => {
'options': options,
'correct': correct,
},
MultipleChoice(:final options, :final correct) => {
'options': options,
'correct': [...correct]..sort(),
},
DragAndDrop(:final targets, :final items) => {
'targets': targets,
'items': [
for (final item in items) {'text': item.text, 'target': item.target},
],
},
FillInTheBlank(:final passage, :final blanks) => {
'passage': passage,
'blanks': blanks,
},
};
/// The exercise of [kind] in [json]; a [FormatException] for anything else.
Exercise exerciseFromJson(StepKind kind, Object json) => switch ((kind, json)) {
(
StepKind.singleChoice,
{'options': final List<Object?> options, 'correct': final int? correct},
) =>
SingleChoice(options: _strings(options), correct: correct),
(
StepKind.multipleChoice,
{
'options': final List<Object?> options,
'correct': final List<Object?> correct,
},
) =>
MultipleChoice(options: _strings(options), correct: _ints(correct).toSet()),
(
StepKind.dragAndDrop,
{
'targets': final List<Object?> targets,
'items': final List<Object?> items,
},
) =>
DragAndDrop(
targets: _strings(targets),
items: [
for (final item in items)
switch (item) {
{'text': final String text, 'target': final int target} => DragItem(
text: text,
target: target,
),
_ => throw const FormatException('A malformed item.'),
},
],
),
(
StepKind.fillInTheBlank,
{'passage': final String passage, 'blanks': final List<Object?> blanks},
) =>
FillInTheBlank(passage: passage, blanks: _strings(blanks)),
_ => throw FormatException('Not a $kind exercise.'),
};
/// [answer] as the answer log stores it: its kind's JSON.
String encodeAnswer(ExerciseAnswer answer) => jsonEncode(switch (answer) {
SingleChoiceAnswer(:final selected) => {'selected': selected},
MultipleChoiceAnswer(:final selected) => {
'selected': [...selected]..sort(),
},
DragAndDropAnswer(:final placements) => {
'placements': {
for (final MapEntry(key: item, value: target) in placements.entries)
'$item': target,
},
},
FillInTheBlankAnswer(:final words) => {'words': words},
});
/// The answer to a step of [kind] in [text], as [encodeAnswer] wrote it; a
/// [FormatException] for anything else.
ExerciseAnswer decodeAnswer(StepKind kind, String text) =>
switch ((kind, jsonDecode(text))) {
(StepKind.singleChoice, {'selected': final int selected}) =>
SingleChoiceAnswer(selected),
(StepKind.multipleChoice, {'selected': final List<Object?> selected}) =>
MultipleChoiceAnswer(_ints(selected).toSet()),
(
StepKind.dragAndDrop,
{'placements': final Map<String, Object?> placements},
) =>
DragAndDropAnswer({
for (final MapEntry(key: item, value: target) in placements.entries)
int.parse(item): switch (target) {
final int target => target,
_ => throw const FormatException('A malformed placement.'),
},
}),
(StepKind.fillInTheBlank, {'words': final List<Object?> words}) =>
FillInTheBlankAnswer([
for (final word in words)
switch (word) {
null => null,
final String word => word,
_ => throw const FormatException('A malformed word.'),
},
]),
_ => throw FormatException('Not an answer to a $kind step.'),
};
StepSource _source(Object? json) => switch (json) {
{'title': final String title} => StepSource(
title: title,
authors: _optional(json, 'authors'),
year: _optional(json, 'year'),
url: _optional(json, 'url'),
),
_ => throw const FormatException('A malformed source.'),
};
String _optional(Map<Object?, Object?> json, String key) => switch (json[key]) {
null => '',
final String value => value,
_ => throw FormatException('A source with a malformed $key.'),
};
List<String> _strings(List<Object?> json) => [
for (final value in json)
switch (value) {
final String text => text,
_ => throw const FormatException('Text expected.'),
},
];
List<int> _ints(List<Object?> json) => [
for (final value in json)
switch (value) {
final int number => number,
_ => throw const FormatException('A number expected.'),
},
];

View file

@ -0,0 +1,28 @@
/// An answer's row as the device's database stores it.
typedef AnswerRow = ({
String id,
String stepId,
DateTime answeredAt,
String outcome,
String? answer,
});
/// The boundary to the answer log of learning paths in this person's local,
/// synced database. Tests replace it with a fake.
abstract interface class PathAnswerGateway() {
/// The answers to the steps [stepIds], unordered; every answer without.
Future<List<AnswerRow>> answers({Set<String>? stepIds});
/// Appends [answers]; the ids given are ignored, each row gets its own.
Future<void> insert(
List<({String stepId, DateTime answeredAt, String outcome, String? answer})>
answers,
);
/// Deletes the fresh starts of the steps [stepIds] made at [at], to undo
/// them.
Future<void> deleteResets(Set<String> stepIds, DateTime at);
/// Fires when a sync brought changes from the person's other devices.
Stream<void> get remoteChanges;
}

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