glp-companion/lib/features/nutrition/data/repositories/nutrition_repository.dart

146 lines
5.2 KiB
Dart

import 'package:drift/drift.dart';
import '../../../../core/database/app_database.dart';
import '../../domain/entities/food_log_entry.dart';
import '../../domain/entities/hydration_log_entry.dart';
import '../../domain/entities/meal_type.dart';
import '../../domain/repositories/nutrition_repository.dart';
/// Drift-backed implementation of [INutritionRepository].
///
/// Owns the translation between persistence records ([FoodLogRecord],
/// [HydrationLogRecord]) and the pure-Dart nutrition entities. The meal slot
/// is persisted as its stable enum name string so the database schema stays
/// decoupled from domain types.
class NutritionRepository implements INutritionRepository {
NutritionRepository(this._database);
final AppDatabase _database;
// ---------------------------------------------------------------------------
// Food diary
// ---------------------------------------------------------------------------
@override
Stream<List<FoodLogEntry>> watchFoodLogs({int limit = 500}) => _database
.watchFoodLogs(limit: limit)
.map((records) => records.map(_mapFood).toList(growable: false));
@override
Future<List<FoodLogEntry>> getFoodLogs({
int limit = 500,
DateTime? from,
DateTime? to,
}) async =>
(await _database.getFoodLogs(limit: limit, from: from, to: to))
.map(_mapFood)
.toList(growable: false);
@override
Future<FoodLogEntry> saveFoodLog(FoodLogEntry entry) async {
final companion = _toFoodCompanion(entry);
final FoodLogRecord record;
if (entry.id == null) {
record = await _database.insertFoodLog(companion);
} else {
await _database.updateFoodLog(entry.id!, companion);
record = await (_database.select(_database.foodLogsTable)
..where((t) => t.id.equals(entry.id!)))
.getSingle();
}
return _mapFood(record);
}
@override
Future<void> deleteFoodLog(int id) => _database.deleteFoodLog(id);
// ---------------------------------------------------------------------------
// Hydration
// ---------------------------------------------------------------------------
@override
Stream<List<HydrationLogEntry>> watchHydrationLogs({int limit = 500}) =>
_database.watchHydrationLogs(limit: limit).map(
(records) => records.map(_mapHydration).toList(growable: false),
);
@override
Future<List<HydrationLogEntry>> getHydrationLogs({
int limit = 500,
DateTime? from,
DateTime? to,
}) async =>
(await _database.getHydrationLogs(limit: limit, from: from, to: to))
.map(_mapHydration)
.toList(growable: false);
@override
Future<HydrationLogEntry> saveHydrationLog(HydrationLogEntry entry) async {
final companion = _toHydrationCompanion(entry);
final HydrationLogRecord record;
if (entry.id == null) {
record = await _database.insertHydrationLog(companion);
} else {
await _database.updateHydrationLog(entry.id!, companion);
record = await (_database.select(_database.hydrationLogsTable)
..where((t) => t.id.equals(entry.id!)))
.getSingle();
}
return _mapHydration(record);
}
@override
Future<void> deleteHydrationLog(int id) => _database.deleteHydrationLog(id);
// ---------------------------------------------------------------------------
// Mapping
// ---------------------------------------------------------------------------
/// Converts a persistence record into the domain entity. Unknown meal keys
/// fall back to [MealType.snack] so legacy rows never break the diary.
FoodLogEntry _mapFood(FoodLogRecord record) => FoodLogEntry(
id: record.id,
timestamp: record.timestamp,
mealType: MealType.tryParse(record.mealType) ?? MealType.snack,
description: record.description,
proteinGrams: record.proteinGrams,
calories: record.calories,
carbsGrams: record.carbsGrams,
fatGrams: record.fatGrams,
);
/// Converts a domain entity into an insert/update companion.
FoodLogsTableCompanion _toFoodCompanion(FoodLogEntry entry) =>
FoodLogsTableCompanion(
id: entry.id == null ? const Value.absent() : Value(entry.id!),
timestamp: Value(entry.timestamp),
mealType: Value(entry.mealType.persistenceKey),
description: Value(entry.description),
proteinGrams: Value(entry.proteinGrams),
calories: Value(entry.calories),
carbsGrams: entry.carbsGrams == null
? const Value.absent()
: Value(entry.carbsGrams!),
fatGrams: entry.fatGrams == null
? const Value.absent()
: Value(entry.fatGrams!),
);
/// Converts a persistence record into the domain entity.
HydrationLogEntry _mapHydration(HydrationLogRecord record) =>
HydrationLogEntry(
id: record.id,
timestamp: record.timestamp,
volumeOz: record.volumeOz,
);
/// Converts a domain entity into an insert/update companion.
HydrationLogsTableCompanion _toHydrationCompanion(HydrationLogEntry entry) =>
HydrationLogsTableCompanion(
id: entry.id == null ? const Value.absent() : Value(entry.id!),
timestamp: Value(entry.timestamp),
volumeOz: Value(entry.volumeOz),
);
}