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