127 lines
5.0 KiB
Dart
127 lines
5.0 KiB
Dart
import 'package:drift/drift.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'connection/unsupported.dart'
|
|
if (dart.library.ffi) 'connection/native.dart'
|
|
if (dart.library.js_interop) 'connection/web.dart';
|
|
|
|
part 'app_database.g.dart';
|
|
|
|
/// Immutable log of GLP-1 injections and scheduled doses.
|
|
///
|
|
/// [injectionSite] stores the anatomical rotation quadrant as a stable string
|
|
/// value (the `InjectionQuadrant.name` identifier of the medication domain
|
|
/// enum) so the persistence schema stays decoupled from domain types and the
|
|
/// `core` layer never imports feature code.
|
|
@DataClassName('MedicationDoseRecord')
|
|
class MedicationDosesTable extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
|
|
/// Commercial or compounded name of the GLP-1/GIP agonist (e.g. Tirzepatide).
|
|
TextColumn get compoundName => text().withLength(min: 1, max: 128)();
|
|
|
|
/// Total injected quantity in milligrams.
|
|
RealColumn get doseMg => real()();
|
|
|
|
/// When the dose was prescribed to be taken (titration schedule anchor).
|
|
DateTimeColumn get scheduledTime => dateTime()();
|
|
|
|
/// When the dose was actually administered; `null` while still pending.
|
|
DateTimeColumn get administeredTime => dateTime().nullable()();
|
|
|
|
/// Rotation quadrant identifier (e.g. `abdomenLeftLower`).
|
|
TextColumn get injectionSite => text().nullable()();
|
|
|
|
TextColumn get notes => text().nullable()();
|
|
}
|
|
|
|
/// Root drift database for the GLP-1 Companion.
|
|
///
|
|
/// On native platforms the connection is backed by SQLCipher: the 256-bit AES
|
|
/// master key provisioned by `CryptoManager` is applied via `PRAGMA key`
|
|
/// immediately after the database opens (see `connection/native.dart`). Web
|
|
/// builds transparently fall back to a WASM SQLite database, since SQLCipher
|
|
/// cannot run inside the browser.
|
|
@DriftDatabase(tables: [MedicationDosesTable])
|
|
class AppDatabase extends _$AppDatabase {
|
|
/// Opens the platform-appropriate (encrypted on native) database connection.
|
|
AppDatabase() : super(openDatabaseConnection());
|
|
|
|
/// Injects a custom executor; used by unit tests with an in-memory database.
|
|
AppDatabase.forTesting(super.executor);
|
|
|
|
@override
|
|
int get schemaVersion => 1;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Injection history CRUD
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// These helpers intentionally operate on drift records/companions only:
|
|
// the `core` layer never imports feature domain types, so mapping between
|
|
// `MedicationDoseRecord` and the medication feature's `DoseEntry` entity
|
|
// happens in `features/medication/data/repositories/medication_repository.dart`.
|
|
|
|
/// Emits the stored dose history, newest first, and re-emits on every write.
|
|
///
|
|
/// Ordering uses the administered time (falling back to the scheduled time
|
|
/// for pending rows) so backdated entries sort chronologically.
|
|
Stream<List<MedicationDoseRecord>> watchDoseEntries({int limit = 500}) =>
|
|
(_selectDoseEntries()..limit(limit)).watch();
|
|
|
|
/// One-shot snapshot of the dose history, newest first.
|
|
Future<List<MedicationDoseRecord>> getDoseEntries({int limit = 500}) =>
|
|
(_selectDoseEntries()..limit(limit)).get();
|
|
|
|
/// Inserts a new row and returns the persisted record including its
|
|
/// auto-incremented primary key.
|
|
Future<MedicationDoseRecord> insertDoseEntry(
|
|
MedicationDosesTableCompanion entry,
|
|
) async {
|
|
final id = await into(medicationDosesTable).insert(entry);
|
|
return (select(medicationDosesTable)..where((t) => t.id.equals(id)))
|
|
.getSingle();
|
|
}
|
|
|
|
/// Partially updates the row identified by [id] (only non-absent companion
|
|
/// columns are written).
|
|
Future<void> updateDoseEntry(
|
|
int id,
|
|
MedicationDosesTableCompanion updates,
|
|
) =>
|
|
(update(medicationDosesTable)..where((t) => t.id.equals(id))).write(
|
|
updates,
|
|
);
|
|
|
|
/// Deletes the row identified by [id]; returns the number of removed rows.
|
|
Future<int> deleteDoseEntry(int id) =>
|
|
(delete(medicationDosesTable)..where((t) => t.id.equals(id))).go();
|
|
|
|
/// Most recent row that was actually administered, or `null` when no dose
|
|
/// has been logged yet.
|
|
Future<MedicationDoseRecord?> latestAdministeredDose() =>
|
|
(select(medicationDosesTable)
|
|
..where((t) => t.administeredTime.isNotNull())
|
|
..orderBy([
|
|
(t) => OrderingTerm.desc(t.administeredTime),
|
|
(t) => OrderingTerm.desc(t.id),
|
|
])
|
|
..limit(1))
|
|
.getSingleOrNull();
|
|
|
|
/// Shared newest-first ordering for [watchDoseEntries] and [getDoseEntries].
|
|
SimpleSelectStatement<$MedicationDosesTableTable, MedicationDoseRecord>
|
|
_selectDoseEntries() => select(medicationDosesTable)
|
|
..orderBy([
|
|
(t) => OrderingTerm.desc(t.administeredTime),
|
|
(t) => OrderingTerm.desc(t.id),
|
|
]);
|
|
}
|
|
|
|
/// Application-wide database instance; closed when the container is disposed.
|
|
final appDatabaseProvider = Provider<AppDatabase>((ref) {
|
|
final database = AppDatabase();
|
|
ref.onDispose(database.close);
|
|
return database;
|
|
});
|