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 UndoPayload {
128 commands: vec![UndoCommand::DeleteCreatedNote {
129 task_id: task_id.clone(),
130 note_id: note_id.clone(),
131 note_add_change_id: change_id.clone(),
132 restoration_change_ids: Vec::new(),
133 }],
134 },
135 )
136 .await?;
137 }
138 tx.commit().await?;
139 info!(task_id = %task_id, note_id = %note_id, "note added");
140 Ok(NoteOutcome {
141 task_id: task_id.clone(),
142 note_id,
143 change_id,
144 })
145}
146
147async fn edit_note_operation(
148 conn: &mut SqliteConnection,
149 workspace: &Workspace,
150 task_id: &crate::ids::TaskId,
151 note_id: &str,
152 body: String,
153 tui_undo: bool,
154) -> Result<NoteEditOutcome> {
155 let edited_at = now();
156 let mut tx = begin_immediate(conn).await?;
157 crate::operations::route_recurrence_task_mutation(
158 &mut tx,
159 workspace,
160 task_id,
161 RecurrenceTaskMutation::Structural(RecurrenceStructuralMutation::Notes),
162 &edited_at,
163 )
164 .await?;
165 let before = sqlx::query_scalar::<_, String>(
166 "SELECT body FROM notes WHERE workspace_id = ? AND task_id = ? AND id = ?",
167 )
168 .bind(&workspace.id)
169 .bind(task_id)
170 .bind(note_id)
171 .fetch_optional(&mut *tx)
172 .await?;
173 let found = before.is_some();
174 let changed = before.as_ref().is_some_and(|before| before != &body);
175 if changed {
176 sqlx::query("UPDATE notes SET body = ? WHERE workspace_id = ? AND task_id = ? AND id = ?")
177 .bind(&body)
178 .bind(&workspace.id)
179 .bind(task_id)
180 .bind(note_id)
181 .execute(&mut *tx)
182 .await?;
183 sqlx::query("UPDATE tasks SET queue_activity_at = ? WHERE workspace_id = ? AND id = ?")
184 .bind(&edited_at)
185 .bind(&workspace.id)
186 .bind(task_id)
187 .execute(&mut *tx)
188 .await?;
189 append_change(
190 &mut tx,
191 ChangeEntity::Task,
192 task_id,
193 Some("notes"),
194 op_type::NOTE_EDIT,
195 ChangePayload::workspace(workspace)
196 .set("note_id", note_id)
197 .set("body", &body)
198 .set("edited_at", edited_at),
199 )
200 .await?;
201 if tui_undo {
202 record_tui_undo(
203 &mut tx,
204 &workspace.id,
205 UndoPayload {
206 commands: vec![UndoCommand::SetNoteBody {
207 task_id: task_id.clone(),
208 note_id: note_id.to_string(),
209 before: before.expect("changed note has before body"),
210 after: body,
211 }],
212 },
213 )
214 .await?;
215 }
216 }
217 tx.commit().await?;
218 if changed {
219 info!(task_id = %task_id, note_id = %note_id, "note edited");
220 }
221 Ok(NoteEditOutcome {
222 task_id: task_id.clone(),
223 note_id: note_id.to_string(),
224 found,
225 changed,
226 })
227}
228
229async fn delete_note_operation(
230 conn: &mut SqliteConnection,
231 workspace: &Workspace,
232 task_id: &crate::ids::TaskId,
233 note_id: &str,
234 tui_undo: bool,
235) -> Result<NoteDeleteOutcome> {
236 let deleted_at = now();
237 let mut tx = begin_immediate(conn).await?;
238 crate::operations::route_recurrence_task_mutation(
239 &mut tx,
240 workspace,
241 task_id,
242 RecurrenceTaskMutation::Structural(RecurrenceStructuralMutation::Notes),
243 &deleted_at,
244 )
245 .await?;
246 let before = sqlx::query_as::<_, (String, String, String)>(
247 "SELECT body, created_at, change_id FROM notes WHERE workspace_id = ? AND task_id = ? AND id = ?",
248 )
249 .bind(&workspace.id)
250 .bind(task_id)
251 .bind(note_id)
252 .fetch_optional(&mut *tx)
253 .await?;
254 let deleted =
255 sqlx::query("DELETE FROM notes WHERE workspace_id = ? AND task_id = ? AND id = ?")
256 .bind(&workspace.id)
257 .bind(task_id)
258 .bind(note_id)
259 .execute(&mut *tx)
260 .await?
261 .rows_affected();
262 if deleted > 0 {
263 sqlx::query("UPDATE tasks SET queue_activity_at = ? WHERE workspace_id = ? AND id = ?")
264 .bind(&deleted_at)
265 .bind(&workspace.id)
266 .bind(task_id)
267 .execute(&mut *tx)
268 .await?;
269 append_change(
270 &mut tx,
271 ChangeEntity::Task,
272 task_id,
273 Some("notes"),
274 op_type::NOTE_DELETE,
275 ChangePayload::workspace(workspace)
276 .set("note_id", note_id)
277 .set(
278 "body",
279 &before.as_ref().expect("deleted note has before state").0,
280 )
281 .set("deleted_at", deleted_at),
282 )
283 .await?;
284 if tui_undo {
285 let (body, created_at, note_add_change_id) =
286 before.expect("deleted note has before state");
287 record_tui_undo(
288 &mut tx,
289 &workspace.id,
290 UndoPayload {
291 commands: vec![UndoCommand::RestoreDeletedNote {
292 task_id: task_id.clone(),
293 note_id: note_id.to_string(),
294 body,
295 created_at,
296 note_add_change_id,
297 }],
298 },
299 )
300 .await?;
301 }
302 }
303 tx.commit().await?;
304 if deleted > 0 {
305 info!(task_id = %task_id, note_id = %note_id, "note deleted");
306 }
307 Ok(NoteDeleteOutcome {
308 task_id: task_id.clone(),
309 note_id: note_id.to_string(),
310 changed: deleted > 0,
311 })
312}