forked from eva/focus-flow
feat(data): prepare MongoDB-friendly repository interfaces
This commit is contained in:
parent
abf069db1f
commit
305090250a
5 changed files with 508 additions and 21 deletions
|
|
@ -1,8 +1,10 @@
|
||||||
# V1 Block 09 — Persistence Preparation
|
# V1 Block 09 — Persistence Preparation
|
||||||
|
|
||||||
Status: Planned
|
Status: In progress — Chunk 9.1 complete
|
||||||
|
|
||||||
Purpose: Prepare the domain layer for local persistence without overbuilding sync or database implementation too early.
|
Purpose: Prepare the domain layer for future MongoDB-backed persistence without adding a MongoDB driver, network behavior, sync behavior, or database runtime dependency too early.
|
||||||
|
|
||||||
|
MongoDB is the committed persistence target. This block should make the core persistence-friendly and MongoDB-document-friendly while keeping the scheduling engine independent from database APIs.
|
||||||
|
|
||||||
## Chunk 9.1 — Repository interface
|
## Chunk 9.1 — Repository interface
|
||||||
|
|
||||||
|
|
@ -19,16 +21,27 @@ Define repository interfaces for:
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- Do not couple scheduling engine directly to SQLite.
|
- Do not couple the scheduling engine directly to MongoDB APIs.
|
||||||
- Use interfaces that a future Drift/SQLite layer can implement.
|
- Use interfaces that a future MongoDB adapter can implement.
|
||||||
- Keep current implementation in-memory if needed.
|
- Keep the current implementation in-memory if needed.
|
||||||
- Repository interfaces should preserve domain rules rather than bypassing services.
|
- Repository interfaces should preserve domain rules rather than bypassing services.
|
||||||
|
- Do not introduce alternative database assumptions.
|
||||||
|
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
||||||
- Domain services can depend on interfaces.
|
- Domain services can depend on interfaces.
|
||||||
- Tests can use fake/in-memory repositories.
|
- Tests can use fake/in-memory repositories.
|
||||||
- Interfaces do not import Drift, SQLite, Flutter, or platform-specific APIs.
|
- Interfaces do not import MongoDB, Flutter, platform-specific APIs, or network/client APIs.
|
||||||
|
- Interface names and method shapes are compatible with document-style persistence.
|
||||||
|
|
||||||
|
Completed:
|
||||||
|
|
||||||
|
- Added pure Dart repository interfaces for tasks, projects, locked blocks/overrides, and scheduling state snapshots.
|
||||||
|
- Added in-memory repository implementations for tests and early app wiring.
|
||||||
|
- Added scheduling snapshot state that can rebuild `SchedulingInput` and `SchedulingResult` without persistence APIs.
|
||||||
|
- Exported repository interfaces through the public core library.
|
||||||
|
- Added repository tests covering task/project/locked-block/snapshot fake usage.
|
||||||
|
- Verified with `dart format lib test`, `dart analyze`, and `dart test`.
|
||||||
|
|
||||||
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the persistence edge-case test suite.
|
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the persistence edge-case test suite.
|
||||||
|
|
||||||
|
|
@ -53,70 +66,87 @@ Cover these cases:
|
||||||
- backlog age behavior has a documented source timestamp or TODO before database work
|
- backlog age behavior has a documented source timestamp or TODO before database work
|
||||||
- in-memory fake repositories can round-trip saved records without losing scheduling fields
|
- in-memory fake repositories can round-trip saved records without losing scheduling fields
|
||||||
- repository operations do not mutate input objects unexpectedly
|
- repository operations do not mutate input objects unexpectedly
|
||||||
|
- document-shaped maps preserve all fields needed by future MongoDB persistence
|
||||||
|
- generated document field names are stable and migration-friendly
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- This chunk may add tests and small model helpers needed to make persistence behavior testable.
|
- This chunk may add tests and small model helpers needed to make persistence behavior testable.
|
||||||
- Do not add Drift/SQLite implementation yet.
|
- Do not add a MongoDB driver, MongoDB client, Atlas setup, connection string, local MongoDB service requirement, or database adapter yet.
|
||||||
|
- Do not add alternative database mappings or non-MongoDB terminology.
|
||||||
- Do not add sync, networking, cloud accounts, or background services.
|
- Do not add sync, networking, cloud accounts, or background services.
|
||||||
- Do not mark this chunk complete unless `dart analyze` and `dart test` pass.
|
- Do not mark this chunk complete unless `dart analyze` and `dart test` pass.
|
||||||
|
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
||||||
- Persistence-sensitive test group exists.
|
- Persistence-sensitive test group exists.
|
||||||
- Tests capture stable IDs, enum mapping, nullable-field behavior, and DateTime convention.
|
- Tests capture stable IDs, enum mapping, nullable-field behavior, DateTime convention, and document-shaped mapping expectations.
|
||||||
- Any unresolved persistence detail is documented as a TODO in the plan or architecture notes.
|
- Any unresolved persistence detail is documented as a TODO in the plan or architecture notes.
|
||||||
|
- No alternative-database references remain in the active persistence plan.
|
||||||
|
|
||||||
BREAKPOINT: Stop here. Confirm `high` mode before model serialization changes.
|
BREAKPOINT: Stop here. Confirm `high` mode before model serialization changes.
|
||||||
|
|
||||||
## Chunk 9.3 — Serialization-safe models
|
## Chunk 9.3 — MongoDB-document-safe models
|
||||||
|
|
||||||
Recommended Codex level: high
|
Recommended Codex level: high
|
||||||
|
|
||||||
Tasks:
|
Tasks:
|
||||||
|
|
||||||
Make models persistence-friendly:
|
Make models persistence-friendly and MongoDB-document-friendly:
|
||||||
|
|
||||||
- stable IDs
|
- stable IDs
|
||||||
- enum serialization helpers or clear mapping plan
|
- enum serialization helpers or clear mapping plan
|
||||||
- DateTime handling convention
|
- DateTime handling convention
|
||||||
- nullable fields documented
|
- nullable fields documented
|
||||||
- nullable field clear helpers where needed
|
- nullable field clear helpers where needed
|
||||||
- migration-safe field names where possible
|
- migration-safe document field names where possible
|
||||||
- map/json-like conversion helpers only if they do not over-couple the domain layer
|
- document-shaped map conversion helpers only if they do not over-couple the domain layer
|
||||||
|
- clear mapping rules for nested task statistics and future child-task ownership fields
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Keep model helpers independent from MongoDB client libraries.
|
||||||
|
- Prefer plain Dart map/document shapes that a later adapter can translate into MongoDB documents.
|
||||||
|
- Do not introduce database connection logic in this chunk.
|
||||||
|
- Do not add table assumptions, joins, or relational schema requirements.
|
||||||
|
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
||||||
- Models can round-trip through map/json-like structures or have a clear TODO for Drift mappings.
|
- Models can round-trip through document-shaped map/json-like structures or have a clear TODO for MongoDB document mappings.
|
||||||
- Tests cover at least task serialization if implemented.
|
- Tests cover at least task serialization if implemented.
|
||||||
- Tests cover enum persistence mapping if implemented.
|
- Tests cover enum persistence mapping if implemented.
|
||||||
- Tests cover intentional clearing of nullable fields if helper behavior is added.
|
- Tests cover intentional clearing of nullable fields if helper behavior is added.
|
||||||
|
- Tests confirm `not set` reward remains distinct from very low reward through mapping.
|
||||||
|
|
||||||
BREAKPOINT: Stop here. Confirm `low` mode before documenting the explicit non-sync boundary.
|
BREAKPOINT: Stop here. Confirm `low` mode before documenting the explicit non-sync boundary.
|
||||||
|
|
||||||
## Chunk 9.4 — Explicit non-sync boundary
|
## Chunk 9.4 — Explicit non-sync / no-database-runtime boundary
|
||||||
|
|
||||||
Recommended Codex level: low
|
Recommended Codex level: low
|
||||||
|
|
||||||
Tasks:
|
Tasks:
|
||||||
|
|
||||||
Document that V1 persistence is local-first and sync is out of scope.
|
Document that V1 persistence work prepares for MongoDB but does not implement runtime database access, sync, accounts, or background services.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- No network sync code.
|
- No network sync code.
|
||||||
- No cloud assumptions.
|
- No cloud assumptions.
|
||||||
|
- No MongoDB connection strings.
|
||||||
|
- No Atlas setup.
|
||||||
|
- No local MongoDB server requirement.
|
||||||
- No background service implementation.
|
- No background service implementation.
|
||||||
- No mobile notification or background reconciliation implementation in this block.
|
- No mobile notification or background reconciliation implementation in this block.
|
||||||
|
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
||||||
- README or architecture doc states sync is future work.
|
- README or architecture doc states MongoDB is the committed persistence target.
|
||||||
- Docs distinguish local persistence preparation from actual sync.
|
- README or architecture doc states this block does not require a running MongoDB instance.
|
||||||
|
- Docs distinguish MongoDB persistence preparation from actual sync or database adapter implementation.
|
||||||
- Wishlist/future notes contain sync as a later feature, not a V1 requirement.
|
- Wishlist/future notes contain sync as a later feature, not a V1 requirement.
|
||||||
|
|
||||||
Commit suggestion:
|
Commit suggestion:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
feat(data): prepare persistence interfaces for scheduling core
|
feat(data): prepare MongoDB-friendly persistence interfaces
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# V1 Block 09 — Persistence Preparation
|
# V1 Block 09 — Persistence Preparation
|
||||||
|
|
||||||
Status: Planned
|
Status: In progress — Chunk 9.1 complete
|
||||||
|
|
||||||
Purpose: Prepare the domain layer for future MongoDB-backed persistence without adding a MongoDB driver, network behavior, sync behavior, or database runtime dependency too early.
|
Purpose: Prepare the domain layer for future MongoDB-backed persistence without adding a MongoDB driver, network behavior, sync behavior, or database runtime dependency too early.
|
||||||
|
|
||||||
|
|
@ -34,6 +34,15 @@ Acceptance criteria:
|
||||||
- Interfaces do not import MongoDB, Flutter, platform-specific APIs, or network/client APIs.
|
- Interfaces do not import MongoDB, Flutter, platform-specific APIs, or network/client APIs.
|
||||||
- Interface names and method shapes are compatible with document-style persistence.
|
- Interface names and method shapes are compatible with document-style persistence.
|
||||||
|
|
||||||
|
Completed:
|
||||||
|
|
||||||
|
- Added pure Dart repository interfaces for tasks, projects, locked blocks/overrides, and scheduling state snapshots.
|
||||||
|
- Added in-memory repository implementations for tests and early app wiring.
|
||||||
|
- Added scheduling snapshot state that can rebuild `SchedulingInput` and `SchedulingResult` without persistence APIs.
|
||||||
|
- Exported repository interfaces through the public core library.
|
||||||
|
- Added repository tests covering task/project/locked-block/snapshot fake usage.
|
||||||
|
- Verified with `dart format lib test`, `dart analyze`, and `dart test`.
|
||||||
|
|
||||||
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the persistence edge-case test suite.
|
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the persistence edge-case test suite.
|
||||||
|
|
||||||
## Chunk 9.2 — Persistence edge-case regression test suite
|
## Chunk 9.2 — Persistence edge-case regression test suite
|
||||||
|
|
@ -64,7 +73,7 @@ Rules:
|
||||||
|
|
||||||
- This chunk may add tests and small model helpers needed to make persistence behavior testable.
|
- This chunk may add tests and small model helpers needed to make persistence behavior testable.
|
||||||
- Do not add a MongoDB driver, MongoDB client, Atlas setup, connection string, local MongoDB service requirement, or database adapter yet.
|
- Do not add a MongoDB driver, MongoDB client, Atlas setup, connection string, local MongoDB service requirement, or database adapter yet.
|
||||||
- Do not add alternative database mappings or SQL-specific terminology.
|
- Do not add alternative database mappings or non-MongoDB terminology.
|
||||||
- Do not add sync, networking, cloud accounts, or background services.
|
- Do not add sync, networking, cloud accounts, or background services.
|
||||||
- Do not mark this chunk complete unless `dart analyze` and `dart test` pass.
|
- Do not mark this chunk complete unless `dart analyze` and `dart test` pass.
|
||||||
|
|
||||||
|
|
@ -99,7 +108,7 @@ Rules:
|
||||||
- Keep model helpers independent from MongoDB client libraries.
|
- Keep model helpers independent from MongoDB client libraries.
|
||||||
- Prefer plain Dart map/document shapes that a later adapter can translate into MongoDB documents.
|
- Prefer plain Dart map/document shapes that a later adapter can translate into MongoDB documents.
|
||||||
- Do not introduce database connection logic in this chunk.
|
- Do not introduce database connection logic in this chunk.
|
||||||
- Do not add SQL-style table assumptions, joins, or relational schema requirements.
|
- Do not add table assumptions, joins, or relational schema requirements.
|
||||||
|
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export 'src/backlog.dart';
|
||||||
export 'src/child_tasks.dart';
|
export 'src/child_tasks.dart';
|
||||||
export 'src/locked_time.dart';
|
export 'src/locked_time.dart';
|
||||||
export 'src/quick_capture.dart';
|
export 'src/quick_capture.dart';
|
||||||
|
export 'src/repositories.dart';
|
||||||
export 'src/scheduling_engine.dart';
|
export 'src/scheduling_engine.dart';
|
||||||
export 'src/task_actions.dart';
|
export 'src/task_actions.dart';
|
||||||
export 'src/task_statistics.dart';
|
export 'src/task_statistics.dart';
|
||||||
|
|
|
||||||
300
lib/src/repositories.dart
Normal file
300
lib/src/repositories.dart
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
// Persistence boundary interfaces for the scheduling core.
|
||||||
|
//
|
||||||
|
// These interfaces are intentionally database-client-free. A future MongoDB
|
||||||
|
// adapter can implement them with document storage while the domain layer and
|
||||||
|
// tests keep using the same pure Dart contracts.
|
||||||
|
|
||||||
|
import 'locked_time.dart';
|
||||||
|
import 'models.dart';
|
||||||
|
import 'scheduling_engine.dart';
|
||||||
|
|
||||||
|
/// Repository contract for task documents.
|
||||||
|
abstract interface class TaskRepository {
|
||||||
|
/// Return a task by stable id, or null when it does not exist.
|
||||||
|
Future<Task?> findById(String id);
|
||||||
|
|
||||||
|
/// Return all tasks currently known to the repository.
|
||||||
|
Future<List<Task>> findAll();
|
||||||
|
|
||||||
|
/// Return tasks with a lifecycle status.
|
||||||
|
Future<List<Task>> findByStatus(TaskStatus status);
|
||||||
|
|
||||||
|
/// Return tasks that overlap [window] by scheduled start/end.
|
||||||
|
Future<List<Task>> findScheduledInWindow(SchedulingWindow window);
|
||||||
|
|
||||||
|
/// Save or replace [task] by stable id.
|
||||||
|
Future<void> save(Task task);
|
||||||
|
|
||||||
|
/// Save or replace every task by stable id.
|
||||||
|
Future<void> saveAll(Iterable<Task> tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repository contract for project profile documents.
|
||||||
|
abstract interface class ProjectRepository {
|
||||||
|
/// Return a project by stable id, or null when it does not exist.
|
||||||
|
Future<ProjectProfile?> findById(String id);
|
||||||
|
|
||||||
|
/// Return all projects currently known to the repository.
|
||||||
|
Future<List<ProjectProfile>> findAll();
|
||||||
|
|
||||||
|
/// Save or replace [project] by stable id.
|
||||||
|
Future<void> save(ProjectProfile project);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repository contract for locked-block and one-day override documents.
|
||||||
|
abstract interface class LockedBlockRepository {
|
||||||
|
/// Return a locked block by stable id, or null when it does not exist.
|
||||||
|
Future<LockedBlock?> findBlockById(String id);
|
||||||
|
|
||||||
|
/// Return all locked block definitions.
|
||||||
|
Future<List<LockedBlock>> findAllBlocks();
|
||||||
|
|
||||||
|
/// Return an override by stable id, or null when it does not exist.
|
||||||
|
Future<LockedBlockOverride?> findOverrideById(String id);
|
||||||
|
|
||||||
|
/// Return all one-day overrides.
|
||||||
|
Future<List<LockedBlockOverride>> findAllOverrides();
|
||||||
|
|
||||||
|
/// Save or replace [block] by stable id.
|
||||||
|
Future<void> saveBlock(LockedBlock block);
|
||||||
|
|
||||||
|
/// Save or replace [override] by stable id.
|
||||||
|
Future<void> saveOverride(LockedBlockOverride override);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot of one scheduling operation or persisted planning state.
|
||||||
|
class SchedulingStateSnapshot {
|
||||||
|
const SchedulingStateSnapshot({
|
||||||
|
required this.id,
|
||||||
|
required this.capturedAt,
|
||||||
|
required this.window,
|
||||||
|
required this.tasks,
|
||||||
|
this.lockedIntervals = const <TimeInterval>[],
|
||||||
|
this.requiredVisibleIntervals = const <TimeInterval>[],
|
||||||
|
this.notices = const <SchedulingNotice>[],
|
||||||
|
this.changes = const <SchedulingChange>[],
|
||||||
|
this.overlaps = const <SchedulingOverlap>[],
|
||||||
|
this.operationName,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Stable snapshot id.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// Time this snapshot was created.
|
||||||
|
final DateTime capturedAt;
|
||||||
|
|
||||||
|
/// Planning window represented by the snapshot.
|
||||||
|
final SchedulingWindow window;
|
||||||
|
|
||||||
|
/// Task documents visible to the operation.
|
||||||
|
final List<Task> tasks;
|
||||||
|
|
||||||
|
/// Locked intervals supplied to the scheduler.
|
||||||
|
final List<TimeInterval> lockedIntervals;
|
||||||
|
|
||||||
|
/// Extra visible required intervals supplied to the scheduler.
|
||||||
|
final List<TimeInterval> requiredVisibleIntervals;
|
||||||
|
|
||||||
|
/// Human-readable scheduling notices.
|
||||||
|
final List<SchedulingNotice> notices;
|
||||||
|
|
||||||
|
/// Machine-readable task movements.
|
||||||
|
final List<SchedulingChange> changes;
|
||||||
|
|
||||||
|
/// Analysis-only overlap findings.
|
||||||
|
final List<SchedulingOverlap> overlaps;
|
||||||
|
|
||||||
|
/// Optional operation label, such as `push` or `rollover`.
|
||||||
|
final String? operationName;
|
||||||
|
|
||||||
|
/// Rebuild scheduler input from the persisted state shape.
|
||||||
|
SchedulingInput get input {
|
||||||
|
return SchedulingInput(
|
||||||
|
tasks: List<Task>.unmodifiable(tasks),
|
||||||
|
window: window,
|
||||||
|
lockedIntervals: List<TimeInterval>.unmodifiable(lockedIntervals),
|
||||||
|
requiredVisibleIntervals:
|
||||||
|
List<TimeInterval>.unmodifiable(requiredVisibleIntervals),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuild scheduler result details from the persisted state shape.
|
||||||
|
SchedulingResult get result {
|
||||||
|
return SchedulingResult(
|
||||||
|
tasks: List<Task>.unmodifiable(tasks),
|
||||||
|
notices: List<SchedulingNotice>.unmodifiable(notices),
|
||||||
|
changes: List<SchedulingChange>.unmodifiable(changes),
|
||||||
|
overlaps: List<SchedulingOverlap>.unmodifiable(overlaps),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repository contract for scheduling operation/state snapshot documents.
|
||||||
|
abstract interface class SchedulingSnapshotRepository {
|
||||||
|
/// Return a snapshot by stable id, or null when it does not exist.
|
||||||
|
Future<SchedulingStateSnapshot?> findById(String id);
|
||||||
|
|
||||||
|
/// Return all snapshots overlapping [window].
|
||||||
|
Future<List<SchedulingStateSnapshot>> findInWindow(SchedulingWindow window);
|
||||||
|
|
||||||
|
/// Save or replace [snapshot] by stable id.
|
||||||
|
Future<void> save(SchedulingStateSnapshot snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In-memory task repository useful for tests and early application wiring.
|
||||||
|
class InMemoryTaskRepository implements TaskRepository {
|
||||||
|
InMemoryTaskRepository([Iterable<Task> initialTasks = const []])
|
||||||
|
: _tasksById = {
|
||||||
|
for (final task in initialTasks) task.id: task,
|
||||||
|
};
|
||||||
|
|
||||||
|
final Map<String, Task> _tasksById;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Task?> findById(String id) async {
|
||||||
|
return _tasksById[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Task>> findAll() async {
|
||||||
|
return List<Task>.unmodifiable(_tasksById.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Task>> findByStatus(TaskStatus status) async {
|
||||||
|
return List<Task>.unmodifiable(
|
||||||
|
_tasksById.values.where((task) => task.status == status),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Task>> findScheduledInWindow(SchedulingWindow window) async {
|
||||||
|
return List<Task>.unmodifiable(
|
||||||
|
_tasksById.values.where((task) {
|
||||||
|
final start = task.scheduledStart;
|
||||||
|
final end = task.scheduledEnd;
|
||||||
|
if (start == null || end == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return TimeInterval(start: start, end: end).overlaps(window.interval);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(Task task) async {
|
||||||
|
_tasksById[task.id] = task;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveAll(Iterable<Task> tasks) async {
|
||||||
|
for (final task in tasks) {
|
||||||
|
_tasksById[task.id] = task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In-memory project repository useful for tests and early application wiring.
|
||||||
|
class InMemoryProjectRepository implements ProjectRepository {
|
||||||
|
InMemoryProjectRepository(
|
||||||
|
[Iterable<ProjectProfile> initialProjects = const []])
|
||||||
|
: _projectsById = {
|
||||||
|
for (final project in initialProjects) project.id: project,
|
||||||
|
};
|
||||||
|
|
||||||
|
final Map<String, ProjectProfile> _projectsById;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ProjectProfile?> findById(String id) async {
|
||||||
|
return _projectsById[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ProjectProfile>> findAll() async {
|
||||||
|
return List<ProjectProfile>.unmodifiable(_projectsById.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(ProjectProfile project) async {
|
||||||
|
_projectsById[project.id] = project;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In-memory locked-time repository useful for tests and early wiring.
|
||||||
|
class InMemoryLockedBlockRepository implements LockedBlockRepository {
|
||||||
|
InMemoryLockedBlockRepository({
|
||||||
|
Iterable<LockedBlock> initialBlocks = const [],
|
||||||
|
Iterable<LockedBlockOverride> initialOverrides = const [],
|
||||||
|
}) : _blocksById = {
|
||||||
|
for (final block in initialBlocks) block.id: block,
|
||||||
|
},
|
||||||
|
_overridesById = {
|
||||||
|
for (final override in initialOverrides) override.id: override,
|
||||||
|
};
|
||||||
|
|
||||||
|
final Map<String, LockedBlock> _blocksById;
|
||||||
|
final Map<String, LockedBlockOverride> _overridesById;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LockedBlock?> findBlockById(String id) async {
|
||||||
|
return _blocksById[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LockedBlock>> findAllBlocks() async {
|
||||||
|
return List<LockedBlock>.unmodifiable(_blocksById.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LockedBlockOverride?> findOverrideById(String id) async {
|
||||||
|
return _overridesById[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LockedBlockOverride>> findAllOverrides() async {
|
||||||
|
return List<LockedBlockOverride>.unmodifiable(_overridesById.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveBlock(LockedBlock block) async {
|
||||||
|
_blocksById[block.id] = block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveOverride(LockedBlockOverride override) async {
|
||||||
|
_overridesById[override.id] = override;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In-memory snapshot repository useful for tests and early application wiring.
|
||||||
|
class InMemorySchedulingSnapshotRepository
|
||||||
|
implements SchedulingSnapshotRepository {
|
||||||
|
InMemorySchedulingSnapshotRepository([
|
||||||
|
Iterable<SchedulingStateSnapshot> initialSnapshots = const [],
|
||||||
|
]) : _snapshotsById = {
|
||||||
|
for (final snapshot in initialSnapshots) snapshot.id: snapshot,
|
||||||
|
};
|
||||||
|
|
||||||
|
final Map<String, SchedulingStateSnapshot> _snapshotsById;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SchedulingStateSnapshot?> findById(String id) async {
|
||||||
|
return _snapshotsById[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<SchedulingStateSnapshot>> findInWindow(
|
||||||
|
SchedulingWindow window,
|
||||||
|
) async {
|
||||||
|
return List<SchedulingStateSnapshot>.unmodifiable(
|
||||||
|
_snapshotsById.values.where(
|
||||||
|
(snapshot) => snapshot.window.interval.overlaps(window.interval),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(SchedulingStateSnapshot snapshot) async {
|
||||||
|
_snapshotsById[snapshot.id] = snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
147
test/repositories_test.dart
Normal file
147
test/repositories_test.dart
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
import 'package:adhd_scheduler_core/scheduler_core.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Repository interfaces', () {
|
||||||
|
final createdAt = DateTime(2026, 6, 21, 8);
|
||||||
|
final window = SchedulingWindow(
|
||||||
|
start: DateTime(2026, 6, 21, 9),
|
||||||
|
end: DateTime(2026, 6, 21, 17),
|
||||||
|
);
|
||||||
|
|
||||||
|
test('task repository supports in-memory fakes for domain services',
|
||||||
|
() async {
|
||||||
|
final backlogTask = task(
|
||||||
|
id: 'backlog',
|
||||||
|
title: 'Backlog task',
|
||||||
|
status: TaskStatus.backlog,
|
||||||
|
createdAt: createdAt,
|
||||||
|
);
|
||||||
|
final scheduledTask = task(
|
||||||
|
id: 'scheduled',
|
||||||
|
title: 'Scheduled task',
|
||||||
|
status: TaskStatus.planned,
|
||||||
|
createdAt: createdAt,
|
||||||
|
scheduledStart: DateTime(2026, 6, 21, 10),
|
||||||
|
scheduledEnd: DateTime(2026, 6, 21, 10, 30),
|
||||||
|
);
|
||||||
|
final repository = InMemoryTaskRepository([backlogTask]);
|
||||||
|
|
||||||
|
await repository.save(scheduledTask);
|
||||||
|
|
||||||
|
expect(await repository.findById('backlog'), same(backlogTask));
|
||||||
|
expect(await repository.findByStatus(TaskStatus.backlog), [backlogTask]);
|
||||||
|
expect(
|
||||||
|
(await repository.findScheduledInWindow(window)).map((task) => task.id),
|
||||||
|
['scheduled'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('project repository stores project profiles by stable id', () async {
|
||||||
|
const project = ProjectProfile(
|
||||||
|
id: 'home',
|
||||||
|
name: 'Home',
|
||||||
|
colorKey: 'project-home',
|
||||||
|
);
|
||||||
|
final repository = InMemoryProjectRepository();
|
||||||
|
|
||||||
|
await repository.save(project);
|
||||||
|
|
||||||
|
expect(await repository.findById('home'), same(project));
|
||||||
|
expect(await repository.findAll(), [project]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('locked block repository separates blocks from overrides', () async {
|
||||||
|
final block = LockedBlock(
|
||||||
|
id: 'work',
|
||||||
|
name: 'Work',
|
||||||
|
startTime: const ClockTime(hour: 9, minute: 0),
|
||||||
|
endTime: const ClockTime(hour: 17, minute: 0),
|
||||||
|
recurrence: const LockedBlockRecurrence.weekly(
|
||||||
|
weekdays: {LockedWeekday.monday},
|
||||||
|
),
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: createdAt,
|
||||||
|
);
|
||||||
|
final override = LockedBlockOverride.remove(
|
||||||
|
id: 'holiday',
|
||||||
|
lockedBlockId: block.id,
|
||||||
|
date: DateTime(2026, 6, 22),
|
||||||
|
createdAt: createdAt,
|
||||||
|
);
|
||||||
|
final repository = InMemoryLockedBlockRepository();
|
||||||
|
|
||||||
|
await repository.saveBlock(block);
|
||||||
|
await repository.saveOverride(override);
|
||||||
|
|
||||||
|
expect(await repository.findBlockById('work'), same(block));
|
||||||
|
expect(await repository.findOverrideById('holiday'), same(override));
|
||||||
|
expect(await repository.findAllBlocks(), [block]);
|
||||||
|
expect(await repository.findAllOverrides(), [override]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('scheduling snapshot repository preserves scheduler input shape',
|
||||||
|
() async {
|
||||||
|
final scheduledTask = task(
|
||||||
|
id: 'scheduled',
|
||||||
|
title: 'Scheduled task',
|
||||||
|
status: TaskStatus.planned,
|
||||||
|
createdAt: createdAt,
|
||||||
|
scheduledStart: DateTime(2026, 6, 21, 10),
|
||||||
|
scheduledEnd: DateTime(2026, 6, 21, 10, 30),
|
||||||
|
);
|
||||||
|
final lockedInterval = TimeInterval(
|
||||||
|
start: DateTime(2026, 6, 21, 12),
|
||||||
|
end: DateTime(2026, 6, 21, 13),
|
||||||
|
label: 'Lunch',
|
||||||
|
);
|
||||||
|
final snapshot = SchedulingStateSnapshot(
|
||||||
|
id: 'snapshot-1',
|
||||||
|
capturedAt: DateTime(2026, 6, 21, 8, 30),
|
||||||
|
window: window,
|
||||||
|
tasks: [scheduledTask],
|
||||||
|
lockedIntervals: [lockedInterval],
|
||||||
|
notices: const [
|
||||||
|
SchedulingNotice('Saved schedule'),
|
||||||
|
],
|
||||||
|
operationName: 'load-day',
|
||||||
|
);
|
||||||
|
final repository = InMemorySchedulingSnapshotRepository();
|
||||||
|
|
||||||
|
await repository.save(snapshot);
|
||||||
|
|
||||||
|
final saved = await repository.findById('snapshot-1');
|
||||||
|
expect(saved, same(snapshot));
|
||||||
|
expect(saved!.input.tasks, [scheduledTask]);
|
||||||
|
expect(saved.input.lockedIntervals, [lockedInterval]);
|
||||||
|
expect(saved.result.tasks, [scheduledTask]);
|
||||||
|
expect(saved.result.notices.single.message, 'Saved schedule');
|
||||||
|
expect(
|
||||||
|
(await repository.findInWindow(window)).map((snapshot) => snapshot.id),
|
||||||
|
['snapshot-1'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Task task({
|
||||||
|
required String id,
|
||||||
|
required String title,
|
||||||
|
required TaskStatus status,
|
||||||
|
required DateTime createdAt,
|
||||||
|
DateTime? scheduledStart,
|
||||||
|
DateTime? scheduledEnd,
|
||||||
|
}) {
|
||||||
|
return Task(
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
projectId: 'inbox',
|
||||||
|
type: TaskType.flexible,
|
||||||
|
status: status,
|
||||||
|
durationMinutes: 30,
|
||||||
|
scheduledStart: scheduledStart,
|
||||||
|
scheduledEnd: scheduledEnd,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue