Commit 38a51e6

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-03-27 19:03:40
implement MedicineIntakeRepository
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent e30ebb2
health_data_store/lib/src/repositories/blood_pressure_repository.dart
@@ -27,7 +27,6 @@ class BloodPressureRepository extends Repository<BloodPressureRecord> {
         timeSec,
         ['Systolic', 'Diastolic', 'Pulse'],
       );
-      print(record);
       if (record.sys != null) {
         await txn.insert('Systolic', {
           'entryID': entryID,
@@ -57,7 +56,7 @@ class BloodPressureRepository extends Repository<BloodPressureRecord> {
         'LEFT JOIN Systolic AS s ON t.entryID = s.entryID '
         'LEFT JOIN Diastolic AS d ON t.entryID = d.entryID '
         'LEFT JOIN Pulse AS p ON t.entryID = p.entryID '
-        'WHERE timestampUnixS BETWEEN ? AND ?;',
+      'WHERE timestampUnixS BETWEEN ? AND ?',
       [range.startStamp, range.endStamp]
     );
     final records = <BloodPressureRecord>[];
health_data_store/lib/src/repositories/medicine_intake_repository.dart
@@ -0,0 +1,105 @@
+import 'package:health_data_store/health_data_store.dart';
+import 'package:health_data_store/src/database_helper.dart';
+import 'package:health_data_store/src/database_manager.dart';
+import 'package:health_data_store/src/repositories/repository.dart';
+import 'package:sqflite_common/sqflite.dart';
+
+/// Repository for [MedicineIntake]s.
+///
+/// Provides high level access on intakes saved in a [DatabaseManager] managed
+/// database.
+class MedicineIntakeRepository extends Repository<MedicineIntake> {
+  /// Create a repository for medicine intakes.
+  MedicineIntakeRepository(this._db);
+
+  /// The [DatabaseManager] managed database
+  final Database _db;
+
+  @override
+  Future<void> add(MedicineIntake intake) => _db.transaction((txn) async {
+    // obtain medicine id
+    final medIDRes = await txn.query('Medicine',
+      columns: ['medID'],
+      where: 'designation = ? '
+          'AND color ' +((intake.medicine.color != null) ? '= ?' : 'IS NULL')
+          + ' AND defaultDose ' +((intake.medicine.dosis != null) ? '= ?' : 'IS '
+          'NULL'),
+      whereArgs: [
+        intake.medicine.designation,
+        if (intake.medicine.color != null)
+          intake.medicine.color,
+        if (intake.medicine.dosis != null)
+          intake.medicine.dosis,
+      ],
+    );
+    assert(medIDRes.isNotEmpty);
+    // Assuming intakes only contain medications that have been added
+    final medID = medIDRes.first['medID'];
+
+    // obtain free entry id
+    final id = await DBHelper.getEntryID(
+      txn, intake.time.millisecondsSinceEpoch ~/ 1000,
+      ['Intake'],
+    );
+
+    // store to db
+    await txn.insert('Intake', {
+      'entryID': id,
+      'medID': medID,
+      'dosis': intake.dosis,
+    });
+  });
+
+  @override
+  Future<List<MedicineIntake>> get(DateRange range) async {
+    final results = await _db.rawQuery(
+      'SELECT t.timestampUnixS, dosis, defaultDose, designation, color '
+        'FROM Timestamps AS t '
+        'LEFT JOIN Intake AS i ON t.entryID = i.entryID '
+        'LEFT JOIN Medicine AS m ON m.medID = i.medID '
+      'WHERE t.timestampUnixS BETWEEN ? AND ?'
+      'AND i.dosis IS NOT NULL', // deleted intakes
+      [range.startStamp, range.endStamp]
+    );
+    final intakes = <MedicineIntake>[];
+    for (final r in results) {
+      final timeS = r['timestampUnixS'] as int;
+      intakes.add(MedicineIntake(
+        time: DateTime.fromMillisecondsSinceEpoch(timeS * 1000),
+        dosis: r['dosis'] as double,
+        medicine: Medicine(
+          designation: r['designation'] as String,
+          dosis: r['defaultDose'] as double?,
+          color: r['color'] as int?,
+        ),
+      ));
+    }
+    return intakes;
+  }
+
+  @override
+  Future<void> remove(MedicineIntake intake) => _db.rawDelete(
+    'DELETE FROM Intake WHERE entryID IN ('
+      'SELECT entryID FROM Timestamps '
+      'WHERE timestampUnixS = ?'
+    ') AND dosis = ? '
+    'AND medID IN ('
+      'SELECT medID FROM Medicine '
+      'WHERE designation = ?'
+      'AND color '
+        + ((intake.medicine.color != null) ? '= ?' : 'IS NULL') +
+      ' AND defaultDose '
+        + ((intake.medicine.dosis != null) ? '= ?' : 'IS NULL') +
+    ')',
+    [
+      intake.time.millisecondsSinceEpoch ~/ 1000,
+      intake.dosis,
+      intake.medicine.designation,
+      if (intake.medicine.color != null)
+        intake.medicine.color,
+      if (intake.medicine.dosis != null)
+        intake.medicine.dosis,
+    ]
+  );
+
+}
health_data_store/lib/src/types/medicine_intake.dart
@@ -16,6 +16,6 @@ class MedicineIntake with _$MedicineIntake {
     ///
     /// When the medication has a default value, this must be set to that value,
     /// as it may change.
-    required int dosis,
+    required double dosis,
   }) = _MedicineIntake;
 }
health_data_store/lib/src/database_manager.dart
@@ -47,6 +47,7 @@ class DatabaseManager {
   Database get db => _db.database;
   
   Future<void> _setUpTables() async {
+    // TODO: IF NOT EXISTS ?
     await _db.execute('CREATE TABLE "Medicine" ('
       '"medID"       INTEGER NOT NULL UNIQUE,'
       '"designation" TEXT NOT NULL,'
@@ -58,12 +59,12 @@ class DatabaseManager {
     await _db.execute('CREATE TABLE "Timestamps" ('
       '"entryID"	      INTEGER NOT NULL UNIQUE,'
       '"timestampUnixS"	INTEGER NOT NULL,'
-      'PRIMARY KEY("entryID")'
+      'PRIMARY KEY("entryID")' // TODO: add timezone to determine morning, evening
     ');');
     await _db.execute('CREATE TABLE "Intake" ('
       '"entryID" INTEGER NOT NULL,'
       '"medID"	 INTEGER NOT NULL,'
-      '"dosis"	 INTEGER NOT NULL,'
+      '"dosis"	 REAL NOT NULL,'
       'PRIMARY KEY("entryID"),'
       'FOREIGN KEY("entryID") REFERENCES "Timestamps"("entryID"),'
       'FOREIGN KEY("medID") REFERENCES "Medicine"("medID")'
health_data_store/test/src/repositories/blood_pressure_repository_test.dart
@@ -1,6 +1,5 @@
 
 import 'package:health_data_store/health_data_store.dart';
-import 'package:health_data_store/src/repositories/blood_pressure_repository.dart';
 import 'package:test/test.dart';
 
 import '../database_manager_test.dart';
@@ -84,7 +83,6 @@ void main() {
       start: DateTime.fromMillisecondsSinceEpoch(0),
       end: DateTime.fromMillisecondsSinceEpoch(80000),
     ));
-    print(values1);
     expect(values1, hasLength(2));
     expect(values1, containsAll([r2,r3]));
 
@@ -93,7 +91,6 @@ void main() {
       start: DateTime.fromMillisecondsSinceEpoch(0),
       end: DateTime.fromMillisecondsSinceEpoch(80000),
     ));
-    print(values2);
     expect(values2, hasLength(1));
     expect(values2, containsAll([r3]));
 
@@ -134,4 +131,6 @@ void main() {
     ));
     expect(values, isEmpty);
   });
+
+  // TODO: not return records out of range
 }
health_data_store/test/src/repositories/medicine_intake_repository_test.dart
@@ -0,0 +1,119 @@
+
+import 'package:health_data_store/health_data_store.dart';
+import 'package:health_data_store/src/repositories/medicine_intake_repository.dart';
+import 'package:test/test.dart';
+
+import '../database_manager_test.dart';
+import '../types/medicine_intake_test.dart';
+import '../types/medicine_test.dart';
+
+void main() {
+  sqfliteTestInit();
+  test('should initialize', () async {
+    final db = await mockDBManager();
+    addTearDown(db.close);
+    MedicineIntakeRepository(db.db);
+  });
+  test('should store intakes without errors', () async {
+    final db = await mockDBManager();
+    addTearDown(db.close);
+    final med1 = mockMedicine(designation: 'med1', dosis: 2.4);
+    final med2 = mockMedicine(designation: 'med2',);
+    final medRepo = MedicineRepository(db.db);
+    await medRepo.add(med1);
+    await medRepo.add(med2);
+
+    final repo = MedicineIntakeRepository(db.db);
+    await repo.add(mockIntake(med1));
+    await repo.add(mockIntake(med2));
+    await repo.add(mockIntake(med1, dosis: 123));
+    // This medicine is not added to med repo
+    expect(() async => repo.add(mockIntake(mockMedicine())),
+        throwsA(isA<AssertionError>()));
+  });
+  test('should return stored intakes', () async {
+    final db = await mockDBManager();
+    addTearDown(db.close);
+    final med1 = mockMedicine(dosis: 2.4);
+    final med2 = mockMedicine();
+    final medRepo = MedicineRepository(db.db);
+    await medRepo.add(med1);
+    await medRepo.add(med2);
+
+    final repo = MedicineIntakeRepository(db.db);
+    final t1 = mockIntake(med1, time: 20000);
+    final t2 = mockIntake(med2, time: 76000);
+    final t3 = mockIntake(med1, dosis: 123, time: 50000,);
+    await repo.add(t1);
+    await repo.add(t2);
+    await repo.add(t3);
+    await repo.add(mockIntake(med1));
+
+    final values = await repo.get(DateRange(
+      start: DateTime.fromMillisecondsSinceEpoch(20000),
+      end: DateTime.fromMillisecondsSinceEpoch(80000),
+    ));
+    expect(values, hasLength(3));
+    expect(values, containsAll([t1, t2, t3,]));
+  });
+  test('should remove intakes', () async {
+    final db = await mockDBManager();
+    addTearDown(db.close);
+    final medRepo = MedicineRepository(db.db);
+    final med = mockMedicine();
+    await medRepo.add(med);
+    final repo = MedicineIntakeRepository(db.db);
+    final i1 = mockIntake(med, time: 5000);
+    await repo.add(i1);
+
+    final values1 = await repo.get(DateRange(
+      start: DateTime.fromMillisecondsSinceEpoch(0),
+      end: DateTime.fromMillisecondsSinceEpoch(10000),
+    ));
+    expect(values1, hasLength(1));
+    expect(values1, contains(i1));
+
+    await repo.remove(i1);
+    final values2 = await repo.get(DateRange(
+      start: DateTime.fromMillisecondsSinceEpoch(0),
+      end: DateTime.fromMillisecondsSinceEpoch(10000),
+    ));
+    expect(values2, isEmpty);
+  });
+  test('should remove correct intake when multiple are at same time', () async {
+    final db = await mockDBManager();
+    addTearDown(db.close);
+    final repo = MedicineIntakeRepository(db.db);
+    final medRepo = MedicineRepository(db.db);
+    final med = mockMedicine();
+    await medRepo.add(med);
+    final i1 = mockIntake(med, time: 10000);
+    final i2 = mockIntake(med, time: 10000, dosis: 458);
+    await repo.add(i1);
+    await repo.add(i2);
+
+    await repo.remove(i1);
+    final values2 = await repo.get(DateRange(
+      start: DateTime.fromMillisecondsSinceEpoch(0),
+      end: DateTime.fromMillisecondsSinceEpoch(80000),
+    ));
+    expect(values2, hasLength(1));
+    expect(values2, contains(i2));
+  });
+  test('should not throw when removing non existent record', () async {
+    final db = await mockDBManager();
+    addTearDown(db.close);
+    final medRepo = MedicineRepository(db.db);
+    final med = mockMedicine();
+    await medRepo.add(med);
+    final repo = MedicineIntakeRepository(db.db);
+    final i1 = mockIntake(med);
+
+    await repo.remove(i1);
+    final values = await repo.get(DateRange(
+      start: DateTime.fromMillisecondsSinceEpoch(0),
+      end: DateTime.fromMillisecondsSinceEpoch(80000),
+    ));
+    expect(values, isEmpty);
+  });
+}
health_data_store/test/src/types/medicine_intake_test.dart
@@ -13,3 +13,12 @@ void main() {
     expect(intake.dosis, equals(42));
   });
 }
+
+MedicineIntake mockIntake(Medicine medicine, {
+  int? time,
+  double? dosis,
+}) => MedicineIntake(
+  time: time!=null ? DateTime.fromMillisecondsSinceEpoch(time) : DateTime.now(),
+  medicine: medicine,
+  dosis: dosis ?? medicine.dosis ?? 42,
+);
health_data_store/test/src/types/medicine_test.dart
@@ -1,3 +1,5 @@
+import 'dart:math';
+
 import 'package:health_data_store/src/types/medicine.dart';
 import 'package:test/test.dart';
 
@@ -8,3 +10,12 @@ void main() {
     expect(med.dosis, equals(42));
   });
 }
+
+Medicine mockMedicine({
+  String? designation,
+  double? dosis,
+}) => Medicine(
+  designation: designation ??
+      'med'+(Random().nextInt(899999) + 100000).toString(),
+  dosis: dosis,
+);