Skip to main content

aven_core/
change_log.rs

1use anyhow::Result;
2use serde_json::Value;
3use sqlx::SqliteConnection;
4
5use crate::db::insert_change;
6use crate::workspaces::Workspace;
7
8pub mod op_type {
9    macro_rules! operations {
10        ($($name:ident = $value:literal;)*) => {
11            $(pub const $name: &str = $value;)*
12
13            /// Every registered operation, in released vocabulary order.
14            pub const ALL: &[&str] = &[$($name),*];
15        };
16    }
17
18    operations! {
19        CREATE_TASK = "create_task";
20        MOVE_TASKS = "move_tasks";
21        SET_FIELD = "set_field";
22        RESOLVE_FIELD = "resolve_field";
23        LABEL_ADD = "label_add";
24        LABEL_REMOVE = "label_remove";
25        NOTE_ADD = "note_add";
26        NOTE_EDIT = "note_edit";
27        NOTE_DELETE = "note_delete";
28        DEPENDENCY_ADD = "dependency_add";
29        DEPENDENCY_REMOVE = "dependency_remove";
30        RELATED_ADD = "related_add";
31        RELATED_REMOVE = "related_remove";
32        EPIC_LINK_ADD = "epic_link_add";
33        EPIC_LINK_REMOVE = "epic_link_remove";
34        ATTACHMENT_ADD = "attachment_add";
35        ATTACHMENT_DELETE = "attachment_delete";
36        CREATE_PROJECT = "create_project";
37        SET_PROJECT_METADATA = "set_project_metadata";
38        PROJECT_DELETE = "project_delete";
39        CREATE_LABEL = "create_label";
40        SET_LABEL_NAME = "set_label_name";
41        LABEL_DELETE = "label_delete";
42        LABEL_RESTORE = "label_restore";
43        CREATE_WORKSPACE = "create_workspace";
44        SET_WORKSPACE_FIELD = "set_workspace_field";
45        CREATE_RECURRENCE_SERIES = "create_recurrence_series";
46        UPDATE_RECURRENCE_TEMPLATE = "update_recurrence_template";
47        PROJECT_RECURRENCE_OCCURRENCE = "project_recurrence_occurrence";
48        RESOLVE_RECURRENCE_OCCURRENCE = "resolve_recurrence_occurrence";
49        SET_RECURRENCE_STATE = "set_recurrence_state";
50        CREATE_METADATA_FIELD = "create_metadata_field";
51        SET_METADATA_FIELD = "set_metadata_field";
52        SET_TASK_METADATA = "set_task_metadata";
53        REMOVE_TASK_METADATA = "remove_task_metadata";
54        SET_RECURRENCE_METADATA = "set_recurrence_metadata";
55        REMOVE_RECURRENCE_METADATA = "remove_recurrence_metadata";
56        OPEN_RECURRENCE_PAUSE = "open_recurrence_pause";
57        CLOSE_RECURRENCE_PAUSE = "close_recurrence_pause";
58        STOP_RECURRENCE_SERIES = "stop_recurrence_series";
59        PUBLISH_DEVICE_LABEL = "publish_device_label";
60        DELETE_WORKSPACE = "delete_workspace";
61    }
62}
63
64pub enum ChangeEntity {
65    Task,
66    TaskMove,
67    Project,
68    Label,
69    MetadataField,
70    #[allow(dead_code)]
71    Workspace,
72    RecurrenceSeries,
73}
74
75impl ChangeEntity {
76    pub const fn as_str(&self) -> &'static str {
77        match self {
78            Self::Task => "task",
79            Self::TaskMove => "task_move",
80            Self::Project => "project",
81            Self::Label => "label",
82            Self::MetadataField => "metadata_field",
83            Self::Workspace => "workspace",
84            Self::RecurrenceSeries => "recurrence_series",
85        }
86    }
87}
88
89/// Builder for change payload JSON produced by operations.
90///
91/// Use `.workspace(&workspace)` to seed workspace_id and workspace_key,
92/// then chain `.set(key, value)` for each payload field.
93///
94/// Example:
95/// ```ignore
96/// ChangePayload::workspace(&workspace)
97///     .set("title", draft.title)
98///     .set("project_id", project.id)
99///     .into_value()
100/// ```
101pub struct ChangePayload {
102    map: serde_json::Map<String, serde_json::Value>,
103}
104
105impl ChangePayload {
106    pub fn workspace(workspace: &Workspace) -> Self {
107        let mut map = serde_json::Map::new();
108        map.insert(
109            "workspace_id".to_string(),
110            Value::String(workspace.id.to_string()),
111        );
112        map.insert(
113            "workspace_key".to_string(),
114            Value::String(workspace.key.clone()),
115        );
116        Self { map }
117    }
118
119    pub fn set(mut self, key: &str, value: impl serde::Serialize) -> Self {
120        self.map.insert(
121            key.to_string(),
122            serde_json::to_value(value).expect("change payload value serialization"),
123        );
124        self
125    }
126
127    pub fn into_value(self) -> Value {
128        Value::Object(self.map)
129    }
130}
131
132/// Insert a change-log row using a `ChangeEntity` and pre-built `ChangePayload`.
133///
134/// This wrapper always passes `None` for `base_version`. Task field-level
135/// operations that need version tracking for conflict detection use
136/// `insert_change` directly through `mutation.rs`.
137pub(crate) async fn append_change(
138    conn: &mut SqliteConnection,
139    entity: ChangeEntity,
140    entity_id: &str,
141    field: Option<&str>,
142    op_type: &'static str,
143    payload: ChangePayload,
144) -> Result<String> {
145    insert_change(
146        conn,
147        entity.as_str(),
148        entity_id,
149        field,
150        op_type,
151        payload.into_value(),
152        None,
153    )
154    .await
155}