feat(train): the Loop's days read on the phone (ADR 0030, D37, D38)

The server gains Train's synced `day`, `day_series` (a day's heart
rate, deleted with its day) and `night`, each a sealed payload alone;
the migration `days` only adds their tables, indexes and foreign keys
(to their sync space, and a chunk's to its day, deferred), and the
person's deletion takes them with their spaces.

On Android, `LoopDays.kt` reads a band's days from a date to today,
framed by the SDK's sync start and stop, into lines of JSON, then
deletes on the band what the app kept up to the date it names, never
today; `LoopBridge` answers `readDays` with them, one band call at a
time, and `background` schedules `LoopWorker`, a WorkManager job every
four hours that appends the same lines to the person's
`loop_days.txt` while the app doesn't hold the band.

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 19:53:04 +01:00
commit 88e8693025
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
32 changed files with 24637 additions and 52 deletions

View file

@ -0,0 +1,687 @@
{
"__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": "day",
"dartName": "Day",
"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": "day_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "day_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "day_series",
"dartName": "DaySeries",
"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": "dayId",
"fieldName": "dayId",
"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": "day_series_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "day_series_fk_1",
"columns": [
"dayId"
],
"referenceTable": "day",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "day_series_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "day_series_day_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "dayId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "night",
"dartName": "Night",
"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": "night_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "night_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "profile",
"dartName": "Profile",
"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": "profile_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "profile_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,305 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "day",
"dartName": "Day",
"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": "day_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "day_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "day_series",
"dartName": "DaySeries",
"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": "dayId",
"fieldName": "dayId",
"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": "day_series_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "day_series_fk_1",
"columns": [
"dayId"
],
"referenceTable": "day",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "day_series_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "day_series_day_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "dayId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "night",
"dartName": "Night",
"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": "night_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "night_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -6,6 +6,7 @@ import 'package:serverpod_database/serverpod_database.dart';
part '20261028101012641-workouts/migration.dart';
part '20261028164640000-profile/migration.dart';
part '20261028181405893-days/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -14,6 +15,7 @@ class MigrationRegistry {
static final List<MigrationVersionSql> migrations = [
_Migration20261028101012641_workouts(),
_Migration20261028164640000_profile(),
_Migration20261028181405893_days(),
];
/// List of all client-side database migration versions.

View file

@ -0,0 +1,674 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
/// A day as the person's Polar Loop counted it (ADR 0030, D37): its date, its
/// steps, distance, calories and time in each activity class, its Nightly
/// Recharge and skin temperature, all in its sealed payload (D7). One row per
/// person and date, its ID made from both on the device, so a day read again
/// (today, as it goes on) or on another phone writes the same row, the last
/// writer winning. Its heart rate is its series (`day_series`). Synced.
abstract class Day
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
Day._({
this.id,
this.spaceId,
this.keyId,
required this.payload,
this.nonce,
});
factory Day({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _DayImpl;
factory Day.fromJson(Map<String, dynamic> jsonSerialization) {
return Day(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = DayTable();
static const db = DayRepository._();
@override
_isc.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: gzipped UTF-8 JSON with a `schemaVersion` once
/// opened.
_idt.ByteData payload;
/// The sealing's nonce (ADR 0002's envelope).
_idt.ByteData? nonce;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [Day]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
Day copyWith({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'Day',
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__': 'Day',
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 DayInclude include() {
return DayInclude._();
}
static DayIncludeList includeList({
_isd.WhereExpressionBuilder<DayTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<DayTable>? orderBy,
_isd.OrderByListBuilder<DayTable>? orderByList,
DayInclude? include,
}) {
return DayIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(Day.t),
orderByList: orderByList?.call(Day.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _DayImpl extends Day {
_DayImpl({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [Day]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
Day copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return Day(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class DayUpdateTable extends _isd.UpdateTable<DayTable> {
DayUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
table.keyId,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_isd.ColumnValue(
table.payload,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_isd.ColumnValue(
table.nonce,
value,
);
}
class DayTable extends _isd.Table<_isc.UuidValue?> {
DayTable({super.tableRelation}) : super(tableName: 'day') {
updateTable = DayUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final DayUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
/// The key it is sealed with (ADR 0002's envelope); never null in Train.
late final _isd.ColumnString keyId;
/// The content, sealed: gzipped UTF-8 JSON with a `schemaVersion` once
/// opened.
late final _isd.ColumnByteData payload;
/// The sealing's nonce (ADR 0002's envelope).
late final _isd.ColumnByteData nonce;
@override
List<_isd.Column> get columns => [
id,
spaceId,
keyId,
payload,
nonce,
];
}
class DayInclude extends _isd.IncludeObject {
DayInclude._();
@override
Map<String, _isd.Include?> get includes => {};
@override
_isd.Table<_isc.UuidValue?> get table => Day.t;
}
class DayIncludeList extends _isd.IncludeList {
DayIncludeList._({
_isd.WhereExpressionBuilder<DayTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(Day.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => Day.t;
}
class DayRepository {
const DayRepository._();
/// Returns a list of [Day]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<Day>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<DayTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<DayTable>? orderBy,
_isd.OrderByListBuilder<DayTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<Day>(
where: where?.call(Day.t),
orderBy: orderBy?.call(Day.t),
orderByList: orderByList?.call(Day.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [Day] 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<Day?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<DayTable>? where,
int? offset,
_isd.OrderByBuilder<DayTable>? orderBy,
_isd.OrderByListBuilder<DayTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<Day>(
where: where?.call(Day.t),
orderBy: orderBy?.call(Day.t),
orderByList: orderByList?.call(Day.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [Day] by its [id] or null if no such row exists.
Future<Day?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<Day>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [Day]s in the list and returns the inserted rows.
///
/// The returned [Day]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<Day>> insert(
_isd.DatabaseSession session,
List<Day> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<Day>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [Day] and returns the inserted row.
///
/// The returned [Day] will have its `id` field set.
Future<Day> insertRow(
_isd.DatabaseSession session,
Day row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<Day>(
row,
transaction: transaction,
);
}
/// Upserts all [Day]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 [Day]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<Day>> upsert(
_isd.DatabaseSession session,
List<Day> rows, {
required _isd.ColumnSelections<DayTable> conflictColumns,
_isd.ColumnSelections<DayTable>? updateColumns,
_isd.WhereExpressionBuilder<DayTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<Day>(
rows,
conflictColumns: conflictColumns(Day.t),
updateColumns: updateColumns?.call(Day.t),
updateWhere: updateWhere?.call(Day.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [Day] 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 [Day] will have its `id` field set.
Future<Day?> upsertRow(
_isd.DatabaseSession session,
Day row, {
required _isd.ColumnSelections<DayTable> conflictColumns,
_isd.ColumnSelections<DayTable>? updateColumns,
_isd.WhereExpressionBuilder<DayTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<Day>(
row,
conflictColumns: conflictColumns(Day.t),
updateColumns: updateColumns?.call(Day.t),
updateWhere: updateWhere?.call(Day.t),
transaction: transaction,
);
}
/// Updates all [Day]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<Day>> update(
_isd.DatabaseSession session,
List<Day> rows, {
_isd.ColumnSelections<DayTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<Day>(
rows,
columns: columns?.call(Day.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [Day]. 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<Day> updateRow(
_isd.DatabaseSession session,
Day row, {
_isd.ColumnSelections<DayTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<Day>(
row,
columns: columns?.call(Day.t),
transaction: transaction,
);
}
/// Updates a single [Day] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<Day?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<DayUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<Day>(
id,
columnValues: columnValues(Day.t.updateTable),
transaction: transaction,
);
}
/// Updates all [Day]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<Day>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<DayUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<DayTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<DayTable>? orderBy,
_isd.OrderByListBuilder<DayTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<Day>(
columnValues: columnValues(Day.t.updateTable),
where: where(Day.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(Day.t),
orderByList: orderByList?.call(Day.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [Day]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<Day>> delete(
_isd.DatabaseSession session,
List<Day> rows, {
_isd.OrderByBuilder<DayTable>? orderBy,
_isd.OrderByListBuilder<DayTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<Day>(
rows,
orderBy: orderBy?.call(Day.t),
orderByList: orderByList?.call(Day.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [Day].
Future<Day> deleteRow(
_isd.DatabaseSession session,
Day row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<Day>(
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<Day>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<DayTable> where,
_isd.OrderByBuilder<DayTable>? orderBy,
_isd.OrderByListBuilder<DayTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<Day>(
where: where(Day.t),
orderBy: orderBy?.call(Day.t),
orderByList: orderByList?.call(Day.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<DayTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<Day>(
where: where?.call(Day.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [Day] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<DayTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<Day>(
where: where(Day.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -0,0 +1,782 @@
/* 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_client/src/protocol/protocol.dart' as _ip4ob8rr;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import '../day/day.dart' as _irdxguic;
/// A chunk of a day's 24/7 heart rate from the Polar Loop (ADR 0030, D37):
/// up to three hours of samples, in its sealed payload (D7) as a workout's
/// series are, sealed for its day (`bound`). Its ID is made from its day's and
/// its place in the day, so the day read again rewrites the same chunks.
/// Synced; it goes with its day.
abstract class DaySeries
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
DaySeries._({
this.id,
this.spaceId,
required this.dayId,
this.day,
this.keyId,
required this.payload,
this.nonce,
});
factory DaySeries({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue dayId,
_irdxguic.Day? day,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _DaySeriesImpl;
factory DaySeries.fromJson(Map<String, dynamic> jsonSerialization) {
return DaySeries(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
dayId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['dayId']),
day: jsonSerialization['day'] == null
? null
: _ip4ob8rr.Protocol().deserialize<_irdxguic.Day>(
jsonSerialization['day'],
),
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = DaySeriesTable();
static const db = DaySeriesRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue dayId;
/// The day it is part of.
_irdxguic.Day? day;
/// 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
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [DaySeries]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
DaySeries copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? dayId,
_irdxguic.Day? day,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'DaySeries',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'dayId': dayId.toJson(),
if (day != null) 'day': day?.toJson(),
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'DaySeries',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'dayId': dayId.toJson(),
if (day != null) 'day': day?.toJsonForProtocol(),
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static DaySeriesInclude include({_irdxguic.DayInclude? day}) {
return DaySeriesInclude._(day: day);
}
static DaySeriesIncludeList includeList({
_isd.WhereExpressionBuilder<DaySeriesTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<DaySeriesTable>? orderBy,
_isd.OrderByListBuilder<DaySeriesTable>? orderByList,
DaySeriesInclude? include,
}) {
return DaySeriesIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(DaySeries.t),
orderByList: orderByList?.call(DaySeries.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _DaySeriesImpl extends DaySeries {
_DaySeriesImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue dayId,
_irdxguic.Day? day,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
dayId: dayId,
day: day,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [DaySeries]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
DaySeries copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? dayId,
Object? day = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return DaySeries(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
dayId: dayId ?? this.dayId,
day: day is _irdxguic.Day? ? day : this.day?.copyWith(),
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class DaySeriesUpdateTable extends _isd.UpdateTable<DaySeriesTable> {
DaySeriesUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> dayId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.dayId,
value,
);
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
table.keyId,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_isd.ColumnValue(
table.payload,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_isd.ColumnValue(
table.nonce,
value,
);
}
class DaySeriesTable extends _isd.Table<_isc.UuidValue?> {
DaySeriesTable({super.tableRelation}) : super(tableName: 'day_series') {
updateTable = DaySeriesUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
dayId = _isd.ColumnUuid(
'dayId',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final DaySeriesUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid dayId;
/// The day it is part of.
_irdxguic.DayTable? _day;
/// The key it is sealed with (ADR 0002's envelope); never null in Train.
late final _isd.ColumnString keyId;
/// The content, sealed: gzipped UTF-8 JSON with a `schemaVersion` once
/// opened.
late final _isd.ColumnByteData payload;
/// The sealing's nonce (ADR 0002's envelope).
late final _isd.ColumnByteData nonce;
_irdxguic.DayTable get day {
if (_day != null) return _day!;
_day = _isd.createRelationTable(
relationFieldName: 'day',
field: DaySeries.t.dayId,
foreignField: _irdxguic.Day.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_irdxguic.DayTable(tableRelation: foreignTableRelation),
);
return _day!;
}
@override
List<_isd.Column> get columns => [
id,
spaceId,
dayId,
keyId,
payload,
nonce,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'day') {
return day;
}
return null;
}
}
class DaySeriesInclude extends _isd.IncludeObject {
DaySeriesInclude._({_irdxguic.DayInclude? day}) {
_day = day;
}
_irdxguic.DayInclude? _day;
@override
Map<String, _isd.Include?> get includes => {'day': _day};
@override
_isd.Table<_isc.UuidValue?> get table => DaySeries.t;
}
class DaySeriesIncludeList extends _isd.IncludeList {
DaySeriesIncludeList._({
_isd.WhereExpressionBuilder<DaySeriesTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(DaySeries.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => DaySeries.t;
}
class DaySeriesRepository {
const DaySeriesRepository._();
final attachRow = const DaySeriesAttachRowRepository._();
/// Returns a list of [DaySeries]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<DaySeries>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<DaySeriesTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<DaySeriesTable>? orderBy,
_isd.OrderByListBuilder<DaySeriesTable>? orderByList,
_isd.Transaction? transaction,
DaySeriesInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<DaySeries>(
where: where?.call(DaySeries.t),
orderBy: orderBy?.call(DaySeries.t),
orderByList: orderByList?.call(DaySeries.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [DaySeries] 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<DaySeries?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<DaySeriesTable>? where,
int? offset,
_isd.OrderByBuilder<DaySeriesTable>? orderBy,
_isd.OrderByListBuilder<DaySeriesTable>? orderByList,
_isd.Transaction? transaction,
DaySeriesInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<DaySeries>(
where: where?.call(DaySeries.t),
orderBy: orderBy?.call(DaySeries.t),
orderByList: orderByList?.call(DaySeries.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [DaySeries] by its [id] or null if no such row exists.
Future<DaySeries?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
DaySeriesInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<DaySeries>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [DaySeries]s in the list and returns the inserted rows.
///
/// The returned [DaySeries]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<DaySeries>> insert(
_isd.DatabaseSession session,
List<DaySeries> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<DaySeries>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [DaySeries] and returns the inserted row.
///
/// The returned [DaySeries] will have its `id` field set.
Future<DaySeries> insertRow(
_isd.DatabaseSession session,
DaySeries row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<DaySeries>(
row,
transaction: transaction,
);
}
/// Upserts all [DaySeries]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 [DaySeries]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<DaySeries>> upsert(
_isd.DatabaseSession session,
List<DaySeries> rows, {
required _isd.ColumnSelections<DaySeriesTable> conflictColumns,
_isd.ColumnSelections<DaySeriesTable>? updateColumns,
_isd.WhereExpressionBuilder<DaySeriesTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<DaySeries>(
rows,
conflictColumns: conflictColumns(DaySeries.t),
updateColumns: updateColumns?.call(DaySeries.t),
updateWhere: updateWhere?.call(DaySeries.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [DaySeries] 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 [DaySeries] will have its `id` field set.
Future<DaySeries?> upsertRow(
_isd.DatabaseSession session,
DaySeries row, {
required _isd.ColumnSelections<DaySeriesTable> conflictColumns,
_isd.ColumnSelections<DaySeriesTable>? updateColumns,
_isd.WhereExpressionBuilder<DaySeriesTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<DaySeries>(
row,
conflictColumns: conflictColumns(DaySeries.t),
updateColumns: updateColumns?.call(DaySeries.t),
updateWhere: updateWhere?.call(DaySeries.t),
transaction: transaction,
);
}
/// Updates all [DaySeries]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<DaySeries>> update(
_isd.DatabaseSession session,
List<DaySeries> rows, {
_isd.ColumnSelections<DaySeriesTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<DaySeries>(
rows,
columns: columns?.call(DaySeries.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [DaySeries]. 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<DaySeries> updateRow(
_isd.DatabaseSession session,
DaySeries row, {
_isd.ColumnSelections<DaySeriesTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<DaySeries>(
row,
columns: columns?.call(DaySeries.t),
transaction: transaction,
);
}
/// Updates a single [DaySeries] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<DaySeries?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<DaySeriesUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<DaySeries>(
id,
columnValues: columnValues(DaySeries.t.updateTable),
transaction: transaction,
);
}
/// Updates all [DaySeries]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<DaySeries>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<DaySeriesUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<DaySeriesTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<DaySeriesTable>? orderBy,
_isd.OrderByListBuilder<DaySeriesTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<DaySeries>(
columnValues: columnValues(DaySeries.t.updateTable),
where: where(DaySeries.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(DaySeries.t),
orderByList: orderByList?.call(DaySeries.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [DaySeries]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<DaySeries>> delete(
_isd.DatabaseSession session,
List<DaySeries> rows, {
_isd.OrderByBuilder<DaySeriesTable>? orderBy,
_isd.OrderByListBuilder<DaySeriesTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<DaySeries>(
rows,
orderBy: orderBy?.call(DaySeries.t),
orderByList: orderByList?.call(DaySeries.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [DaySeries].
Future<DaySeries> deleteRow(
_isd.DatabaseSession session,
DaySeries row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<DaySeries>(
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<DaySeries>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<DaySeriesTable> where,
_isd.OrderByBuilder<DaySeriesTable>? orderBy,
_isd.OrderByListBuilder<DaySeriesTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<DaySeries>(
where: where(DaySeries.t),
orderBy: orderBy?.call(DaySeries.t),
orderByList: orderByList?.call(DaySeries.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<DaySeriesTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<DaySeries>(
where: where?.call(DaySeries.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [DaySeries] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<DaySeriesTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<DaySeries>(
where: where(DaySeries.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class DaySeriesAttachRowRepository {
const DaySeriesAttachRowRepository._();
/// Creates a relation between the given [DaySeries] and [Day]
/// by setting the [DaySeries]'s foreign key `dayId` to refer to the [Day].
Future<void> day(
_isd.DatabaseSession session,
DaySeries daySeries,
_irdxguic.Day day, {
_isd.Transaction? transaction,
}) async {
if (daySeries.id == null) {
throw ArgumentError.notNull('daySeries.id');
}
if (day.id == null) {
throw ArgumentError.notNull('day.id');
}
var $daySeries = daySeries.copyWith(dayId: day.id);
await session.db.updateRow<DaySeries>(
$daySeries,
columns: [DaySeries.t.dayId],
transaction: transaction,
);
}
}

View file

@ -0,0 +1,673 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
/// A night's sleep as the person's Polar Loop analysed it (ADR 0030, D37):
/// when it began and ended, its wake phases and cycles, its rating and its
/// skin temperature, all in its sealed payload (D7). One row per person and
/// the date the sleep counts for, its ID made from both on the device, the
/// last writer winning. Synced.
abstract class Night
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
Night._({
this.id,
this.spaceId,
this.keyId,
required this.payload,
this.nonce,
});
factory Night({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _NightImpl;
factory Night.fromJson(Map<String, dynamic> jsonSerialization) {
return Night(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = NightTable();
static const db = NightRepository._();
@override
_isc.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: gzipped UTF-8 JSON with a `schemaVersion` once
/// opened.
_idt.ByteData payload;
/// The sealing's nonce (ADR 0002's envelope).
_idt.ByteData? nonce;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [Night]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
Night copyWith({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'Night',
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__': 'Night',
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 NightInclude include() {
return NightInclude._();
}
static NightIncludeList includeList({
_isd.WhereExpressionBuilder<NightTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<NightTable>? orderBy,
_isd.OrderByListBuilder<NightTable>? orderByList,
NightInclude? include,
}) {
return NightIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(Night.t),
orderByList: orderByList?.call(Night.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _NightImpl extends Night {
_NightImpl({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [Night]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
Night copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return Night(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class NightUpdateTable extends _isd.UpdateTable<NightTable> {
NightUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
table.keyId,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_isd.ColumnValue(
table.payload,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_isd.ColumnValue(
table.nonce,
value,
);
}
class NightTable extends _isd.Table<_isc.UuidValue?> {
NightTable({super.tableRelation}) : super(tableName: 'night') {
updateTable = NightUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final NightUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
/// The key it is sealed with (ADR 0002's envelope); never null in Train.
late final _isd.ColumnString keyId;
/// The content, sealed: gzipped UTF-8 JSON with a `schemaVersion` once
/// opened.
late final _isd.ColumnByteData payload;
/// The sealing's nonce (ADR 0002's envelope).
late final _isd.ColumnByteData nonce;
@override
List<_isd.Column> get columns => [
id,
spaceId,
keyId,
payload,
nonce,
];
}
class NightInclude extends _isd.IncludeObject {
NightInclude._();
@override
Map<String, _isd.Include?> get includes => {};
@override
_isd.Table<_isc.UuidValue?> get table => Night.t;
}
class NightIncludeList extends _isd.IncludeList {
NightIncludeList._({
_isd.WhereExpressionBuilder<NightTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(Night.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => Night.t;
}
class NightRepository {
const NightRepository._();
/// Returns a list of [Night]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<Night>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<NightTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<NightTable>? orderBy,
_isd.OrderByListBuilder<NightTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<Night>(
where: where?.call(Night.t),
orderBy: orderBy?.call(Night.t),
orderByList: orderByList?.call(Night.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [Night] 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<Night?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<NightTable>? where,
int? offset,
_isd.OrderByBuilder<NightTable>? orderBy,
_isd.OrderByListBuilder<NightTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<Night>(
where: where?.call(Night.t),
orderBy: orderBy?.call(Night.t),
orderByList: orderByList?.call(Night.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [Night] by its [id] or null if no such row exists.
Future<Night?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<Night>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [Night]s in the list and returns the inserted rows.
///
/// The returned [Night]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<Night>> insert(
_isd.DatabaseSession session,
List<Night> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<Night>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [Night] and returns the inserted row.
///
/// The returned [Night] will have its `id` field set.
Future<Night> insertRow(
_isd.DatabaseSession session,
Night row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<Night>(
row,
transaction: transaction,
);
}
/// Upserts all [Night]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 [Night]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<Night>> upsert(
_isd.DatabaseSession session,
List<Night> rows, {
required _isd.ColumnSelections<NightTable> conflictColumns,
_isd.ColumnSelections<NightTable>? updateColumns,
_isd.WhereExpressionBuilder<NightTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<Night>(
rows,
conflictColumns: conflictColumns(Night.t),
updateColumns: updateColumns?.call(Night.t),
updateWhere: updateWhere?.call(Night.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [Night] 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 [Night] will have its `id` field set.
Future<Night?> upsertRow(
_isd.DatabaseSession session,
Night row, {
required _isd.ColumnSelections<NightTable> conflictColumns,
_isd.ColumnSelections<NightTable>? updateColumns,
_isd.WhereExpressionBuilder<NightTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<Night>(
row,
conflictColumns: conflictColumns(Night.t),
updateColumns: updateColumns?.call(Night.t),
updateWhere: updateWhere?.call(Night.t),
transaction: transaction,
);
}
/// Updates all [Night]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<Night>> update(
_isd.DatabaseSession session,
List<Night> rows, {
_isd.ColumnSelections<NightTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<Night>(
rows,
columns: columns?.call(Night.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [Night]. 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<Night> updateRow(
_isd.DatabaseSession session,
Night row, {
_isd.ColumnSelections<NightTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<Night>(
row,
columns: columns?.call(Night.t),
transaction: transaction,
);
}
/// Updates a single [Night] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<Night?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<NightUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<Night>(
id,
columnValues: columnValues(Night.t.updateTable),
transaction: transaction,
);
}
/// Updates all [Night]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<Night>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<NightUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<NightTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<NightTable>? orderBy,
_isd.OrderByListBuilder<NightTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<Night>(
columnValues: columnValues(Night.t.updateTable),
where: where(Night.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(Night.t),
orderByList: orderByList?.call(Night.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [Night]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<Night>> delete(
_isd.DatabaseSession session,
List<Night> rows, {
_isd.OrderByBuilder<NightTable>? orderBy,
_isd.OrderByListBuilder<NightTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<Night>(
rows,
orderBy: orderBy?.call(Night.t),
orderByList: orderByList?.call(Night.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [Night].
Future<Night> deleteRow(
_isd.DatabaseSession session,
Night row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<Night>(
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<Night>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<NightTable> where,
_isd.OrderByBuilder<NightTable>? orderBy,
_isd.OrderByListBuilder<NightTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<Night>(
where: where(Night.t),
orderBy: orderBy?.call(Night.t),
orderByList: orderByList?.call(Night.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<NightTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<Night>(
where: where?.call(Night.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [Night] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<NightTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<Night>(
where: where(Night.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -21,11 +21,17 @@ import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart'
as _ipulbpi2;
import 'day/day.dart' as _ikmv9f3y;
import 'day/day_series.dart' as _ir1ofny6;
import 'day/night.dart' as _i17cgxdf;
import 'map/map_source.dart' as _ikw0qg8x;
import 'privacy/content_key.dart' as _imed3x5d;
import 'profile/profile.dart' as _ia5j8fch;
import 'workout/workout.dart' as _i5b9b17z;
import 'workout/workout_series.dart' as _i92mefto;
export 'day/day.dart';
export 'day/day_series.dart';
export 'day/night.dart';
export 'map/map_source.dart';
export 'privacy/content_key.dart';
export 'profile/profile.dart';
@ -103,6 +109,237 @@ class Protocol extends _isd.DatabaseSerializationManager {
],
managed: true,
),
_isd.TableDefinition(
name: 'day',
dartName: 'Day',
schema: 'public',
module: 'halkyon_train',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isd.ColumnDefinition(
name: 'payload',
columnType: _isd.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isd.ColumnDefinition(
name: 'nonce',
columnType: _isd.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'day_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'day_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'day_series',
dartName: 'DaySeries',
schema: 'public',
module: 'halkyon_train',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'dayId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isd.ColumnDefinition(
name: 'payload',
columnType: _isd.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isd.ColumnDefinition(
name: 'nonce',
columnType: _isd.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'day_series_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
_isd.ForeignKeyDefinition(
constraintName: 'day_series_fk_1',
columns: ['dayId'],
referenceTable: 'day',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'day_series_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'day_series_day_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'dayId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'night',
dartName: 'Night',
schema: 'public',
module: 'halkyon_train',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isd.ColumnDefinition(
name: 'payload',
columnType: _isd.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isd.ColumnDefinition(
name: 'nonce',
columnType: _isd.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'night_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'night_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'profile',
dartName: 'Profile',
@ -383,6 +620,15 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
}
if (t == _ikmv9f3y.Day) {
return _ikmv9f3y.Day.fromJson(data) as T;
}
if (t == _ir1ofny6.DaySeries) {
return _ir1ofny6.DaySeries.fromJson(data) as T;
}
if (t == _i17cgxdf.Night) {
return _i17cgxdf.Night.fromJson(data) as T;
}
if (t == _ikw0qg8x.MapSource) {
return _ikw0qg8x.MapSource.fromJson(data) as T;
}
@ -398,6 +644,15 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _i92mefto.WorkoutSeries) {
return _i92mefto.WorkoutSeries.fromJson(data) as T;
}
if (t == _isc.getType<_ikmv9f3y.Day?>()) {
return (data != null ? _ikmv9f3y.Day.fromJson(data) : null) as T;
}
if (t == _isc.getType<_ir1ofny6.DaySeries?>()) {
return (data != null ? _ir1ofny6.DaySeries.fromJson(data) : null) as T;
}
if (t == _isc.getType<_i17cgxdf.Night?>()) {
return (data != null ? _i17cgxdf.Night.fromJson(data) : null) as T;
}
if (t == _isc.getType<_ikw0qg8x.MapSource?>()) {
return (data != null ? _ikw0qg8x.MapSource.fromJson(data) : null) as T;
}
@ -434,6 +689,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
static String? getClassNameForType(Type type) {
return switch (type) {
_ikmv9f3y.Day => 'Day',
_ir1ofny6.DaySeries => 'DaySeries',
_i17cgxdf.Night => 'Night',
_ikw0qg8x.MapSource => 'MapSource',
_imed3x5d.ContentKey => 'ContentKey',
_ia5j8fch.Profile => 'Profile',
@ -456,6 +714,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
switch (data) {
case _ikmv9f3y.Day():
return 'Day';
case _ir1ofny6.DaySeries():
return 'DaySeries';
case _i17cgxdf.Night():
return 'Night';
case _ikw0qg8x.MapSource():
return 'MapSource';
case _imed3x5d.ContentKey():
@ -502,6 +766,15 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName is! String) {
return super.deserializeByClassName(data);
}
if (dataClassName == 'Day') {
return deserialize<_ikmv9f3y.Day>(data['data']);
}
if (dataClassName == 'DaySeries') {
return deserialize<_ir1ofny6.DaySeries>(data['data']);
}
if (dataClassName == 'Night') {
return deserialize<_i17cgxdf.Night>(data['data']);
}
if (dataClassName == 'MapSource') {
return deserialize<_ikw0qg8x.MapSource>(data['data']);
}
@ -596,6 +869,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
}
switch (t) {
case _ikmv9f3y.Day:
return _ikmv9f3y.Day.t;
case _ir1ofny6.DaySeries:
return _ir1ofny6.DaySeries.t;
case _i17cgxdf.Night:
return _i17cgxdf.Night.t;
case _imed3x5d.ContentKey:
return _imed3x5d.ContentKey.t;
case _ia5j8fch.Profile:

View file

@ -11,6 +11,9 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import 'day/day.dart' as _ikmv9f3y;
import 'day/day_series.dart' as _ir1ofny6;
import 'day/night.dart' as _i17cgxdf;
import 'privacy/content_key.dart' as _imed3x5d;
import 'profile/profile.dart' as _ia5j8fch;
import 'workout/workout.dart' as _i5b9b17z;
@ -21,6 +24,9 @@ import 'workout/workout_series.dart' as _i92mefto;
/// shared packages.
final List<_isd.Table> syncTables = [
_imed3x5d.ContentKey.t,
_ikmv9f3y.Day.t,
_ir1ofny6.DaySeries.t,
_i17cgxdf.Night.t,
_ia5j8fch.Profile.t,
_i5b9b17z.Workout.t,
_i92mefto.WorkoutSeries.t,

View file

@ -57,6 +57,8 @@ dependencies {
implementation("androidx.health.connect:connect-client:1.1.0")
// The Polar Loop (ADR 0030, D32, D34): Polar's BLE SDK, under Polar's licence.
implementation("com.github.polarofficial:polar-ble-sdk:8.3.0")
// The Loop read every few hours while Train is closed (ADR 0030, D37).
implementation("androidx.work:work-runtime-ktx:2.10.1")
// The recorder's pure Kotlin parts (auto-pause) tested on the JVM.
testImplementation("junit:junit:4.13.2")
}

View file

@ -0,0 +1,64 @@
package app.halkyon.train
/**
* Maps, lists, strings, numbers, booleans and nulls as one line of JSON, for
* the Loop's file (ADR 0030, D37): Android's `org.json` is a stub in JVM
* tests, and nothing more is needed.
*/
object JsonText {
fun of(value: Any?): String = StringBuilder().also { write(it, value) }.toString()
private fun write(out: StringBuilder, value: Any?) {
when (value) {
null -> out.append("null")
is Boolean -> out.append(value)
is Float -> number(out, value.toDouble())
is Double -> number(out, value)
is Number -> out.append(value.toLong())
is String -> string(out, value)
is Map<*, *> -> {
out.append('{')
var first = true
for ((key, item) in value) {
if (!first) out.append(',')
first = false
string(out, key.toString())
out.append(':')
write(out, item)
}
out.append('}')
}
is Iterable<*> -> {
out.append('[')
var first = true
for (item in value) {
if (!first) out.append(',')
first = false
write(out, item)
}
out.append(']')
}
else -> string(out, value.toString())
}
}
private fun number(out: StringBuilder, value: Double) {
if (value.isNaN() || value.isInfinite()) out.append("null") else out.append(value)
}
private fun string(out: StringBuilder, value: String) {
out.append('"')
for (char in value) {
when {
char == '"' -> out.append("\\\"")
char == '\\' -> out.append("\\\\")
char == '\n' -> out.append("\\n")
char == '\r' -> out.append("\\r")
char == '\t' -> out.append("\\t")
char < ' ' -> out.append(String.format("\\u%04x", char.code))
else -> out.append(char)
}
}
out.append('"')
}
}

View file

@ -13,6 +13,7 @@ import com.polar.sdk.api.model.PolarHrData
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.util.UUID
@ -21,6 +22,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* The Polar Loop for the app (ADR 0030, D32, D34), through Polar's BLE SDK,
@ -31,6 +34,7 @@ import kotlinx.coroutines.launch
*/
class LoopBridge(private val context: Context) : MethodChannel.MethodCallHandler {
private val scope: CoroutineScope = MainScope()
private val calls = Mutex()
private var events: EventChannel.EventSink? = null
// The SDK made on first use, so that an app without a Loop never starts it.
@ -44,8 +48,12 @@ class LoopBridge(private val context: Context) : MethodChannel.MethodCallHandler
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_FILE_TRANSFER,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_CONTROL,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ACTIVITY_DATA,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_SLEEP_DATA,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_TEMPERATURE_DATA,
),
).apply {
holding = true
setAutomaticReconnection(true)
setApiCallback(callback)
}
@ -94,22 +102,40 @@ class LoopBridge(private val context: Context) : MethodChannel.MethodCallHandler
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (call.method == "background") {
// What the background job reads, and where (D37); none to stop it.
LoopWorker.schedule(context, call.argument<Map<String, Any?>>("config"))
result.success(null)
return
}
scope.launch {
try {
val id = call.argument<String>("id")!!
when (call.method) {
"connect" -> result.success(api.connectToDevice(id).let { null })
"disconnect" -> result.success(api.disconnectFromDevice(id).let { null })
"isSetUp" -> result.success(api.isFtuDone(id))
"setUp" -> {
val config = LoopSetup.configOf(call.argument<Map<String, Any?>>("figures")!!, ZonedDateTime.now())
api.doFirstTimeUse(id, config)
api.setLocalTime(id, LocalDateTime.now())
result.success(null)
// One call to the band at a time, as Polar's sync guideline asks.
calls.withLock {
when (call.method) {
"connect" -> result.success(api.connectToDevice(id).let { null })
"disconnect" -> result.success(api.disconnectFromDevice(id).let { null })
"isSetUp" -> result.success(api.isFtuDone(id))
"setUp" -> {
val config = LoopSetup.configOf(call.argument<Map<String, Any?>>("figures")!!, ZonedDateTime.now())
api.doFirstTimeUse(id, config)
api.setLocalTime(id, LocalDateTime.now())
result.success(null)
}
"setClock" -> result.success(api.setLocalTime(id, LocalDateTime.now()).let { null })
"reset" -> result.success(api.doFactoryReset(id).let { null })
"readDays" ->
result.success(
LoopDays.read(
PolarLoopSource(api, id),
from = LocalDate.parse(call.argument<String>("from")!!),
today = LocalDate.now(),
keptUntil = call.argument<String>("keptUntil")?.let(LocalDate::parse),
),
)
else -> result.notImplemented()
}
"setClock" -> result.success(api.setLocalTime(id, LocalDateTime.now()).let { null })
"reset" -> result.success(api.doFactoryReset(id).let { null })
else -> result.notImplemented()
}
} catch (error: SecurityException) {
result.error("refused", error.message, null)
@ -123,7 +149,10 @@ class LoopBridge(private val context: Context) : MethodChannel.MethodCallHandler
fun dispose() {
scope.cancel()
events = null
if (made.isInitialized()) api.shutDown()
if (made.isInitialized()) {
api.shutDown()
holding = false
}
}
private fun send(id: String, event: String, vararg values: Pair<String, Any?>) {
@ -173,8 +202,12 @@ class LoopBridge(private val context: Context) : MethodChannel.MethodCallHandler
override fun htsNotificationReceived(identifier: String, data: PolarHealthThermometerData) {}
}
private companion object {
companion object {
/** Whether the open app holds the SDK, so that the background job leaves the band alone. */
@Volatile
var holding = false
/** Device Information's Firmware Revision String (0x2A26). */
val FIRMWARE_REVISION: UUID = UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb")
private val FIRMWARE_REVISION: UUID = UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb")
}
}

View file

@ -0,0 +1,181 @@
package app.halkyon.train
import com.polar.sdk.api.PolarBleApi
import com.polar.sdk.api.model.PolarSkinTemperatureData
import com.polar.sdk.api.model.activity.Polar247HrSamplesData
import com.polar.sdk.api.model.activity.PolarActiveTime
import com.polar.sdk.api.model.activity.PolarDailySummaryData
import com.polar.sdk.api.model.sleep.PolarNightlyRechargeData
import com.polar.sdk.api.model.sleep.PolarSleepData
import java.time.LocalDate
import java.time.format.DateTimeFormatter
/** What a read of the Loop asks of the band, one call after the other (Polar's sync guideline). */
interface LoopSource {
/** Readies the band: it writes what it holds in memory to its files. */
suspend fun startSync()
suspend fun stopSync()
suspend fun summaries(from: LocalDate, to: LocalDate): List<PolarDailySummaryData>
suspend fun heartRates(from: LocalDate, to: LocalDate): List<Polar247HrSamplesData>
suspend fun sleeps(from: LocalDate, to: LocalDate): List<PolarSleepData>
suspend fun recharges(from: LocalDate, to: LocalDate): List<PolarNightlyRechargeData>
suspend fun skinTemperatures(from: LocalDate, to: LocalDate): List<PolarSkinTemperatureData>
suspend fun delete(type: PolarBleApi.PolarStoredDataType, until: LocalDate)
}
/**
* The Loop's days and nights (ADR 0030, D37, D38): a read from a date to
* today, as plain values for the app (a line of the person's file each, or a
* method channel's answer), after which what the app has kept is deleted on
* the band, never today.
*/
object LoopDays {
/** What the band deletes of a day kept, type by type (D38). */
val deletedTypes =
listOf(
PolarBleApi.PolarStoredDataType.ACTIVITY,
PolarBleApi.PolarStoredDataType.DAILY_SUMMARY,
PolarBleApi.PolarStoredDataType.AUTO_SAMPLE,
PolarBleApi.PolarStoredDataType.SLEEP,
PolarBleApi.PolarStoredDataType.SLEEP_SCORE,
PolarBleApi.PolarStoredDataType.NIGHTLY_RECOVERY,
PolarBleApi.PolarStoredDataType.SKIN_TEMP,
PolarBleApi.PolarStoredDataType.SKIN_CONTACT_CHANGES,
)
/**
* Reads the days from [from] to [today] off [source], a `read` line first
* (when and what), then deletes on the band what the app kept up to
* [keptUntil], which never reaches today.
*/
suspend fun read(
source: LoopSource,
from: LocalDate,
today: LocalDate,
keptUntil: LocalDate?,
): List<Map<String, Any?>> {
val lines = mutableListOf<Map<String, Any?>>(
mapOf("kind" to "read", "on" to dateOf(today), "from" to dateOf(from), "to" to dateOf(today)),
)
source.startSync()
try {
if (!from.isAfter(today)) {
source.summaries(from, today).mapTo(lines, ::summaryOf)
source.heartRates(from, today).mapTo(lines, ::heartOf)
source.sleeps(from, today).mapNotNullTo(lines, ::sleepOf)
source.recharges(from, today).mapNotNullTo(lines, ::rechargeOf)
source.skinTemperatures(from, today).mapNotNullTo(lines, ::skinOf)
}
val until = keptUntil?.let { minOf(it, today.minusDays(1)) }
if (until != null) {
for (type in deletedTypes) source.delete(type, until)
}
} finally {
source.stopSync()
}
return lines
}
fun summaryOf(day: PolarDailySummaryData): Map<String, Any?> {
val times = day.activityClassTimes
return mapOf(
"kind" to "summary",
"date" to day.date?.let(::dateOf),
"steps" to day.steps,
"metres" to day.activityDistance,
"activityCalories" to day.activityCalories,
"trainingCalories" to day.trainingCalories,
"bmrCalories" to day.bmrCalories,
"goal" to day.activityGoalSummary?.activityGoal,
"achieved" to day.activityGoalSummary?.achievedActivity,
"times" to
times?.let {
mapOf(
"nonWear" to secondsOf(it.timeNonWear),
"sleep" to secondsOf(it.timeSleep),
"sedentary" to secondsOf(it.timeSedentary),
"light" to secondsOf(it.timeLightActivity),
"continuousModerate" to secondsOf(it.timeContinuousModerateActivity),
"intermittentModerate" to secondsOf(it.timeIntermittentModerateActivity),
"continuousVigorous" to secondsOf(it.timeContinuousVigorousActivity),
"intermittentVigorous" to secondsOf(it.timeIntermittentVigorousActivity),
)
},
)
}
/** A day's 24/7 heart rate: each measurement's start (seconds into the band's day), its kind and its samples. */
fun heartOf(day: Polar247HrSamplesData): Map<String, Any?> =
mapOf(
"kind" to "heart",
"date" to dateOf(day.date),
"runs" to
day.samples.map { run ->
mapOf(
"at" to run.startTime.toSecondOfDay(),
"trigger" to run.triggerType.name.removePrefix("TRIGGER_TYPE_").lowercase(),
"bpm" to run.hrSamples,
)
},
)
fun sleepOf(night: PolarSleepData): Map<String, Any?>? {
val result = night.result ?: return null
return mapOf(
"kind" to "sleep",
"date" to dateOf(result.sleepResultDate ?: night.date ?: return null),
"start" to result.sleepStartTime?.toOffsetDateTime()?.toString(),
"end" to result.sleepEndTime?.toOffsetDateTime()?.toString(),
"goalMinutes" to result.sleepGoalMinutes,
"phases" to result.sleepWakePhases?.map { listOf(it.secondsFromSleepStart, it.state.name.lowercase()) },
"cycles" to result.sleepCycles?.map { listOf(it.secondsFromSleepStart, it.sleepDepthStart) },
"rating" to result.userSleepRating?.name?.removePrefix("SLEPT_")?.lowercase(),
"batteryRanOut" to result.batteryRanOut,
"skinCelsius" to result.sleepSkinTemperatureResult?.sleepSkinTemperatureCelsius,
"skinDeviation" to result.sleepSkinTemperatureResult?.deviationFromBaseLine,
)
}
fun rechargeOf(recharge: PolarNightlyRechargeData): Map<String, Any?>? {
val date = recharge.sleepResultDate ?: return null
return mapOf(
"kind" to "recharge",
"date" to dateOf(date),
"ansStatus" to recharge.ansStatus,
"recovery" to recharge.recoveryIndicator,
"recoverySubLevel" to recharge.recoveryIndicatorSubLevel,
"ansRate" to recharge.ansRate,
"rri" to recharge.meanNightlyRecoveryRRI,
"rmssd" to recharge.meanNightlyRecoveryRMSSD,
"breathInterval" to recharge.meanNightlyRecoveryRespirationInterval,
"baselineRri" to recharge.meanBaselineRRI,
"baselineRriSd" to recharge.sdBaselineRRI,
"baselineRmssd" to recharge.meanBaselineRMSSD,
"baselineRmssdSd" to recharge.sdBaselineRMSSD,
"baselineBreathInterval" to recharge.meanBaselineRespirationInterval,
"baselineBreathIntervalSd" to recharge.sdBaselineRespirationInterval,
)
}
fun skinOf(skin: PolarSkinTemperatureData): Map<String, Any?>? {
val date = skin.date ?: return null
val samples = skin.result?.skinTemperatureList ?: return null
return mapOf(
"kind" to "skin",
"date" to dateOf(date),
"samples" to samples.map { listOf(it.recordingTimeDeltaMs, it.temperature) },
)
}
private fun secondsOf(time: PolarActiveTime?): Int? =
time?.let { it.hours * 3600 + it.minutes * 60 + it.seconds }
private fun dateOf(date: LocalDate): String = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
}

View file

@ -0,0 +1,159 @@
package app.halkyon.train
import android.content.Context
import android.util.Log
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.polar.androidcommunications.api.ble.model.DisInfo
import com.polar.androidcommunications.api.ble.model.gatt.client.ChargeState
import com.polar.androidcommunications.api.ble.model.gatt.client.PowerSourcesState
import com.polar.sdk.api.PolarBleApi
import com.polar.sdk.api.PolarBleApiCallbackProvider
import com.polar.sdk.api.PolarBleApiDefaultImpl
import com.polar.sdk.api.model.PolarDeviceInfo
import com.polar.sdk.api.model.PolarHealthThermometerData
import com.polar.sdk.api.model.PolarHrData
import java.io.File
import java.io.FileOutputStream
import java.time.LocalDate
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.withTimeout
import org.json.JSONObject
/**
* Reads the person's Loop every few hours while Train is closed (ADR 0030,
* D37, D38): connects, reads the days since those the app kept into the
* person's `loop_days.txt` (a line of JSON each, as [LoopDays] makes them),
* deletes on the band what the app said it kept, and lets go. The app seals
* the lines as it opens; this never seals, as only the app holds the key.
* It leaves the band alone while the open app holds it.
*/
class LoopWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) {
override suspend fun doWork(): Result {
if (LoopBridge.holding) return Result.success()
val config = configOf(applicationContext) ?: return Result.success()
val ready = CompletableDeferred<Unit>()
val api =
PolarBleApiDefaultImpl.defaultImplementation(
applicationContext,
setOf(
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_FILE_TRANSFER,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_CONTROL,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ACTIVITY_DATA,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_SLEEP_DATA,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_TEMPERATURE_DATA,
),
)
api.setApiCallback(ReadyCallback(config.id, ready))
return try {
api.connectToDevice(config.id)
withTimeout(TimeUnit.SECONDS.toMillis(90)) { ready.await() }
val lines =
LoopDays.read(PolarLoopSource(api, config.id), config.from, LocalDate.now(), config.keptUntil)
FileOutputStream(File(config.folder, FILE), true).use { out ->
for (line in lines) out.write((JsonText.of(line) + "\n").toByteArray())
out.fd.sync()
}
Result.success()
} catch (error: Exception) {
// Out of reach, or the band busy: the next run tries again.
Log.i("Loop", "The Loop not read in the background: ${error.javaClass.simpleName}")
Result.success()
} finally {
runCatching { api.disconnectFromDevice(config.id) }
api.shutDown()
}
}
/** What is read and where: the band, the person's folder, from when, and what the app kept. */
private class Config(val folder: File, val id: String, val from: LocalDate, val keptUntil: LocalDate?)
/** Ready once the band's activity can be read. */
private class ReadyCallback(private val id: String, private val ready: CompletableDeferred<Unit>) :
PolarBleApiCallbackProvider {
override fun bleSdkFeatureReady(identifier: String, feature: PolarBleApi.PolarBleSdkFeature) {
if (identifier == id && feature == PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ACTIVITY_DATA) {
ready.complete(Unit)
}
}
override fun blePowerStateChanged(powered: Boolean) {}
override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) {}
override fun deviceConnecting(polarDeviceInfo: PolarDeviceInfo) {}
override fun deviceDisconnected(polarDeviceInfo: PolarDeviceInfo) {}
override fun bleSdkFeaturesReadiness(
identifier: String,
ready: List<PolarBleApi.PolarBleSdkFeature>,
unavailable: List<PolarBleApi.PolarBleSdkFeature>,
) {}
override fun disInformationReceived(identifier: String, uuid: UUID, value: String) {}
override fun disInformationReceived(identifier: String, disInfo: DisInfo) {}
override fun batteryLevelReceived(identifier: String, level: Int) {}
override fun batteryChargingStatusReceived(identifier: String, chargingStatus: ChargeState) {}
override fun powerSourcesStateReceived(identifier: String, powerSourcesState: PowerSourcesState) {}
override fun hrNotificationReceived(identifier: String, data: PolarHrData.PolarHrSample) {}
override fun htsNotificationReceived(identifier: String, data: PolarHealthThermometerData) {}
}
companion object {
/** The lines read in the background, in the person's folder. */
const val FILE = "loop_days.txt"
private const val CONFIG = "loop_background.json"
private const val WORK = "loop-days"
/**
* Reads the band every four hours with [config] (`folder`, `id`, `from`,
* `keptUntil`), or no longer where it is null.
*/
fun schedule(context: Context, config: Map<String, Any?>?) {
val file = File(context.filesDir, CONFIG)
val work = WorkManager.getInstance(context)
if (config == null) {
file.delete()
work.cancelUniqueWork(WORK)
return
}
file.writeText(JsonText.of(config))
work.enqueueUniquePeriodicWork(
WORK,
// A new config is read by the next run; the schedule stays.
ExistingPeriodicWorkPolicy.KEEP,
PeriodicWorkRequestBuilder<LoopWorker>(4, TimeUnit.HOURS)
.setConstraints(Constraints.Builder().setRequiresBatteryNotLow(true).build())
.build(),
)
}
private fun configOf(context: Context): Config? {
val file = File(context.filesDir, CONFIG)
if (!file.exists()) return null
return runCatching {
val json = JSONObject(file.readText())
Config(
folder = File(json.getString("folder")),
id = json.getString("id"),
from = LocalDate.parse(json.getString("from")),
keptUntil = json.optString("keptUntil").takeIf { it.isNotEmpty() && it != "null" }?.let(LocalDate::parse),
)
}.getOrNull()
}
}
}

View file

@ -0,0 +1,29 @@
package app.halkyon.train
import com.polar.sdk.api.PolarBleApi
import java.time.LocalDate
/** [LoopSource] on Polar's SDK, for the band [id] (ADR 0030, D37). */
class PolarLoopSource(private val api: PolarBleApi, private val id: String) : LoopSource {
override suspend fun startSync() {
api.sendInitializationAndStartSyncNotifications(id)
}
override suspend fun stopSync() {
api.sendTerminateAndStopSyncNotifications(id)
}
override suspend fun summaries(from: LocalDate, to: LocalDate) = api.getDailySummaryData(id, from, to)
override suspend fun heartRates(from: LocalDate, to: LocalDate) = api.get247HrSamples(id, from, to)
override suspend fun sleeps(from: LocalDate, to: LocalDate) = api.getSleep(id, from, to)
override suspend fun recharges(from: LocalDate, to: LocalDate) = api.getNightlyRecharge(id, from, to)
override suspend fun skinTemperatures(from: LocalDate, to: LocalDate) = api.getSkinTemperature(id, from, to)
override suspend fun delete(type: PolarBleApi.PolarStoredDataType, until: LocalDate) {
api.deleteStoredDeviceData(id, type, until)
}
}

View file

@ -0,0 +1,264 @@
package app.halkyon.train
import com.polar.sdk.api.PolarBleApi
import com.polar.sdk.api.model.PolarSkinTemperatureData
import com.polar.sdk.api.model.PolarSkinTemperatureDataSample
import com.polar.sdk.api.model.PolarSkinTemperatureResult
import com.polar.sdk.api.model.SkinTemperatureMeasurementType
import com.polar.sdk.api.model.SkinTemperatureSensorLocation
import com.polar.sdk.api.model.activity.AutomaticSampleTriggerType
import com.polar.sdk.api.model.activity.Polar247HrSamples
import com.polar.sdk.api.model.activity.Polar247HrSamplesData
import com.polar.sdk.api.model.activity.PolarActiveTime
import com.polar.sdk.api.model.activity.PolarActiveTimeData
import com.polar.sdk.api.model.activity.PolarActivityGoalSummary
import com.polar.sdk.api.model.activity.PolarDailySummaryData
import com.polar.sdk.api.model.sleep.PolarNightlyRechargeData
import com.polar.sdk.api.model.sleep.PolarSleepAnalysisResult
import com.polar.sdk.api.model.sleep.PolarSleepData
import com.polar.sdk.api.model.sleep.SleepCycle
import com.polar.sdk.api.model.sleep.SleepRating
import com.polar.sdk.api.model.sleep.SleepSkinTemperatureResult
import com.polar.sdk.api.model.sleep.SleepWakePhase
import com.polar.sdk.api.model.sleep.SleepWakeState
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneOffset
import java.time.ZonedDateTime
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
// The Loop's days and nights as the app takes them (ADR 0030, D37), and what
// a read deletes on the band (D38): only what the app kept, never today.
class LoopDaysTest {
private val today = LocalDate.of(2026, 10, 28)
private val yesterday = today.minusDays(1)
private fun time(minutes: Int) = PolarActiveTime(minutes / 60, minutes % 60, 0, 0)
private val summary =
PolarDailySummaryData(
yesterday,
420,
120,
1500,
9876,
PolarActivityGoalSummary(100f, 87.5f, time(0), time(0), time(0)),
PolarActiveTimeData(
yesterday,
time(30),
time(450),
time(600),
time(180),
time(40),
time(20),
time(10),
time(5),
),
7012.5f,
null,
null,
)
private val heart =
Polar247HrSamplesData(
yesterday,
listOf(
Polar247HrSamples(LocalTime.of(0, 10), listOf(58, 57, 59), AutomaticSampleTriggerType.TRIGGER_TYPE_TIMED),
Polar247HrSamples(LocalTime.of(17, 30, 5), listOf(141), AutomaticSampleTriggerType.TRIGGER_TYPE_HIGH_ACTIVITY),
),
)
private val zone = ZoneOffset.ofHours(1)
private val sleep =
PolarSleepData(
today,
PolarSleepAnalysisResult(
ZonedDateTime.of(2026, 10, 27, 23, 15, 0, 0, zone),
ZonedDateTime.of(2026, 10, 28, 7, 5, 0, 0, zone),
null,
480,
listOf(SleepWakePhase(0, SleepWakeState.WAKE), SleepWakePhase(600, SleepWakeState.NONREM12)),
emptyList(),
null,
null,
null,
SleepRating.SLEPT_SOMEWHAT_WELL,
"A1",
false,
listOf(SleepCycle(600, 1.5f)),
today,
null,
SleepSkinTemperatureResult(today, 33.4f, -0.2f),
),
)
private val recharge =
PolarNightlyRechargeData(
LocalDateTime.of(2026, 10, 28, 7, 30),
null,
2.5f,
4,
1,
5,
null,
1050,
48,
3800,
1000,
60,
45,
8,
3700,
200,
null,
null,
null,
today,
)
private val skin =
PolarSkinTemperatureData(
today,
PolarSkinTemperatureResult(
"A1",
SkinTemperatureSensorLocation.values().first(),
SkinTemperatureMeasurementType.values().first(),
listOf(PolarSkinTemperatureDataSample(0, 33.1f), PolarSkinTemperatureDataSample(60000, 33.3f)),
),
)
private inner class FakeBand : LoopSource {
val calls = mutableListOf<String>()
override suspend fun startSync() {
calls += "start"
}
override suspend fun stopSync() {
calls += "stop"
}
override suspend fun summaries(from: LocalDate, to: LocalDate) = listOf(summary).also { calls += "summaries $from $to" }
override suspend fun heartRates(from: LocalDate, to: LocalDate) = listOf(heart).also { calls += "heart" }
override suspend fun sleeps(from: LocalDate, to: LocalDate) = listOf(sleep).also { calls += "sleep" }
override suspend fun recharges(from: LocalDate, to: LocalDate) = listOf(recharge).also { calls += "recharge" }
override suspend fun skinTemperatures(from: LocalDate, to: LocalDate) = listOf(skin).also { calls += "skin" }
override suspend fun delete(type: PolarBleApi.PolarStoredDataType, until: LocalDate) {
calls += "delete $type $until"
}
}
@Test
fun `a read says when it was and what it covers, framed by the sync calls`() {
val band = FakeBand()
val lines = runBlocking { LoopDays.read(band, yesterday, today, keptUntil = null) }
assertEquals(mapOf("kind" to "read", "on" to "2026-10-28", "from" to "2026-10-27", "to" to "2026-10-28"), lines.first())
assertEquals(listOf("summary", "heart", "sleep", "recharge", "skin"), lines.drop(1).map { it["kind"] })
assertEquals("start", band.calls.first())
assertEquals("summaries 2026-10-27 2026-10-28", band.calls[1])
assertEquals("stop", band.calls.last())
assertTrue("nothing kept, nothing deleted", band.calls.none { it.startsWith("delete") })
}
@Test
fun `the day's summary keeps its figures and its time in each class in seconds`() {
val line = LoopDays.summaryOf(summary)
assertEquals("2026-10-27", line["date"])
assertEquals(9876, line["steps"])
assertEquals(7012.5f, line["metres"])
assertEquals(420, line["activityCalories"])
assertEquals(87.5f, line["achieved"])
@Suppress("UNCHECKED_CAST")
val times = line["times"] as Map<String, Int?>
assertEquals(450 * 60, times["sleep"])
assertEquals(5 * 60, times["intermittentVigorous"])
}
@Test
fun `the day's heart rate keeps each measurement's start, kind and samples`() {
@Suppress("UNCHECKED_CAST")
val runs = LoopDays.heartOf(heart)["runs"] as List<Map<String, Any?>>
assertEquals(mapOf("at" to 600, "trigger" to "timed", "bpm" to listOf(58, 57, 59)), runs[0])
assertEquals(17 * 3600 + 30 * 60 + 5, runs[1]["at"])
assertEquals("high_activity", runs[1]["trigger"])
}
@Test
fun `a night counts for the day it ended, with its times, phases and skin`() {
val line = LoopDays.sleepOf(sleep)!!
assertEquals("2026-10-28", line["date"])
assertEquals("2026-10-27T23:15+01:00", line["start"])
assertEquals(listOf(listOf(0, "wake"), listOf(600, "nonrem12")), line["phases"])
assertEquals("somewhat_well", line["rating"])
assertEquals(33.4f, line["skinCelsius"])
}
@Test
fun `Nightly Recharge and skin temperature keep their figures`() {
val recharge = LoopDays.rechargeOf(recharge)!!
assertEquals("2026-10-28", recharge["date"])
assertEquals(4, recharge["recovery"])
assertEquals(48, recharge["rmssd"])
val skin = LoopDays.skinOf(skin)!!
assertEquals(listOf(listOf(0L, 33.1f), listOf(60000L, 33.3f)), skin["samples"])
}
@Test
fun `what the app kept is deleted after the read, type by type`() {
val band = FakeBand()
runBlocking { LoopDays.read(band, today, today, keptUntil = yesterday) }
val deleted = band.calls.filter { it.startsWith("delete") }
assertEquals(LoopDays.deletedTypes.map { "delete $it 2026-10-27" }, deleted)
assertTrue(band.calls.indexOf("skin") < band.calls.indexOf(deleted.first()))
}
@Test
fun `today is never deleted, whatever the app says it kept`() {
val band = FakeBand()
runBlocking { LoopDays.read(band, today, today, keptUntil = today) }
assertTrue(band.calls.filter { it.startsWith("delete") }.all { it.endsWith("2026-10-27") })
}
@Test
fun `the sync is stopped even when the band fails`() {
val band =
object : LoopSource by FakeBand() {
val calls = mutableListOf<String>()
override suspend fun stopSync() {
calls += "stop"
}
override suspend fun heartRates(from: LocalDate, to: LocalDate): List<Polar247HrSamplesData> =
throw IllegalStateException("gone")
}
runCatching { runBlocking { LoopDays.read(band, today, today, keptUntil = yesterday) } }
assertEquals(listOf("stop"), band.calls)
}
@Test
fun `lines are written as JSON`() {
assertEquals(
"""{"kind":"read","n":[1,2.5,null,true],"s":"a\"b"}""",
JsonText.of(mapOf("kind" to "read", "n" to listOf(1, 2.5, null, true), "s" to "a\"b")),
)
}
}

View file

@ -0,0 +1,21 @@
### A day as the person's Polar Loop counted it (ADR 0030, D37): its date, its
### steps, distance, calories and time in each activity class, its Nightly
### Recharge and skin temperature, all in its sealed payload (D7). One row per
### person and date, its ID made from both on the device, so a day read again
### (today, as it goes on) or on another phone writes the same row, the last
### writer winning. Its heart rate is its series (`day_series`). Synced.
class: Day
table: day
database: sync
fields:
### 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).
day_space_idx:
fields: spaceId

View file

@ -0,0 +1,25 @@
### A chunk of a day's 24/7 heart rate from the Polar Loop (ADR 0030, D37):
### up to three hours of samples, in its sealed payload (D7) as a workout's
### series are, sealed for its day (`bound`). Its ID is made from its day's and
### its place in the day, so the day read again rewrites the same chunks.
### Synced; it goes with its day.
class: DaySeries
table: day_series
database: sync
fields:
### The day it is part of.
day: Day?, 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).
day_series_space_idx:
fields: spaceId
# A day's chunks, to read them together.
day_series_day_idx:
fields: dayId

View file

@ -0,0 +1,20 @@
### A night's sleep as the person's Polar Loop analysed it (ADR 0030, D37):
### when it began and ended, its wake phases and cycles, its rating and its
### skin temperature, all in its sealed payload (D7). One row per person and
### the date the sleep counts for, its ID made from both on the device, the
### last writer winning. Synced.
class: Night
table: night
database: sync
fields:
### 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).
night_space_idx:
fields: spaceId

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