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