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> watchDoseEntries({int limit = 500}) => (_selectDoseEntries()..limit(limit)).watch(); /// One-shot snapshot of the dose history, newest first. Future> getDoseEntries({int limit = 500}) => (_selectDoseEntries()..limit(limit)).get(); /// Inserts a new row and returns the persisted record including its /// auto-incremented primary key. Future 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 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 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 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((ref) { final database = AppDatabase(); ref.onDispose(database.close); return database; });