feat(train): the sync engine and Train's first models (ADR 0030, D14)

Train's server syncs with serverpod_offline_sync as Learn's does, and
holds three synced tables, every payload sealed on the devices:
content_key (the person's data key, sealed to the account), workout (its
type, times, name and totals, changed when edited) and workout_series
(chunks of a workout's samples, written once). A person's space goes
with them, with everything in it.

The migration only adds: the three tables, each indexed on spaceId and
cascading with its space (a series also with its workout), and the sync
engine's own tables.

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-28 11:13:04 +01:00
commit 7b20851a0a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
21 changed files with 31210 additions and 5 deletions

View file

@ -3,7 +3,14 @@ type: server
client_package_path: ../halkyon_train_client
server_test_tools_path: test/integration/test_tools
# Private data sync through serverpod_offline_sync (ADR 0001, ADR 0030's D12): its
# module, and the experimental `database: sync` for the models it syncs.
modules:
serverpod_offline_sync:
nickname: offline_sync
# What every Halkyon server has (ADR 0014): `client.modules.base` in the app.
halkyon_base:
nickname: base
experimental_features:
databaseSync: true

View file

@ -19,6 +19,8 @@ import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
as _iacs;
import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
as _iais;
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart'
as _izehhkf5;
export 'future_calls.dart' show ServerpodFutureCallsGetter;
class Endpoints extends _is.EndpointDispatch {
@ -28,6 +30,8 @@ class Endpoints extends _is.EndpointDispatch {
..initializeEndpoints(server);
modules['serverpod_auth_idp'] = _iais.Endpoints()
..initializeEndpoints(server);
modules['serverpod_offline_sync'] = _izehhkf5.Endpoints()
..initializeEndpoints(server);
modules['halkyon_oidc'] = _ipoqfiqr.Endpoints()
..initializeEndpoints(server);
modules['serverpod_auth_core'] = _iacs.Endpoints()

View file

@ -0,0 +1,646 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod/serverpod.dart' as _is;
/// A data key of the person's (ADR 0030, D7 and D15): the one their workouts
/// and everything else of Train's are sealed with. Sealed on a device to the
/// account's X25519 key and signed by it (halkyon_keys' `sealDataKey`), under
/// an ID that says what it is for, so only the person's devices open it and
/// the instance can't relabel it; the instance keeps it as it is. Synced, so
/// the key arrives with the data, works offline and goes with the space.
/// Never updated.
abstract class ContentKey
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
ContentKey._({
this.id,
this.spaceId,
required this.dataKeyId,
required this.sealedKey,
});
factory ContentKey({
_is.UuidValue? id,
int? spaceId,
required String dataKeyId,
required _idt.ByteData sealedKey,
}) = _ContentKeyImpl;
factory ContentKey.fromJson(Map<String, dynamic> jsonSerialization) {
return ContentKey(
id: jsonSerialization['id'] == null
? null
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
dataKeyId: jsonSerialization['dataKeyId'] as String,
sealedKey: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['sealedKey'],
),
);
}
static final t = ContentKeyTable();
static const db = ContentKeyRepository._();
@override
_is.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
/// The key's ID, which the rows it seals name in their `keyId`; sealed
/// with the key too, so the column is never trusted.
String dataKeyId;
/// The sealed key: the sealer's Ed25519 signature, then the sealed box.
_idt.ByteData sealedKey;
@override
_is.Table<_is.UuidValue?> get table => t;
/// Returns a shallow copy of this [ContentKey]
/// with some or all fields replaced by the given arguments.
@_is.useResult
ContentKey copyWith({
_is.UuidValue? id,
int? spaceId,
String? dataKeyId,
_idt.ByteData? sealedKey,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'ContentKey',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'dataKeyId': dataKeyId,
'sealedKey': sealedKey.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'ContentKey',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'dataKeyId': dataKeyId,
'sealedKey': sealedKey.toJson(),
};
}
static ContentKeyInclude include() {
return ContentKeyInclude._();
}
static ContentKeyIncludeList includeList({
_is.WhereExpressionBuilder<ContentKeyTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<ContentKeyTable>? orderBy,
_is.OrderByListBuilder<ContentKeyTable>? orderByList,
ContentKeyInclude? include,
}) {
return ContentKeyIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(ContentKey.t),
orderByList: orderByList?.call(ContentKey.t),
include: include,
);
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _ContentKeyImpl extends ContentKey {
_ContentKeyImpl({
_is.UuidValue? id,
int? spaceId,
required String dataKeyId,
required _idt.ByteData sealedKey,
}) : super._(
id: id,
spaceId: spaceId,
dataKeyId: dataKeyId,
sealedKey: sealedKey,
);
/// Returns a shallow copy of this [ContentKey]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
ContentKey copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
String? dataKeyId,
_idt.ByteData? sealedKey,
}) {
return ContentKey(
id: id is _is.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
dataKeyId: dataKeyId ?? this.dataKeyId,
sealedKey: sealedKey ?? this.sealedKey.clone(),
);
}
}
class ContentKeyUpdateTable extends _is.UpdateTable<ContentKeyTable> {
ContentKeyUpdateTable(super.table);
_is.ColumnValue<int, int> spaceId(int? value) => _is.ColumnValue(
table.spaceId,
value,
);
_is.ColumnValue<String, String> dataKeyId(String value) => _is.ColumnValue(
table.dataKeyId,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> sealedKey(
_idt.ByteData value,
) => _is.ColumnValue(
table.sealedKey,
value,
);
}
class ContentKeyTable extends _is.Table<_is.UuidValue?> {
ContentKeyTable({super.tableRelation}) : super(tableName: 'content_key') {
updateTable = ContentKeyUpdateTable(this);
spaceId = _is.ColumnInt(
'spaceId',
this,
);
dataKeyId = _is.ColumnString(
'dataKeyId',
this,
);
sealedKey = _is.ColumnByteData(
'sealedKey',
this,
);
}
late final ContentKeyUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _is.ColumnInt spaceId;
/// The key's ID, which the rows it seals name in their `keyId`; sealed
/// with the key too, so the column is never trusted.
late final _is.ColumnString dataKeyId;
/// The sealed key: the sealer's Ed25519 signature, then the sealed box.
late final _is.ColumnByteData sealedKey;
@override
List<_is.Column> get columns => [
id,
spaceId,
dataKeyId,
sealedKey,
];
}
class ContentKeyInclude extends _is.IncludeObject {
ContentKeyInclude._();
@override
Map<String, _is.Include?> get includes => {};
@override
_is.Table<_is.UuidValue?> get table => ContentKey.t;
}
class ContentKeyIncludeList extends _is.IncludeList {
ContentKeyIncludeList._({
_is.WhereExpressionBuilder<ContentKeyTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(ContentKey.t);
}
@override
Map<String, _is.Include?> get includes => include?.includes ?? {};
@override
_is.Table<_is.UuidValue?> get table => ContentKey.t;
}
class ContentKeyRepository {
const ContentKeyRepository._();
/// Returns a list of [ContentKey]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<ContentKey>> find(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<ContentKeyTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<ContentKeyTable>? orderBy,
_is.OrderByListBuilder<ContentKeyTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.find<ContentKey>(
where: where?.call(ContentKey.t),
orderBy: orderBy?.call(ContentKey.t),
orderByList: orderByList?.call(ContentKey.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [ContentKey] 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<ContentKey?> findFirstRow(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<ContentKeyTable>? where,
int? offset,
_is.OrderByBuilder<ContentKeyTable>? orderBy,
_is.OrderByListBuilder<ContentKeyTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<ContentKey>(
where: where?.call(ContentKey.t),
orderBy: orderBy?.call(ContentKey.t),
orderByList: orderByList?.call(ContentKey.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [ContentKey] by its [id] or null if no such row exists.
Future<ContentKey?> findById(
_is.DatabaseSession session,
_is.UuidValue id, {
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findById<ContentKey>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [ContentKey]s in the list and returns the inserted rows.
///
/// The returned [ContentKey]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<ContentKey>> insert(
_is.DatabaseSession session,
List<ContentKey> rows, {
_is.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<ContentKey>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [ContentKey] and returns the inserted row.
///
/// The returned [ContentKey] will have its `id` field set.
Future<ContentKey> insertRow(
_is.DatabaseSession session,
ContentKey row, {
_is.Transaction? transaction,
}) async {
return session.db.insertRow<ContentKey>(
row,
transaction: transaction,
);
}
/// Upserts all [ContentKey]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 [ContentKey]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<ContentKey>> upsert(
_is.DatabaseSession session,
List<ContentKey> rows, {
required _is.ColumnSelections<ContentKeyTable> conflictColumns,
_is.ColumnSelections<ContentKeyTable>? updateColumns,
_is.WhereExpressionBuilder<ContentKeyTable>? updateWhere,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<ContentKey>(
rows,
conflictColumns: conflictColumns(ContentKey.t),
updateColumns: updateColumns?.call(ContentKey.t),
updateWhere: updateWhere?.call(ContentKey.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [ContentKey] 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 [ContentKey] will have its `id` field set.
Future<ContentKey?> upsertRow(
_is.DatabaseSession session,
ContentKey row, {
required _is.ColumnSelections<ContentKeyTable> conflictColumns,
_is.ColumnSelections<ContentKeyTable>? updateColumns,
_is.WhereExpressionBuilder<ContentKeyTable>? updateWhere,
_is.Transaction? transaction,
}) async {
return session.db.upsertRow<ContentKey>(
row,
conflictColumns: conflictColumns(ContentKey.t),
updateColumns: updateColumns?.call(ContentKey.t),
updateWhere: updateWhere?.call(ContentKey.t),
transaction: transaction,
);
}
/// Updates all [ContentKey]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<ContentKey>> update(
_is.DatabaseSession session,
List<ContentKey> rows, {
_is.ColumnSelections<ContentKeyTable>? columns,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<ContentKey>(
rows,
columns: columns?.call(ContentKey.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [ContentKey]. 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<ContentKey> updateRow(
_is.DatabaseSession session,
ContentKey row, {
_is.ColumnSelections<ContentKeyTable>? columns,
_is.Transaction? transaction,
}) async {
return session.db.updateRow<ContentKey>(
row,
columns: columns?.call(ContentKey.t),
transaction: transaction,
);
}
/// Updates a single [ContentKey] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<ContentKey?> updateById(
_is.DatabaseSession session,
_is.UuidValue id, {
required _is.ColumnValueListBuilder<ContentKeyUpdateTable> columnValues,
_is.Transaction? transaction,
}) async {
return session.db.updateById<ContentKey>(
id,
columnValues: columnValues(ContentKey.t.updateTable),
transaction: transaction,
);
}
/// Updates all [ContentKey]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<ContentKey>> updateWhere(
_is.DatabaseSession session, {
required _is.ColumnValueListBuilder<ContentKeyUpdateTable> columnValues,
required _is.WhereExpressionBuilder<ContentKeyTable> where,
int? limit,
int? offset,
_is.OrderByBuilder<ContentKeyTable>? orderBy,
_is.OrderByListBuilder<ContentKeyTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<ContentKey>(
columnValues: columnValues(ContentKey.t.updateTable),
where: where(ContentKey.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(ContentKey.t),
orderByList: orderByList?.call(ContentKey.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [ContentKey]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<ContentKey>> delete(
_is.DatabaseSession session,
List<ContentKey> rows, {
_is.OrderByBuilder<ContentKeyTable>? orderBy,
_is.OrderByListBuilder<ContentKeyTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<ContentKey>(
rows,
orderBy: orderBy?.call(ContentKey.t),
orderByList: orderByList?.call(ContentKey.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [ContentKey].
Future<ContentKey> deleteRow(
_is.DatabaseSession session,
ContentKey row, {
_is.Transaction? transaction,
}) async {
return session.db.deleteRow<ContentKey>(
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<ContentKey>> deleteWhere(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<ContentKeyTable> where,
_is.OrderByBuilder<ContentKeyTable>? orderBy,
_is.OrderByListBuilder<ContentKeyTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<ContentKey>(
where: where(ContentKey.t),
orderBy: orderBy?.call(ContentKey.t),
orderByList: orderByList?.call(ContentKey.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<ContentKeyTable>? where,
int? limit,
_is.Transaction? transaction,
}) async {
return session.db.count<ContentKey>(
where: where?.call(ContentKey.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [ContentKey] rows matching the [where] expression.
Future<void> lockRows(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<ContentKeyTable> where,
required _is.LockMode lockMode,
required _is.Transaction transaction,
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
}) async {
return session.db.lockRows<ContentKey>(
where: where(ContentKey.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -19,6 +19,15 @@ import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
as _iacs;
import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
as _iais;
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart'
as _izehhkf5;
import 'privacy/content_key.dart' as _imed3x5d;
import 'workout/workout.dart' as _i5b9b17z;
import 'workout/workout_series.dart' as _i92mefto;
export 'privacy/content_key.dart';
export 'workout/workout.dart';
export 'workout/workout_series.dart';
export 'sync_tables.dart';
class Protocol extends _is.DatabaseSerializationManager {
Protocol._();
@ -28,8 +37,234 @@ class Protocol extends _is.DatabaseSerializationManager {
static final Protocol _instance = Protocol._().._registerHostProtocols();
static List<_isp.TableDefinition> get targetTableDefinitions => [
_isp.TableDefinition(
name: 'content_key',
dartName: 'ContentKey',
schema: 'public',
module: 'halkyon_train',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isp.ColumnDefinition(
name: 'spaceId',
columnType: _isp.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isp.ColumnDefinition(
name: 'dataKeyId',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'sealedKey',
columnType: _isp.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
constraintName: 'content_key_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isp.IndexDefinition(
indexName: 'content_key_space_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isp.TableDefinition(
name: 'workout',
dartName: 'Workout',
schema: 'public',
module: 'halkyon_train',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isp.ColumnDefinition(
name: 'spaceId',
columnType: _isp.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isp.ColumnDefinition(
name: 'keyId',
columnType: _isp.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isp.ColumnDefinition(
name: 'payload',
columnType: _isp.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isp.ColumnDefinition(
name: 'nonce',
columnType: _isp.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
constraintName: 'workout_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isp.IndexDefinition(
indexName: 'workout_space_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isp.TableDefinition(
name: 'workout_series',
dartName: 'WorkoutSeries',
schema: 'public',
module: 'halkyon_train',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isp.ColumnDefinition(
name: 'spaceId',
columnType: _isp.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isp.ColumnDefinition(
name: 'workoutId',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isp.ColumnDefinition(
name: 'keyId',
columnType: _isp.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isp.ColumnDefinition(
name: 'payload',
columnType: _isp.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isp.ColumnDefinition(
name: 'nonce',
columnType: _isp.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
constraintName: 'workout_series_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
_isp.ForeignKeyDefinition(
constraintName: 'workout_series_fk_1',
columns: ['workoutId'],
referenceTable: 'workout',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isp.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isp.IndexDefinition(
indexName: 'workout_series_space_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isp.IndexDefinition(
indexName: 'workout_series_workout_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'workoutId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
..._ik8molhd.Protocol.targetTableDefinitions,
..._iais.Protocol.targetTableDefinitions,
..._izehhkf5.Protocol.targetTableDefinitions,
..._ipoqfiqr.Protocol.targetTableDefinitions,
..._iacs.Protocol.targetTableDefinitions,
..._isp.Protocol.targetTableDefinitions,
@ -62,12 +297,34 @@ class Protocol extends _is.DatabaseSerializationManager {
}
}
if (t == _imed3x5d.ContentKey) {
return _imed3x5d.ContentKey.fromJson(data) as T;
}
if (t == _i5b9b17z.Workout) {
return _i5b9b17z.Workout.fromJson(data) as T;
}
if (t == _i92mefto.WorkoutSeries) {
return _i92mefto.WorkoutSeries.fromJson(data) as T;
}
if (t == _is.getType<_imed3x5d.ContentKey?>()) {
return (data != null ? _imed3x5d.ContentKey.fromJson(data) : null) as T;
}
if (t == _is.getType<_i5b9b17z.Workout?>()) {
return (data != null ? _i5b9b17z.Workout.fromJson(data) : null) as T;
}
if (t == _is.getType<_i92mefto.WorkoutSeries?>()) {
return (data != null ? _i92mefto.WorkoutSeries.fromJson(data) : null)
as T;
}
try {
return _ik8molhd.Protocol().deserialize<T>(data, t);
} on _is.DeserializationTypeNotFoundException catch (_) {}
try {
return _iais.Protocol().deserialize<T>(data, t);
} on _is.DeserializationTypeNotFoundException catch (_) {}
try {
return _izehhkf5.Protocol().deserialize<T>(data, t);
} on _is.DeserializationTypeNotFoundException catch (_) {}
try {
return _ipoqfiqr.Protocol().deserialize<T>(data, t);
} on _is.DeserializationTypeNotFoundException catch (_) {}
@ -82,6 +339,9 @@ class Protocol extends _is.DatabaseSerializationManager {
static String? getClassNameForType(Type type) {
return switch (type) {
_imed3x5d.ContentKey => 'ContentKey',
_i5b9b17z.Workout => 'Workout',
_i92mefto.WorkoutSeries => 'WorkoutSeries',
_ => null,
};
}
@ -98,6 +358,14 @@ class Protocol extends _is.DatabaseSerializationManager {
);
}
switch (data) {
case _imed3x5d.ContentKey():
return 'ContentKey';
case _i5b9b17z.Workout():
return 'Workout';
case _i92mefto.WorkoutSeries():
return 'WorkoutSeries';
}
className = _ik8molhd.Protocol().getClassNameForObject(data);
if (className != null) {
return className.contains('.') ? className : 'halkyon_base.$className';
@ -108,6 +376,12 @@ class Protocol extends _is.DatabaseSerializationManager {
? className
: 'serverpod_auth_idp.$className';
}
className = _izehhkf5.Protocol().getClassNameForObject(data);
if (className != null) {
return className.contains('.')
? className
: 'serverpod_offline_sync.$className';
}
className = _ipoqfiqr.Protocol().getClassNameForObject(data);
if (className != null) {
return className.contains('.') ? className : 'halkyon_oidc.$className';
@ -131,6 +405,15 @@ class Protocol extends _is.DatabaseSerializationManager {
if (dataClassName is! String) {
return super.deserializeByClassName(data);
}
if (dataClassName == 'ContentKey') {
return deserialize<_imed3x5d.ContentKey>(data['data']);
}
if (dataClassName == 'Workout') {
return deserialize<_i5b9b17z.Workout>(data['data']);
}
if (dataClassName == 'WorkoutSeries') {
return deserialize<_i92mefto.WorkoutSeries>(data['data']);
}
if (dataClassName.startsWith('halkyon_base.')) {
data['className'] = dataClassName.substring(13);
return _ik8molhd.Protocol().deserializeByClassName(data);
@ -139,6 +422,10 @@ class Protocol extends _is.DatabaseSerializationManager {
data['className'] = dataClassName.substring(19);
return _iais.Protocol().deserializeByClassName(data);
}
if (dataClassName.startsWith('serverpod_offline_sync.')) {
data['className'] = dataClassName.substring(23);
return _izehhkf5.Protocol().deserializeByClassName(data);
}
if (dataClassName.startsWith('halkyon_oidc.')) {
data['className'] = dataClassName.substring(13);
return _ipoqfiqr.Protocol().deserializeByClassName(data);
@ -157,6 +444,7 @@ class Protocol extends _is.DatabaseSerializationManager {
void _registerHostProtocols() {
_ik8molhd.Protocol().registerHostProtocol('halkyon_train', this);
_iais.Protocol().registerHostProtocol('halkyon_train', this);
_izehhkf5.Protocol().registerHostProtocol('halkyon_train', this);
_ipoqfiqr.Protocol().registerHostProtocol('halkyon_train', this);
_iacs.Protocol().registerHostProtocol('halkyon_train', this);
}
@ -175,6 +463,12 @@ class Protocol extends _is.DatabaseSerializationManager {
return table;
}
}
{
var table = _izehhkf5.Protocol().getTableForType(t);
if (table != null) {
return table;
}
}
{
var table = _ipoqfiqr.Protocol().getTableForType(t);
if (table != null) {
@ -193,6 +487,14 @@ class Protocol extends _is.DatabaseSerializationManager {
return table;
}
}
switch (t) {
case _imed3x5d.ContentKey:
return _imed3x5d.ContentKey.t;
case _i5b9b17z.Workout:
return _i5b9b17z.Workout.t;
case _i92mefto.WorkoutSeries:
return _i92mefto.WorkoutSeries.t;
}
return null;
}
@ -218,6 +520,9 @@ class Protocol extends _is.DatabaseSerializationManager {
try {
return _iais.Protocol().mapRecordToJson(record);
} catch (_) {}
try {
return _izehhkf5.Protocol().mapRecordToJson(record);
} catch (_) {}
try {
return _ipoqfiqr.Protocol().mapRecordToJson(record);
} catch (_) {}

View file

@ -12,8 +12,11 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:io' as _idi;
import 'package:serverpod/serverpod.dart' as _is;
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart'
as _izehhkf5;
import 'endpoints.dart' as _iavctuc6;
import 'protocol.dart' as _il2as5qe;
import 'sync_tables.dart' as _ii3f3u05;
export 'package:serverpod/serverpod.dart' hide Serverpod;
export 'future_calls.dart' show ServerpodFutureCallsGetter;
@ -26,6 +29,10 @@ export 'future_calls.dart' show ServerpodFutureCallsGetter;
/// ```dart
/// final pod = Serverpod(args);
/// ```
///
/// The `serverpod_offline_sync` engine is initialized with the tables
/// declared with `database: sync`, wrapping any provided
/// [databaseInterceptor] with `offlineSyncDatabaseInterceptor`.
class Serverpod extends _is.Serverpod {
Serverpod(
List<String> args, {
@ -56,6 +63,19 @@ class Serverpod extends _is.Serverpod {
securityContextConfig: securityContextConfig,
experimentalFeatures: experimentalFeatures,
runtimeParametersBuilder: runtimeParametersBuilder,
databaseInterceptor: databaseInterceptor,
);
databaseInterceptor:
(
session,
inner,
) => _izehhkf5.offlineSyncDatabaseInterceptor(
session,
databaseInterceptor?.call(
session,
inner,
) ??
inner,
),
) {
initializeOfflineSync(syncTables: _ii3f3u05.syncTables);
}
}

View file

@ -0,0 +1,25 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod/serverpod.dart' as _is;
import 'privacy/content_key.dart' as _imed3x5d;
import 'workout/workout.dart' as _i5b9b17z;
import 'workout/workout_series.dart' as _i92mefto;
/// The tables synchronized between client and server by the
/// `serverpod_offline_sync` package, including the ones owned by modules and
/// shared packages.
final List<_is.Table> syncTables = [
_imed3x5d.ContentKey.t,
_i5b9b17z.Workout.t,
_i92mefto.WorkoutSeries.t,
];

View file

@ -0,0 +1,671 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod/serverpod.dart' as _is;
/// A workout (ADR 0030, D14): a run, a ride, a row. Everything about it is in
/// its sealed payload (D7): its type, when it started and ended, its name,
/// notes and totals; the instance sees that it exists, its size and when it
/// changed. Changed when edited, the last writer winning, as a studyset's
/// payload in Learn; its samples are its series (`workout_series`), which
/// never change. Synced.
abstract class Workout
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
Workout._({
this.id,
this.spaceId,
this.keyId,
required this.payload,
this.nonce,
});
factory Workout({
_is.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _WorkoutImpl;
factory Workout.fromJson(Map<String, dynamic> jsonSerialization) {
return Workout(
id: jsonSerialization['id'] == null
? null
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
keyId: jsonSerialization['keyId'] as String?,
payload: _is.ByteDataJsonExtension.fromJson(jsonSerialization['payload']),
nonce: jsonSerialization['nonce'] == null
? null
: _is.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = WorkoutTable();
static const db = WorkoutRepository._();
@override
_is.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
/// The key it is sealed with (ADR 0002's envelope); never null in Train,
/// where everything is sealed.
String? keyId;
/// The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
_idt.ByteData payload;
/// The sealing's nonce (ADR 0002's envelope).
_idt.ByteData? nonce;
@override
_is.Table<_is.UuidValue?> get table => t;
/// Returns a shallow copy of this [Workout]
/// with some or all fields replaced by the given arguments.
@_is.useResult
Workout copyWith({
_is.UuidValue? id,
int? spaceId,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'Workout',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'Workout',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static WorkoutInclude include() {
return WorkoutInclude._();
}
static WorkoutIncludeList includeList({
_is.WhereExpressionBuilder<WorkoutTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<WorkoutTable>? orderBy,
_is.OrderByListBuilder<WorkoutTable>? orderByList,
WorkoutInclude? include,
}) {
return WorkoutIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(Workout.t),
orderByList: orderByList?.call(Workout.t),
include: include,
);
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _WorkoutImpl extends Workout {
_WorkoutImpl({
_is.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [Workout]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
Workout copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return Workout(
id: id is _is.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class WorkoutUpdateTable extends _is.UpdateTable<WorkoutTable> {
WorkoutUpdateTable(super.table);
_is.ColumnValue<int, int> spaceId(int? value) => _is.ColumnValue(
table.spaceId,
value,
);
_is.ColumnValue<String, String> keyId(String? value) => _is.ColumnValue(
table.keyId,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_is.ColumnValue(
table.payload,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_is.ColumnValue(
table.nonce,
value,
);
}
class WorkoutTable extends _is.Table<_is.UuidValue?> {
WorkoutTable({super.tableRelation}) : super(tableName: 'workout') {
updateTable = WorkoutUpdateTable(this);
spaceId = _is.ColumnInt(
'spaceId',
this,
);
keyId = _is.ColumnString(
'keyId',
this,
);
payload = _is.ColumnByteData(
'payload',
this,
);
nonce = _is.ColumnByteData(
'nonce',
this,
);
}
late final WorkoutUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _is.ColumnInt spaceId;
/// The key it is sealed with (ADR 0002's envelope); never null in Train,
/// where everything is sealed.
late final _is.ColumnString keyId;
/// The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
late final _is.ColumnByteData payload;
/// The sealing's nonce (ADR 0002's envelope).
late final _is.ColumnByteData nonce;
@override
List<_is.Column> get columns => [
id,
spaceId,
keyId,
payload,
nonce,
];
}
class WorkoutInclude extends _is.IncludeObject {
WorkoutInclude._();
@override
Map<String, _is.Include?> get includes => {};
@override
_is.Table<_is.UuidValue?> get table => Workout.t;
}
class WorkoutIncludeList extends _is.IncludeList {
WorkoutIncludeList._({
_is.WhereExpressionBuilder<WorkoutTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(Workout.t);
}
@override
Map<String, _is.Include?> get includes => include?.includes ?? {};
@override
_is.Table<_is.UuidValue?> get table => Workout.t;
}
class WorkoutRepository {
const WorkoutRepository._();
/// Returns a list of [Workout]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<Workout>> find(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<WorkoutTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<WorkoutTable>? orderBy,
_is.OrderByListBuilder<WorkoutTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.find<Workout>(
where: where?.call(Workout.t),
orderBy: orderBy?.call(Workout.t),
orderByList: orderByList?.call(Workout.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [Workout] 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<Workout?> findFirstRow(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<WorkoutTable>? where,
int? offset,
_is.OrderByBuilder<WorkoutTable>? orderBy,
_is.OrderByListBuilder<WorkoutTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<Workout>(
where: where?.call(Workout.t),
orderBy: orderBy?.call(Workout.t),
orderByList: orderByList?.call(Workout.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [Workout] by its [id] or null if no such row exists.
Future<Workout?> findById(
_is.DatabaseSession session,
_is.UuidValue id, {
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findById<Workout>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [Workout]s in the list and returns the inserted rows.
///
/// The returned [Workout]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<Workout>> insert(
_is.DatabaseSession session,
List<Workout> rows, {
_is.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<Workout>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [Workout] and returns the inserted row.
///
/// The returned [Workout] will have its `id` field set.
Future<Workout> insertRow(
_is.DatabaseSession session,
Workout row, {
_is.Transaction? transaction,
}) async {
return session.db.insertRow<Workout>(
row,
transaction: transaction,
);
}
/// Upserts all [Workout]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 [Workout]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<Workout>> upsert(
_is.DatabaseSession session,
List<Workout> rows, {
required _is.ColumnSelections<WorkoutTable> conflictColumns,
_is.ColumnSelections<WorkoutTable>? updateColumns,
_is.WhereExpressionBuilder<WorkoutTable>? updateWhere,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<Workout>(
rows,
conflictColumns: conflictColumns(Workout.t),
updateColumns: updateColumns?.call(Workout.t),
updateWhere: updateWhere?.call(Workout.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [Workout] 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 [Workout] will have its `id` field set.
Future<Workout?> upsertRow(
_is.DatabaseSession session,
Workout row, {
required _is.ColumnSelections<WorkoutTable> conflictColumns,
_is.ColumnSelections<WorkoutTable>? updateColumns,
_is.WhereExpressionBuilder<WorkoutTable>? updateWhere,
_is.Transaction? transaction,
}) async {
return session.db.upsertRow<Workout>(
row,
conflictColumns: conflictColumns(Workout.t),
updateColumns: updateColumns?.call(Workout.t),
updateWhere: updateWhere?.call(Workout.t),
transaction: transaction,
);
}
/// Updates all [Workout]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<Workout>> update(
_is.DatabaseSession session,
List<Workout> rows, {
_is.ColumnSelections<WorkoutTable>? columns,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<Workout>(
rows,
columns: columns?.call(Workout.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [Workout]. 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<Workout> updateRow(
_is.DatabaseSession session,
Workout row, {
_is.ColumnSelections<WorkoutTable>? columns,
_is.Transaction? transaction,
}) async {
return session.db.updateRow<Workout>(
row,
columns: columns?.call(Workout.t),
transaction: transaction,
);
}
/// Updates a single [Workout] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<Workout?> updateById(
_is.DatabaseSession session,
_is.UuidValue id, {
required _is.ColumnValueListBuilder<WorkoutUpdateTable> columnValues,
_is.Transaction? transaction,
}) async {
return session.db.updateById<Workout>(
id,
columnValues: columnValues(Workout.t.updateTable),
transaction: transaction,
);
}
/// Updates all [Workout]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<Workout>> updateWhere(
_is.DatabaseSession session, {
required _is.ColumnValueListBuilder<WorkoutUpdateTable> columnValues,
required _is.WhereExpressionBuilder<WorkoutTable> where,
int? limit,
int? offset,
_is.OrderByBuilder<WorkoutTable>? orderBy,
_is.OrderByListBuilder<WorkoutTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<Workout>(
columnValues: columnValues(Workout.t.updateTable),
where: where(Workout.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(Workout.t),
orderByList: orderByList?.call(Workout.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [Workout]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<Workout>> delete(
_is.DatabaseSession session,
List<Workout> rows, {
_is.OrderByBuilder<WorkoutTable>? orderBy,
_is.OrderByListBuilder<WorkoutTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<Workout>(
rows,
orderBy: orderBy?.call(Workout.t),
orderByList: orderByList?.call(Workout.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [Workout].
Future<Workout> deleteRow(
_is.DatabaseSession session,
Workout row, {
_is.Transaction? transaction,
}) async {
return session.db.deleteRow<Workout>(
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<Workout>> deleteWhere(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<WorkoutTable> where,
_is.OrderByBuilder<WorkoutTable>? orderBy,
_is.OrderByListBuilder<WorkoutTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<Workout>(
where: where(Workout.t),
orderBy: orderBy?.call(Workout.t),
orderByList: orderByList?.call(Workout.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<WorkoutTable>? where,
int? limit,
_is.Transaction? transaction,
}) async {
return session.db.count<Workout>(
where: where?.call(Workout.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [Workout] rows matching the [where] expression.
Future<void> lockRows(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<WorkoutTable> where,
required _is.LockMode lockMode,
required _is.Transaction transaction,
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
}) async {
return session.db.lockRows<Workout>(
where: where(Workout.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -0,0 +1,786 @@
/* 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_train_server/src/generated/protocol.dart' as _iw76w3x7;
import 'package:serverpod/serverpod.dart' as _is;
import '../workout/workout.dart' as _ickoe38i;
/// A chunk of one of a workout's series (ADR 0030, D14): up to 10 minutes of
/// positions, heart rate, strokes or pauses, written once as the workout is
/// saved and never changed, as Learn's review log. Its kind, its time and
/// its samples are in its sealed payload (D7): gzipped JSON, each column a
/// list of whole-number differences from the value before, sealed for its
/// workout (`bound`), so the instance can't move it to another. Synced; it
/// goes with its workout.
abstract class WorkoutSeries
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
WorkoutSeries._({
this.id,
this.spaceId,
required this.workoutId,
this.workout,
this.keyId,
required this.payload,
this.nonce,
});
factory WorkoutSeries({
_is.UuidValue? id,
int? spaceId,
required _is.UuidValue workoutId,
_ickoe38i.Workout? workout,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _WorkoutSeriesImpl;
factory WorkoutSeries.fromJson(Map<String, dynamic> jsonSerialization) {
return WorkoutSeries(
id: jsonSerialization['id'] == null
? null
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
workoutId: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['workoutId'],
),
workout: jsonSerialization['workout'] == null
? null
: _iw76w3x7.Protocol().deserialize<_ickoe38i.Workout>(
jsonSerialization['workout'],
),
keyId: jsonSerialization['keyId'] as String?,
payload: _is.ByteDataJsonExtension.fromJson(jsonSerialization['payload']),
nonce: jsonSerialization['nonce'] == null
? null
: _is.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = WorkoutSeriesTable();
static const db = WorkoutSeriesRepository._();
@override
_is.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_is.UuidValue workoutId;
/// The workout it is part of.
_ickoe38i.Workout? workout;
/// The key it is sealed with (ADR 0002's envelope); never null in Train.
String? keyId;
/// The content, sealed: gzipped UTF-8 JSON with a `schemaVersion` once
/// opened.
_idt.ByteData payload;
/// The sealing's nonce (ADR 0002's envelope).
_idt.ByteData? nonce;
@override
_is.Table<_is.UuidValue?> get table => t;
/// Returns a shallow copy of this [WorkoutSeries]
/// with some or all fields replaced by the given arguments.
@_is.useResult
WorkoutSeries copyWith({
_is.UuidValue? id,
int? spaceId,
_is.UuidValue? workoutId,
_ickoe38i.Workout? workout,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'WorkoutSeries',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'workoutId': workoutId.toJson(),
if (workout != null) 'workout': workout?.toJson(),
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'WorkoutSeries',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'workoutId': workoutId.toJson(),
if (workout != null) 'workout': workout?.toJsonForProtocol(),
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static WorkoutSeriesInclude include({_ickoe38i.WorkoutInclude? workout}) {
return WorkoutSeriesInclude._(workout: workout);
}
static WorkoutSeriesIncludeList includeList({
_is.WhereExpressionBuilder<WorkoutSeriesTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<WorkoutSeriesTable>? orderBy,
_is.OrderByListBuilder<WorkoutSeriesTable>? orderByList,
WorkoutSeriesInclude? include,
}) {
return WorkoutSeriesIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(WorkoutSeries.t),
orderByList: orderByList?.call(WorkoutSeries.t),
include: include,
);
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _WorkoutSeriesImpl extends WorkoutSeries {
_WorkoutSeriesImpl({
_is.UuidValue? id,
int? spaceId,
required _is.UuidValue workoutId,
_ickoe38i.Workout? workout,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
workoutId: workoutId,
workout: workout,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [WorkoutSeries]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
WorkoutSeries copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_is.UuidValue? workoutId,
Object? workout = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return WorkoutSeries(
id: id is _is.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
workoutId: workoutId ?? this.workoutId,
workout: workout is _ickoe38i.Workout?
? workout
: this.workout?.copyWith(),
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class WorkoutSeriesUpdateTable extends _is.UpdateTable<WorkoutSeriesTable> {
WorkoutSeriesUpdateTable(super.table);
_is.ColumnValue<int, int> spaceId(int? value) => _is.ColumnValue(
table.spaceId,
value,
);
_is.ColumnValue<_is.UuidValue, _is.UuidValue> workoutId(
_is.UuidValue value,
) => _is.ColumnValue(
table.workoutId,
value,
);
_is.ColumnValue<String, String> keyId(String? value) => _is.ColumnValue(
table.keyId,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_is.ColumnValue(
table.payload,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_is.ColumnValue(
table.nonce,
value,
);
}
class WorkoutSeriesTable extends _is.Table<_is.UuidValue?> {
WorkoutSeriesTable({super.tableRelation})
: super(tableName: 'workout_series') {
updateTable = WorkoutSeriesUpdateTable(this);
spaceId = _is.ColumnInt(
'spaceId',
this,
);
workoutId = _is.ColumnUuid(
'workoutId',
this,
);
keyId = _is.ColumnString(
'keyId',
this,
);
payload = _is.ColumnByteData(
'payload',
this,
);
nonce = _is.ColumnByteData(
'nonce',
this,
);
}
late final WorkoutSeriesUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _is.ColumnInt spaceId;
late final _is.ColumnUuid workoutId;
/// The workout it is part of.
_ickoe38i.WorkoutTable? _workout;
/// The key it is sealed with (ADR 0002's envelope); never null in Train.
late final _is.ColumnString keyId;
/// The content, sealed: gzipped UTF-8 JSON with a `schemaVersion` once
/// opened.
late final _is.ColumnByteData payload;
/// The sealing's nonce (ADR 0002's envelope).
late final _is.ColumnByteData nonce;
_ickoe38i.WorkoutTable get workout {
if (_workout != null) return _workout!;
_workout = _is.createRelationTable(
relationFieldName: 'workout',
field: WorkoutSeries.t.workoutId,
foreignField: _ickoe38i.Workout.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_ickoe38i.WorkoutTable(tableRelation: foreignTableRelation),
);
return _workout!;
}
@override
List<_is.Column> get columns => [
id,
spaceId,
workoutId,
keyId,
payload,
nonce,
];
@override
_is.Table? getRelationTable(String relationField) {
if (relationField == 'workout') {
return workout;
}
return null;
}
}
class WorkoutSeriesInclude extends _is.IncludeObject {
WorkoutSeriesInclude._({_ickoe38i.WorkoutInclude? workout}) {
_workout = workout;
}
_ickoe38i.WorkoutInclude? _workout;
@override
Map<String, _is.Include?> get includes => {'workout': _workout};
@override
_is.Table<_is.UuidValue?> get table => WorkoutSeries.t;
}
class WorkoutSeriesIncludeList extends _is.IncludeList {
WorkoutSeriesIncludeList._({
_is.WhereExpressionBuilder<WorkoutSeriesTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(WorkoutSeries.t);
}
@override
Map<String, _is.Include?> get includes => include?.includes ?? {};
@override
_is.Table<_is.UuidValue?> get table => WorkoutSeries.t;
}
class WorkoutSeriesRepository {
const WorkoutSeriesRepository._();
final attachRow = const WorkoutSeriesAttachRowRepository._();
/// Returns a list of [WorkoutSeries]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<WorkoutSeries>> find(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<WorkoutSeriesTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<WorkoutSeriesTable>? orderBy,
_is.OrderByListBuilder<WorkoutSeriesTable>? orderByList,
_is.Transaction? transaction,
WorkoutSeriesInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.find<WorkoutSeries>(
where: where?.call(WorkoutSeries.t),
orderBy: orderBy?.call(WorkoutSeries.t),
orderByList: orderByList?.call(WorkoutSeries.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [WorkoutSeries] 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<WorkoutSeries?> findFirstRow(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<WorkoutSeriesTable>? where,
int? offset,
_is.OrderByBuilder<WorkoutSeriesTable>? orderBy,
_is.OrderByListBuilder<WorkoutSeriesTable>? orderByList,
_is.Transaction? transaction,
WorkoutSeriesInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<WorkoutSeries>(
where: where?.call(WorkoutSeries.t),
orderBy: orderBy?.call(WorkoutSeries.t),
orderByList: orderByList?.call(WorkoutSeries.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [WorkoutSeries] by its [id] or null if no such row exists.
Future<WorkoutSeries?> findById(
_is.DatabaseSession session,
_is.UuidValue id, {
_is.Transaction? transaction,
WorkoutSeriesInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findById<WorkoutSeries>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [WorkoutSeries]s in the list and returns the inserted rows.
///
/// The returned [WorkoutSeries]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<WorkoutSeries>> insert(
_is.DatabaseSession session,
List<WorkoutSeries> rows, {
_is.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<WorkoutSeries>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [WorkoutSeries] and returns the inserted row.
///
/// The returned [WorkoutSeries] will have its `id` field set.
Future<WorkoutSeries> insertRow(
_is.DatabaseSession session,
WorkoutSeries row, {
_is.Transaction? transaction,
}) async {
return session.db.insertRow<WorkoutSeries>(
row,
transaction: transaction,
);
}
/// Upserts all [WorkoutSeries]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 [WorkoutSeries]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<WorkoutSeries>> upsert(
_is.DatabaseSession session,
List<WorkoutSeries> rows, {
required _is.ColumnSelections<WorkoutSeriesTable> conflictColumns,
_is.ColumnSelections<WorkoutSeriesTable>? updateColumns,
_is.WhereExpressionBuilder<WorkoutSeriesTable>? updateWhere,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<WorkoutSeries>(
rows,
conflictColumns: conflictColumns(WorkoutSeries.t),
updateColumns: updateColumns?.call(WorkoutSeries.t),
updateWhere: updateWhere?.call(WorkoutSeries.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [WorkoutSeries] 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 [WorkoutSeries] will have its `id` field set.
Future<WorkoutSeries?> upsertRow(
_is.DatabaseSession session,
WorkoutSeries row, {
required _is.ColumnSelections<WorkoutSeriesTable> conflictColumns,
_is.ColumnSelections<WorkoutSeriesTable>? updateColumns,
_is.WhereExpressionBuilder<WorkoutSeriesTable>? updateWhere,
_is.Transaction? transaction,
}) async {
return session.db.upsertRow<WorkoutSeries>(
row,
conflictColumns: conflictColumns(WorkoutSeries.t),
updateColumns: updateColumns?.call(WorkoutSeries.t),
updateWhere: updateWhere?.call(WorkoutSeries.t),
transaction: transaction,
);
}
/// Updates all [WorkoutSeries]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<WorkoutSeries>> update(
_is.DatabaseSession session,
List<WorkoutSeries> rows, {
_is.ColumnSelections<WorkoutSeriesTable>? columns,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<WorkoutSeries>(
rows,
columns: columns?.call(WorkoutSeries.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [WorkoutSeries]. 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<WorkoutSeries> updateRow(
_is.DatabaseSession session,
WorkoutSeries row, {
_is.ColumnSelections<WorkoutSeriesTable>? columns,
_is.Transaction? transaction,
}) async {
return session.db.updateRow<WorkoutSeries>(
row,
columns: columns?.call(WorkoutSeries.t),
transaction: transaction,
);
}
/// Updates a single [WorkoutSeries] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<WorkoutSeries?> updateById(
_is.DatabaseSession session,
_is.UuidValue id, {
required _is.ColumnValueListBuilder<WorkoutSeriesUpdateTable> columnValues,
_is.Transaction? transaction,
}) async {
return session.db.updateById<WorkoutSeries>(
id,
columnValues: columnValues(WorkoutSeries.t.updateTable),
transaction: transaction,
);
}
/// Updates all [WorkoutSeries]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<WorkoutSeries>> updateWhere(
_is.DatabaseSession session, {
required _is.ColumnValueListBuilder<WorkoutSeriesUpdateTable> columnValues,
required _is.WhereExpressionBuilder<WorkoutSeriesTable> where,
int? limit,
int? offset,
_is.OrderByBuilder<WorkoutSeriesTable>? orderBy,
_is.OrderByListBuilder<WorkoutSeriesTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<WorkoutSeries>(
columnValues: columnValues(WorkoutSeries.t.updateTable),
where: where(WorkoutSeries.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(WorkoutSeries.t),
orderByList: orderByList?.call(WorkoutSeries.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [WorkoutSeries]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<WorkoutSeries>> delete(
_is.DatabaseSession session,
List<WorkoutSeries> rows, {
_is.OrderByBuilder<WorkoutSeriesTable>? orderBy,
_is.OrderByListBuilder<WorkoutSeriesTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<WorkoutSeries>(
rows,
orderBy: orderBy?.call(WorkoutSeries.t),
orderByList: orderByList?.call(WorkoutSeries.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [WorkoutSeries].
Future<WorkoutSeries> deleteRow(
_is.DatabaseSession session,
WorkoutSeries row, {
_is.Transaction? transaction,
}) async {
return session.db.deleteRow<WorkoutSeries>(
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<WorkoutSeries>> deleteWhere(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<WorkoutSeriesTable> where,
_is.OrderByBuilder<WorkoutSeriesTable>? orderBy,
_is.OrderByListBuilder<WorkoutSeriesTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<WorkoutSeries>(
where: where(WorkoutSeries.t),
orderBy: orderBy?.call(WorkoutSeries.t),
orderByList: orderByList?.call(WorkoutSeries.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<WorkoutSeriesTable>? where,
int? limit,
_is.Transaction? transaction,
}) async {
return session.db.count<WorkoutSeries>(
where: where?.call(WorkoutSeries.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [WorkoutSeries] rows matching the [where] expression.
Future<void> lockRows(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<WorkoutSeriesTable> where,
required _is.LockMode lockMode,
required _is.Transaction transaction,
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
}) async {
return session.db.lockRows<WorkoutSeries>(
where: where(WorkoutSeries.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class WorkoutSeriesAttachRowRepository {
const WorkoutSeriesAttachRowRepository._();
/// Creates a relation between the given [WorkoutSeries] and [Workout]
/// by setting the [WorkoutSeries]'s foreign key `workoutId` to refer to the [Workout].
Future<void> workout(
_is.DatabaseSession session,
WorkoutSeries workoutSeries,
_ickoe38i.Workout workout, {
_is.Transaction? transaction,
}) async {
if (workoutSeries.id == null) {
throw ArgumentError.notNull('workoutSeries.id');
}
if (workout.id == null) {
throw ArgumentError.notNull('workout.id');
}
var $workoutSeries = workoutSeries.copyWith(workoutId: workout.id);
await session.db.updateRow<WorkoutSeries>(
$workoutSeries,
columns: [WorkoutSeries.t.workoutId],
transaction: transaction,
);
}
}

View file

@ -0,0 +1,21 @@
### A data key of the person's (ADR 0030, D7 and D15): the one their workouts
### and everything else of Train's are sealed with. Sealed on a device to the
### account's X25519 key and signed by it (halkyon_keys' `sealDataKey`), under
### an ID that says what it is for, so only the person's devices open it and
### the instance can't relabel it; the instance keeps it as it is. Synced, so
### the key arrives with the data, works offline and goes with the space.
### Never updated.
class: ContentKey
table: content_key
database: sync
fields:
### The key's ID, which the rows it seals name in their `keyId`; sealed
### with the key too, so the column is never trusted.
dataKeyId: String
### The sealed key: the sealer's Ed25519 signature, then the sealed box.
sealedKey: ByteData
indexes:
# Deleting a space cascades into every synced table, and Postgres indexes no
# foreign key by itself: without this, deleting a person reads the table.
content_key_space_idx:
fields: spaceId

View file

@ -1,4 +1,5 @@
import 'package:halkyon_base_server/base.dart';
import 'package:halkyon_train_server/src/train_data.dart';
/// Where this project's source is published, which the instance names
/// unless its operator sets `SOURCE_URL`. Both licences require offering it
@ -16,8 +17,8 @@ const minimumAppBuild = 1;
/// Halkyon server its sign-in, the account and its deletion, the pruning and
/// the instance's basics: the instance says it serves `train` (the app accepts
/// only such an instance; never change it), sign-in mails name the app, and
/// the instance names this project's source. Train keeps nothing of a person's
/// yet, so it deletes nothing of its own with them. It holds the instance's
/// the instance names this project's source. A person's workouts go with
/// them ([deleteTrainData]). It holds the instance's
/// register, or is a member of the one [accounts] names (ADR 0016; the
/// server reads it from the operator's environment, `ACCOUNT_REGISTER_URL`).
/// The server sets it up before it starts (`server.dart`), and so do its
@ -28,5 +29,6 @@ HalkyonBase trainBase({AccountsMode accounts = const HoldsRegister()}) =>
appName: 'Halkyon Train',
upstreamSourceCodeUrl: upstreamSourceCodeUrl,
minimumAppBuild: minimumAppBuild,
deletePersonData: const [deleteTrainData],
accounts: accounts,
);

View file

@ -0,0 +1,30 @@
import 'package:halkyon_base_server/base.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
/// What Halkyon Train deletes of a person who goes (ADR 0014): halkyon_base's
/// `deletePerson` runs it in its transaction, once their sessions ended (a
/// device with a valid session would upload everything again) and before
/// their identities and the person go. Their personal space goes with
/// everything in it: the foreign keys cascade into every synced table (their
/// keys, workouts and series) and the sync engine's bookkeeping.
Future<AfterDeletion?> deleteTrainData(
Session session,
UuidValue authUserId,
Transaction transaction,
) async {
// DELETE FROM "offline_sync_space_members" WHERE "userUuid" = $1
await OfflineSyncSpaceMember.db.deleteWhere(
session,
where: (t) => t.userUuid.equals(authUserId),
transaction: transaction,
);
// The personal space is keyed on the person's id.
// DELETE FROM "offline_sync_spaces" WHERE "uuidSpaceId" = $1
await OfflineSyncSpace.db.deleteWhere(
session,
where: (t) => t.uuidSpaceId.equals(authUserId),
transaction: transaction,
);
return null;
}

View file

@ -0,0 +1,21 @@
### A workout (ADR 0030, D14): a run, a ride, a row. Everything about it is in
### its sealed payload (D7): its type, when it started and ended, its name,
### notes and totals; the instance sees that it exists, its size and when it
### changed. Changed when edited, the last writer winning, as a studyset's
### payload in Learn; its samples are its series (`workout_series`), which
### never change. Synced.
class: Workout
table: workout
database: sync
fields:
### The key it is sealed with (ADR 0002's envelope); never null in Train,
### where everything is sealed.
keyId: String?
### The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
payload: ByteData
### The sealing's nonce (ADR 0002's envelope).
nonce: ByteData?
indexes:
# Deleting a space finds its rows through this index (see content_key).
workout_space_idx:
fields: spaceId

View file

@ -0,0 +1,27 @@
### A chunk of one of a workout's series (ADR 0030, D14): up to 10 minutes of
### positions, heart rate, strokes or pauses, written once as the workout is
### saved and never changed, as Learn's review log. Its kind, its time and
### its samples are in its sealed payload (D7): gzipped JSON, each column a
### list of whole-number differences from the value before, sealed for its
### workout (`bound`), so the instance can't move it to another. Synced; it
### goes with its workout.
class: WorkoutSeries
table: workout_series
database: sync
fields:
### The workout it is part of.
workout: Workout?, relation(onDelete=Cascade, deferred)
### The key it is sealed with (ADR 0002's envelope); never null in Train.
keyId: String?
### The content, sealed: gzipped UTF-8 JSON with a `schemaVersion` once
### opened.
payload: ByteData
### The sealing's nonce (ADR 0002's envelope).
nonce: ByteData?
indexes:
# Deleting a space finds its rows through this index (see content_key).
workout_series_space_idx:
fields: spaceId
# A workout's chunks, to read them together.
workout_series_workout_idx:
fields: workoutId

View file

@ -0,0 +1,318 @@
{
"__className__": "serverpod.DatabaseDefinition",
"schemaVersion": 2,
"moduleName": "halkyon_train",
"tables": [
{
"__className__": "serverpod.TableDefinition",
"name": "content_key",
"dartName": "ContentKey",
"module": "halkyon_train",
"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": "dataKeyId",
"fieldName": "dataKeyId",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "sealedKey",
"fieldName": "sealedKey",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "content_key_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "content_key_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "workout",
"dartName": "Workout",
"module": "halkyon_train",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "workout_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "workout_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "workout_series",
"dartName": "WorkoutSeries",
"module": "halkyon_train",
"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": "workoutId",
"fieldName": "workoutId",
"columnType": 7,
"isNullable": false,
"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": "workout_series_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "workout_series_fk_1",
"columns": [
"workoutId"
],
"referenceTable": "workout",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "workout_series_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "workout_series_workout_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "workoutId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
],
"installedModules": [
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "halkyon_base",
"version": "20261026140329453-assistant-budgets"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod",
"version": "20260824182259319"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_idp",
"version": "20260924105404509"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_offline_sync",
"version": "20260914143806119"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "halkyon_oidc",
"version": "20261023180644988"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_core",
"version": "20260924105232991"
}
],
"migrationApiVersion": 1
}

View file

@ -0,0 +1,446 @@
BEGIN;
--
-- Function: gen_random_uuid_v7()
-- Source: https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74
-- License: MIT (copyright notice included on the generator source code).
--
create or replace function gen_random_uuid_v7()
returns uuid
as $$
begin
-- use random v4 uuid as starting point (which has the same variant we need)
-- then overlay timestamp
-- then set version 7 by flipping the 2 and 1 bit in the version 4 string
return encode(
set_bit(
set_bit(
overlay(uuid_send(gen_random_uuid())
placing substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3)
from 1 for 6
),
52, 1
),
53, 1
),
'hex')::uuid;
end
$$
language plpgsql
volatile;
--
-- ACTION CREATE TABLE
--
CREATE TABLE "content_key" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
"spaceId" bigint,
"dataKeyId" text NOT NULL,
"sealedKey" bytea NOT NULL
);
-- Indexes
CREATE INDEX "content_key_space_idx" ON "content_key" USING btree ("spaceId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "workout" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
"spaceId" bigint,
"keyId" text,
"payload" bytea NOT NULL,
"nonce" bytea
);
-- Indexes
CREATE INDEX "workout_space_idx" ON "workout" USING btree ("spaceId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "workout_series" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
"spaceId" bigint,
"workoutId" uuid NOT NULL,
"keyId" text,
"payload" bytea NOT NULL,
"nonce" bytea
);
-- Indexes
CREATE INDEX "workout_series_space_idx" ON "workout_series" USING btree ("spaceId");
CREATE INDEX "workout_series_workout_idx" ON "workout_series" USING btree ("workoutId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_attempted_value" (
"id" bigserial PRIMARY KEY,
"fieldId" bigint NOT NULL,
"value" jsonb NOT NULL,
"projectionReason" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_attempted_value__fieldId__unique_idx" ON "crdt_data_attempted_value" USING btree ("fieldId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_fields" (
"id" bigserial PRIMARY KEY,
"hlcDatetime" timestamp without time zone NOT NULL,
"hlcCounter" bigint NOT NULL,
"rowId" bigint NOT NULL,
"columnId" bigint NOT NULL,
"nodeId" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_fields_row_column_idx" ON "crdt_data_fields" USING btree ("rowId", "columnId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_rows" (
"id" bigserial PRIMARY KEY,
"hlcDatetime" timestamp without time zone NOT NULL,
"hlcCounter" bigint NOT NULL,
"spaceId" bigint NOT NULL,
"tblId" bigint NOT NULL,
"uuidRowId" uuid NOT NULL,
"nodeId" bigint NOT NULL,
"visibility" bigint NOT NULL DEFAULT 0
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_rows_space_tbl_row_idx" ON "crdt_data_rows" USING btree ("spaceId", "tblId", "uuidRowId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_tombstone" (
"id" bigserial PRIMARY KEY,
"hlcDatetime" timestamp without time zone NOT NULL,
"hlcCounter" bigint NOT NULL,
"rowId" bigint NOT NULL,
"nodeId" bigint NOT NULL,
"clFlag" bigint NOT NULL,
"reason" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_tombstone_row_idx" ON "crdt_data_tombstone" USING btree ("rowId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_nodes" (
"id" bigserial PRIMARY KEY,
"uuidNodeId" uuid NOT NULL DEFAULT gen_random_uuid_v7(),
"lastHlc" jsonb
);
-- Indexes
CREATE UNIQUE INDEX "crdt_nodes__uuidNodeId__unique_idx" ON "crdt_nodes" USING btree ("uuidNodeId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_schema_columns" (
"id" bigserial PRIMARY KEY,
"tblId" bigint NOT NULL,
"name" text NOT NULL,
"columnType" text NOT NULL,
"dartType" text NOT NULL,
"isNullable" boolean NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_schema_columns_table_column_idx" ON "crdt_schema_columns" USING btree ("tblId", "name");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_schema_tables" (
"id" bigserial PRIMARY KEY,
"name" text NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_schema_tables__name__unique_idx" ON "crdt_schema_tables" USING btree ("name");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_integrity_violations" (
"id" bigserial PRIMARY KEY,
"type" text NOT NULL,
"domainTableName" text NOT NULL,
"uuidRowId" uuid NOT NULL,
"ownerSpaceUuid" uuid,
"incomingSpaceUuid" uuid NOT NULL,
"operation" text NOT NULL,
"uuidNodeId" uuid,
"crdtDataRowId" bigint,
"hlcDatetime" timestamp without time zone,
"hlcCounter" bigint,
"firstSeenAt" timestamp without time zone NOT NULL,
"lastSeenAt" timestamp without time zone NOT NULL,
"occurrences" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_integrity_violations_key_idx" ON "offline_sync_integrity_violations" USING btree ("type", "operation", "domainTableName", "uuidRowId", "ownerSpaceUuid", "incomingSpaceUuid");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_space_members" (
"id" bigserial PRIMARY KEY,
"spaceId" bigint NOT NULL,
"userUuid" uuid NOT NULL,
"role" text NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_space_member_unique_idx" ON "offline_sync_space_members" USING btree ("userUuid", "spaceId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_space_nodes" (
"id" bigserial PRIMARY KEY,
"spaceId" bigint NOT NULL,
"nodeId" bigint NOT NULL,
"lastReceivedHlc" jsonb
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_space_node_unique_idx" ON "offline_sync_space_nodes" USING btree ("spaceId", "nodeId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_spaces" (
"id" bigserial PRIMARY KEY,
"uuidSpaceId" uuid NOT NULL DEFAULT gen_random_uuid_v7(),
"currentNodeId" bigint
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_spaces__uuidSpaceId__unique_idx" ON "offline_sync_spaces" USING btree ("uuidSpaceId");
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "content_key"
ADD CONSTRAINT "content_key_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "workout"
ADD CONSTRAINT "workout_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "workout_series"
ADD CONSTRAINT "workout_series_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "workout_series"
ADD CONSTRAINT "workout_series_fk_1"
FOREIGN KEY("workoutId")
REFERENCES "workout"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION
DEFERRABLE INITIALLY DEFERRED;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_attempted_value"
ADD CONSTRAINT "crdt_data_attempted_value_fk_0"
FOREIGN KEY("fieldId")
REFERENCES "crdt_data_fields"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_fields"
ADD CONSTRAINT "crdt_data_fields_fk_0"
FOREIGN KEY("rowId")
REFERENCES "crdt_data_rows"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_fields"
ADD CONSTRAINT "crdt_data_fields_fk_1"
FOREIGN KEY("columnId")
REFERENCES "crdt_schema_columns"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_fields"
ADD CONSTRAINT "crdt_data_fields_fk_2"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_rows"
ADD CONSTRAINT "crdt_data_rows_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_rows"
ADD CONSTRAINT "crdt_data_rows_fk_1"
FOREIGN KEY("tblId")
REFERENCES "crdt_schema_tables"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_rows"
ADD CONSTRAINT "crdt_data_rows_fk_2"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_tombstone"
ADD CONSTRAINT "crdt_data_tombstone_fk_0"
FOREIGN KEY("rowId")
REFERENCES "crdt_data_rows"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_tombstone"
ADD CONSTRAINT "crdt_data_tombstone_fk_1"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_schema_columns"
ADD CONSTRAINT "crdt_schema_columns_fk_0"
FOREIGN KEY("tblId")
REFERENCES "crdt_schema_tables"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "offline_sync_space_members"
ADD CONSTRAINT "offline_sync_space_members_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "offline_sync_space_nodes"
ADD CONSTRAINT "offline_sync_space_nodes_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "offline_sync_space_nodes"
ADD CONSTRAINT "offline_sync_space_nodes_fk_1"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "offline_sync_spaces"
ADD CONSTRAINT "offline_sync_spaces_fk_0"
FOREIGN KEY("currentNodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- MIGRATION VERSION FOR halkyon_train
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_train', '20261028101012641-workouts', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20261028101012641-workouts', "timestamp" = now();
--
-- MIGRATION VERSION FOR halkyon_base
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_base', '20261026140329453-assistant-budgets', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20261026140329453-assistant-budgets', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod', '20260824182259319', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260824182259319', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_auth_idp
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_auth_idp', '20260924105404509', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260924105404509', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_offline_sync
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_offline_sync', '20260914143806119', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260914143806119', "timestamp" = now();
--
-- MIGRATION VERSION FOR halkyon_oidc
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_oidc', '20261023180644988', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20261023180644988', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_auth_core
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_auth_core', '20260924105232991', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260924105232991', "timestamp" = now();
COMMIT;

View file

@ -5,3 +5,4 @@
### the conflict by removing and recreating the conflicting migration.
20261028075229917-initial
20261028101012641-workouts

View file

@ -16,6 +16,7 @@ dependencies:
path: ../../../packages/halkyon_base_server
serverpod: 4.0.4
serverpod_auth_idp_server: 4.0.4
serverpod_offline_sync_server: 0.0.10
dev_dependencies:
serverpod_test: 4.0.4

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