Skip to main content

aven_core/operations/tasks/
notes.rs

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", &note_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(&note_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}