Skip to main content

max / goingson

4.1 KB · 159 lines History Blame Raw
1 //! Annotation repository methods for SqliteTaskRepository.
2 //!
3 //! Handles annotation CRUD: listing, adding, and deleting annotations on tasks.
4
5 use chrono::Utc;
6 use sqlx::SqlitePool;
7 use std::collections::HashMap;
8
9 use goingson_core::{AnnotationId, Annotation, CoreError, Result, TaskId, UserId};
10
11 use crate::utils::{format_datetime_now, parse_datetime, parse_uuid};
12
13 /// Row struct for annotations from SQLite.
14 #[derive(Debug, Clone, sqlx::FromRow)]
15 pub(crate) struct AnnotationRow {
16 pub id: String,
17 pub task_id: String,
18 pub timestamp: String,
19 pub note: String,
20 }
21
22 impl TryFrom<AnnotationRow> for Annotation {
23 type Error = CoreError;
24
25 fn try_from(row: AnnotationRow) -> std::result::Result<Self, Self::Error> {
26 Ok(Annotation {
27 id: parse_uuid(&row.id)?.into(),
28 task_id: parse_uuid(&row.task_id)?.into(),
29 timestamp: parse_datetime(&row.timestamp)?,
30 note: row.note,
31 })
32 }
33 }
34
35 /// Batch-fetch annotations for multiple tasks by their IDs.
36 pub(crate) async fn get_annotations_for_tasks(
37 pool: &SqlitePool,
38 task_ids: &[String],
39 ) -> Result<HashMap<TaskId, Vec<Annotation>>> {
40 if task_ids.is_empty() {
41 return Ok(HashMap::new());
42 }
43
44 // SQLite doesn't have ANY(), use IN with placeholder generation
45 let placeholders: Vec<String> = task_ids.iter().map(|_| "?".to_string()).collect();
46 let query = format!(
47 r#"
48 SELECT id, task_id, timestamp, note
49 FROM annotations
50 WHERE task_id IN ({})
51 ORDER BY timestamp DESC
52 "#,
53 placeholders.join(",")
54 );
55
56 let mut q = sqlx::query_as::<_, AnnotationRow>(&query);
57 for id in task_ids {
58 q = q.bind(id);
59 }
60
61 let rows = q.fetch_all(pool).await.map_err(CoreError::database)?;
62
63 let mut map: HashMap<TaskId, Vec<Annotation>> = HashMap::new();
64 for row in rows {
65 let annotation = Annotation::try_from(row)?;
66 map.entry(annotation.task_id).or_default().push(annotation);
67 }
68
69 Ok(map)
70 }
71
72 /// Get all annotations for a single task.
73 pub(crate) async fn get_annotations_for_task(
74 pool: &SqlitePool,
75 task_id: TaskId,
76 ) -> Result<Vec<Annotation>> {
77 let rows = sqlx::query_as::<_, AnnotationRow>(
78 r#"
79 SELECT id, task_id, timestamp, note
80 FROM annotations
81 WHERE task_id = ?
82 ORDER BY timestamp DESC
83 "#,
84 )
85 .bind(task_id.to_string())
86 .fetch_all(pool)
87 .await
88 .map_err(CoreError::database)?;
89
90 rows.into_iter().map(Annotation::try_from).collect()
91 }
92
93 /// Add an annotation to a task (verifies task ownership).
94 pub(crate) async fn add_annotation(
95 pool: &SqlitePool,
96 task_id: TaskId,
97 user_id: UserId,
98 note: &str,
99 ) -> Result<Option<Annotation>> {
100 let task_exists: (i64,) = sqlx::query_as(
101 "SELECT COUNT(*) FROM tasks WHERE id = ? AND user_id = ?"
102 )
103 .bind(task_id.to_string())
104 .bind(user_id.to_string())
105 .fetch_one(pool)
106 .await
107 .map_err(CoreError::database)?;
108
109 if task_exists.0 == 0 {
110 return Ok(None);
111 }
112
113 let id = AnnotationId::new();
114 let now = format_datetime_now();
115
116 sqlx::query(
117 r#"
118 INSERT INTO annotations (id, task_id, timestamp, note)
119 VALUES (?, ?, ?, ?)
120 "#,
121 )
122 .bind(id.to_string())
123 .bind(task_id.to_string())
124 .bind(&now)
125 .bind(note)
126 .execute(pool)
127 .await
128 .map_err(CoreError::database)?;
129
130 Ok(Some(Annotation {
131 id,
132 task_id,
133 timestamp: Utc::now(),
134 note: note.to_string(),
135 }))
136 }
137
138 /// Delete an annotation (verifies task ownership).
139 pub(crate) async fn delete_annotation(
140 pool: &SqlitePool,
141 annotation_id: AnnotationId,
142 user_id: UserId,
143 ) -> Result<bool> {
144 let result = sqlx::query(
145 r#"
146 DELETE FROM annotations
147 WHERE id = ?
148 AND task_id IN (SELECT id FROM tasks WHERE user_id = ?)
149 "#
150 )
151 .bind(annotation_id.to_string())
152 .bind(user_id.to_string())
153 .execute(pool)
154 .await
155 .map_err(CoreError::database)?;
156
157 Ok(result.rows_affected() > 0)
158 }
159