feat: Drift local SQLite database — tables, repositories, and generated code

- Add 4 Drift table definitions (Users, Tasks, PetStates, TaskCompletions)
- Add AppDatabase with driftDatabase() connection (drift_flutter 0.2.8 API)
- Add TaskRepository, PetRepository, UserRepository with entity mappers
- Add domain entities and enums (TaskEntity, PetStateEntity, UserEntity, TaskStatus, PetType, TaskCompletionEntity)
- Run build_runner to generate app_database.g.dart
- flutter analyze: no issues

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
TrochtaOndrej
2026-03-18 00:05:17 +01:00
parent 03e1ab1d24
commit bb0dcdf83f
12 changed files with 3999 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import '../models/tables.dart';
part 'app_database.g.dart';
@DriftDatabase(tables: [Users, Tasks, PetStates, TaskCompletions])
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor])
: super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(name: 'kittie_mobile_db');
}
}
File diff suppressed because it is too large Load Diff
+75
View File
@@ -0,0 +1,75 @@
import 'package:drift/drift.dart';
/// User profiles — device identity and pet selection.
class Users extends Table {
TextColumn get id => text()();
TextColumn get deviceId => text().unique()();
TextColumn get name => text().withDefault(const Constant('Friend'))();
TextColumn get petType => text().withDefault(const Constant('cat'))();
TextColumn get petName => text().withDefault(const Constant('Yoshi'))();
DateTimeColumn get createdAt =>
dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt =>
dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
/// Tasks — the core task items with sync support.
class Tasks extends Table {
TextColumn get id => text()();
TextColumn get userId => text().references(Users, #id)();
TextColumn get title => text()();
TextColumn get description => text().nullable()();
IntColumn get energyLevel => integer().withDefault(const Constant(2))();
TextColumn get status =>
text().withDefault(const Constant('pending'))();
BoolColumn get isSelfCare =>
boolean().withDefault(const Constant(false))();
DateTimeColumn get deadline => dateTime().nullable()();
TextColumn get photoPath => text().nullable()();
DateTimeColumn get snoozeUntil => dateTime().nullable()();
IntColumn get sortOrder => integer().withDefault(const Constant(0))();
DateTimeColumn get createdAt =>
dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt =>
dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get syncedAt => dateTime().nullable()();
TextColumn get localId => text().nullable().unique()();
@override
Set<Column> get primaryKey => {id};
}
/// Pet state — energy, streaks, and unlocked items per user.
class PetStates extends Table {
TextColumn get id => text()();
TextColumn get userId => text().unique().references(Users, #id)();
IntColumn get energyToday => integer().nullable()();
DateTimeColumn get energyDate => dateTime().nullable()();
IntColumn get streakDays => integer().withDefault(const Constant(0))();
IntColumn get totalTasksDone =>
integer().withDefault(const Constant(0))();
TextColumn get unlockedItems =>
text().withDefault(const Constant(''))();
DateTimeColumn get updatedAt =>
dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
/// Task completion log — records each completion for rewards and analytics.
class TaskCompletions extends Table {
TextColumn get id => text()();
TextColumn get userId => text().references(Users, #id)();
TextColumn get taskId => text().nullable()();
TextColumn get title => text()();
IntColumn get energyLevel => integer()();
DateTimeColumn get completedAt =>
dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
+97
View File
@@ -0,0 +1,97 @@
import 'package:drift/drift.dart';
import '../../domain/entities/pet_state_entity.dart';
import '../db/app_database.dart';
/// Data access layer for pet state.
class PetRepository {
final AppDatabase _db;
PetRepository(this._db);
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
Future<PetStateEntity?> getPetState(String userId) async {
final row = await (_db.select(_db.petStates)
..where((t) => t.userId.equals(userId)))
.getSingleOrNull();
return row == null ? null : _toEntity(row);
}
Stream<PetStateEntity?> watchPetState(String userId) {
return (_db.select(_db.petStates)
..where((t) => t.userId.equals(userId)))
.watchSingleOrNull()
.map((row) => row == null ? null : _toEntity(row));
}
// ---------------------------------------------------------------------------
// Mutations
// ---------------------------------------------------------------------------
Future<void> updatePetState(PetStateEntity entity) async {
await (_db.update(_db.petStates)
..where((t) => t.id.equals(entity.id)))
.write(_toCompanion(entity));
}
Future<void> setEnergyToday(String userId, int energy) async {
final now = DateTime.now();
await (_db.update(_db.petStates)
..where((t) => t.userId.equals(userId)))
.write(PetStatesCompanion(
energyToday: Value(energy),
energyDate: Value(now),
updatedAt: Value(now),
));
}
Future<void> incrementStreak(String userId) async {
final row = await (_db.select(_db.petStates)
..where((t) => t.userId.equals(userId)))
.getSingle();
await (_db.update(_db.petStates)
..where((t) => t.userId.equals(userId)))
.write(PetStatesCompanion(
streakDays: Value(row.streakDays + 1),
updatedAt: Value(DateTime.now()),
));
}
Future<void> insertPetState(PetStateEntity entity) async {
await _db.into(_db.petStates).insert(_toCompanion(entity));
}
// ---------------------------------------------------------------------------
// Mappers
// ---------------------------------------------------------------------------
PetStateEntity _toEntity(PetState row) {
return PetStateEntity(
id: row.id,
userId: row.userId,
energyToday: row.energyToday,
energyDate: row.energyDate,
streakDays: row.streakDays,
totalTasksDone: row.totalTasksDone,
unlockedItems: row.unlockedItems,
updatedAt: row.updatedAt,
);
}
PetStatesCompanion _toCompanion(PetStateEntity e) {
return PetStatesCompanion(
id: Value(e.id),
userId: Value(e.userId),
energyToday: Value(e.energyToday),
energyDate: Value(e.energyDate),
streakDays: Value(e.streakDays),
totalTasksDone: Value(e.totalTasksDone),
unlockedItems: Value(e.unlockedItems),
updatedAt: Value(e.updatedAt),
);
}
}
+141
View File
@@ -0,0 +1,141 @@
import 'package:drift/drift.dart';
import 'package:uuid/uuid.dart';
import '../../domain/entities/task_completion_entity.dart';
import '../../domain/entities/task_entity.dart';
import '../../domain/entities/task_status.dart';
import '../db/app_database.dart';
/// Data access layer for tasks and task completions.
class TaskRepository {
final AppDatabase _db;
static const _uuid = Uuid();
TaskRepository(this._db);
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
Future<List<TaskEntity>> getAllTasks(String userId) async {
final rows = await (_db.select(_db.tasks)
..where((t) => t.userId.equals(userId))
..orderBy([(t) => OrderingTerm.asc(t.sortOrder)]))
.get();
return rows.map(_toEntity).toList();
}
Future<List<TaskEntity>> getTodayTasks(String userId) async {
final rows = await (_db.select(_db.tasks)
..where((t) =>
t.userId.equals(userId) &
t.status.equals(TaskStatus.pending.name))
..orderBy([(t) => OrderingTerm.asc(t.sortOrder)]))
.get();
return rows.map(_toEntity).toList();
}
Future<TaskEntity?> getTaskById(String id) async {
final row = await (_db.select(_db.tasks)
..where((t) => t.id.equals(id)))
.getSingleOrNull();
return row == null ? null : _toEntity(row);
}
Stream<List<TaskEntity>> watchTasks(String userId) {
return (_db.select(_db.tasks)
..where((t) => t.userId.equals(userId))
..orderBy([(t) => OrderingTerm.asc(t.sortOrder)]))
.watch()
.map((rows) => rows.map(_toEntity).toList());
}
// ---------------------------------------------------------------------------
// Mutations
// ---------------------------------------------------------------------------
Future<void> insertTask(TaskEntity entity) async {
await _db.into(_db.tasks).insert(_toCompanion(entity));
}
Future<void> updateTask(TaskEntity entity) async {
await (_db.update(_db.tasks)..where((t) => t.id.equals(entity.id)))
.write(_toCompanion(entity));
}
Future<void> deleteTask(String id) async {
await (_db.delete(_db.tasks)..where((t) => t.id.equals(id))).go();
}
/// Marks a task as done and creates a [TaskCompletionEntity] log entry.
Future<void> completeTask(String taskId) async {
await _db.transaction(() async {
final task = await (_db.select(_db.tasks)
..where((t) => t.id.equals(taskId)))
.getSingle();
final now = DateTime.now();
// Update task status to done.
await (_db.update(_db.tasks)..where((t) => t.id.equals(taskId)))
.write(TasksCompanion(
status: Value(TaskStatus.done.name),
updatedAt: Value(now),
));
// Log the completion.
await _db.into(_db.taskCompletions).insert(TaskCompletionsCompanion(
id: Value(_uuid.v4()),
userId: Value(task.userId),
taskId: Value(taskId),
title: Value(task.title),
energyLevel: Value(task.energyLevel),
completedAt: Value(now),
));
});
}
// ---------------------------------------------------------------------------
// Mappers
// ---------------------------------------------------------------------------
TaskEntity _toEntity(Task row) {
return TaskEntity(
id: row.id,
userId: row.userId,
title: row.title,
description: row.description,
energyLevel: row.energyLevel,
status: TaskStatus.fromString(row.status),
isSelfCare: row.isSelfCare,
deadline: row.deadline,
photoPath: row.photoPath,
snoozeUntil: row.snoozeUntil,
sortOrder: row.sortOrder,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
syncedAt: row.syncedAt,
localId: row.localId,
);
}
TasksCompanion _toCompanion(TaskEntity e) {
return TasksCompanion(
id: Value(e.id),
userId: Value(e.userId),
title: Value(e.title),
description: Value(e.description),
energyLevel: Value(e.energyLevel),
status: Value(e.status.name),
isSelfCare: Value(e.isSelfCare),
deadline: Value(e.deadline),
photoPath: Value(e.photoPath),
snoozeUntil: Value(e.snoozeUntil),
sortOrder: Value(e.sortOrder),
createdAt: Value(e.createdAt),
updatedAt: Value(e.updatedAt),
syncedAt: Value(e.syncedAt),
localId: Value(e.localId),
);
}
}
@@ -0,0 +1,78 @@
import 'package:drift/drift.dart';
import '../../domain/entities/pet_type.dart';
import '../../domain/entities/user_entity.dart';
import '../db/app_database.dart';
/// Data access layer for user profiles.
class UserRepository {
final AppDatabase _db;
UserRepository(this._db);
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
/// Returns the first (and typically only) user row, or `null`.
Future<UserEntity?> getCurrentUser() async {
final row = await (_db.select(_db.users)..limit(1)).getSingleOrNull();
return row == null ? null : _toEntity(row);
}
// ---------------------------------------------------------------------------
// Mutations
// ---------------------------------------------------------------------------
Future<void> createUser({
required String id,
required String deviceId,
required String name,
required PetType petType,
required String petName,
}) async {
final now = DateTime.now();
await _db.into(_db.users).insert(UsersCompanion(
id: Value(id),
deviceId: Value(deviceId),
name: Value(name),
petType: Value(petType.name),
petName: Value(petName),
createdAt: Value(now),
updatedAt: Value(now),
));
}
Future<void> updateUser(UserEntity entity) async {
await (_db.update(_db.users)..where((t) => t.id.equals(entity.id)))
.write(_toCompanion(entity));
}
// ---------------------------------------------------------------------------
// Mappers
// ---------------------------------------------------------------------------
UserEntity _toEntity(User row) {
return UserEntity(
id: row.id,
deviceId: row.deviceId,
name: row.name,
petType: PetType.fromString(row.petType),
petName: row.petName,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
);
}
UsersCompanion _toCompanion(UserEntity e) {
return UsersCompanion(
id: Value(e.id),
deviceId: Value(e.deviceId),
name: Value(e.name),
petType: Value(e.petType.name),
petName: Value(e.petName),
createdAt: Value(e.createdAt),
updatedAt: Value(e.updatedAt),
);
}
}
+44
View File
@@ -0,0 +1,44 @@
/// Pure domain entity for pet state.
class PetStateEntity {
final String id;
final String userId;
final int? energyToday;
final DateTime? energyDate;
final int streakDays;
final int totalTasksDone;
final String unlockedItems;
final DateTime updatedAt;
const PetStateEntity({
required this.id,
required this.userId,
this.energyToday,
this.energyDate,
this.streakDays = 0,
this.totalTasksDone = 0,
this.unlockedItems = '',
required this.updatedAt,
});
PetStateEntity copyWith({
String? id,
String? userId,
int? energyToday,
DateTime? energyDate,
int? streakDays,
int? totalTasksDone,
String? unlockedItems,
DateTime? updatedAt,
}) {
return PetStateEntity(
id: id ?? this.id,
userId: userId ?? this.userId,
energyToday: energyToday ?? this.energyToday,
energyDate: energyDate ?? this.energyDate,
streakDays: streakDays ?? this.streakDays,
totalTasksDone: totalTasksDone ?? this.totalTasksDone,
unlockedItems: unlockedItems ?? this.unlockedItems,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
+10
View File
@@ -0,0 +1,10 @@
/// Available pet companions.
enum PetType {
dog,
cat,
capybara;
/// Parse from database string value.
static PetType fromString(String value) =>
PetType.values.firstWhere((e) => e.name == value);
}
@@ -0,0 +1,18 @@
/// Pure domain entity for a task completion log entry.
class TaskCompletionEntity {
final String id;
final String userId;
final String? taskId;
final String title;
final int energyLevel;
final DateTime completedAt;
const TaskCompletionEntity({
required this.id,
required this.userId,
this.taskId,
required this.title,
required this.energyLevel,
required this.completedAt,
});
}
+74
View File
@@ -0,0 +1,74 @@
import 'task_status.dart';
/// Pure domain entity for a task.
class TaskEntity {
final String id;
final String userId;
final String title;
final String? description;
final int energyLevel;
final TaskStatus status;
final bool isSelfCare;
final DateTime? deadline;
final String? photoPath;
final DateTime? snoozeUntil;
final int sortOrder;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime? syncedAt;
final String? localId;
const TaskEntity({
required this.id,
required this.userId,
required this.title,
this.description,
this.energyLevel = 2,
this.status = TaskStatus.pending,
this.isSelfCare = false,
this.deadline,
this.photoPath,
this.snoozeUntil,
this.sortOrder = 0,
required this.createdAt,
required this.updatedAt,
this.syncedAt,
this.localId,
});
TaskEntity copyWith({
String? id,
String? userId,
String? title,
String? description,
int? energyLevel,
TaskStatus? status,
bool? isSelfCare,
DateTime? deadline,
String? photoPath,
DateTime? snoozeUntil,
int? sortOrder,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? syncedAt,
String? localId,
}) {
return TaskEntity(
id: id ?? this.id,
userId: userId ?? this.userId,
title: title ?? this.title,
description: description ?? this.description,
energyLevel: energyLevel ?? this.energyLevel,
status: status ?? this.status,
isSelfCare: isSelfCare ?? this.isSelfCare,
deadline: deadline ?? this.deadline,
photoPath: photoPath ?? this.photoPath,
snoozeUntil: snoozeUntil ?? this.snoozeUntil,
sortOrder: sortOrder ?? this.sortOrder,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
syncedAt: syncedAt ?? this.syncedAt,
localId: localId ?? this.localId,
);
}
}
+12
View File
@@ -0,0 +1,12 @@
/// All possible states a task can be in.
enum TaskStatus {
pending,
done,
snoozed,
someday,
archived;
/// Parse from database string value.
static TaskStatus fromString(String value) =>
TaskStatus.values.firstWhere((e) => e.name == value);
}
+42
View File
@@ -0,0 +1,42 @@
import 'pet_type.dart';
/// Pure domain entity for a user profile.
class UserEntity {
final String id;
final String deviceId;
final String name;
final PetType petType;
final String petName;
final DateTime createdAt;
final DateTime updatedAt;
const UserEntity({
required this.id,
required this.deviceId,
required this.name,
required this.petType,
required this.petName,
required this.createdAt,
required this.updatedAt,
});
UserEntity copyWith({
String? id,
String? deviceId,
String? name,
PetType? petType,
String? petName,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return UserEntity(
id: id ?? this.id,
deviceId: deviceId ?? this.deviceId,
name: name ?? this.name,
petType: petType ?? this.petType,
petName: petName ?? this.petName,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}