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