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 SET_FIELD: &str = "set_field";
11    pub const RESOLVE_FIELD: &str = "resolve_field";
12    pub const LABEL_ADD: &str = "label_add";
13    pub const LABEL_REMOVE: &str = "label_remove";
14    pub const NOTE_ADD: &str = "note_add";
15    pub const NOTE_DELETE: &str = "note_delete";
16    pub const DEPENDENCY_ADD: &str = "dependency_add";
17    pub const DEPENDENCY_REMOVE: &str = "dependency_remove";
18    pub const EPIC_LINK_ADD: &str = "epic_link_add";
19    pub const EPIC_LINK_REMOVE: &str = "epic_link_remove";
20    pub const ATTACHMENT_ADD: &str = "attachment_add";
21    pub const ATTACHMENT_DELETE: &str = "attachment_delete";
22    pub const CREATE_PROJECT: &str = "create_project";
23    pub const SET_PROJECT_METADATA: &str = "set_project_metadata";
24    pub const PROJECT_DELETE: &str = "project_delete";
25    pub const CREATE_LABEL: &str = "create_label";
26    pub const LABEL_DELETE: &str = "label_delete";
27    pub const CREATE_WORKSPACE: &str = "create_workspace";
28    pub const SET_WORKSPACE_FIELD: &str = "set_workspace_field";
29}
30
31pub enum ChangeEntity {
32    Task,
33    Project,
34    Label,
35    #[allow(dead_code)]
36    Workspace,
37}
38
39impl ChangeEntity {
40    pub const fn as_str(&self) -> &'static str {
41        match self {
42            Self::Task => "task",
43            Self::Project => "project",
44            Self::Label => "label",
45            Self::Workspace => "workspace",
46        }
47    }
48}
49
50/// Builder for change payload JSON produced by operations.
51///
52/// Use `.workspace(&workspace)` to seed workspace_id and workspace_key,
53/// then chain `.set(key, value)` for each payload field.
54///
55/// Example:
56/// ```ignore
57/// ChangePayload::workspace(&workspace)
58///     .set("title", draft.title)
59///     .set("project_id", project.id)
60///     .into_value()
61/// ```
62pub struct ChangePayload {
63    map: serde_json::Map<String, serde_json::Value>,
64}
65
66impl ChangePayload {
67    pub fn workspace(workspace: &Workspace) -> Self {
68        let mut map = serde_json::Map::new();
69        map.insert(
70            "workspace_id".to_string(),
71            Value::String(workspace.id.to_string()),
72        );
73        map.insert(
74            "workspace_key".to_string(),
75            Value::String(workspace.key.clone()),
76        );
77        Self { map }
78    }
79
80    pub fn set(mut self, key: &str, value: impl serde::Serialize) -> Self {
81        self.map.insert(
82            key.to_string(),
83            serde_json::to_value(value).expect("change payload value serialization"),
84        );
85        self
86    }
87
88    pub fn into_value(self) -> Value {
89        Value::Object(self.map)
90    }
91}
92
93/// Insert a change-log row using a `ChangeEntity` and pre-built `ChangePayload`.
94///
95/// This wrapper always passes `None` for `base_version`. Task field-level
96/// operations that need version tracking for conflict detection use
97/// `insert_change` directly through `mutation.rs`.
98pub(crate) async fn append_change(
99    conn: &mut SqliteConnection,
100    entity: ChangeEntity,
101    entity_id: &str,
102    field: Option<&str>,
103    op_type: &'static str,
104    payload: ChangePayload,
105) -> Result<String> {
106    insert_change(
107        conn,
108        entity.as_str(),
109        entity_id,
110        field,
111        op_type,
112        payload.into_value(),
113        None,
114    )
115    .await
116}