1use super::{NoteDeleteOutcome, NoteEditOutcome, NoteOutcome};
2use crate::change_log::{ChangeEntity, ChangePayload, append_change, op_type};
3use crate::db::{Database, begin_immediate};
4use crate::ids::{TaskId, new_id, now};
5use crate::operations::{RecurrenceStructuralMutation, RecurrenceTaskMutation};
6use crate::undo::{UndoCommand, UndoPayload, record_tui_undo};
7use crate::workspaces::Workspace;
8use anyhow::Result;
9use sqlx::SqliteConnection;
10use tracing::info;
11
12impl Database {
13 pub async fn add_note(
14 &self,
15 workspace: &Workspace,
16 task_id: &TaskId,
17 body: String,
18 ) -> Result<NoteOutcome> {
19 let mut conn = self.acquire_writer().await?;
20 add_note_operation(&mut conn, workspace, task_id, body, false).await
21 }
22
23 pub async fn add_note_with_tui_undo(
24 &self,
25 workspace: &Workspace,
26 task_id: &TaskId,
27 body: String,
28 ) -> Result<NoteOutcome> {
29 let mut conn = self.acquire_writer().await?;
30 add_note_operation(&mut conn, workspace, task_id, body, true).await
31 }
32
33 pub async fn edit_note(
34 &self,
35 workspace: &Workspace,
36 task_id: &TaskId,
37 note_id: &str,
38 body: String,
39 ) -> Result<NoteEditOutcome> {
40 let mut conn = self.acquire_writer().await?;
41 edit_note_operation(&mut conn, workspace, task_id, note_id, body, false).await
42 }
43
44 pub async fn edit_note_with_tui_undo(
45 &self,
46 workspace: &Workspace,
47 task_id: &TaskId,
48 note_id: &str,
49 body: String,
50 ) -> Result<NoteEditOutcome> {
51 let mut conn = self.acquire_writer().await?;
52 edit_note_operation(&mut conn, workspace, task_id, note_id, body, true).await
53 }
54
55 pub async fn delete_note(
56 &self,
57 workspace: &Workspace,
58 task_id: &TaskId,
59 note_id: &str,
60 ) -> Result<NoteDeleteOutcome> {
61 let mut conn = self.acquire_writer().await?;
62 delete_note_operation(&mut conn, workspace, task_id, note_id, false).await
63 }
64
65 pub async fn delete_note_with_tui_undo(
66 &self,
67 workspace: &Workspace,
68 task_id: &TaskId,
69 note_id: &str,
70 ) -> Result<NoteDeleteOutcome> {
71 let mut conn = self.acquire_writer().await?;
72 delete_note_operation(&mut conn, workspace, task_id, note_id, true).await
73 }
74}
75
76pub(in crate::operations) async fn add_note_operation(
77 conn: &mut SqliteConnection,
78 workspace: &Workspace,
79 task_id: &crate::ids::TaskId,
80 body: String,
81 tui_undo: bool,
82) -> Result<NoteOutcome> {
83 let ts = now();
84 let note_id = new_id();
85 let mut tx = begin_immediate(conn).await?;
86 crate::operations::route_recurrence_task_mutation(
87 &mut tx,
88 workspace,
89 task_id,
90 RecurrenceTaskMutation::Structural(RecurrenceStructuralMutation::Notes),
91 &ts,
92 )
93 .await?;
94 let change_id = append_change(
95 &mut tx,
96 ChangeEntity::Task,
97 task_id,
98 Some("notes"),
99 op_type::NOTE_ADD,
100 ChangePayload::workspace(workspace)
101 .set("note_id", ¬e_id)
102 .set("body", &body)
103 .set("created_at", &ts),
104 )
105 .await?;
106 sqlx::query(
107 "INSERT INTO notes(workspace_id, id, task_id, body, created_at, change_id) VALUES (?, ?, ?, ?, ?, ?)",
108 )
109 .bind(&workspace.id)
110 .bind(¬e_id)
111 .bind(task_id)
112 .bind(&body)
113 .bind(&ts)
114 .bind(&change_id)
115 .execute(&mut *tx)
116 .await?;
117 sqlx::query("UPDATE tasks SET queue_activity_at = ? WHERE workspace_id = ? AND id = ?")
118 .bind(&ts)
119 .bind(&workspace.id)
120 .bind(task_id)
121 .execute(&mut *tx)
122 .await?;
123 if tui_undo {
124 record_tui_undo(
125 &mut tx,
126 &workspace.id,
127 &format!("note {note_id}"),
128 UndoPayload {
129 commands: vec![UndoCommand::DeleteCreatedNote {
130 task_id: task_id.clone(),
131 note_id: note_id.clone(),
132 note_add_change_id: change_id.clone(),
133 restoration_change_ids: Vec::new(),
134 }],
135 },
136 )
137 .await?;
138 }
139 tx.commit().await?;
140 info!(task_id = %task_id, note_id = %note_id, "note added");
141 Ok(NoteOutcome {
142 task_id: task_id.clone(),
143 note_id,
144 change_id,
145 })
146}
147
148async fn edit_note_operation(
149 conn: &mut SqliteConnection,
150 workspace: &Workspace,
151 task_id: &crate::ids::TaskId,
152 note_id: &str,
153 body: String,
154 tui_undo: bool,
155) -> Result<NoteEditOutcome> {
156 let edited_at = now();
157 let mut tx = begin_immediate(conn).await?;
158 crate::operations::route_recurrence_task_mutation(
159 &mut tx,
160 workspace,
161 task_id,
162 RecurrenceTaskMutation::Structural(RecurrenceStructuralMutation::Notes),
163 &edited_at,
164 )
165 .await?;
166 let before = sqlx::query_scalar::<_, String>(
167 "SELECT body FROM notes WHERE workspace_id = ? AND task_id = ? AND id = ?",
168 )
169 .bind(&workspace.id)
170 .bind(task_id)
171 .bind(note_id)
172 .fetch_optional(&mut *tx)
173 .await?;
174 let found = before.is_some();
175 let changed = before.as_ref().is_some_and(|before| before != &body);
176 if changed {
177 sqlx::query("UPDATE notes SET body = ? WHERE workspace_id = ? AND task_id = ? AND id = ?")
178 .bind(&body)
179 .bind(&workspace.id)
180 .bind(task_id)
181 .bind(note_id)
182 .execute(&mut *tx)
183 .await?;
184 sqlx::query("UPDATE tasks SET queue_activity_at = ? WHERE workspace_id = ? AND id = ?")
185 .bind(&edited_at)
186 .bind(&workspace.id)
187 .bind(task_id)
188 .execute(&mut *tx)
189 .await?;
190 append_change(
191 &mut tx,
192 ChangeEntity::Task,
193 task_id,
194 Some("notes"),
195 op_type::NOTE_EDIT,
196 ChangePayload::workspace(workspace)
197 .set("note_id", note_id)
198 .set("body", &body)
199 .set("edited_at", edited_at),
200 )
201 .await?;
202 if tui_undo {
203 record_tui_undo(
204 &mut tx,
205 &workspace.id,
206 &format!("edit note {note_id}"),
207 UndoPayload {
208 commands: vec![UndoCommand::SetNoteBody {
209 task_id: task_id.clone(),
210 note_id: note_id.to_string(),
211 before: before.expect("changed note has before body"),
212 after: body,
213 }],
214 },
215 )
216 .await?;
217 }
218 }
219 tx.commit().await?;
220 if changed {
221 info!(task_id = %task_id, note_id = %note_id, "note edited");
222 }
223 Ok(NoteEditOutcome {
224 task_id: task_id.clone(),
225 note_id: note_id.to_string(),
226 found,
227 changed,
228 })
229}
230
231async fn delete_note_operation(
232 conn: &mut SqliteConnection,
233 workspace: &Workspace,
234 task_id: &crate::ids::TaskId,
235 note_id: &str,
236 tui_undo: bool,
237) -> Result<NoteDeleteOutcome> {
238 let deleted_at = now();
239 let mut tx = begin_immediate(conn).await?;
240 crate::operations::route_recurrence_task_mutation(
241 &mut tx,
242 workspace,
243 task_id,
244 RecurrenceTaskMutation::Structural(RecurrenceStructuralMutation::Notes),
245 &deleted_at,
246 )
247 .await?;
248 let before = sqlx::query_as::<_, (String, String, String)>(
249 "SELECT body, created_at, change_id FROM notes WHERE workspace_id = ? AND task_id = ? AND id = ?",
250 )
251 .bind(&workspace.id)
252 .bind(task_id)
253 .bind(note_id)
254 .fetch_optional(&mut *tx)
255 .await?;
256 let deleted =
257 sqlx::query("DELETE FROM notes WHERE workspace_id = ? AND task_id = ? AND id = ?")
258 .bind(&workspace.id)
259 .bind(task_id)
260 .bind(note_id)
261 .execute(&mut *tx)
262 .await?
263 .rows_affected();
264 if deleted > 0 {
265 sqlx::query("UPDATE tasks SET queue_activity_at = ? WHERE workspace_id = ? AND id = ?")
266 .bind(&deleted_at)
267 .bind(&workspace.id)
268 .bind(task_id)
269 .execute(&mut *tx)
270 .await?;
271 append_change(
272 &mut tx,
273 ChangeEntity::Task,
274 task_id,
275 Some("notes"),
276 op_type::NOTE_DELETE,
277 ChangePayload::workspace(workspace)
278 .set("note_id", note_id)
279 .set(
280 "body",
281 &before.as_ref().expect("deleted note has before state").0,
282 )
283 .set("deleted_at", deleted_at),
284 )
285 .await?;
286 if tui_undo {
287 let (body, created_at, note_add_change_id) =
288 before.expect("deleted note has before state");
289 record_tui_undo(
290 &mut tx,
291 &workspace.id,
292 &format!("delete note {note_id}"),
293 UndoPayload {
294 commands: vec![UndoCommand::RestoreDeletedNote {
295 task_id: task_id.clone(),
296 note_id: note_id.to_string(),
297 body,
298 created_at,
299 note_add_change_id,
300 }],
301 },
302 )
303 .await?;
304 }
305 }
306 tx.commit().await?;
307 if deleted > 0 {
308 info!(task_id = %task_id, note_id = %note_id, "note deleted");
309 }
310 Ok(NoteDeleteOutcome {
311 task_id: task_id.clone(),
312 note_id: note_id.to_string(),
313 changed: deleted > 0,
314 })
315}