max / goingson
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, |