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(()); }
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 let _ = format!("{:?}", cloned);
176 }
177}