Skip to main content

rullst_orm/
audit.rs

1use crate::Orm;
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Debug, Serialize, Deserialize)]
5pub struct AuditLog {
6    pub id: i32,
7    pub model_type: String,
8    pub model_id: i32,
9    pub event: String,
10    pub old_values: Option<String>,
11    pub new_values: Option<String>,
12    pub created_at: Option<String>,
13}
14
15pub async fn log_audit(
16    model_type: &str,
17    model_id: i32,
18    event: &str,
19    old_values: Option<String>,
20    new_values: Option<String>,
21) -> Result<(), crate::Error> {
22    let pool = Orm::pool();
23    let driver = Orm::driver();
24
25    if driver == "postgres" {
26        sqlx::query(
27            "INSERT INTO rullst_audits (model_type, model_id, event, old_values, new_values) VALUES ($1, $2, $3, $4, $5)"
28        )
29        .bind(model_type)
30        .bind(model_id)
31        .bind(event)
32        .bind(old_values)
33        .bind(new_values)
34        .execute(pool)
35        .await?;
36    } else {
37        sqlx::query(
38            "INSERT INTO rullst_audits (model_type, model_id, event, old_values, new_values) VALUES (?, ?, ?, ?, ?)"
39        )
40        .bind(model_type)
41        .bind(model_id)
42        .bind(event)
43        .bind(old_values)
44        .bind(new_values)
45        .execute(pool)
46        .await?;
47    }
48
49    Ok(())
50}
51
52pub async fn log_audit_diff(
53    model_type: &str,
54    model_id: i32,
55    event: &str,
56    old_json: &str,
57    new_json: &str,
58) -> Result<(), crate::Error> {
59    let old_val: serde_json::Value =
60        serde_json::from_str(old_json).unwrap_or(serde_json::Value::Null);
61    let new_val: serde_json::Value =
62        serde_json::from_str(new_json).unwrap_or(serde_json::Value::Null);
63
64    let mut diff_old = serde_json::Map::new();
65    let mut diff_new = serde_json::Map::new();
66
67    if let (Some(old_obj), Some(new_obj)) = (old_val.as_object(), new_val.as_object()) {
68        for (k, v) in old_obj {
69            if let Some(new_v) = new_obj.get(k)
70                && v != new_v
71            {
72                diff_old.insert(k.clone(), v.clone());
73                diff_new.insert(k.clone(), new_v.clone());
74            }
75        }
76    }
77
78    if diff_old.is_empty() && diff_new.is_empty() {
79        return Ok(()); // Nothing changed
80    }
81
82    let final_old = serde_json::to_string(&diff_old).ok();
83    let final_new = serde_json::to_string(&diff_new).ok();
84
85    log_audit(model_type, model_id, event, final_old, final_new).await
86}
87
88pub async fn create_audit_table() -> Result<(), crate::Error> {
89    let pool = Orm::pool();
90    let driver = Orm::driver();
91
92    let query = if driver == "postgres" {
93        r#"
94        CREATE TABLE IF NOT EXISTS rullst_audits (
95            id SERIAL PRIMARY KEY,
96            model_type VARCHAR(255) NOT NULL,
97            model_id INT NOT NULL,
98            event VARCHAR(50) NOT NULL,
99            old_values TEXT,
100            new_values TEXT,
101            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
102        )
103        "#
104    } else if driver == "mysql" {
105        r#"
106        CREATE TABLE IF NOT EXISTS rullst_audits (
107            id INT AUTO_INCREMENT PRIMARY KEY,
108            model_type VARCHAR(255) NOT NULL,
109            model_id INT NOT NULL,
110            event VARCHAR(50) NOT NULL,
111            old_values TEXT,
112            new_values TEXT,
113            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
114        )
115        "#
116    } else {
117        r#"
118        CREATE TABLE IF NOT EXISTS rullst_audits (
119            id INTEGER PRIMARY KEY AUTOINCREMENT,
120            model_type TEXT NOT NULL,
121            model_id INTEGER NOT NULL,
122            event TEXT NOT NULL,
123            old_values TEXT,
124            new_values TEXT,
125            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
126        )
127        "#
128    };
129
130    sqlx::query(query).execute(pool).await?;
131    Ok(())
132}
133
134#[cfg(test)]
135mod tests {
136    use super::AuditLog;
137
138    #[test]
139    fn test_audit_log_serialization_round_trip() {
140        let log = AuditLog {
141            id: 1,
142            model_type: "User".to_string(),
143            model_id: 42,
144            event: "created".to_string(),
145            old_values: None,
146            new_values: Some(r#"{"name":"Alice"}"#.to_string()),
147            created_at: Some("2024-01-01T00:00:00Z".to_string()),
148        };
149
150        let json_str = serde_json::to_string(&log).expect("serialize");
151        assert!(json_str.contains("\"model_type\":\"User\""));
152        assert!(json_str.contains("\"event\":\"created\""));
153
154        let deserialized: AuditLog = serde_json::from_str(&json_str).expect("deserialize");
155        assert_eq!(deserialized.id, 1);
156        assert_eq!(deserialized.model_id, 42);
157        assert_eq!(deserialized.event, "created");
158        assert!(deserialized.old_values.is_none());
159    }
160
161    #[test]
162    fn test_audit_log_clone_debug() {
163        let log = AuditLog {
164            id: 5,
165            model_type: "Post".to_string(),
166            model_id: 99,
167            event: "updated".to_string(),
168            old_values: Some(r#"{"title":"Old"}"#.to_string()),
169            new_values: Some(r#"{"title":"New"}"#.to_string()),
170            created_at: None,
171        };
172        let cloned = log.clone();
173        assert_eq!(cloned.model_type, "Post");
174        // Debug must not panic
175        let _ = format!("{:?}", cloned);
176    }
177}