- Drift 21 tables (8 catalog + 11 user + habit_dose_variants + meta_kv) with R1~R10 CHECK constraints and 19 indexes - 8 hand-crafted seed JSON catalogs in app/assets/seed/ (refs 84, protocols 34, methodologies 21, frame_patterns 30, reward_menu_items 30, break_protocols 8, common_frames 5, diet_patterns 5) - 6 domain functions: recommend_variant, compute_streak, validate_frame_level, active_habit_quota, weekly_minimum_ratio, seed_importer (transactional, idempotent) - 4 vertical-slice Riverpod screens: HabitList, HabitCreate, CheckIn, Streak - 31 unit tests passing; flutter analyze clean - OQ-5 streak semantics: missing entry ≠ explicit blank (missing = end of history; only TrackerValue.blank triggers Never-miss-twice) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
66 lines
2.2 KiB
Dart
66 lines
2.2 KiB
Dart
import 'package:drift/drift.dart' as drift;
|
|
import 'package:drift/native.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../core/constants.dart';
|
|
import '../core/time.dart';
|
|
import '../data/db/app_database.dart';
|
|
import '../data/db/daos/habit_dao.dart';
|
|
import '../data/db/daos/meta_dao.dart';
|
|
import '../data/db/daos/tracker_dao.dart';
|
|
import '../data/seed/seed_importer.dart';
|
|
|
|
/// Override in tests with an in-memory database.
|
|
final appDatabaseProvider = Provider<AppDatabase>((ref) {
|
|
throw UnimplementedError('appDatabaseProvider must be overridden in main()');
|
|
});
|
|
|
|
Future<AppDatabase> openProductionDatabase() async {
|
|
final file = await appDatabaseFile();
|
|
return AppDatabase(NativeDatabase.createInBackground(file));
|
|
}
|
|
|
|
final habitDaoProvider = Provider<HabitDao>((ref) {
|
|
return HabitDao(ref.watch(appDatabaseProvider));
|
|
});
|
|
|
|
final trackerDaoProvider = Provider<TrackerDao>((ref) {
|
|
return TrackerDao(ref.watch(appDatabaseProvider));
|
|
});
|
|
|
|
final metaDaoProvider = Provider<MetaDao>((ref) {
|
|
return MetaDao(ref.watch(appDatabaseProvider));
|
|
});
|
|
|
|
/// One-time bootstrap: ensure default user row + seed catalogs.
|
|
final bootstrapProvider = FutureProvider<void>((ref) async {
|
|
final db = ref.watch(appDatabaseProvider);
|
|
// Ensure default user.
|
|
final existing = await (db.select(db.users)
|
|
..where((t) => t.id.equals(kLocalDefaultUserId)))
|
|
.getSingleOrNull();
|
|
if (existing == null) {
|
|
await db.into(db.users).insert(UsersCompanion.insert(
|
|
id: kLocalDefaultUserId,
|
|
displayName: const drift.Value('You'),
|
|
createdAt: nowKst().toIso8601String(),
|
|
));
|
|
}
|
|
// Seed catalogs (idempotent).
|
|
await SeedImporter(db).importIfNeeded();
|
|
if (kDebugMode) {
|
|
debugPrint('bootstrap done');
|
|
}
|
|
});
|
|
|
|
/// Active habits stream for current user.
|
|
final activeHabitsProvider = StreamProvider<List<Habit>>((ref) {
|
|
final db = ref.watch(appDatabaseProvider);
|
|
return (db.select(db.habits)
|
|
..where((t) => t.userId.equals(kLocalDefaultUserId))
|
|
..where((t) => t.status.equals('active'))
|
|
..orderBy([(t) => drift.OrderingTerm.asc(t.startedAt)]))
|
|
.watch();
|
|
});
|