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:
parent
24de3c0a4e
commit
88e8693025
32 changed files with 24637 additions and 52 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
674
apps/train/halkyon_train_client/lib/src/protocol/day/day.dart
Normal file
674
apps/train/halkyon_train_client/lib/src/protocol/day/day.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
673
apps/train/halkyon_train_client/lib/src/protocol/day/night.dart
Normal file
673
apps/train/halkyon_train_client/lib/src/protocol/day/night.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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('"')
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")),
|
||||
)
|
||||
}
|
||||
}
|
||||
21
apps/train/halkyon_train_server/lib/src/day/day.spy.yaml
Normal file
21
apps/train/halkyon_train_server/lib/src/day/day.spy.yaml
Normal 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
|
||||
|
|
@ -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
|
||||
20
apps/train/halkyon_train_server/lib/src/day/night.spy.yaml
Normal file
20
apps/train/halkyon_train_server/lib/src/day/night.spy.yaml
Normal 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
Loading…
Reference in a new issue