Skip to main content

max / goingson

Decompose core repository god-module and TaskRepository trait crates/core/src/repository.rs (1103 lines, ~19 traits/214 methods) split into a repository/ directory grouped by domain (task, project, event, email, user, contact, review, stats, search, misc), re-exported via `pub use`. The fat TaskRepository trait (52 methods) is decomposed into four capability sub-traits — TaskCrud, TaskAnnotations, TaskScheduling, TaskTimeTracking — with TaskRepository kept as an empty supertrait plus a blanket impl, so the `Arc<dyn TaskRepository>` consumer and all call sites are unchanged. The SqliteTaskRepository impl is regrouped into four matching impl blocks; test files import the specific sub-traits they exercise. No behavior change.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-13 01:40 UTC
Signed with PGP, not checked
Commit: fbe4a5e749305a1942e12c777fe71f3b22338568
Parent: 68d79f5
23 files changed, +1220 insertions, -548 deletions
@@ -2,7 +2,7 @@
2 2
3 3 mod common;
4 4
5 - use goingson_core::TaskRepository;
5 + use goingson_core::TaskAnnotations;
6 6 use goingson_db_sqlite::SqliteTaskRepository;
7 7
8 8 #[tokio::test]
@@ -77,7 +77,7 @@
77 77 #[tokio::test]
78 78 async fn restore_handles_recurring_task_self_fk() {
79 79 use goingson_core::{
80 - NewProject, NewTask, ProjectRepository, TaskRepository, TaskStatus,
80 + NewProject, NewTask, ProjectRepository, TaskCrud, TaskStatus,
81 81 };
82 82 use goingson_db_sqlite::{SqliteProjectRepository, SqliteTaskRepository};
83 83
@@ -3,7 +3,7 @@
3 3 mod common;
4 4
5 5 use chrono::{Duration, TimeZone, Utc};
6 - use goingson_core::{NewTask, Priority, TaskRepository};
6 + use goingson_core::{NewTask, Priority, TaskCrud, TaskScheduling};
7 7 use goingson_db_sqlite::SqliteTaskRepository;
8 8
9 9 // ---- Focus ----
@@ -10,7 +10,7 @@
10 10
11 11 mod common;
12 12
13 - use goingson_core::{NewTask, Recurrence, TaskId, TaskRepository};
13 + use goingson_core::{NewTask, Recurrence, TaskId, TaskCrud};
14 14 use goingson_db_sqlite::SqliteTaskRepository;
15 15 use sqlx::SqlitePool;
16 16
@@ -12,7 +12,7 @@
12 12 use goingson_core::{
13 13 AttachmentRepository, DailyNoteRepository, EventRepository, MilestoneRepository, NewAttachment,
14 14 NewEvent, NewMilestone, NewProject, NewTask, PositiveMinutes, ProjectRepository, SyncAccountRepository,
15 - TaskRepository, TaskStatus,
15 + TaskAnnotations, TaskCrud, TaskTimeTracking, TaskStatus,
16 16 };
17 17 use goingson_db_sqlite::{
18 18 restore_all, SqliteAttachmentRepository, SqliteDailyNoteRepository, SqliteEventRepository,
@@ -4,7 +4,7 @@
4 4
5 5 use goingson_core::{
6 6 NewProject, NewTask, Priority, ProjectRepository, ProjectStatus, ProjectType, SearchQuery,
7 - SearchRepository, SearchResultType, TaskRepository, UserId,
7 + SearchRepository, SearchResultType, TaskCrud, UserId,
8 8 };
9 9 use goingson_db_sqlite::{SqliteProjectRepository, SqliteSearchRepository, SqliteTaskRepository};
10 10
@@ -5,7 +5,7 @@
5 5 use chrono::{Duration, Utc};
6 6 use goingson_core::{
7 7 EmailRepository, EventRepository, NewEmail, NewEvent, NewProject, NewTask, Priority,
8 - ProjectRepository, ProjectStatus, ProjectType, Recurrence, StatsRepository, TaskRepository,
8 + ProjectRepository, ProjectStatus, ProjectType, Recurrence, StatsRepository, TaskCrud,
9 9 };
10 10 use goingson_db_sqlite::{
11 11 SqliteEmailRepository, SqliteEventRepository, SqliteProjectRepository, SqliteStatsRepository,
@@ -2,7 +2,7 @@
2 2
3 3 mod common;
4 4
5 - use goingson_core::TaskRepository;
5 + use goingson_core::TaskAnnotations;
6 6 use goingson_db_sqlite::SqliteTaskRepository;
7 7
8 8 #[tokio::test]
@@ -5,7 +5,7 @@
5 5 use chrono::{Duration, Utc};
6 6 use goingson_core::{
7 7 NewProject, NewTask, Priority, ProjectRepository, Recurrence, RecurrenceRule, TaskFilterQuery,
8 - TaskRepository, TaskStatus, UpdateTask,
8 + TaskCrud, TaskScheduling, TaskStatus, UpdateTask,
9 9 };
10 10 use goingson_db_sqlite::{SqliteProjectRepository, SqliteTaskRepository};
11 11
@@ -2,7 +2,7 @@
2 2
3 3 mod common;
4 4
5 - use goingson_core::{CoreError, PositiveMinutes, TaskId, TaskRepository};
5 + use goingson_core::{CoreError, PositiveMinutes, TaskId, TaskCrud, TaskTimeTracking};
6 6 use goingson_db_sqlite::SqliteTaskRepository;
7 7
8 8 #[tokio::test]
@@ -15,7 +15,8 @@
15 15 use goingson_core::{
16 16 calculate_urgency, AnnotationId, Annotation, ContactId, CoreError, DbValue, MilestoneId, NewTask,
17 17 ParseableEnum, Priority, PositiveMinutes, ProjectId, Recurrence, Result, SortDirection, SubtaskId,
18 - Subtask, Task, TaskFilterQuery, TaskId, TaskRepository, TaskSortColumn, TaskStatus, TimeSession,
18 + Subtask, Task, TaskAnnotations, TaskCrud, TaskFilterQuery, TaskId, TaskScheduling,
19 + TaskSortColumn, TaskStatus, TaskTimeTracking, TimeSession,
19 20 TimeTrackingSummary, UpdateTask, UserId,
20 21 };
21 22
@@ -258,7 +259,7 @@
258 259 }
259 260
260 261 #[async_trait]
261 - impl TaskRepository for SqliteTaskRepository {
262 + impl TaskCrud for SqliteTaskRepository {
262 263 #[tracing::instrument(skip_all)]
263 264 async fn list_all(&self, user_id: UserId) -> Result<Vec<Task>> {
264 265 let sql = format!(
@@ -830,6 +831,45 @@
830 831 Ok(count)
831 832 }
832 833
834 + // ---- Reporting (delegated to task_repo_state) ----
835 +
836 + #[tracing::instrument(skip_all)]
837 + async fn list_completed_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>> {
838 + task_repo_state::list_completed_between(&self.pool, user_id, start, end).await
839 + }
840 +
841 + #[tracing::instrument(skip_all)]
842 + async fn list_became_overdue_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>> {
843 + task_repo_state::list_became_overdue_between(&self.pool, user_id, start, end).await
844 + }
845 +
846 + #[tracing::instrument(skip_all)]
847 + async fn list_due_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>> {
848 + task_repo_state::list_due_between(&self.pool, user_id, start, end).await
849 + }
850 +
851 + #[tracing::instrument(skip_all)]
852 + async fn list_created_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>> {
853 + task_repo_state::list_created_between(&self.pool, user_id, start, end).await
854 + }
855 +
856 + #[tracing::instrument(skip_all)]
857 + async fn list_recurrence_chain(&self, root_id: TaskId, user_id: UserId) -> Result<Vec<Task>> {
858 + let sql = format!(
859 + "SELECT {} FROM tasks t LEFT JOIN projects p ON t.project_id = p.id AND p.user_id = ? LEFT JOIN contacts ct ON ct.id = t.contact_id WHERE (t.recurrence_parent_id = ? OR t.id = ?) AND t.user_id = ? ORDER BY t.created_at DESC",
860 + TASK_SELECT_COLUMNS
861 + );
862 + query_tasks(&self.pool, &sql, &[
863 + user_id.to_string(),
864 + root_id.to_string(),
865 + root_id.to_string(),
866 + user_id.to_string(),
867 + ]).await
868 + }
869 + }
870 +
871 + #[async_trait]
872 + impl TaskAnnotations for SqliteTaskRepository {
833 873 // ---- Annotations (delegated to annotation_repo) ----
834 874
835 875 #[tracing::instrument(skip_all)]
@@ -889,7 +929,10 @@
889 929 &linked_task.status,
890 930 ).await
891 931 }
932 + }
892 933
934 + #[async_trait]
935 + impl TaskScheduling for SqliteTaskRepository {
893 936 // ---- Snooze (delegated to task_repo_state) ----
894 937
895 938 #[tracing::instrument(skip_all)]
@@ -963,33 +1006,14 @@
963 1006 task_repo_state::clear_all_focus(&self.pool, user_id).await
964 1007 }
965 1008
966 - // ---- Reporting (delegated to task_repo_state) ----
967 -
968 - #[tracing::instrument(skip_all)]
969 - async fn list_completed_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>> {
970 - task_repo_state::list_completed_between(&self.pool, user_id, start, end).await
971 - }
972 -
973 - #[tracing::instrument(skip_all)]
974 - async fn list_became_overdue_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>> {
975 - task_repo_state::list_became_overdue_between(&self.pool, user_id, start, end).await
976 - }
977 -
978 - #[tracing::instrument(skip_all)]
979 - async fn list_due_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>> {
980 - task_repo_state::list_due_between(&self.pool, user_id, start, end).await
981 - }
982 -
983 - #[tracing::instrument(skip_all)]
984 - async fn list_created_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>> {
985 - task_repo_state::list_created_between(&self.pool, user_id, start, end).await
986 - }
987 -
988 1009 #[tracing::instrument(skip_all)]
989 1010 async fn list_available_for_focus(&self, user_id: UserId, limit: i64) -> Result<Vec<Task>> {
990 1011 task_repo_state::list_available_for_focus(&self.pool, user_id, limit).await
991 1012 }
1013 + }
992 1014
1015 + #[async_trait]
1016 + impl TaskTimeTracking for SqliteTaskRepository {
993 1017 // ---- Time Tracking (delegated to time_session_repo) ----
994 1018
995 1019 #[tracing::instrument(skip_all)]
@@ -1031,18 +1055,4 @@
1031 1055 async fn get_time_summary(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<TimeTrackingSummary>> {
1032 1056 time_session_repo::get_time_summary(&self.pool, user_id, start, end).await
1033 1057 }
1034 -
1035 - #[tracing::instrument(skip_all)]
1036 - async fn list_recurrence_chain(&self, root_id: TaskId, user_id: UserId) -> Result<Vec<Task>> {
1037 - let sql = format!(
1038 - "SELECT {} FROM tasks t LEFT JOIN projects p ON t.project_id = p.id AND p.user_id = ? LEFT JOIN contacts ct ON ct.id = t.contact_id WHERE (t.recurrence_parent_id = ? OR t.id = ?) AND t.user_id = ? ORDER BY t.created_at DESC",
1039 - TASK_SELECT_COLUMNS
1040 - );
1041 - query_tasks(&self.pool, &sql, &[
1042 - user_id.to_string(),
1043 - root_id.to_string(),
1044 - root_id.to_string(),
1045 - user_id.to_string(),
1046 - ]).await
1047 - }
1048 1058 }
@@ -1,1103 +1,0 @@
1 - //! Repository trait definitions for data access abstraction.
2 - //!
3 - //! This module defines the contracts for data persistence operations.
4 - //! Implementations are provided by database-specific crates (e.g., `goingson-db-sqlite`).
5 -
6 - use async_trait::async_trait;
7 - use chrono::{DateTime, NaiveDate, Utc};
8 - use std::collections::HashSet;
9 - use crate::id_types::{
10 - AnnotationId, AttachmentId, ContactEmailId, ContactId, ContactPhoneId, CustomFieldId,
11 - EmailAccountId, EmailId, EventId, MilestoneId, ProjectId, SavedViewId,
12 - SocialHandleId, SubtaskId, SyncAccountId, TaskId, UserId,
13 - };
14 - use uuid::Uuid;
15 -
16 - use crate::contact::{
17 - Contact, ContactCustomField, ContactEmail, ContactEmailEntry, ContactPhone, NewContact, NewContactCustomField,
18 - NewContactEmail, NewContactPhone, NewSocialHandle, SocialHandle, UpdateContact,
19 - };
20 - use crate::error::CoreError;
21 - use crate::models::{
22 - Annotation, Attachment, Email, EmailAccount, EmailAuthType, EmailThread, Event,
23 - FolderSyncState, NewAttachment, NewEmail, NewEmailWithTracking, NewEvent, NewProject,
24 - NewSavedView, NewTask, PositiveMinutes, Project, SavedView, Subtask, Task, TaskFilterQuery, TimeSession,
25 - TimeTrackingSummary, UpdateTask, User,
26 - };
27 -
28 - /// Convenience type alias for repository operation results.
29 - pub type Result<T> = std::result::Result<T, CoreError>;
30 -
31 - /// Repository for project CRUD operations.
32 - ///
33 - /// All operations are scoped to a specific user for multi-tenancy support.
34 - #[async_trait]
35 - pub trait ProjectRepository: Send + Sync {
36 - /// Lists all projects for a user.
37 - async fn list_all(&self, user_id: UserId) -> Result<Vec<Project>>;
38 -
39 - /// Retrieves a project by ID, returning `None` if not found.
40 - async fn get_by_id(&self, id: ProjectId, user_id: UserId) -> Result<Option<Project>>;
41 -
42 - /// Creates a new project.
43 - async fn create(&self, user_id: UserId, project: NewProject) -> Result<Project>;
44 -
45 - /// Restores a project verbatim from a backup, preserving its original ID
46 - /// and `created_at`. Idempotent (`INSERT OR IGNORE`): re-restoring the same
47 - /// backup is a no-op rather than a duplicate.
48 - async fn restore(&self, user_id: UserId, project: &Project) -> Result<()>;
49 -
50 - /// Updates an existing project, returning `None` if not found.
51 - async fn update(
52 - &self,
53 - id: ProjectId,
54 - user_id: UserId,
55 - project: crate::models::UpdateProject,
56 - ) -> Result<Option<Project>>;
57 -
58 - /// Deletes a project, returning `true` if deleted.
59 - async fn delete(&self, id: ProjectId, user_id: UserId) -> Result<bool>;
60 -
61 - /// Finds a project by exact name match.
62 - async fn find_by_name(&self, user_id: UserId, name: &str) -> Result<Option<Project>>;
63 - }
64 -
65 - /// Repository for task management operations.
66 - ///
67 - /// Provides CRUD operations plus specialized functionality for annotations,
68 - /// subtasks, snoozing, follow-up tracking, and time-block scheduling.
69 - #[async_trait]
70 - pub trait TaskRepository: Send + Sync {
71 - /// Lists all non-deleted tasks for a user.
72 - async fn list_all(&self, user_id: UserId) -> Result<Vec<Task>>;
73 -
74 - /// Lists tasks belonging to a specific project.
75 - async fn list_by_project(&self, user_id: UserId, project_id: ProjectId) -> Result<Vec<Task>>;
76 -
77 - /// Lists tasks linked to a specific contact.
78 - async fn list_by_contact(&self, user_id: UserId, contact_id: ContactId) -> Result<Vec<Task>>;
79 -
80 - /// Lists tasks matching the given filter criteria with pagination.
81 - /// Returns (tasks, total_count) for pagination UI.
82 - async fn list_filtered(&self, user_id: UserId, query: TaskFilterQuery) -> Result<(Vec<Task>, i64)>;
83 -
84 - /// Retrieves a task by ID.
85 - async fn get_by_id(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>>;
86 -
87 - /// Lightweight fetch of fields needed for update logic (avoids annotation/subtask/session sub-queries).
88 - /// Returns (created_at, status, completed_at, scheduled_start, scheduled_duration).
89 - async fn get_update_context(&self, id: TaskId, user_id: UserId) -> Result<Option<crate::models::TaskUpdateContext>>;
90 -
91 - /// Creates a new task.
92 - async fn create(&self, user_id: UserId, task: NewTask) -> Result<Task>;
93 -
94 - /// Restores a task verbatim from a backup, preserving its original ID,
95 - /// `status`, `completed_at`, and `created_at`. Idempotent (`INSERT OR
96 - /// IGNORE`). Annotations and subtasks are restored separately by the caller.
97 - async fn restore(&self, user_id: UserId, task: &Task) -> Result<()>;
98 -
99 - /// Updates an existing task.
100 - async fn update(&self, id: TaskId, user_id: UserId, task: UpdateTask) -> Result<Option<Task>>;
101 -
102 - /// Sets the project for many tasks in one transaction. Returns rows affected.
103 - /// Avoids the per-task fetch+update round-trips the bulk UI did before
104 - /// (ultra-fuzz Run #27 Perf S4). Project does not affect urgency.
105 - async fn bulk_set_project(&self, user_id: UserId, ids: &[TaskId], project_id: Option<ProjectId>) -> Result<usize>;
106 -
107 - /// Sets the priority for many tasks in one transaction, recomputing each task's
108 - /// urgency (priority is an urgency input). Returns rows affected.
109 - async fn bulk_set_priority(&self, user_id: UserId, ids: &[TaskId], priority: crate::models::Priority) -> Result<usize>;
110 -
111 - /// Soft-deletes a task.
112 - async fn delete(&self, id: TaskId, user_id: UserId) -> Result<bool>;
113 -
114 - /// Marks a task as started.
115 - async fn start(&self, id: TaskId, user_id: UserId) -> Result<bool>;
116 -
117 - /// Marks a task as completed and returns it.
118 - /// The caller is responsible for handling recurrence and milestone auto-completion.
119 - async fn complete(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>>;
120 -
121 - /// Atomically completes a task and creates the next recurring instance.
122 - /// Returns (completed_task, next_task). If the task has no recurrence,
123 - /// next_task is None. The entire operation is wrapped in a transaction
124 - /// so a crash cannot break the recurrence chain.
125 - async fn complete_recurring(&self, id: TaskId, user_id: UserId, next: Option<NewTask>) -> Result<(Option<Task>, Option<Task>)>;
126 -
127 - /// Counts non-deleted, non-completed tasks in a milestone.
128 - async fn count_incomplete_by_milestone(&self, milestone_id: MilestoneId, user_id: UserId) -> Result<i64>;
129 -
130 - /// Gets all annotations for a task.
131 - async fn get_annotations_for_task(&self, task_id: TaskId) -> Result<Vec<Annotation>>;
132 -
133 - /// Adds an annotation (note) to a task.
134 - async fn add_annotation(&self, task_id: TaskId, user_id: UserId, note: &str) -> Result<Option<Annotation>>;
135 -
136 - /// Deletes an annotation.
137 - async fn delete_annotation(&self, annotation_id: AnnotationId, user_id: UserId) -> Result<bool>;
138 -
139 - /// Gets all subtasks for a task.
140 - async fn get_subtasks_for_task(&self, task_id: TaskId) -> Result<Vec<Subtask>>;
141 -
142 - /// Adds a subtask to a task.
143 - async fn add_subtask(&self, task_id: TaskId, user_id: UserId, text: &str) -> Result<Option<Subtask>>;
144 -
145 - /// Toggles a subtask's completion status.
146 - async fn toggle_subtask(&self, subtask_id: SubtaskId, user_id: UserId) -> Result<Option<Subtask>>;
147 -
148 - /// Updates subtask text.
149 - async fn update_subtask(&self, subtask_id: SubtaskId, user_id: UserId, text: &str) -> Result<Option<Subtask>>;
150 -
151 - /// Deletes a subtask.
152 - async fn delete_subtask(&self, subtask_id: SubtaskId, user_id: UserId) -> Result<bool>;
153 -
154 - /// Adds a linked task as a subtask.
155 - ///
156 - /// This creates a subtask that links to another task, enabling multi-phase
157 - /// features where Phase 2 is a full task linked as a subtask of Phase 1.
158 - /// The linked subtask's completion status syncs with the linked task's status.
159 - async fn add_subtask_link(&self, task_id: TaskId, user_id: UserId, linked_task_id: TaskId) -> Result<Option<Subtask>>;
160 -
161 - /// Snoozes a task until the specified time.
162 - async fn snooze(&self, id: TaskId, user_id: UserId, until: DateTime<Utc>) -> Result<Option<Task>>;
163 -
164 - /// Removes snooze from a task.
165 - async fn unsnooze(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>>;
166 -
167 - /// Lists all currently snoozed tasks.
168 - async fn list_snoozed(&self, user_id: UserId) -> Result<Vec<Task>>;
169 -
170 - /// Marks a task as waiting for external response.
171 - async fn mark_waiting(&self, id: TaskId, user_id: UserId, expected_response: Option<DateTime<Utc>>) -> Result<Option<Task>>;
172 -
173 - /// Clears the waiting status from a task.
174 - async fn clear_waiting(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>>;
175 -
176 - /// Lists all tasks marked as waiting.
177 - async fn list_waiting(&self, user_id: UserId) -> Result<Vec<Task>>;
178 -
179 - /// Lists tasks scheduled for a specific date.
180 - async fn list_scheduled_for_date(&self, user_id: UserId, date: NaiveDate) -> Result<Vec<Task>>;
181 -
182 - /// Lists unscheduled tasks due on a specific date.
183 - async fn list_unscheduled_due_on_date(&self, user_id: UserId, date: NaiveDate) -> Result<Vec<Task>>;
184 -
185 - /// Lists unscheduled tasks whose `due` falls within a UTC instant window
186 - /// (inclusive upper bound). Lets callers honor a user-local day boundary
187 - /// instead of an implicit UTC one.
188 - async fn list_unscheduled_due_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>>;
189 -
190 - /// Updates a task's time-block schedule.
191 - async fn update_schedule(&self, id: TaskId, user_id: UserId, start: Option<DateTime<Utc>>, duration: Option<i32>) -> Result<Option<Task>>;
192 -
193 - /// Sets or clears the focus status on a task.
194 - async fn set_focus(&self, id: TaskId, user_id: UserId, is_focus: bool) -> Result<Option<Task>>;
195 -
196 - /// Lists all tasks marked as focus.
197 - async fn list_focused(&self, user_id: UserId) -> Result<Vec<Task>>;
198 -
199 - /// Clears focus from all tasks.
200 - async fn clear_all_focus(&self, user_id: UserId) -> Result<u64>;
201 -
202 - /// Lists tasks completed within a date range (for weekly review).
203 - async fn list_completed_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>>;
204 -
205 - /// Lists tasks that became overdue within a date range (for weekly review).
206 - async fn list_became_overdue_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>>;
207 -
208 - /// Lists tasks due within a date range (for weekly review).
209 - async fn list_due_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>>;
210 -
211 - /// Lists tasks created within a date range (for monthly review).
212 - async fn list_created_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>>;
213 -
214 - /// Lists high-priority pending tasks for focus selection.
215 - async fn list_available_for_focus(&self, user_id: UserId, limit: i64) -> Result<Vec<Task>>;
216 -
217 - // ---- Time Tracking ----
218 -
219 - /// Starts a timer on a task. Fails if any session is already active for the user.
220 - async fn start_timer(&self, task_id: TaskId, user_id: UserId) -> Result<TimeSession>;
221 -
222 - /// Stops the active timer on a task, updating duration and actual_minutes cache.
223 - async fn stop_timer(&self, task_id: TaskId, user_id: UserId) -> Result<Option<TimeSession>>;
224 -
225 - /// Discards the active timer without updating actual_minutes.
226 - async fn discard_timer(&self, task_id: TaskId, user_id: UserId) -> Result<bool>;
227 -
228 - /// Gets the currently active timer for a user (at most one).
229 - async fn get_active_timer(&self, user_id: UserId) -> Result<Option<(TimeSession, String)>>;
230 -
231 - /// Lists all time sessions for a task.
232 - async fn list_time_sessions(&self, task_id: TaskId, user_id: UserId) -> Result<Vec<TimeSession>>;
233 -
234 - /// Lists every time session for a user across all tasks (for full backup export).
235 - async fn list_all_time_sessions(&self, user_id: UserId) -> Result<Vec<TimeSession>>;
236 -
237 - /// Logs a manual time entry (retroactive, no live timer).
238 - ///
239 - /// `minutes` is a validated [`PositiveMinutes`], so the repository cannot be
240 - /// handed a zero/negative duration.
241 - async fn log_manual_time(&self, task_id: TaskId, user_id: UserId, minutes: PositiveMinutes, date: DateTime<Utc>) -> Result<TimeSession>;
242 -
243 - /// Gets aggregated time tracking summary grouped by project and date.
244 - async fn get_time_summary(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<TimeTrackingSummary>>;
245 -
246 - /// Lists all tasks in a recurrence chain (root + all descendants).
247 - async fn list_recurrence_chain(&self, root_id: TaskId, user_id: UserId) -> Result<Vec<Task>>;
248 - }
249 -
250 - /// Repository for calendar event operations.
251 - ///
252 - /// Events can be standalone or linked to tasks (for time-blocking).
253 - ///
254 - /// # Ordering Contract
255 - ///
256 - /// All methods returning `Vec<Event>` **MUST** return results sorted by
257 - /// `start_time ASC`. This is enforced at the SQL level (`ORDER BY e.start_time ASC`)
258 - /// and callers rely on this guarantee — no post-fetch sorting is needed.
259 - #[async_trait]
260 - pub trait EventRepository: Send + Sync {
261 - /// Lists all events for a user, ordered by `start_time ASC`.
262 - async fn list_all(&self, user_id: UserId) -> Result<Vec<Event>>;
263 -
264 - /// Lists events belonging to a specific project, ordered by `start_time ASC`.
265 - async fn list_by_project(&self, user_id: UserId, project_id: ProjectId) -> Result<Vec<Event>>;
266 -
267 - /// Lists events linked to a specific contact, ordered by `start_time DESC`.
268 - async fn list_by_contact(&self, user_id: UserId, contact_id: ContactId) -> Result<Vec<Event>>;
269 -
270 - /// Retrieves an event by ID.
271 - async fn get_by_id(&self, id: EventId, user_id: UserId) -> Result<Option<Event>>;
272 -
273 - /// Creates a new event.
274 - async fn create(&self, user_id: UserId, event: NewEvent) -> Result<Event>;
275 -
276 - /// Restores an event verbatim from a backup, preserving its original ID and
277 - /// all metadata the normal create path never sets — `block_type`,
278 - /// `external_source`/`external_id`, `recurrence_parent_id`, `is_read_only`,
279 - /// `snoozed_until`, and reminder offsets. Idempotent (`INSERT OR IGNORE`).
280 - async fn restore(&self, user_id: UserId, event: &Event) -> Result<()>;
281 -
282 - /// Updates an existing event.
283 - async fn update(&self, id: EventId, user_id: UserId, event: crate::models::UpdateEvent) -> Result<Option<Event>>;
284 -
285 - /// Deletes an event.
286 - async fn delete(&self, id: EventId, user_id: UserId) -> Result<bool>;
287 -
288 - /// Records the external source/id for an event (e.g. after an iCal import),
289 - /// used to dedup on re-import.
290 - async fn set_external_ref(
291 - &self,
292 - id: EventId,
293 - user_id: UserId,
294 - source: &str,
295 - external_id: &str,
296 - ) -> Result<()>;
297 -
298 - /// Deletes multiple events by ID, returning the number deleted.
299 - async fn delete_many(&self, ids: &[EventId], user_id: UserId) -> Result<u64>;
300 -
301 - /// Gets events starting within the next N days, ordered by `start_time ASC`.
302 - async fn get_upcoming(&self, user_id: UserId, days: i64) -> Result<Vec<Event>>;
303 -
304 - /// Finds the event linked to a specific task (for time-blocking).
305 - async fn get_by_linked_task(&self, user_id: UserId, task_id: TaskId) -> Result<Option<Event>>;
306 -
307 - /// Deletes the event linked to a task.
308 - async fn delete_by_linked_task(&self, user_id: UserId, task_id: TaskId) -> Result<bool>;
309 -
310 - /// Lists events occurring on a specific date, ordered by `start_time ASC`.
311 - async fn list_for_date(&self, user_id: UserId, date: NaiveDate) -> Result<Vec<Event>>;
312 -
313 - /// Lists events within a date range (for weekly review), ordered by `start_time ASC`.
314 - async fn list_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Event>>;
315 -
316 - /// Lists all recurring events (recurrence != 'None' or recurrence_rule is set).
317 - async fn list_recurring(&self, user_id: UserId) -> Result<Vec<Event>>;
318 -
319 - /// Finds an event by external source and ID (for dedup during import).
320 - async fn find_by_external_id(&self, source: &str, ext_id: &str, user_id: UserId) -> Result<Option<Event>>;
321 -
322 - /// Snoozes an event until `until`. Returns the updated event, or `None` if not found.
323 - async fn snooze(&self, id: EventId, user_id: UserId, until: DateTime<Utc>) -> Result<Option<Event>>;
324 -
325 - /// Clears any snooze on an event. Returns the updated event, or `None` if not found.
326 - async fn unsnooze(&self, id: EventId, user_id: UserId) -> Result<Option<Event>>;
327 -
328 - /// Lists currently snoozed events (snoozed_until is in the future).
329 - async fn list_snoozed(&self, user_id: UserId) -> Result<Vec<Event>>;
330 - }
331 -
332 - /// Repository for email message operations.
333 - ///
334 - /// Supports IMAP-synced emails with read/archive status, project linking,
335 - /// snoozing, and follow-up tracking.
336 - #[async_trait]
337 - pub trait EmailRepository: Send + Sync {
338 - /// Lists all emails, optionally including archived. Selects full bodies and is
339 - /// unbounded -- intended for backup export, NOT for list views (use
340 - /// [`list_metadata`](Self::list_metadata) there to avoid materializing every
341 - /// body at once).
342 - async fn list_all(&self, user_id: UserId, include_archived: bool) -> Result<Vec<Email>>;
343 -
344 - /// Lists emails for a flat list view: body/html_body are omitted (empty) and the
345 - /// result is capped, so a large mailbox doesn't load every body into memory
346 - /// (ultra-fuzz Run #27 Perf S3). Open an email via `get_by_id` for its body.
347 - async fn list_metadata(&self, user_id: UserId, include_archived: bool) -> Result<Vec<Email>>;
348 -
349 - /// Lists emails grouped by thread, with metadata pre-computed and pagination.
350 - /// Returns (threads, total_count) sorted by most recent email (newest first).
351 - /// Optional `folder` filter restricts to emails from a specific source_folder.
352 - /// Optional `label` filter restricts to emails with a specific label.
353 - async fn list_threaded(&self, user_id: UserId, include_archived: bool, offset: Option<i64>, limit: Option<i64>, folder: Option<&str>, label: Option<&str>) -> Result<(Vec<EmailThread>, i64)>;
354 -
355 - /// Lists emails linked to a specific project.
356 - async fn list_by_project(&self, user_id: UserId, project_id: ProjectId) -> Result<Vec<Email>>;
357 -
358 - /// Lists emails sent from or to any of the given addresses.
359 - async fn list_by_addresses(&self, user_id: UserId, addresses: &[&str]) -> Result<Vec<Email>>;
360 -
361 - /// Lists emails not linked to any project.
362 - async fn list_unlinked(&self, user_id: UserId) -> Result<Vec<Email>>;
363 -
364 - /// Retrieves an email by ID.
365 - async fn get_by_id(&self, id: EmailId, user_id: UserId) -> Result<Option<Email>>;
366 -
367 - /// Replaces an email's body with a full (re-fetched) version and clears the
368 - /// `body_truncated` flag. Used to lazily load a body truncated at sync.
369 - async fn set_full_body(&self, id: EmailId, user_id: UserId, body: &str) -> Result<()>;
370 -
371 - /// Creates a new email record.
372 - async fn create(&self, user_id: UserId, email: NewEmail) -> Result<Email>;
373 -
374 - /// Restores an email from a backup, preserving its original ID and
375 - /// `message_id` so re-restore dedupes. Round-trips the durable fields
376 - /// (addresses, subject, body, html_body, read/archived/outgoing flags,
377 - /// received_at, threading, labels); transient IMAP-sync and draft-compose
378 - /// state is not restored (emails re-sync from the server). Idempotent
379 - /// (`INSERT OR IGNORE`).
380 - async fn restore(&self, user_id: UserId, email: &Email) -> Result<()>;
381 -
382 - /// Creates an email with follow-up tracking fields.
383 - async fn create_with_tracking(&self, user_id: UserId, email: NewEmailWithTracking) -> Result<Email>;
384 -
385 - /// Batch-inserts emails with tracking in a single transaction, skipping post-insert SELECTs.
386 - /// Returns the count of successfully inserted emails.
387 - async fn create_with_tracking_batch(&self, user_id: UserId, emails: Vec<NewEmailWithTracking>) -> Result<usize>;
388 -
389 - /// Deletes an email.
390 - async fn delete(&self, id: EmailId, user_id: UserId) -> Result<bool>;
391 -
392 - /// Marks an email as read.
393 - async fn mark_read(&self, id: EmailId, user_id: UserId) -> Result<bool>;
394 -
395 - /// Marks an email as unread.
396 - async fn mark_unread(&self, id: EmailId, user_id: UserId) -> Result<bool>;
397 -
398 - /// Archives an email.
399 - async fn archive(&self, id: EmailId, user_id: UserId) -> Result<bool>;
400 -
401 - /// Unarchives an email.
402 - async fn unarchive(&self, id: EmailId, user_id: UserId) -> Result<bool>;
403 -
404 - /// Updates the IMAP source folder for an email.
405 - async fn update_source_folder(&self, id: EmailId, user_id: UserId, new_folder: &str) -> Result<bool>;
406 -
407 - /// Marks all emails as read, returning the count updated.
408 - async fn mark_all_read(&self, user_id: UserId) -> Result<u64>;
409 -
410 - /// Links or unlinks an email to a project.
411 - async fn link_to_project(&self, id: EmailId, user_id: UserId, project_id: Option<ProjectId>) -> Result<bool>;
412 -
413 - /// Counts unread emails.
414 - async fn count_unread(&self, user_id: UserId) -> Result<i64>;
415 -
416 - /// Checks if an email with the given Message-ID header exists.
417 - async fn exists_by_message_id(&self, user_id: UserId, message_id: &str) -> Result<bool>;
418 -
419 - /// Batch check for existing Message-IDs, returns the set that exist.
420 - async fn exists_by_message_ids(&self, user_id: UserId, message_ids: &[&str]) -> Result<HashSet<String>>;
421 -
422 - /// Batch check which email addresses have appeared as senders.
423 - /// Returns the set of addresses (lowercased) that have sent at least one email.
424 - async fn exists_as_senders(&self, user_id: UserId, addresses: &[&str]) -> Result<HashSet<String>>;
425 -
426 - /// Snoozes an email until the specified time.
427 - async fn snooze(&self, id: EmailId, user_id: UserId, until: DateTime<Utc>) -> Result<Option<Email>>;
428 -
429 - /// Removes snooze from an email.
430 - async fn unsnooze(&self, id: EmailId, user_id: UserId) -> Result<Option<Email>>;
431 -
432 - /// Lists all currently snoozed emails.
433 - async fn list_snoozed(&self, user_id: UserId) -> Result<Vec<Email>>;
434 -
435 - /// Marks an email as waiting for response.
436 - async fn mark_waiting(&self, id: EmailId, user_id: UserId, expected_response: Option<DateTime<Utc>>) -> Result<Option<Email>>;
437 -
438 - /// Clears the waiting status from an email.
439 - async fn clear_waiting(&self, id: EmailId, user_id: UserId) -> Result<Option<Email>>;
440 -
441 - /// Lists all emails marked as waiting.
442 - async fn list_waiting(&self, user_id: UserId) -> Result<Vec<Email>>;
443 -
444 - /// Lists all emails in a thread, ordered by date ascending.
445 - async fn list_by_thread(&self, user_id: UserId, thread_id: &str) -> Result<Vec<Email>>;
446 -
447 - /// Gets an email by its Message-ID header.
448 - async fn get_by_message_id(&self, user_id: UserId, message_id: &str) -> Result<Option<Email>>;
449 -
450 - /// Updates labels/tags on an email.
451 - async fn update_labels(&self, id: EmailId, user_id: UserId, labels: &[String]) -> Result<Option<Email>>;
452 -
453 - /// Lists distinct source_folder values across all non-draft emails.
454 - async fn list_folders(&self, user_id: UserId) -> Result<Vec<String>>;
455 -
456 - /// Lists all distinct labels used across all emails.
457 - async fn list_labels(&self, user_id: UserId) -> Result<Vec<String>>;
458 -
459 - /// Lists all draft emails.
460 - async fn list_drafts(&self, user_id: UserId) -> Result<Vec<Email>>;
461 -
462 - /// Creates or updates a draft email.
463 - #[allow(clippy::too_many_arguments)]
464 - async fn save_draft(&self, id: EmailId, user_id: UserId, from: &str, to: &str, cc: Option<&str>, bcc: Option<&str>, subject: &str, body: &str, account_id: Option<EmailAccountId>, in_reply_to: Option<&str>, references: Option<&str>, thread_id: Option<&str>) -> Result<Email>;
465 - }
466 -
467 - /// Repository for user account operations.
468 - #[async_trait]
469 - pub trait UserRepository: Send + Sync {
470 - /// Creates a new user account with hashed password.
471 - async fn create(&self, email: &str, password: &str, display_name: &str) -> Result<User>;
472 -
473 - /// Finds a user by email address.
474 - async fn find_by_email(&self, email: &str) -> Result<Option<User>>;
475 -
476 - /// Authenticates a user, returning the user if credentials are valid.
477 - async fn authenticate(&self, email: &str, password: &str) -> Result<Option<User>>;
478 -
479 - /// Updates the user's last login timestamp.
480 - async fn update_last_login(&self, user_id: UserId) -> Result<()>;
481 - }
482 -
483 - /// Repository for email account (IMAP/SMTP/OAuth2) configuration.
484 - #[allow(clippy::too_many_arguments)]
485 - #[async_trait]
486 - pub trait EmailAccountRepository: Send + Sync {
487 - /// Lists all email accounts for a user.
488 - async fn list_by_user(&self, user_id: UserId) -> Result<Vec<EmailAccount>>;
489 -
490 - /// Retrieves an email account by ID.
491 - async fn get_by_id(&self, id: EmailAccountId, user_id: UserId) -> Result<Option<EmailAccount>>;
492 -
493 - /// Creates a new email account configuration (password-based IMAP/SMTP).
494 - async fn create(
495 - &self,
496 - user_id: UserId,
497 - account_name: &str,
498 - email_address: &str,
499 - imap_server: &str,
500 - imap_port: i32,
Lines truncated