Skip to main content

max / goingson

5.5 KB · 150 lines History Blame Raw
1 //! Reads shared across the task modules: batch hydration of task rows and the
2 //! single-task fetches every write path re-reads through.
3
4 use goingson_core::{ParseableEnum, Result, Task, TaskId, TaskStatus, UserId};
5 use rusqlite::{Connection, params, params_from_iter};
6
7 use crate::utils::{parse_datetime, parse_uuid, query_all, query_opt};
8
9 use super::row::{TASK_SELECT_COLUMNS, TaskRowWithProject};
10 use crate::repository::{annotation_repo, status_token_repo, subtask_repo, time_session_repo};
11
12 /// Converts task rows to Task objects with annotations, subtasks, and active sessions.
13 ///
14 /// This helper encapsulates the common pattern of:
15 /// 1. Extracting task IDs from rows
16 /// 2. Batch-fetching annotations, subtasks, and active sessions for all tasks
17 /// 3. Converting each row to a Task with its related data
18 ///
19 /// Returns an empty vec if rows is empty (no database calls made).
20 pub(crate) fn rows_to_tasks(conn: &Connection, rows: Vec<TaskRowWithProject>) -> Result<Vec<Task>> {
21 if rows.is_empty() {
22 return Ok(vec![]);
23 }
24
25 let task_ids: Vec<String> = rows.iter().map(|r| r.id.clone()).collect();
26 let annotations_map = annotation_repo::get_annotations_for_tasks(conn, &task_ids)?;
27 let subtasks_map = subtask_repo::get_subtasks_for_tasks(conn, &task_ids)?;
28 let tokens_map = status_token_repo::get_tokens_for_tasks(conn, &task_ids)?;
29 let active_sessions = time_session_repo::get_active_sessions_for_tasks(conn, &task_ids)?;
30
31 let mut tasks = Vec::with_capacity(rows.len());
32 for row in rows {
33 let id: TaskId = parse_uuid(&row.id)?.into();
34 let annotations = annotations_map.get(&id).cloned().unwrap_or_default();
35 let subtasks = subtasks_map.get(&id).cloned().unwrap_or_default();
36 let status_tokens = tokens_map.get(&id).cloned().unwrap_or_default();
37 let mut task = row.into_task(annotations, subtasks, status_tokens)?;
38 task.active_session = active_sessions.get(&id).cloned();
39 tasks.push(task);
40 }
41
42 Ok(tasks)
43 }
44
45 /// Fetch only the fields needed for update logic; avoids annotation/subtask/session sub-queries.
46 pub(crate) fn get_task_update_context(
47 conn: &Connection,
48 id: TaskId,
49 user_id: UserId,
50 ) -> Result<Option<goingson_core::models::TaskUpdateContext>> {
51 struct Row {
52 created_at: String,
53 status: String,
54 completed_at: Option<String>,
55 scheduled_start: Option<String>,
56 scheduled_duration: Option<i32>,
57 }
58
59 impl Row {
60 fn from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Self> {
61 Ok(Self {
62 created_at: row.get("created_at")?,
63 status: row.get("status")?,
64 completed_at: row.get("completed_at")?,
65 scheduled_start: row.get("scheduled_start")?,
66 scheduled_duration: row.get("scheduled_duration")?,
67 })
68 }
69 }
70
71 let row = query_opt(
72 conn,
73 "SELECT created_at, status, completed_at, scheduled_start, scheduled_duration FROM tasks WHERE id = ? AND user_id = ?",
74 params![id.to_string(), user_id.to_string()],
75 Row::from_row,
76 )?;
77
78 match row {
79 Some(r) => Ok(Some(goingson_core::models::TaskUpdateContext {
80 created_at: parse_datetime(&r.created_at)?,
81 status: TaskStatus::from_str_or_default(&r.status),
82 completed_at: r
83 .completed_at
84 .as_ref()
85 .map(|s| parse_datetime(s))
86 .transpose()?,
87 scheduled_start: r
88 .scheduled_start
89 .as_ref()
90 .map(|s| parse_datetime(s))
91 .transpose()?,
92 scheduled_duration: r.scheduled_duration,
93 })),
94 None => Ok(None),
95 }
96 }
97
98 /// Fetch a single task by ID and user, with annotations and subtasks.
99 pub(crate) fn get_task_by_id(
100 conn: &Connection,
101 id: TaskId,
102 user_id: UserId,
103 ) -> Result<Option<Task>> {
104 let sql = format!(
105 r"
106 SELECT {TASK_SELECT_COLUMNS}
107 FROM tasks t
108 LEFT JOIN projects p ON t.project_id = p.id AND p.user_id = ?
109 LEFT JOIN contacts ct ON ct.id = t.contact_id
110 WHERE t.id = ? AND t.user_id = ?
111 "
112 );
113 let row = query_opt(
114 conn,
115 &sql,
116 params![user_id.to_string(), id.to_string(), user_id.to_string()],
117 TaskRowWithProject::from_row,
118 )?;
119
120 match row {
121 Some(row) => {
122 let annotations = annotation_repo::get_annotations_for_task(conn, id)?;
123 let subtasks = subtask_repo::get_subtasks_for_task(conn, id)?;
124 let status_tokens = status_token_repo::get_tokens_for_task(conn, id)?;
125 let active_sessions = time_session_repo::get_active_sessions_for_tasks(
126 conn,
127 std::slice::from_ref(&row.id),
128 )?;
129 let mut task = row.into_task(annotations, subtasks, status_tokens)?;
130 task.active_session = active_sessions.get(&task.id).cloned();
131 Ok(Some(task))
132 }
133 None => Ok(None),
134 }
135 }
136
137 /// Run a task query with string bind parameters and convert rows to tasks.
138 ///
139 /// Handles the common pattern of: format SQL with TASK_SELECT_COLUMNS,
140 /// bind string params in order, fetch rows, convert via rows_to_tasks.
141 pub(crate) fn query_tasks(conn: &Connection, sql: &str, binds: &[String]) -> Result<Vec<Task>> {
142 let rows = query_all(
143 conn,
144 sql,
145 params_from_iter(binds.iter()),
146 TaskRowWithProject::from_row,
147 )?;
148 rows_to_tasks(conn, rows)
149 }
150