Merge NDM-App-190: Drift Database — tables, DAOs, repositories
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# Drift Database Layer
|
||||
|
||||
**Branch:** `NDM-App-190-drift-database-tables-daos-repositories`
|
||||
**Commits:** `bb0dcdf`, `8256e57`
|
||||
|
||||
## Overview
|
||||
|
||||
Implements the local SQLite persistence layer using Drift 2.x. The database is the offline-first source of truth. All data access goes through typed repository classes that map between Drift-generated data classes and pure domain entities.
|
||||
|
||||
---
|
||||
|
||||
## Files Created
|
||||
|
||||
### `lib/data/models/tables.dart`
|
||||
Four Drift table definitions:
|
||||
|
||||
| Table | Description |
|
||||
|---|---|
|
||||
| `Users` | Device identity, pet selection, pet name |
|
||||
| `Tasks` | Core task items with sync tracking columns |
|
||||
| `PetStates` | Energy, streak, total completions, unlocked items |
|
||||
| `TaskCompletions` | Append-only completion log for rewards/analytics |
|
||||
|
||||
Key design details:
|
||||
- `Tasks` has `@TableIndex` on `userId` (also on `TaskCompletions`).
|
||||
- `localId` on `Tasks` is a nullable unique column for external sync identity.
|
||||
- `PetStates.userId` is unique — one pet state row per user.
|
||||
- Enums (`petType`, `status`) are stored as `text` strings; conversion is done in the repository layer.
|
||||
|
||||
### `lib/data/db/app_database.dart`
|
||||
`@DriftDatabase` class wiring all four tables. Uses `driftDatabase(name: 'kittie_mobile_db')` from `drift_flutter` (the correct API for `drift_flutter ^0.2.0`). `schemaVersion = 1`.
|
||||
|
||||
Generated part file: `lib/data/db/app_database.g.dart`.
|
||||
|
||||
### `lib/data/repositories/task_repository.dart`
|
||||
```dart
|
||||
TaskRepository(AppDatabase db)
|
||||
```
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `getAllTasks(userId)` | All tasks ordered by `sortOrder` |
|
||||
| `getTodayTasks(userId)` | Pending tasks only, ordered by `sortOrder` |
|
||||
| `getTaskById(id)` | Single task or null |
|
||||
| `watchTasks(userId)` | Reactive stream of all tasks |
|
||||
| `insertTask(entity)` | Insert from domain entity |
|
||||
| `updateTask(entity)` | Full-row update by id |
|
||||
| `deleteTask(id)` | Hard delete |
|
||||
| `completeTask(taskId)` | Transaction: sets `status=done`, inserts `TaskCompletion`; throws `ArgumentError` if task not found |
|
||||
|
||||
### `lib/data/repositories/pet_repository.dart`
|
||||
```dart
|
||||
PetRepository(AppDatabase db)
|
||||
```
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `getPetState(userId)` | Single pet state or null |
|
||||
| `watchPetState(userId)` | Reactive stream (nullable) |
|
||||
| `insertPetState(entity)` | First-time insert |
|
||||
| `updatePetState(entity)` | Full-row update |
|
||||
| `setEnergyToday(userId, energy)` | Partial update: energyToday + energyDate + updatedAt |
|
||||
| `incrementStreak(userId)` | Transaction: read current streak, write +1 |
|
||||
|
||||
### `lib/data/repositories/user_repository.dart`
|
||||
```dart
|
||||
UserRepository(AppDatabase db)
|
||||
```
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `getCurrentUser()` | First row (limit 1) or null; single-user device model |
|
||||
| `createUser(id, deviceId, name, petType, petName)` | Named params for clarity |
|
||||
| `updateUser(entity)` | Full-row update |
|
||||
|
||||
---
|
||||
|
||||
## Domain Entities (`lib/domain/entities/`)
|
||||
|
||||
Pure Dart classes — no Drift imports. Used across layers.
|
||||
|
||||
| File | Contents |
|
||||
|---|---|
|
||||
| `task_entity.dart` | `TaskEntity` with `copyWith` (sentinel `_unset` pattern for nullable fields) |
|
||||
| `task_status.dart` | `TaskStatus` enum: `pending`, `done`, `snoozed`, `someday`, `archived` |
|
||||
| `pet_state_entity.dart` | `PetStateEntity` |
|
||||
| `pet_type.dart` | `PetType` enum: `dog`, `cat`, `capybara` |
|
||||
| `user_entity.dart` | `UserEntity` |
|
||||
| `task_completion_entity.dart` | `TaskCompletionEntity` |
|
||||
|
||||
Each enum has a `fromString(String)` factory that parses DB text values.
|
||||
|
||||
---
|
||||
|
||||
## Patterns
|
||||
|
||||
### Entity ↔ Drift mapping
|
||||
Every repository has private `_toEntity(Row)` and `_toCompanion(Entity)` methods. No Drift types leak outside the repository.
|
||||
|
||||
### Transactions
|
||||
`completeTask` and `incrementStreak` wrap multi-step writes in `_db.transaction(...)`.
|
||||
|
||||
### `copyWith` for nullable fields
|
||||
`TaskEntity.copyWith` uses a private `const _unset = Object()` sentinel so callers can explicitly pass `null` to clear a nullable field without ambiguity.
|
||||
|
||||
---
|
||||
|
||||
## Tests (`test/data/repositories/`)
|
||||
|
||||
30 unit tests using `NativeDatabase.memory()` (real in-memory SQLite, no mocking).
|
||||
|
||||
| File | Tests |
|
||||
|---|---|
|
||||
| `user_repository_test.dart` | 6 |
|
||||
| `task_repository_test.dart` | 12 |
|
||||
| `pet_repository_test.dart` | 12 |
|
||||
|
||||
Each test group creates a fresh `AppDatabase` in `setUp` and closes it in `tearDown`.
|
||||
|
||||
---
|
||||
|
||||
## Code Generation
|
||||
|
||||
After any change to `tables.dart` or `app_database.dart`, regenerate with:
|
||||
|
||||
```bash
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
```
|
||||
@@ -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
@@ -0,0 +1,77 @@
|
||||
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.
|
||||
@TableIndex(name: 'idx_tasks_user_id', columns: {#userId})
|
||||
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.
|
||||
@TableIndex(name: 'idx_task_completions_user_id', columns: {#userId})
|
||||
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};
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
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 {
|
||||
await _db.transaction(() 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:uuid/uuid.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();
|
||||
}
|
||||
|
||||
/// Returns all [TaskStatus.pending] tasks for [userId], ordered by sortOrder.
|
||||
/// "Today's tasks" in this app means all pending tasks — there is no
|
||||
/// date-based filter; energy level and pet mood drive prioritisation instead.
|
||||
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 completion log entry atomically.
|
||||
///
|
||||
/// Throws [ArgumentError] if [taskId] does not exist.
|
||||
Future<void> completeTask(String taskId) async {
|
||||
await _db.transaction(() async {
|
||||
final task = await (_db.select(_db.tasks)
|
||||
..where((t) => t.id.equals(taskId)))
|
||||
.getSingleOrNull();
|
||||
|
||||
if (task == null) {
|
||||
throw ArgumentError.value(taskId, 'taskId', 'Task not found');
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
|
||||
await (_db.update(_db.tasks)..where((t) => t.id.equals(taskId)))
|
||||
.write(TasksCompanion(
|
||||
status: Value(TaskStatus.done.name),
|
||||
updatedAt: Value(now),
|
||||
));
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Sentinel distinguishes "not provided" from "explicitly null" in copyWith.
|
||||
const _unset = Object();
|
||||
|
||||
/// 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,
|
||||
Object? energyToday = _unset,
|
||||
Object? energyDate = _unset,
|
||||
int? streakDays,
|
||||
int? totalTasksDone,
|
||||
String? unlockedItems,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return PetStateEntity(
|
||||
id: id ?? this.id,
|
||||
userId: userId ?? this.userId,
|
||||
energyToday: identical(energyToday, _unset)
|
||||
? this.energyToday
|
||||
: energyToday as int?,
|
||||
energyDate: identical(energyDate, _unset)
|
||||
? this.energyDate
|
||||
: energyDate as DateTime?,
|
||||
streakDays: streakDays ?? this.streakDays,
|
||||
totalTasksDone: totalTasksDone ?? this.totalTasksDone,
|
||||
unlockedItems: unlockedItems ?? this.unlockedItems,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'task_status.dart';
|
||||
|
||||
// Sentinel distinguishes "not provided" from "explicitly null" in copyWith.
|
||||
const _unset = Object();
|
||||
|
||||
/// 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,
|
||||
Object? description = _unset,
|
||||
int? energyLevel,
|
||||
TaskStatus? status,
|
||||
bool? isSelfCare,
|
||||
Object? deadline = _unset,
|
||||
Object? photoPath = _unset,
|
||||
Object? snoozeUntil = _unset,
|
||||
int? sortOrder,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
Object? syncedAt = _unset,
|
||||
Object? localId = _unset,
|
||||
}) {
|
||||
return TaskEntity(
|
||||
id: id ?? this.id,
|
||||
userId: userId ?? this.userId,
|
||||
title: title ?? this.title,
|
||||
description:
|
||||
identical(description, _unset) ? this.description : description as String?,
|
||||
energyLevel: energyLevel ?? this.energyLevel,
|
||||
status: status ?? this.status,
|
||||
isSelfCare: isSelfCare ?? this.isSelfCare,
|
||||
deadline: identical(deadline, _unset) ? this.deadline : deadline as DateTime?,
|
||||
photoPath:
|
||||
identical(photoPath, _unset) ? this.photoPath : photoPath as String?,
|
||||
snoozeUntil: identical(snoozeUntil, _unset)
|
||||
? this.snoozeUntil
|
||||
: snoozeUntil as DateTime?,
|
||||
sortOrder: sortOrder ?? this.sortOrder,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
syncedAt:
|
||||
identical(syncedAt, _unset) ? this.syncedAt : syncedAt as DateTime?,
|
||||
localId: identical(localId, _unset) ? this.localId : localId as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user