Merge branch 'train-days' into integration
# Conflicts: # apps/train/halkyon_train_flutter/lib/src/app/train_paths.dart # docs/adr/0030-halkyon-train.md
This commit is contained in:
commit
81adeb91cc
12 changed files with 25705 additions and 410 deletions
|
|
@ -49,6 +49,7 @@ import 'search/search_refused.dart' as _ihbxkwke;
|
|||
import 'strength/strength_plan.dart' as _imwvu2mn;
|
||||
import 'weather/weather_hour.dart' as _i9m2f53e;
|
||||
import 'weather/weather_refused.dart' as _ieu1vbq0;
|
||||
import 'weight/body_weight.dart' as _i1mqkkn3;
|
||||
import 'workout/workout.dart' as _i5b9b17z;
|
||||
import 'workout/workout_series.dart' as _i92mefto;
|
||||
export 'day/day.dart';
|
||||
|
|
@ -73,6 +74,7 @@ export 'search/search_refused.dart';
|
|||
export 'strength/strength_plan.dart';
|
||||
export 'weather/weather_hour.dart';
|
||||
export 'weather/weather_refused.dart';
|
||||
export 'weight/body_weight.dart';
|
||||
export 'workout/workout.dart';
|
||||
export 'workout/workout_series.dart';
|
||||
export 'sync_tables.dart';
|
||||
|
|
@ -85,6 +87,73 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
static final Protocol _instance = Protocol._().._registerHostProtocols();
|
||||
|
||||
static List<_isp.TableDefinition> get targetTableDefinitions => [
|
||||
_isp.TableDefinition(
|
||||
name: 'body_weight',
|
||||
dartName: 'BodyWeight',
|
||||
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: 'body_weight_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: 'body_weight_space_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'spaceId',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
),
|
||||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'content_key',
|
||||
dartName: 'ContentKey',
|
||||
|
|
@ -977,6 +1046,9 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _ieu1vbq0.WeatherRefused) {
|
||||
return _ieu1vbq0.WeatherRefused.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i1mqkkn3.BodyWeight) {
|
||||
return _i1mqkkn3.BodyWeight.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i5b9b17z.Workout) {
|
||||
return _i5b9b17z.Workout.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -1055,6 +1127,9 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return (data != null ? _ieu1vbq0.WeatherRefused.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_i1mqkkn3.BodyWeight?>()) {
|
||||
return (data != null ? _i1mqkkn3.BodyWeight.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_i5b9b17z.Workout?>()) {
|
||||
return (data != null ? _i5b9b17z.Workout.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -1144,6 +1219,7 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
_imwvu2mn.StrengthPlan => 'StrengthPlan',
|
||||
_i9m2f53e.WeatherHour => 'WeatherHour',
|
||||
_ieu1vbq0.WeatherRefused => 'WeatherRefused',
|
||||
_i1mqkkn3.BodyWeight => 'BodyWeight',
|
||||
_i5b9b17z.Workout => 'Workout',
|
||||
_i92mefto.WorkoutSeries => 'WorkoutSeries',
|
||||
_ => null,
|
||||
|
|
@ -1207,6 +1283,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return 'WeatherHour';
|
||||
case _ieu1vbq0.WeatherRefused():
|
||||
return 'WeatherRefused';
|
||||
case _i1mqkkn3.BodyWeight():
|
||||
return 'BodyWeight';
|
||||
case _i5b9b17z.Workout():
|
||||
return 'Workout';
|
||||
case _i92mefto.WorkoutSeries():
|
||||
|
|
@ -1317,6 +1395,9 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName == 'WeatherRefused') {
|
||||
return deserialize<_ieu1vbq0.WeatherRefused>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'BodyWeight') {
|
||||
return deserialize<_i1mqkkn3.BodyWeight>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'Workout') {
|
||||
return deserialize<_i5b9b17z.Workout>(data['data']);
|
||||
}
|
||||
|
|
@ -1415,6 +1496,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return _i0mgeh8t.SavedRoute.t;
|
||||
case _imwvu2mn.StrengthPlan:
|
||||
return _imwvu2mn.StrengthPlan.t;
|
||||
case _i1mqkkn3.BodyWeight:
|
||||
return _i1mqkkn3.BodyWeight.t;
|
||||
case _i5b9b17z.Workout:
|
||||
return _i5b9b17z.Workout.t;
|
||||
case _i92mefto.WorkoutSeries:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import 'privacy/content_key.dart' as _imed3x5d;
|
|||
import 'profile/profile.dart' as _ia5j8fch;
|
||||
import 'route/route.dart' as _i0mgeh8t;
|
||||
import 'strength/strength_plan.dart' as _imwvu2mn;
|
||||
import 'weight/body_weight.dart' as _i1mqkkn3;
|
||||
import 'workout/workout.dart' as _i5b9b17z;
|
||||
import 'workout/workout_series.dart' as _i92mefto;
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ import 'workout/workout_series.dart' as _i92mefto;
|
|||
/// `serverpod_offline_sync` package, including the ones owned by modules and
|
||||
/// shared packages.
|
||||
final List<_is.Table> syncTables = [
|
||||
_i1mqkkn3.BodyWeight.t,
|
||||
_imed3x5d.ContentKey.t,
|
||||
_ikmv9f3y.Day.t,
|
||||
_ir1ofny6.DaySeries.t,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,668 @@
|
|||
/* 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 weight the person measured (ADR 0030, D64): when, how heavy and where
|
||||
/// it came from (entered in Train, or read from Health Connect with that
|
||||
/// record's ID, so that it is read once), in its sealed payload (D7); the
|
||||
/// instance sees that it exists, its size and when it changed. A row each
|
||||
/// measurement. Synced.
|
||||
abstract class BodyWeight
|
||||
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
|
||||
BodyWeight._({
|
||||
this.id,
|
||||
this.spaceId,
|
||||
this.keyId,
|
||||
required this.payload,
|
||||
this.nonce,
|
||||
});
|
||||
|
||||
factory BodyWeight({
|
||||
_is.UuidValue? id,
|
||||
int? spaceId,
|
||||
String? keyId,
|
||||
required _idt.ByteData payload,
|
||||
_idt.ByteData? nonce,
|
||||
}) = _BodyWeightImpl;
|
||||
|
||||
factory BodyWeight.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return BodyWeight(
|
||||
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 = BodyWeightTable();
|
||||
|
||||
static const db = BodyWeightRepository._();
|
||||
|
||||
@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.
|
||||
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 [BodyWeight]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
BodyWeight copyWith({
|
||||
_is.UuidValue? id,
|
||||
int? spaceId,
|
||||
String? keyId,
|
||||
_idt.ByteData? payload,
|
||||
_idt.ByteData? nonce,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'BodyWeight',
|
||||
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__': 'BodyWeight',
|
||||
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 BodyWeightInclude include() {
|
||||
return BodyWeightInclude._();
|
||||
}
|
||||
|
||||
static BodyWeightIncludeList includeList({
|
||||
_is.WhereExpressionBuilder<BodyWeightTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<BodyWeightTable>? orderBy,
|
||||
_is.OrderByListBuilder<BodyWeightTable>? orderByList,
|
||||
BodyWeightInclude? include,
|
||||
}) {
|
||||
return BodyWeightIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(BodyWeight.t),
|
||||
orderByList: orderByList?.call(BodyWeight.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _BodyWeightImpl extends BodyWeight {
|
||||
_BodyWeightImpl({
|
||||
_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 [BodyWeight]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
BodyWeight copyWith({
|
||||
Object? id = _Undefined,
|
||||
Object? spaceId = _Undefined,
|
||||
Object? keyId = _Undefined,
|
||||
_idt.ByteData? payload,
|
||||
Object? nonce = _Undefined,
|
||||
}) {
|
||||
return BodyWeight(
|
||||
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 BodyWeightUpdateTable extends _is.UpdateTable<BodyWeightTable> {
|
||||
BodyWeightUpdateTable(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 BodyWeightTable extends _is.Table<_is.UuidValue?> {
|
||||
BodyWeightTable({super.tableRelation}) : super(tableName: 'body_weight') {
|
||||
updateTable = BodyWeightUpdateTable(this);
|
||||
spaceId = _is.ColumnInt(
|
||||
'spaceId',
|
||||
this,
|
||||
);
|
||||
keyId = _is.ColumnString(
|
||||
'keyId',
|
||||
this,
|
||||
);
|
||||
payload = _is.ColumnByteData(
|
||||
'payload',
|
||||
this,
|
||||
);
|
||||
nonce = _is.ColumnByteData(
|
||||
'nonce',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final BodyWeightUpdateTable 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.
|
||||
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 BodyWeightInclude extends _is.IncludeObject {
|
||||
BodyWeightInclude._();
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => {};
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => BodyWeight.t;
|
||||
}
|
||||
|
||||
class BodyWeightIncludeList extends _is.IncludeList {
|
||||
BodyWeightIncludeList._({
|
||||
_is.WhereExpressionBuilder<BodyWeightTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(BodyWeight.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => BodyWeight.t;
|
||||
}
|
||||
|
||||
class BodyWeightRepository {
|
||||
const BodyWeightRepository._();
|
||||
|
||||
/// Returns a list of [BodyWeight]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<BodyWeight>> find(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<BodyWeightTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<BodyWeightTable>? orderBy,
|
||||
_is.OrderByListBuilder<BodyWeightTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<BodyWeight>(
|
||||
where: where?.call(BodyWeight.t),
|
||||
orderBy: orderBy?.call(BodyWeight.t),
|
||||
orderByList: orderByList?.call(BodyWeight.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [BodyWeight] 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<BodyWeight?> findFirstRow(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<BodyWeightTable>? where,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<BodyWeightTable>? orderBy,
|
||||
_is.OrderByListBuilder<BodyWeightTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<BodyWeight>(
|
||||
where: where?.call(BodyWeight.t),
|
||||
orderBy: orderBy?.call(BodyWeight.t),
|
||||
orderByList: orderByList?.call(BodyWeight.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [BodyWeight] by its [id] or null if no such row exists.
|
||||
Future<BodyWeight?> findById(
|
||||
_is.DatabaseSession session,
|
||||
_is.UuidValue id, {
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<BodyWeight>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [BodyWeight]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [BodyWeight]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<BodyWeight>> insert(
|
||||
_is.DatabaseSession session,
|
||||
List<BodyWeight> rows, {
|
||||
_is.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<BodyWeight>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [BodyWeight] and returns the inserted row.
|
||||
///
|
||||
/// The returned [BodyWeight] will have its `id` field set.
|
||||
Future<BodyWeight> insertRow(
|
||||
_is.DatabaseSession session,
|
||||
BodyWeight row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<BodyWeight>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [BodyWeight]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 [BodyWeight]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<BodyWeight>> upsert(
|
||||
_is.DatabaseSession session,
|
||||
List<BodyWeight> rows, {
|
||||
required _is.ColumnSelections<BodyWeightTable> conflictColumns,
|
||||
_is.ColumnSelections<BodyWeightTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<BodyWeightTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<BodyWeight>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(BodyWeight.t),
|
||||
updateColumns: updateColumns?.call(BodyWeight.t),
|
||||
updateWhere: updateWhere?.call(BodyWeight.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [BodyWeight] 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 [BodyWeight] will have its `id` field set.
|
||||
Future<BodyWeight?> upsertRow(
|
||||
_is.DatabaseSession session,
|
||||
BodyWeight row, {
|
||||
required _is.ColumnSelections<BodyWeightTable> conflictColumns,
|
||||
_is.ColumnSelections<BodyWeightTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<BodyWeightTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<BodyWeight>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(BodyWeight.t),
|
||||
updateColumns: updateColumns?.call(BodyWeight.t),
|
||||
updateWhere: updateWhere?.call(BodyWeight.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [BodyWeight]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<BodyWeight>> update(
|
||||
_is.DatabaseSession session,
|
||||
List<BodyWeight> rows, {
|
||||
_is.ColumnSelections<BodyWeightTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<BodyWeight>(
|
||||
rows,
|
||||
columns: columns?.call(BodyWeight.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [BodyWeight]. 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<BodyWeight> updateRow(
|
||||
_is.DatabaseSession session,
|
||||
BodyWeight row, {
|
||||
_is.ColumnSelections<BodyWeightTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<BodyWeight>(
|
||||
row,
|
||||
columns: columns?.call(BodyWeight.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [BodyWeight] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<BodyWeight?> updateById(
|
||||
_is.DatabaseSession session,
|
||||
_is.UuidValue id, {
|
||||
required _is.ColumnValueListBuilder<BodyWeightUpdateTable> columnValues,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<BodyWeight>(
|
||||
id,
|
||||
columnValues: columnValues(BodyWeight.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [BodyWeight]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<BodyWeight>> updateWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.ColumnValueListBuilder<BodyWeightUpdateTable> columnValues,
|
||||
required _is.WhereExpressionBuilder<BodyWeightTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<BodyWeightTable>? orderBy,
|
||||
_is.OrderByListBuilder<BodyWeightTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<BodyWeight>(
|
||||
columnValues: columnValues(BodyWeight.t.updateTable),
|
||||
where: where(BodyWeight.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(BodyWeight.t),
|
||||
orderByList: orderByList?.call(BodyWeight.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [BodyWeight]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<BodyWeight>> delete(
|
||||
_is.DatabaseSession session,
|
||||
List<BodyWeight> rows, {
|
||||
_is.OrderByBuilder<BodyWeightTable>? orderBy,
|
||||
_is.OrderByListBuilder<BodyWeightTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<BodyWeight>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(BodyWeight.t),
|
||||
orderByList: orderByList?.call(BodyWeight.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [BodyWeight].
|
||||
Future<BodyWeight> deleteRow(
|
||||
_is.DatabaseSession session,
|
||||
BodyWeight row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<BodyWeight>(
|
||||
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<BodyWeight>> deleteWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<BodyWeightTable> where,
|
||||
_is.OrderByBuilder<BodyWeightTable>? orderBy,
|
||||
_is.OrderByListBuilder<BodyWeightTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<BodyWeight>(
|
||||
where: where(BodyWeight.t),
|
||||
orderBy: orderBy?.call(BodyWeight.t),
|
||||
orderByList: orderByList?.call(BodyWeight.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<BodyWeightTable>? where,
|
||||
int? limit,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<BodyWeight>(
|
||||
where: where?.call(BodyWeight.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [BodyWeight] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<BodyWeightTable> where,
|
||||
required _is.LockMode lockMode,
|
||||
required _is.Transaction transaction,
|
||||
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<BodyWeight>(
|
||||
where: where(BodyWeight.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
### A weight the person measured (ADR 0030, D64): when, how heavy and where
|
||||
### it came from (entered in Train, or read from Health Connect with that
|
||||
### record's ID, so that it is read once), in its sealed payload (D7); the
|
||||
### instance sees that it exists, its size and when it changed. A row each
|
||||
### measurement. Synced.
|
||||
class: BodyWeight
|
||||
table: body_weight
|
||||
database: sync
|
||||
fields:
|
||||
### The key it is sealed with (ADR 0002's envelope); never null in Train.
|
||||
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).
|
||||
body_weight_space_idx:
|
||||
fields: spaceId
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "body_weight",
|
||||
"dartName": "BodyWeight",
|
||||
"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": "body_weight_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "body_weight_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
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 "body_weight" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
|
||||
"spaceId" bigint,
|
||||
"keyId" text,
|
||||
"payload" bytea NOT NULL,
|
||||
"nonce" bytea
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "body_weight_space_idx" ON "body_weight" USING btree ("spaceId");
|
||||
|
||||
--
|
||||
-- ACTION CREATE FOREIGN KEY
|
||||
--
|
||||
ALTER TABLE ONLY "body_weight"
|
||||
ADD CONSTRAINT "body_weight_fk_0"
|
||||
FOREIGN KEY("spaceId")
|
||||
REFERENCES "offline_sync_spaces"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION;
|
||||
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_train
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_train', '20261103204323683-body-weights', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261103204323683-body-weights', "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;
|
||||
|
|
@ -12,3 +12,4 @@
|
|||
20261028231042668-routes
|
||||
20261102103211357-training-places
|
||||
20261102113104496-strength-plans
|
||||
20261103204323683-body-weights
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_train_server/src/generated/protocol.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod(
|
||||
"A person's weights (ADR 0030, D64)",
|
||||
(sessionBuilder, endpoints) {
|
||||
late Session session;
|
||||
late UuidValue alice;
|
||||
late UuidValue bob;
|
||||
final sealed = ByteData.sublistView(Uint8List.fromList([1, 2, 3]));
|
||||
|
||||
setUp(() {
|
||||
// As person_deletion_test: the test pod is built without the
|
||||
// generated Serverpod class, which sets up sync for the server.
|
||||
Serverpod.instance.initializeOfflineSync(syncTables: syncTables);
|
||||
session = sessionBuilder.build();
|
||||
alice = const Uuid().v7obj();
|
||||
bob = const Uuid().v7obj();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
for (final space in [alice, bob]) {
|
||||
await session.db.unsafeExecute(
|
||||
'DELETE FROM "offline_sync_spaces" WHERE "uuidSpaceId" = @space',
|
||||
parameters: QueryParameters.named({'space': space.uuid}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
OfflineSyncDatabase sync() => _syncDatabases[session]!;
|
||||
|
||||
/// Writes a weight in [person]'s own space, as their device's sync
|
||||
/// does.
|
||||
Future<BodyWeight> weightOf(UuidValue person) =>
|
||||
sync().transactionForUser(
|
||||
person,
|
||||
(transaction) => BodyWeight.db.insertRow(
|
||||
session,
|
||||
BodyWeight(keyId: 'p1.0.key', payload: sealed, nonce: sealed),
|
||||
transaction: transaction,
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
"is written in the person's own space and read back there",
|
||||
() async {
|
||||
final written = await weightOf(alice);
|
||||
|
||||
final read = await sync().transactionForUser(
|
||||
alice,
|
||||
(transaction) =>
|
||||
BodyWeight.db.find(session, transaction: transaction),
|
||||
);
|
||||
expect([for (final weight in read) weight.id], [written.id]);
|
||||
},
|
||||
);
|
||||
|
||||
test("is denied to anyone else: someone else's reading finds none of "
|
||||
'it', () async {
|
||||
await weightOf(alice);
|
||||
|
||||
final read = await sync().transactionForUser(
|
||||
bob,
|
||||
(transaction) =>
|
||||
BodyWeight.db.find(session, transaction: transaction),
|
||||
);
|
||||
expect(read, isEmpty);
|
||||
});
|
||||
|
||||
test("is denied to anyone else: writing in another person's space is "
|
||||
'refused before anything is written', () async {
|
||||
await expectLater(
|
||||
sync().transactionForUser(
|
||||
bob,
|
||||
spaceId: alice,
|
||||
(transaction) => BodyWeight.db.insertRow(
|
||||
session,
|
||||
BodyWeight(keyId: 'p1.0.key', payload: sealed, nonce: sealed),
|
||||
transaction: transaction,
|
||||
),
|
||||
),
|
||||
throwsA(isA<OfflineSyncSpaceMembershipException>()),
|
||||
);
|
||||
// Nothing landed in Alice's space.
|
||||
final rows = await session.db.unsafeQuery(
|
||||
'SELECT count(*) FROM "body_weight" t '
|
||||
'JOIN "offline_sync_spaces" s ON s."id" = t."spaceId" '
|
||||
'WHERE s."uuidSpaceId" = @space',
|
||||
parameters: QueryParameters.named({'space': alice.uuid}),
|
||||
);
|
||||
expect(rows.first.first, 0);
|
||||
});
|
||||
},
|
||||
// As person_deletion_test: the sync engine's database, writing in
|
||||
// transactions of its own, so the tests delete what they made.
|
||||
databaseInterceptor: (session, inner) {
|
||||
final database = offlineSyncDatabaseInterceptor(session, inner);
|
||||
if (database is OfflineSyncDatabase) {
|
||||
_syncDatabases[session] = database;
|
||||
}
|
||||
return database;
|
||||
},
|
||||
rollbackDatabase: RollbackDatabase.disabled,
|
||||
);
|
||||
}
|
||||
|
||||
/// The sync engine's database of each session, as the test pod's own
|
||||
/// database wraps it.
|
||||
final _syncDatabases = Expando<OfflineSyncDatabase>();
|
||||
|
|
@ -53,8 +53,8 @@ void main() {
|
|||
|
||||
test(
|
||||
'deleting a person deletes their space with every key, workout, '
|
||||
"series, profile, day, day's series, night, plan, route and training "
|
||||
"place in it, and nobody else's",
|
||||
"series, profile, day, day's series, night, plan, route, training "
|
||||
"place, strength plan and weight in it, and nobody else's",
|
||||
() async {
|
||||
for (final person in [alice, bob]) {
|
||||
await _workoutOf(session, person);
|
||||
|
|
@ -71,6 +71,7 @@ void main() {
|
|||
'route',
|
||||
'training_place',
|
||||
'strength_plan',
|
||||
'body_weight',
|
||||
]) {
|
||||
expect(await rowsOf(alice, table), 1, reason: table);
|
||||
}
|
||||
|
|
@ -99,6 +100,7 @@ void main() {
|
|||
'route',
|
||||
'training_place',
|
||||
'strength_plan',
|
||||
'body_weight',
|
||||
]) {
|
||||
expect(await rowsOf(alice, table), 0, reason: table);
|
||||
expect(await rowsOf(bob, table), 1, reason: table);
|
||||
|
|
@ -196,4 +198,9 @@ Future<void> _workoutOf(Session session, UuidValue person) =>
|
|||
StrengthPlan(keyId: 'p1.0.key', payload: sealed, nonce: sealed),
|
||||
transaction: transaction,
|
||||
);
|
||||
await BodyWeight.db.insertRow(
|
||||
session,
|
||||
BodyWeight(keyId: 'p1.0.key', payload: sealed, nonce: sealed),
|
||||
transaction: transaction,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue