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
Commit: fbe4a5e749305a1942e12c777fe71f3b22338568
Parent: 68d79f5
23 files changed, +1220 insertions, -548 deletions
@@ -1,1103 +0,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
@@ -0,0 +1,105 @@
1 + use super::*;
2 +
3 + /// Repository for contact CRUD operations and sub-collection management.
4 + ///
5 + /// Contacts have sub-collections (emails, phones, social handles) stored in
6 + /// separate tables to enable querying by email address for future integrations.
7 + #[async_trait]
8 + pub trait ContactRepository: Send + Sync {
9 + /// Lists all contacts for a user.
10 + async fn list_all(&self, user_id: UserId) -> Result<Vec<Contact>>;
11 +
12 + /// Retrieves a contact by ID, returning `None` if not found.
13 + async fn get_by_id(&self, id: ContactId, user_id: UserId) -> Result<Option<Contact>>;
14 +
15 + /// Creates a new contact.
16 + async fn create(&self, user_id: UserId, contact: NewContact) -> Result<Contact>;
17 +
18 + /// Restores a contact verbatim from a backup, preserving its original ID,
19 + /// `created_at`, and `updated_at`. Idempotent (`INSERT OR IGNORE`).
20 + /// Sub-collections (emails, phones, social handles, custom fields) are
21 + /// restored separately by the caller.
22 + async fn restore(&self, user_id: UserId, contact: &Contact) -> Result<()>;
23 +
24 + /// Updates an existing contact, returning `None` if not found.
25 + async fn update(&self, id: ContactId, user_id: UserId, contact: UpdateContact) -> Result<Option<Contact>>;
26 +
27 + /// Deletes a contact (CASCADE removes sub-entities), returning `true` if deleted.
28 + async fn delete(&self, id: ContactId, user_id: UserId) -> Result<bool>;
29 +
30 + /// Records the external source/id for a contact (e.g. after a vCard import),
31 + /// used to dedup on re-import.
32 + async fn set_external_ref(
33 + &self,
34 + id: ContactId,
35 + user_id: UserId,
36 + source: &str,
37 + external_id: &str,
38 + ) -> Result<()>;
39 +
40 + /// Deletes multiple contacts by ID, returning the number deleted.
41 + async fn delete_many(&self, ids: &[ContactId], user_id: UserId) -> Result<u64>;
42 +
43 + /// Adds a tag to multiple contacts (skips contacts that already have the tag).
44 + async fn tag_many(&self, ids: &[ContactId], user_id: UserId, tag: &str) -> Result<u64>;
45 +
46 + /// Lists contacts matching a tag.
47 + async fn list_by_tag(&self, user_id: UserId, tag: &str) -> Result<Vec<Contact>>;
48 +
49 + /// Lists contacts matching a search query and/or tag filter.
50 + /// Searches across display_name, nickname, company, title, notes, and email addresses.
51 + async fn list_filtered(&self, user_id: UserId, search: Option<&str>, tag: Option<&str>, include_implicit: bool) -> Result<Vec<Contact>>;
52 +
53 + /// Flat (name, email) directory for compose autocomplete — one JOIN, no
54 + /// per-contact sub-collection hydration. One row per contact email address.
55 + async fn list_email_directory(&self, user_id: UserId, include_implicit: bool) -> Result<Vec<ContactEmailEntry>>;
56 +
57 + /// Finds a contact by email address.
58 + async fn find_by_email(&self, user_id: UserId, email: &str) -> Result<Option<Contact>>;
59 +
60 + /// Batch check which email addresses belong to known contacts.
61 + /// Returns the set of addresses (lowercased) that match at least one contact.
62 + async fn find_emails_in_contacts(&self, user_id: UserId, addresses: &[&str]) -> Result<HashSet<String>>;
63 +
64 + /// Promotes an implicit contact to explicit by setting is_implicit = 0.
65 + async fn promote_contact(&self, id: ContactId, user_id: UserId) -> Result<Option<Contact>>;
66 +
67 + /// Finds a contact by external source and ID (for dedup during import).
68 + async fn find_by_external_id(&self, source: &str, ext_id: &str, user_id: UserId) -> Result<Option<Contact>>;
69 +
70 + /// Adds an email address to a contact.
71 + async fn add_email(&self, contact_id: ContactId, user_id: UserId, email: NewContactEmail) -> Result<ContactEmail>;
72 +
73 + /// Removes an email address from a contact.
74 + async fn remove_email(&self, email_id: ContactEmailId, user_id: UserId) -> Result<bool>;
75 +
76 + /// Adds a phone number to a contact.
77 + async fn add_phone(&self, contact_id: ContactId, user_id: UserId, phone: NewContactPhone) -> Result<ContactPhone>;
78 +
79 + /// Removes a phone number from a contact.
80 + async fn remove_phone(&self, phone_id: ContactPhoneId, user_id: UserId) -> Result<bool>;
81 +
82 + /// Adds a social handle to a contact.
83 + async fn add_social_handle(&self, contact_id: ContactId, user_id: UserId, handle: NewSocialHandle) -> Result<SocialHandle>;
84 +
85 + /// Removes a social handle from a contact.
86 + async fn remove_social_handle(&self, handle_id: SocialHandleId, user_id: UserId) -> Result<bool>;
87 +
88 + /// Adds a custom field to a contact.
89 + async fn add_custom_field(&self, contact_id: ContactId, user_id: UserId, field: NewContactCustomField) -> Result<ContactCustomField>;
90 +
91 + /// Removes a custom field from a contact.
92 + async fn remove_custom_field(&self, field_id: CustomFieldId, user_id: UserId) -> Result<bool>;
93 +
94 + /// Updates a contact email row (address/label/is_primary). Returns the updated row, or `None` if not found.
95 + async fn update_email(&self, email_id: ContactEmailId, user_id: UserId, email: NewContactEmail) -> Result<Option<ContactEmail>>;
96 +
97 + /// Updates a contact phone row (number/label/is_primary). Returns the updated row, or `None` if not found.
98 + async fn update_phone(&self, phone_id: ContactPhoneId, user_id: UserId, phone: NewContactPhone) -> Result<Option<ContactPhone>>;
99 +
100 + /// Updates a social handle row (platform/handle/url). Returns the updated row, or `None` if not found.
101 + async fn update_social_handle(&self, handle_id: SocialHandleId, user_id: UserId, handle: NewSocialHandle) -> Result<Option<SocialHandle>>;
102 +
103 + /// Updates a custom field row (label/value/url). Returns the updated row, or `None` if not found.
104 + async fn update_custom_field(&self, field_id: CustomFieldId, user_id: UserId, field: NewContactCustomField) -> Result<Option<ContactCustomField>>;
105 + }
@@ -0,0 +1,284 @@
1 + use super::*;
2 +
3 + /// Repository for email message operations.
4 + ///
5 + /// Supports IMAP-synced emails with read/archive status, project linking,
6 + /// snoozing, and follow-up tracking.
7 + #[async_trait]
8 + pub trait EmailRepository: Send + Sync {
9 + /// Lists all emails, optionally including archived. Selects full bodies and is
10 + /// unbounded -- intended for backup export, NOT for list views (use
11 + /// [`list_metadata`](Self::list_metadata) there to avoid materializing every
12 + /// body at once).
13 + async fn list_all(&self, user_id: UserId, include_archived: bool) -> Result<Vec<Email>>;
14 +
15 + /// Lists emails for a flat list view: body/html_body are omitted (empty) and the
16 + /// result is capped, so a large mailbox doesn't load every body into memory
17 + /// (ultra-fuzz Run #27 Perf S3). Open an email via `get_by_id` for its body.
18 + async fn list_metadata(&self, user_id: UserId, include_archived: bool) -> Result<Vec<Email>>;
19 +
20 + /// Lists emails grouped by thread, with metadata pre-computed and pagination.
21 + /// Returns (threads, total_count) sorted by most recent email (newest first).
22 + /// Optional `folder` filter restricts to emails from a specific source_folder.
23 + /// Optional `label` filter restricts to emails with a specific label.
24 + 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)>;
25 +
26 + /// Lists emails linked to a specific project.
27 + async fn list_by_project(&self, user_id: UserId, project_id: ProjectId) -> Result<Vec<Email>>;
28 +
29 + /// Lists emails sent from or to any of the given addresses.
30 + async fn list_by_addresses(&self, user_id: UserId, addresses: &[&str]) -> Result<Vec<Email>>;
31 +
32 + /// Lists emails not linked to any project.
33 + async fn list_unlinked(&self, user_id: UserId) -> Result<Vec<Email>>;
34 +
35 + /// Retrieves an email by ID.
36 + async fn get_by_id(&self, id: EmailId, user_id: UserId) -> Result<Option<Email>>;
37 +
38 + /// Replaces an email's body with a full (re-fetched) version and clears the
39 + /// `body_truncated` flag. Used to lazily load a body truncated at sync.
40 + async fn set_full_body(&self, id: EmailId, user_id: UserId, body: &str) -> Result<()>;
41 +
42 + /// Creates a new email record.
43 + async fn create(&self, user_id: UserId, email: NewEmail) -> Result<Email>;
44 +
45 + /// Restores an email from a backup, preserving its original ID and
46 + /// `message_id` so re-restore dedupes. Round-trips the durable fields
47 + /// (addresses, subject, body, html_body, read/archived/outgoing flags,
48 + /// received_at, threading, labels); transient IMAP-sync and draft-compose
49 + /// state is not restored (emails re-sync from the server). Idempotent
50 + /// (`INSERT OR IGNORE`).
51 + async fn restore(&self, user_id: UserId, email: &Email) -> Result<()>;
52 +
53 + /// Creates an email with follow-up tracking fields.
54 + async fn create_with_tracking(&self, user_id: UserId, email: NewEmailWithTracking) -> Result<Email>;
55 +
56 + /// Batch-inserts emails with tracking in a single transaction, skipping post-insert SELECTs.
57 + /// Returns the count of successfully inserted emails.
58 + async fn create_with_tracking_batch(&self, user_id: UserId, emails: Vec<NewEmailWithTracking>) -> Result<usize>;
59 +
60 + /// Deletes an email.
61 + async fn delete(&self, id: EmailId, user_id: UserId) -> Result<bool>;
62 +
63 + /// Marks an email as read.
64 + async fn mark_read(&self, id: EmailId, user_id: UserId) -> Result<bool>;
65 +
66 + /// Marks an email as unread.
67 + async fn mark_unread(&self, id: EmailId, user_id: UserId) -> Result<bool>;
68 +
69 + /// Archives an email.
70 + async fn archive(&self, id: EmailId, user_id: UserId) -> Result<bool>;
71 +
72 + /// Unarchives an email.
73 + async fn unarchive(&self, id: EmailId, user_id: UserId) -> Result<bool>;
74 +
75 + /// Updates the IMAP source folder for an email.
76 + async fn update_source_folder(&self, id: EmailId, user_id: UserId, new_folder: &str) -> Result<bool>;
77 +
78 + /// Marks all emails as read, returning the count updated.
79 + async fn mark_all_read(&self, user_id: UserId) -> Result<u64>;
80 +
81 + /// Links or unlinks an email to a project.
82 + async fn link_to_project(&self, id: EmailId, user_id: UserId, project_id: Option<ProjectId>) -> Result<bool>;
83 +
84 + /// Counts unread emails.
85 + async fn count_unread(&self, user_id: UserId) -> Result<i64>;
86 +
87 + /// Checks if an email with the given Message-ID header exists.
88 + async fn exists_by_message_id(&self, user_id: UserId, message_id: &str) -> Result<bool>;
89 +
90 + /// Batch check for existing Message-IDs, returns the set that exist.
91 + async fn exists_by_message_ids(&self, user_id: UserId, message_ids: &[&str]) -> Result<HashSet<String>>;
92 +
93 + /// Batch check which email addresses have appeared as senders.
94 + /// Returns the set of addresses (lowercased) that have sent at least one email.
95 + async fn exists_as_senders(&self, user_id: UserId, addresses: &[&str]) -> Result<HashSet<String>>;
96 +
97 + /// Snoozes an email until the specified time.
98 + async fn snooze(&self, id: EmailId, user_id: UserId, until: DateTime<Utc>) -> Result<Option<Email>>;
99 +
100 + /// Removes snooze from an email.
101 + async fn unsnooze(&self, id: EmailId, user_id: UserId) -> Result<Option<Email>>;
102 +
103 + /// Lists all currently snoozed emails.
104 + async fn list_snoozed(&self, user_id: UserId) -> Result<Vec<Email>>;
105 +
106 + /// Marks an email as waiting for response.
107 + async fn mark_waiting(&self, id: EmailId, user_id: UserId, expected_response: Option<DateTime<Utc>>) -> Result<Option<Email>>;
108 +
109 + /// Clears the waiting status from an email.
110 + async fn clear_waiting(&self, id: EmailId, user_id: UserId) -> Result<Option<Email>>;
111 +
112 + /// Lists all emails marked as waiting.
113 + async fn list_waiting(&self, user_id: UserId) -> Result<Vec<Email>>;
114 +
115 + /// Lists all emails in a thread, ordered by date ascending.
116 + async fn list_by_thread(&self, user_id: UserId, thread_id: &str) -> Result<Vec<Email>>;
117 +
118 + /// Gets an email by its Message-ID header.
119 + async fn get_by_message_id(&self, user_id: UserId, message_id: &str) -> Result<Option<Email>>;
120 +
121 + /// Updates labels/tags on an email.
122 + async fn update_labels(&self, id: EmailId, user_id: UserId, labels: &[String]) -> Result<Option<Email>>;
123 +
124 + /// Lists distinct source_folder values across all non-draft emails.
125 + async fn list_folders(&self, user_id: UserId) -> Result<Vec<String>>;
126 +
127 + /// Lists all distinct labels used across all emails.
128 + async fn list_labels(&self, user_id: UserId) -> Result<Vec<String>>;
129 +
130 + /// Lists all draft emails.
131 + async fn list_drafts(&self, user_id: UserId) -> Result<Vec<Email>>;
132 +
133 + /// Creates or updates a draft email.
134 + #[allow(clippy::too_many_arguments)]
135 + 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>;
136 + }
137 +
138 + /// Repository for email account (IMAP/SMTP/OAuth2) configuration.
139 + #[allow(clippy::too_many_arguments)]
140 + #[async_trait]
141 + pub trait EmailAccountRepository: Send + Sync {
142 + /// Lists all email accounts for a user.
143 + async fn list_by_user(&self, user_id: UserId) -> Result<Vec<EmailAccount>>;
144 +
145 + /// Retrieves an email account by ID.
146 + async fn get_by_id(&self, id: EmailAccountId, user_id: UserId) -> Result<Option<EmailAccount>>;
147 +
148 + /// Creates a new email account configuration (password-based IMAP/SMTP).
149 + async fn create(
150 + &self,
151 + user_id: UserId,
152 + account_name: &str,
153 + email_address: &str,
154 + imap_server: &str,
155 + imap_port: i32,
156 + smtp_server: &str,
157 + smtp_port: i32,
158 + username: &str,
159 + password: &str,
160 + use_tls: bool,
161 + archive_folder_name: Option<&str>,
162 + ) -> Result<EmailAccount>;
163 +
164 + /// Creates a new OAuth2 email account (Fastmail JMAP).
165 + async fn create_oauth(
166 + &self,
167 + user_id: UserId,
168 + account_name: &str,
169 + email_address: &str,
170 + access_token: &str,
171 + refresh_token: &str,
172 + expires_at: DateTime<Utc>,
173 + jmap_session_url: &str,
174 + jmap_account_id: &str,
175 + ) -> Result<EmailAccount>;
176 +
177 + /// Creates a new OAuth2 email account with IMAP/SMTP (Google, Microsoft, Yahoo).
178 + async fn create_oauth_imap(
179 + &self,
180 + user_id: UserId,
181 + account_name: &str,
182 + email_address: &str,
183 + auth_type: EmailAuthType,
184 + access_token: &str,
185 + refresh_token: &str,
186 + expires_at: DateTime<Utc>,
187 + imap_server: &str,
188 + imap_port: i32,
189 + smtp_server: &str,
190 + smtp_port: i32,
191 + ) -> Result<EmailAccount>;
192 +
193 + /// Updates an email account. Password is only updated if provided.
194 + async fn update(
195 + &self,
196 + id: EmailAccountId,
197 + user_id: UserId,
198 + account_name: &str,
199 + email_address: &str,
200 + imap_server: &str,
201 + imap_port: i32,
202 + smtp_server: &str,
203 + smtp_port: i32,
204 + username: &str,
205 + password: Option<&str>,
206 + use_tls: bool,
207 + archive_folder_name: Option<&str>,
208 + ) -> Result<Option<EmailAccount>>;
209 +
210 + /// Updates OAuth2 tokens for an account.
211 + async fn update_oauth_tokens(
212 + &self,
213 + id: EmailAccountId,
214 + user_id: UserId,
215 + access_token: &str,
216 + refresh_token: Option<&str>,
217 + expires_at: DateTime<Utc>,
218 + ) -> Result<Option<EmailAccount>>;
219 +
220 + /// Updates JMAP session info for an account.
221 + async fn update_jmap_session(
222 + &self,
223 + id: EmailAccountId,
224 + user_id: UserId,
225 + session_url: &str,
226 + account_id: &str,
227 + ) -> Result<Option<EmailAccount>>;
228 +
229 + /// Deletes an email account.
230 + async fn delete(&self, id: EmailAccountId, user_id: UserId) -> Result<bool>;
231 +
232 + /// Updates the last sync timestamp.
233 + async fn update_last_sync(&self, id: EmailAccountId, user_id: UserId) -> Result<bool>;
234 +
235 + /// Updates the sync interval setting for an account.
236 + async fn update_sync_interval(&self, id: EmailAccountId, user_id: UserId, interval_minutes: Option<i32>) -> Result<Option<EmailAccount>>;
237 +
238 + /// Updates the email signature for an account.
239 + async fn update_signature(&self, id: EmailAccountId, user_id: UserId, signature: Option<&str>) -> Result<Option<EmailAccount>>;
240 +
241 + /// Updates the notification preference for an account.
242 + async fn update_notify_new_emails(&self, id: EmailAccountId, user_id: UserId, enabled: bool) -> Result<Option<EmailAccount>>;
243 +
244 + /// Lists accounts that need automatic sync based on their sync_interval_minutes.
245 + /// Returns accounts where sync is enabled and last_sync_at + interval < now.
246 + async fn list_accounts_needing_sync(&self, user_id: UserId) -> Result<Vec<EmailAccount>>;
247 +
248 + /// Gets the IMAP folder sync state for incremental UID-based fetching.
249 + async fn get_folder_sync_state(&self, account_id: EmailAccountId, folder: &str) -> Result<Option<FolderSyncState>>;
250 +
251 + /// Upserts the IMAP folder sync state after a successful sync.
252 + async fn upsert_folder_sync_state(&self, account_id: EmailAccountId, folder: &str, uid_validity: u32, last_seen_uid: u32) -> Result<()>;
253 +
254 + /// Deletes stale folder sync state (e.g. on UIDVALIDITY change).
255 + async fn delete_folder_sync_state(&self, account_id: EmailAccountId, folder: &str) -> Result<()>;
256 + }
257 +
258 + /// Repository for file attachment operations.
259 + #[async_trait]
260 + pub trait AttachmentRepository: Send + Sync {
261 + /// Creates a new attachment record.
262 + async fn create(&self, user_id: UserId, attachment: NewAttachment) -> Result<Attachment>;
263 +
264 + /// Lists attachments for a task.
265 + async fn list_for_task(&self, task_id: TaskId, user_id: UserId) -> Result<Vec<Attachment>>;
266 +
267 + /// Lists attachments for a project.
268 + async fn list_for_project(&self, project_id: ProjectId, user_id: UserId) -> Result<Vec<Attachment>>;
269 +
270 + /// Retrieves an attachment by ID.
271 + async fn get_by_id(&self, id: AttachmentId, user_id: UserId) -> Result<Option<Attachment>>;
272 +
273 + /// Deletes an attachment record, returning `true` if deleted.
274 + async fn delete(&self, id: AttachmentId, user_id: UserId) -> Result<bool>;
275 +
276 + /// Lists all attachments sharing a blob hash (for dedup checks).
277 + async fn list_by_blob_hash(&self, blob_hash: &str, user_id: UserId) -> Result<Vec<Attachment>>;
278 +
279 + /// Lists all distinct blob hashes for a user (for blob sync).
280 + async fn list_all_blob_hashes(&self, user_id: UserId) -> Result<Vec<String>>;
281 +
282 + /// Lists every attachment record for a user (for full backup export).
283 + async fn list_all(&self, user_id: UserId) -> Result<Vec<Attachment>>;
284 + }
@@ -0,0 +1,83 @@
1 + use super::*;
2 +
3 + /// Repository for calendar event operations.
4 + ///
5 + /// Events can be standalone or linked to tasks (for time-blocking).
6 + ///
7 + /// # Ordering Contract
8 + ///
9 + /// All methods returning `Vec<Event>` **MUST** return results sorted by
10 + /// `start_time ASC`. This is enforced at the SQL level (`ORDER BY e.start_time ASC`)
11 + /// and callers rely on this guarantee — no post-fetch sorting is needed.
12 + #[async_trait]
13 + pub trait EventRepository: Send + Sync {
14 + /// Lists all events for a user, ordered by `start_time ASC`.
15 + async fn list_all(&self, user_id: UserId) -> Result<Vec<Event>>;
16 +
17 + /// Lists events belonging to a specific project, ordered by `start_time ASC`.
18 + async fn list_by_project(&self, user_id: UserId, project_id: ProjectId) -> Result<Vec<Event>>;
19 +
20 + /// Lists events linked to a specific contact, ordered by `start_time DESC`.
21 + async fn list_by_contact(&self, user_id: UserId, contact_id: ContactId) -> Result<Vec<Event>>;
22 +
23 + /// Retrieves an event by ID.
24 + async fn get_by_id(&self, id: EventId, user_id: UserId) -> Result<Option<Event>>;
25 +
26 + /// Creates a new event.
27 + async fn create(&self, user_id: UserId, event: NewEvent) -> Result<Event>;
28 +
29 + /// Restores an event verbatim from a backup, preserving its original ID and
30 + /// all metadata the normal create path never sets — `block_type`,
31 + /// `external_source`/`external_id`, `recurrence_parent_id`, `is_read_only`,
32 + /// `snoozed_until`, and reminder offsets. Idempotent (`INSERT OR IGNORE`).
33 + async fn restore(&self, user_id: UserId, event: &Event) -> Result<()>;
34 +
35 + /// Updates an existing event.
36 + async fn update(&self, id: EventId, user_id: UserId, event: crate::models::UpdateEvent) -> Result<Option<Event>>;
37 +
38 + /// Deletes an event.
39 + async fn delete(&self, id: EventId, user_id: UserId) -> Result<bool>;
40 +
41 + /// Records the external source/id for an event (e.g. after an iCal import),
42 + /// used to dedup on re-import.
43 + async fn set_external_ref(
44 + &self,
45 + id: EventId,
46 + user_id: UserId,
47 + source: &str,
48 + external_id: &str,
49 + ) -> Result<()>;
50 +
51 + /// Deletes multiple events by ID, returning the number deleted.
52 + async fn delete_many(&self, ids: &[EventId], user_id: UserId) -> Result<u64>;
53 +
54 + /// Gets events starting within the next N days, ordered by `start_time ASC`.
55 + async fn get_upcoming(&self, user_id: UserId, days: i64) -> Result<Vec<Event>>;
56 +
57 + /// Finds the event linked to a specific task (for time-blocking).
58 + async fn get_by_linked_task(&self, user_id: UserId, task_id: TaskId) -> Result<Option<Event>>;
59 +
60 + /// Deletes the event linked to a task.
61 + async fn delete_by_linked_task(&self, user_id: UserId, task_id: TaskId) -> Result<bool>;
62 +
63 + /// Lists events occurring on a specific date, ordered by `start_time ASC`.
64 + async fn list_for_date(&self, user_id: UserId, date: NaiveDate) -> Result<Vec<Event>>;
65 +
66 + /// Lists events within a date range (for weekly review), ordered by `start_time ASC`.
67 + async fn list_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Event>>;
68 +
69 + /// Lists all recurring events (recurrence != 'None' or recurrence_rule is set).
70 + async fn list_recurring(&self, user_id: UserId) -> Result<Vec<Event>>;
71 +
72 + /// Finds an event by external source and ID (for dedup during import).
73 + async fn find_by_external_id(&self, source: &str, ext_id: &str, user_id: UserId) -> Result<Option<Event>>;
74 +
75 + /// Snoozes an event until `until`. Returns the updated event, or `None` if not found.
76 + async fn snooze(&self, id: EventId, user_id: UserId, until: DateTime<Utc>) -> Result<Option<Event>>;
77 +
78 + /// Clears any snooze on an event. Returns the updated event, or `None` if not found.
79 + async fn unsnooze(&self, id: EventId, user_id: UserId) -> Result<Option<Event>>;
80 +
81 + /// Lists currently snoozed events (snoozed_until is in the future).
82 + async fn list_snoozed(&self, user_id: UserId) -> Result<Vec<Event>>;
83 + }
@@ -0,0 +1,46 @@
1 + use super::*;
2 +
3 + /// Repository for saved view / filter configurations.
4 + #[async_trait]
5 + pub trait SavedViewRepository: Send + Sync {
6 + /// Lists all saved views for a user.
7 + async fn list_all(&self, user_id: UserId) -> Result<Vec<SavedView>>;
8 +
9 + /// Lists pinned views for sidebar display.
10 + async fn list_pinned(&self, user_id: UserId) -> Result<Vec<SavedView>>;
11 +
12 + /// Gets a saved view by ID.
13 + async fn get_by_id(&self, id: SavedViewId, user_id: UserId) -> Result<Option<SavedView>>;
14 +
15 + /// Creates a new saved view.
16 + async fn create(&self, user_id: UserId, view: NewSavedView) -> Result<SavedView>;
17 +
18 + /// Updates an existing saved view.
19 + async fn update(&self, id: SavedViewId, user_id: UserId, view: NewSavedView) -> Result<Option<SavedView>>;
20 +
21 + /// Deletes a saved view.
22 + async fn delete(&self, id: SavedViewId, user_id: UserId) -> Result<bool>;
23 +
24 + /// Toggles the pinned status of a view.
25 + async fn toggle_pinned(&self, id: SavedViewId, user_id: UserId) -> Result<Option<SavedView>>;
26 +
27 + /// Updates the position of a view in the sidebar.
28 + async fn update_position(&self, id: SavedViewId, user_id: UserId, position: i32) -> Result<Option<SavedView>>;
29 + }
30 +
31 + /// Repository for backup settings management.
32 + #[async_trait]
33 + pub trait BackupSettingsRepository: Send + Sync {
34 + /// Gets the backup settings for a user.
35 + async fn get(&self, user_id: UserId) -> Result<Option<crate::models::BackupSettings>>;
36 +
37 + /// Creates or updates backup settings.
38 + async fn upsert(
39 + &self,
40 + user_id: UserId,
41 + settings: crate::models::NewBackupSettings,
42 + ) -> Result<crate::models::BackupSettings>;
43 +
44 + /// Updates the last backup timestamp.
45 + async fn update_last_backup_at(&self, user_id: UserId, timestamp: DateTime<Utc>) -> Result<()>;
46 + }
@@ -0,0 +1,51 @@
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 + mod contact;
32 + mod email;
33 + mod event;
34 + mod misc;
35 + mod project;
36 + mod review;
37 + mod search;
38 + mod stats;
39 + mod task;
40 + mod user;
41 +
42 + pub use contact::*;
43 + pub use email::*;
44 + pub use event::*;
45 + pub use misc::*;
46 + pub use project::*;
47 + pub use review::*;
48 + pub use search::*;
49 + pub use stats::*;
50 + pub use task::*;
51 + pub use user::*;
@@ -0,0 +1,68 @@
1 + use super::*;
2 +
3 + /// Repository for project CRUD operations.
4 + ///
5 + /// All operations are scoped to a specific user for multi-tenancy support.
6 + #[async_trait]
7 + pub trait ProjectRepository: Send + Sync {
8 + /// Lists all projects for a user.
9 + async fn list_all(&self, user_id: UserId) -> Result<Vec<Project>>;
10 +
11 + /// Retrieves a project by ID, returning `None` if not found.
12 + async fn get_by_id(&self, id: ProjectId, user_id: UserId) -> Result<Option<Project>>;
13 +
14 + /// Creates a new project.
15 + async fn create(&self, user_id: UserId, project: NewProject) -> Result<Project>;
16 +
17 + /// Restores a project verbatim from a backup, preserving its original ID
18 + /// and `created_at`. Idempotent (`INSERT OR IGNORE`): re-restoring the same
19 + /// backup is a no-op rather than a duplicate.
20 + async fn restore(&self, user_id: UserId, project: &Project) -> Result<()>;
21 +
22 + /// Updates an existing project, returning `None` if not found.
23 + async fn update(
24 + &self,
25 + id: ProjectId,
26 + user_id: UserId,
27 + project: crate::models::UpdateProject,
28 + ) -> Result<Option<Project>>;
29 +
30 + /// Deletes a project, returning `true` if deleted.
31 + async fn delete(&self, id: ProjectId, user_id: UserId) -> Result<bool>;
32 +
33 + /// Finds a project by exact name match.
34 + async fn find_by_name(&self, user_id: UserId, name: &str) -> Result<Option<Project>>;
35 + }
36 +
37 + /// Repository for milestone management within projects.
38 + #[async_trait]
39 + pub trait MilestoneRepository: Send + Sync {
40 + /// Lists all milestones for a project, ordered by position.
41 + async fn list_by_project(&self, project_id: ProjectId, user_id: UserId) -> Result<Vec<crate::models::Milestone>>;
42 +
43 + /// Lists every milestone for a user across all projects (for full backup export).
44 + async fn list_all(&self, user_id: UserId) -> Result<Vec<crate::models::Milestone>>;
45 +
46 + /// Gets a milestone by ID.
47 + async fn get_by_id(&self, id: MilestoneId, user_id: UserId) -> Result<Option<crate::models::Milestone>>;
48 +
49 + /// Creates a new milestone.
50 + async fn create(&self, user_id: UserId, milestone: crate::models::NewMilestone) -> Result<crate::models::Milestone>;
51 +
52 + /// Updates an existing milestone.
53 + async fn update(
54 + &self,
55 + id: MilestoneId,
56 + user_id: UserId,
57 + name: &str,
58 + description: &str,
59 + target_date: Option<NaiveDate>,
60 + status: &crate::models::MilestoneStatus,
61 + ) -> Result<Option<crate::models::Milestone>>;
62 +
63 + /// Deletes a milestone.
64 + async fn delete(&self, id: MilestoneId, user_id: UserId) -> Result<bool>;
65 +
66 + /// Reorders milestones within a project.
67 + async fn reorder(&self, project_id: ProjectId, user_id: UserId, milestone_ids: &[MilestoneId]) -> Result<()>;
68 + }
@@ -0,0 +1,61 @@
1 + use super::*;
2 +
3 + /// Repository for weekly review tracking.
4 + #[async_trait]
5 + pub trait WeeklyReviewRepository: Send + Sync {
6 + /// Gets the weekly review for a specific week.
7 + async fn get_for_week(&self, user_id: UserId, week_start: NaiveDate) -> Result<Option<crate::models::WeeklyReview>>;
8 +
9 + /// Creates or updates a weekly review.
10 + async fn upsert(&self, user_id: UserId, week_start: NaiveDate, notes: &str) -> Result<crate::models::WeeklyReview>;
11 +
12 + /// Lists every weekly review for a user across all weeks (for full backup export).
13 + async fn list_all(&self, user_id: UserId) -> Result<Vec<crate::models::WeeklyReview>>;
14 +
15 + /// Checks if the current week's review is completed.
16 + async fn is_current_week_completed(&self, user_id: UserId) -> Result<bool>;
17 +
18 + /// Sets vacation days for a specific week.
19 + async fn set_vacation_days(&self, user_id: UserId, week_start: NaiveDate, days: &[u8]) -> Result<()>;
20 + }
21 +
22 + /// Repository for daily review notes.
23 + #[async_trait]
24 + pub trait DailyNoteRepository: Send + Sync {
25 + /// Gets the daily note for a specific date.
26 + async fn get_by_date(&self, user_id: UserId, date: NaiveDate) -> Result<Option<crate::models::DailyNote>>;
27 +
28 + /// Creates or updates a daily note (upsert by user_id + date).
29 + async fn upsert(&self, user_id: UserId, date: NaiveDate, went_well: &str, could_improve: &str, is_reviewed: bool) -> Result<crate::models::DailyNote>;
30 +
31 + /// Lists every daily note for a user (for full backup export).
32 + async fn list_all(&self, user_id: UserId) -> Result<Vec<crate::models::DailyNote>>;
33 + }
34 +
35 + /// Repository for monthly review goals and reflections.
36 + #[async_trait]
37 + pub trait MonthlyReviewRepository: Send + Sync {
38 + /// Gets all goals for a specific month.
39 + async fn list_goals(&self, user_id: UserId, month: &str) -> Result<Vec<crate::models::MonthlyGoal>>;
40 +
41 + /// Lists every goal for a user across all months (for full backup export).
42 + async fn list_all_goals(&self, user_id: UserId) -> Result<Vec<crate::models::MonthlyGoal>>;
43 +
44 + /// Lists every reflection for a user across all months (for full backup export).
45 + async fn list_all_reflections(&self, user_id: UserId) -> Result<Vec<crate::models::MonthlyReflection>>;
46 +
47 + /// Creates or updates a goal for a month at a given position (1-3).
48 + async fn upsert_goal(&self, user_id: UserId, month: &str, text: &str, position: i32) -> Result<crate::models::MonthlyGoal>;
49 +
50 + /// Updates the status of a goal.
51 + async fn update_goal_status(&self, id: crate::MonthlyGoalId, user_id: UserId, status: &crate::models::MonthlyGoalStatus) -> Result<Option<crate::models::MonthlyGoal>>;
52 +
53 + /// Deletes a goal.
54 + async fn delete_goal(&self, id: crate::MonthlyGoalId, user_id: UserId) -> Result<bool>;
55 +
56 + /// Gets the reflection for a specific month.
57 + async fn get_reflection(&self, user_id: UserId, month: &str) -> Result<Option<crate::models::MonthlyReflection>>;
58 +
59 + /// Creates or updates the reflection for a month.
60 + async fn upsert_reflection(&self, user_id: UserId, month: &str, highlight: &str, change: &str) -> Result<crate::models::MonthlyReflection>;
61 + }
@@ -0,0 +1,161 @@
1 + use super::*;
2 +
3 + /// Type of item in search results.
4 + #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5 + #[serde(rename_all = "lowercase")]
6 + pub enum SearchResultType {
7 + /// A task item.
8 + Task,
9 + /// An email item.
10 + Email,
11 + /// A project item.
12 + Project,
13 + /// A calendar event.
14 + Event,
15 + /// A contact.
16 + Contact,
17 + }
18 +
19 + /// A single search result item with ranking information.
20 + #[derive(Debug, Clone, serde::Serialize)]
21 + pub struct SearchResultItem {
22 + /// Item ID.
23 + pub id: Uuid,
24 + /// Type of the result.
25 + pub result_type: SearchResultType,
26 + /// Display title.
27 + pub title: String,
28 + /// Text snippet showing match context.
29 + pub snippet: Option<String>,
30 + /// Associated project ID if any.
31 + pub project_id: Option<ProjectId>,
32 + /// Associated project name if any.
33 + pub project_name: Option<String>,
34 + /// Relevance rank (higher is better).
35 + pub rank: f64,
36 + }
37 +
38 + /// Search query with filtering and pagination options.
39 + #[derive(Debug, Clone, Default)]
40 + pub struct SearchQuery {
41 + /// The search terms (FTS text after filters extracted).
42 + pub query: String,
43 + /// Filter to specific result types.
44 + pub types: Option<Vec<SearchResultType>>,
45 + /// Filter to a specific project by ID.
46 + pub project_id: Option<ProjectId>,
47 + /// Filter to a specific project by name (partial match).
48 + pub project_name: Option<String>,
49 + /// Filter to items on or after this date.
50 + pub date_from: Option<DateTime<Utc>>,
51 + /// Filter to items on or before this date.
52 + pub date_to: Option<DateTime<Utc>>,
53 + /// Maximum results to return.
54 + pub limit: Option<i64>,
55 + /// Offset for pagination.
56 + pub offset: Option<i64>,
57 + /// `is:` filters for time/state conditions.
58 + pub is_filters: Vec<crate::search_parser::IsFilter>,
59 + /// Priority filter (`priority:high` etc).
60 + pub priority: Option<crate::models::Priority>,
61 + /// Tags to include (`tag:name`).
62 + pub tags_include: Vec<String>,
63 + /// Tags to exclude (`-tag:name`).
64 + pub tags_exclude: Vec<String>,
65 + }
66 +
67 + impl SearchQuery {
68 + /// Creates a new search query with default options.
69 + pub fn new(query: impl Into<String>) -> Self {
70 + Self {
71 + query: query.into(),
72 + types: None,
73 + project_id: None,
74 + project_name: None,
75 + date_from: None,
76 + date_to: None,
77 + limit: Some(50),
78 + offset: None,
79 + is_filters: Vec::new(),
80 + priority: None,
81 + tags_include: Vec::new(),
82 + tags_exclude: Vec::new(),
83 + }
84 + }
85 +
86 + /// Filters results to specific types.
87 + pub fn with_types(mut self, types: Vec<SearchResultType>) -> Self {
88 + self.types = Some(types);
89 + self
90 + }
91 +
92 + /// Filters results to a specific project by ID.
93 + pub fn with_project(mut self, project_id: ProjectId) -> Self {
94 + self.project_id = Some(project_id);
95 + self
96 + }
97 +
98 + /// Filters results to a specific project by name.
99 + pub fn with_project_name(mut self, name: impl Into<String>) -> Self {
100 + self.project_name = Some(name.into());
101 + self
102 + }
103 +
104 + /// Sets the maximum number of results. Negative values are clamped to 0:
105 + /// SQLite treats a negative LIMIT as unbounded, so an unclamped negative
106 + /// would silently disable the cap instead of enforcing it.
107 + pub fn with_limit(mut self, limit: i64) -> Self {
108 + self.limit = Some(limit.max(0));
109 + self
110 + }
111 +
112 + /// Sets the pagination offset. Negative values are clamped to 0.
113 + pub fn with_offset(mut self, offset: i64) -> Self {
114 + self.offset = Some(offset.max(0));
115 + self
116 + }
117 +
118 + /// Filters results to items on or after this date.
119 + pub fn with_date_from(mut self, date: DateTime<Utc>) -> Self {
120 + self.date_from = Some(date);
121 + self
122 + }
123 +
124 + /// Filters results to items on or before this date.
125 + pub fn with_date_to(mut self, date: DateTime<Utc>) -> Self {
126 + self.date_to = Some(date);
127 + self
128 + }
129 +
130 + /// Adds `is:` filters.
131 + pub fn with_is_filters(mut self, filters: Vec<crate::search_parser::IsFilter>) -> Self {
132 + self.is_filters = filters;
133 + self
134 + }
135 +
136 + /// Sets priority filter.
137 + pub fn with_priority(mut self, priority: crate::models::Priority) -> Self {
138 + self.priority = Some(priority);
139 + self
140 + }
141 +
142 + /// Sets tags to include.
143 + pub fn with_tags_include(mut self, tags: Vec<String>) -> Self {
144 + self.tags_include = tags;
145 + self
146 + }
147 +
148 + /// Sets tags to exclude.
149 + pub fn with_tags_exclude(mut self, tags: Vec<String>) -> Self {
150 + self.tags_exclude = tags;
151 + self
152 + }
153 + }
154 +
155 + /// Repository for full-text search across all content types.
156 + #[async_trait]
157 + pub trait SearchRepository: Send + Sync {
158 + /// Searches across all indexed content using FTS.
159 + /// Returns (results, total_count) where total_count is the pre-pagination count.
160 + async fn search(&self, user_id: UserId, query: SearchQuery) -> Result<(Vec<SearchResultItem>, usize)>;
161 + }
@@ -0,0 +1,54 @@
1 + use super::*;
2 +
3 + /// Aggregated statistics for the dashboard.
4 + #[derive(Debug, Clone, serde::Serialize)]
5 + pub struct DashboardStats {
6 + /// Number of tasks due today.
7 + pub tasks_due_today: i64,
8 + /// Number of tasks due within the next 7 days.
9 + pub tasks_due_this_week: i64,
10 + /// Number of overdue tasks.
11 + pub overdue_count: i64,
12 + /// Number of unread emails.
13 + pub unread_emails: i64,
14 + /// Number of events in the next 7 days.
15 + pub upcoming_events: i64,
16 + /// Number of active projects.
17 + pub active_projects: i64,
18 + /// Top tasks by urgency score.
19 + pub high_urgency_tasks: Vec<HighUrgencyTask>,
20 + }
21 +
22 + /// A task with high urgency for dashboard display.
23 + #[derive(Debug, Clone, serde::Serialize)]
24 + pub struct HighUrgencyTask {
25 + /// Task ID as string.
26 + pub id: String,
27 + /// Task description.
28 + pub description: String,
29 + /// Calculated urgency score.
30 + pub urgency: f64,
31 + /// Task status.
32 + pub status: String,
33 + /// Due date if set, formatted as ISO string.
34 + pub due: Option<String>,
35 + }
36 +
37 + /// Repository for dashboard statistics.
38 + #[async_trait]
39 + pub trait StatsRepository: Send + Sync {
40 + /// Computes aggregated dashboard statistics for a user.
41 + ///
42 + /// Calendar-day windows (`today_start`, `tomorrow_start`, `week_end`) are
43 + /// passed in as UTC instants derived from the user's *local* day, so "due
44 + /// today/this week" respect the user's timezone rather than UTC midnights.
45 + /// `now` is the reference instant for overdue/upcoming comparisons.
46 + async fn get_dashboard_stats(
47 + &self,
48 + user_id: UserId,
49 + now: DateTime<Utc>,
50 + today_start: DateTime<Utc>,
51 + tomorrow_start: DateTime<Utc>,
52 + week_end: DateTime<Utc>,
53 + ) -> Result<DashboardStats>;
54 + }
@@ -0,0 +1,213 @@
1 + use super::*;
2 +
3 + /// Core task entity persistence: CRUD, listing/filtering, bulk edits,
4 + /// lifecycle transitions, and date-range read queries used by reviews.
5 + #[async_trait]
6 + pub trait TaskCrud: Send + Sync {
7 + /// Lists all non-deleted tasks for a user.
8 + async fn list_all(&self, user_id: UserId) -> Result<Vec<Task>>;
9 +
10 + /// Lists tasks belonging to a specific project.
11 + async fn list_by_project(&self, user_id: UserId, project_id: ProjectId) -> Result<Vec<Task>>;
12 +
13 + /// Lists tasks linked to a specific contact.
14 + async fn list_by_contact(&self, user_id: UserId, contact_id: ContactId) -> Result<Vec<Task>>;
15 +
16 + /// Lists tasks matching the given filter criteria with pagination.
17 + /// Returns (tasks, total_count) for pagination UI.
18 + async fn list_filtered(&self, user_id: UserId, query: TaskFilterQuery) -> Result<(Vec<Task>, i64)>;
19 +
20 + /// Retrieves a task by ID.
21 + async fn get_by_id(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>>;
22 +
23 + /// Lightweight fetch of fields needed for update logic (avoids annotation/subtask/session sub-queries).
24 + /// Returns (created_at, status, completed_at, scheduled_start, scheduled_duration).
25 + async fn get_update_context(&self, id: TaskId, user_id: UserId) -> Result<Option<crate::models::TaskUpdateContext>>;
26 +
27 + /// Creates a new task.
28 + async fn create(&self, user_id: UserId, task: NewTask) -> Result<Task>;
29 +
30 + /// Restores a task verbatim from a backup, preserving its original ID,
31 + /// `status`, `completed_at`, and `created_at`. Idempotent (`INSERT OR
32 + /// IGNORE`). Annotations and subtasks are restored separately by the caller.
33 + async fn restore(&self, user_id: UserId, task: &Task) -> Result<()>;
34 +
35 + /// Updates an existing task.
36 + async fn update(&self, id: TaskId, user_id: UserId, task: UpdateTask) -> Result<Option<Task>>;
37 +
38 + /// Sets the project for many tasks in one transaction. Returns rows affected.
39 + /// Avoids the per-task fetch+update round-trips the bulk UI did before
40 + /// (ultra-fuzz Run #27 Perf S4). Project does not affect urgency.
41 + async fn bulk_set_project(&self, user_id: UserId, ids: &[TaskId], project_id: Option<ProjectId>) -> Result<usize>;
42 +
43 + /// Sets the priority for many tasks in one transaction, recomputing each task's
44 + /// urgency (priority is an urgency input). Returns rows affected.
45 + async fn bulk_set_priority(&self, user_id: UserId, ids: &[TaskId], priority: crate::models::Priority) -> Result<usize>;
46 +
47 + /// Soft-deletes a task.
48 + async fn delete(&self, id: TaskId, user_id: UserId) -> Result<bool>;
49 +
50 + /// Marks a task as started.
51 + async fn start(&self, id: TaskId, user_id: UserId) -> Result<bool>;
52 +
53 + /// Marks a task as completed and returns it.
54 + /// The caller is responsible for handling recurrence and milestone auto-completion.
55 + async fn complete(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>>;
56 +
57 + /// Atomically completes a task and creates the next recurring instance.
58 + /// Returns (completed_task, next_task). If the task has no recurrence,
59 + /// next_task is None. The entire operation is wrapped in a transaction
60 + /// so a crash cannot break the recurrence chain.
61 + async fn complete_recurring(&self, id: TaskId, user_id: UserId, next: Option<NewTask>) -> Result<(Option<Task>, Option<Task>)>;
62 +
63 + /// Counts non-deleted, non-completed tasks in a milestone.
64 + async fn count_incomplete_by_milestone(&self, milestone_id: MilestoneId, user_id: UserId) -> Result<i64>;
65 +
66 + /// Lists tasks completed within a date range (for weekly review).
67 + async fn list_completed_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>>;
68 +
69 + /// Lists tasks that became overdue within a date range (for weekly review).
70 + async fn list_became_overdue_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>>;
71 +
72 + /// Lists tasks due within a date range (for weekly review).
73 + async fn list_due_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>>;
74 +
75 + /// Lists tasks created within a date range (for monthly review).
76 + async fn list_created_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>>;
77 +
78 + /// Lists all tasks in a recurrence chain (root + all descendants).
79 + async fn list_recurrence_chain(&self, root_id: TaskId, user_id: UserId) -> Result<Vec<Task>>;
80 + }
81 +
82 + /// Annotation (note) and subtask persistence for a task.
83 + #[async_trait]
84 + pub trait TaskAnnotations: Send + Sync {
85 + /// Gets all annotations for a task.
86 + async fn get_annotations_for_task(&self, task_id: TaskId) -> Result<Vec<Annotation>>;
87 +
88 + /// Adds an annotation (note) to a task.
89 + async fn add_annotation(&self, task_id: TaskId, user_id: UserId, note: &str) -> Result<Option<Annotation>>;
90 +
91 + /// Deletes an annotation.
92 + async fn delete_annotation(&self, annotation_id: AnnotationId, user_id: UserId) -> Result<bool>;
93 +
94 + /// Gets all subtasks for a task.
95 + async fn get_subtasks_for_task(&self, task_id: TaskId) -> Result<Vec<Subtask>>;
96 +
97 + /// Adds a subtask to a task.
98 + async fn add_subtask(&self, task_id: TaskId, user_id: UserId, text: &str) -> Result<Option<Subtask>>;
99 +
100 + /// Toggles a subtask's completion status.
101 + async fn toggle_subtask(&self, subtask_id: SubtaskId, user_id: UserId) -> Result<Option<Subtask>>;
102 +
103 + /// Updates subtask text.
104 + async fn update_subtask(&self, subtask_id: SubtaskId, user_id: UserId, text: &str) -> Result<Option<Subtask>>;
105 +
106 + /// Deletes a subtask.
107 + async fn delete_subtask(&self, subtask_id: SubtaskId, user_id: UserId) -> Result<bool>;
108 +
109 + /// Adds a linked task as a subtask.
110 + ///
111 + /// This creates a subtask that links to another task, enabling multi-phase
112 + /// features where Phase 2 is a full task linked as a subtask of Phase 1.
113 + /// The linked subtask's completion status syncs with the linked task's status.
114 + async fn add_subtask_link(&self, task_id: TaskId, user_id: UserId, linked_task_id: TaskId) -> Result<Option<Subtask>>;
115 + }
116 +
117 + /// Scheduling and triage state: snooze, waiting, focus, time-block schedule,
118 + /// and the date-scoped listings that drive the planner views.
119 + #[async_trait]
120 + pub trait TaskScheduling: Send + Sync {
121 + /// Snoozes a task until the specified time.
122 + async fn snooze(&self, id: TaskId, user_id: UserId, until: DateTime<Utc>) -> Result<Option<Task>>;
123 +
124 + /// Removes snooze from a task.
125 + async fn unsnooze(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>>;
126 +
127 + /// Lists all currently snoozed tasks.
128 + async fn list_snoozed(&self, user_id: UserId) -> Result<Vec<Task>>;
129 +
130 + /// Marks a task as waiting for external response.
131 + async fn mark_waiting(&self, id: TaskId, user_id: UserId, expected_response: Option<DateTime<Utc>>) -> Result<Option<Task>>;
132 +
133 + /// Clears the waiting status from a task.
134 + async fn clear_waiting(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>>;
135 +
136 + /// Lists all tasks marked as waiting.
137 + async fn list_waiting(&self, user_id: UserId) -> Result<Vec<Task>>;
138 +
139 + /// Lists tasks scheduled for a specific date.
140 + async fn list_scheduled_for_date(&self, user_id: UserId, date: NaiveDate) -> Result<Vec<Task>>;
141 +
142 + /// Lists unscheduled tasks due on a specific date.
143 + async fn list_unscheduled_due_on_date(&self, user_id: UserId, date: NaiveDate) -> Result<Vec<Task>>;
144 +
145 + /// Lists unscheduled tasks whose `due` falls within a UTC instant window
146 + /// (inclusive upper bound). Lets callers honor a user-local day boundary
147 + /// instead of an implicit UTC one.
148 + async fn list_unscheduled_due_between(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Task>>;
149 +
150 + /// Updates a task's time-block schedule.
151 + async fn update_schedule(&self, id: TaskId, user_id: UserId, start: Option<DateTime<Utc>>, duration: Option<i32>) -> Result<Option<Task>>;
152 +
153 + /// Sets or clears the focus status on a task.
154 + async fn set_focus(&self, id: TaskId, user_id: UserId, is_focus: bool) -> Result<Option<Task>>;
155 +
156 + /// Lists all tasks marked as focus.
157 + async fn list_focused(&self, user_id: UserId) -> Result<Vec<Task>>;
158 +
159 + /// Clears focus from all tasks.
160 + async fn clear_all_focus(&self, user_id: UserId) -> Result<u64>;
161 +
162 + /// Lists high-priority pending tasks for focus selection.
163 + async fn list_available_for_focus(&self, user_id: UserId, limit: i64) -> Result<Vec<Task>>;
164 + }
165 +
166 + /// Time-tracking persistence: live timers, manual entries, and summaries.
167 + #[async_trait]
168 + pub trait TaskTimeTracking: Send + Sync {
169 + /// Starts a timer on a task. Fails if any session is already active for the user.
170 + async fn start_timer(&self, task_id: TaskId, user_id: UserId) -> Result<TimeSession>;
171 +
172 + /// Stops the active timer on a task, updating duration and actual_minutes cache.
173 + async fn stop_timer(&self, task_id: TaskId, user_id: UserId) -> Result<Option<TimeSession>>;
174 +
175 + /// Discards the active timer without updating actual_minutes.
176 + async fn discard_timer(&self, task_id: TaskId, user_id: UserId) -> Result<bool>;
177 +
178 + /// Gets the currently active timer for a user (at most one).
179 + async fn get_active_timer(&self, user_id: UserId) -> Result<Option<(TimeSession, String)>>;
180 +
181 + /// Lists all time sessions for a task.
182 + async fn list_time_sessions(&self, task_id: TaskId, user_id: UserId) -> Result<Vec<TimeSession>>;
183 +
184 + /// Lists every time session for a user across all tasks (for full backup export).
185 + async fn list_all_time_sessions(&self, user_id: UserId) -> Result<Vec<TimeSession>>;
186 +
187 + /// Logs a manual time entry (retroactive, no live timer).
188 + ///
189 + /// `minutes` is a validated [`PositiveMinutes`], so the repository cannot be
190 + /// handed a zero/negative duration.
191 + async fn log_manual_time(&self, task_id: TaskId, user_id: UserId, minutes: PositiveMinutes, date: DateTime<Utc>) -> Result<TimeSession>;
192 +
193 + /// Gets aggregated time tracking summary grouped by project and date.
194 + async fn get_time_summary(&self, user_id: UserId, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<TimeTrackingSummary>>;
195 + }
196 +
197 + /// Aggregate task-persistence contract.
198 + ///
199 + /// This is the trait consumers hold as `Arc<dyn TaskRepository>`. It carries no
200 + /// methods of its own; the surface lives in the four capability sub-traits
201 + /// ([`TaskCrud`], [`TaskAnnotations`], [`TaskScheduling`], [`TaskTimeTracking`]).
202 + /// The blanket impl below means any type implementing all four automatically
203 + /// satisfies `TaskRepository`, so a single trait object still exposes the whole
204 + /// API while the definition stays split by concern.
205 + pub trait TaskRepository:
206 + TaskCrud + TaskAnnotations + TaskScheduling + TaskTimeTracking
207 + {
208 + }
209 +
210 + impl<T> TaskRepository for T where
211 + T: TaskCrud + TaskAnnotations + TaskScheduling + TaskTimeTracking
212 + {
213 + }
@@ -0,0 +1,36 @@
1 + use super::*;
2 +
3 + /// Repository for user account operations.
4 + #[async_trait]
5 + pub trait UserRepository: Send + Sync {
6 + /// Creates a new user account with hashed password.
7 + async fn create(&self, email: &str, password: &str, display_name: &str) -> Result<User>;
8 +
9 + /// Finds a user by email address.
10 + async fn find_by_email(&self, email: &str) -> Result<Option<User>>;
11 +
12 + /// Authenticates a user, returning the user if credentials are valid.
13 + async fn authenticate(&self, email: &str, password: &str) -> Result<Option<User>>;
14 +
15 + /// Updates the user's last login timestamp.
16 + async fn update_last_login(&self, user_id: UserId) -> Result<()>;
17 + }
18 +
19 + /// Repository for sync account CRUD operations.
20 + #[async_trait]
21 + pub trait SyncAccountRepository: Send + Sync {
22 + /// Lists all sync accounts for a user.
23 + async fn list_all(&self, user_id: UserId) -> Result<Vec<crate::models::SyncAccount>>;
24 +
25 + /// Retrieves a sync account by ID.
26 + async fn get_by_id(&self, id: SyncAccountId, user_id: UserId) -> Result<Option<crate::models::SyncAccount>>;
27 +
28 + /// Creates a new sync account.
29 + async fn create(&self, user_id: UserId, provider: &str, account_name: &str, email: Option<&str>) -> Result<crate::models::SyncAccount>;
30 +
31 + /// Updates a sync account.
32 + async fn update(&self, id: SyncAccountId, user_id: UserId, account_name: &str, sync_calendars: bool, sync_contacts: bool, enabled: bool) -> Result<Option<crate::models::SyncAccount>>;
33 +
34 + /// Deletes a sync account.
35 + async fn delete(&self, id: SyncAccountId, user_id: UserId) -> Result<bool>;
36 + }
@@ -15,7 +15,8 @@ use sqlx::SqlitePool;
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 @@ pub(crate) async fn query_tasks(pool: &SqlitePool, sql: &str, binds: &[String])
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 @@ impl TaskRepository for SqliteTaskRepository {
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 @@ impl TaskRepository for SqliteTaskRepository {
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 @@ impl TaskRepository for SqliteTaskRepository {
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 @@ impl TaskRepository for SqliteTaskRepository {
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 }
@@ -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 @@ async fn schema_tables_are_all_classified() {
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 @@ use goingson_core::backup_restore::RestoreInput;
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 @@ mod common;
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 @@ mod common;
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,