glp-companion/lib/core/database/app_database.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;
});