1use flow_core::Result;
2use rusqlite::Connection;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ChangeEvent {
8 pub id: i64,
9 pub feature_id: i64,
10 pub event_type: String,
11 pub field: Option<String>,
12 pub old_value: Option<String>,
13 pub new_value: Option<String>,
14 pub agent: Option<String>,
15 pub source: String,
16 pub created_at: String,
17}
18
19#[allow(clippy::too_many_arguments)]
21pub fn log_event(
22 conn: &Connection,
23 feature_id: i64,
24 event_type: &str,
25 field: Option<&str>,
26 old_value: Option<&str>,
27 new_value: Option<&str>,
28 agent: Option<&str>,
29 source: &str,
30) -> Result<()> {
31 conn.execute(
32 r"
33 INSERT INTO change_events (feature_id, event_type, field, old_value, new_value, agent, source)
34 VALUES (?, ?, ?, ?, ?, ?, ?)
35 ",
36 rusqlite::params![
37 feature_id,
38 event_type,
39 field,
40 old_value,
41 new_value,
42 agent,
43 source,
44 ],
45 )
46 .map_err(|e| flow_core::FlowError::Database(format!("log event failed: {e}")))?;
47
48 Ok(())
49}
50
51pub fn get_events(conn: &Connection, feature_id: i64) -> Result<Vec<ChangeEvent>> {
53 let mut stmt = conn
54 .prepare(
55 r"
56 SELECT id, feature_id, event_type, field, old_value, new_value, agent, source, created_at
57 FROM change_events
58 WHERE feature_id = ?
59 ORDER BY created_at ASC
60 ",
61 )
62 .map_err(|e| flow_core::FlowError::Database(format!("prepare failed: {e}")))?;
63
64 let events = stmt
65 .query_map([feature_id], |row| {
66 Ok(ChangeEvent {
67 id: row.get(0)?,
68 feature_id: row.get(1)?,
69 event_type: row.get(2)?,
70 field: row.get(3)?,
71 old_value: row.get(4)?,
72 new_value: row.get(5)?,
73 agent: row.get(6)?,
74 source: row.get(7)?,
75 created_at: row.get(8)?,
76 })
77 })
78 .map_err(|e| flow_core::FlowError::Database(format!("query failed: {e}")))?
79 .collect::<std::result::Result<Vec<_>, _>>()
80 .map_err(|e| flow_core::FlowError::Database(format!("row parse failed: {e}")))?;
81
82 Ok(events)
83}
84
85#[cfg(test)]
86#[allow(clippy::significant_drop_tightening)]
87mod tests {
88 use super::*;
89 use crate::feature::FeatureStore;
90 use crate::Database;
91
92 #[test]
93 fn test_log_and_get_events() {
94 let db = Database::open_in_memory().unwrap();
95 let conn = db.writer().lock().unwrap();
96
97 let feature = FeatureStore::create(
99 &conn,
100 &flow_core::CreateFeatureInput {
101 name: "Test Feature".to_string(),
102 description: String::new(),
103 priority: Some(1),
104 category: String::new(),
105 steps: vec![],
106 dependencies: vec![],
107 },
108 )
109 .unwrap();
110
111 log_event(
113 &conn,
114 feature.id,
115 "status_change",
116 Some("status"),
117 Some("pending"),
118 Some("in_progress"),
119 Some("agent-1"),
120 "api",
121 )
122 .unwrap();
123
124 log_event(
125 &conn,
126 feature.id,
127 "status_change",
128 Some("status"),
129 Some("in_progress"),
130 Some("completed"),
131 Some("agent-1"),
132 "api",
133 )
134 .unwrap();
135
136 let events = get_events(&conn, feature.id).unwrap();
138 assert_eq!(events.len(), 2);
139 assert_eq!(events[0].event_type, "status_change");
140 assert_eq!(events[0].old_value, Some("pending".to_string()));
141 assert_eq!(events[0].new_value, Some("in_progress".to_string()));
142 assert_eq!(events[1].new_value, Some("completed".to_string()));
143 }
144
145 #[test]
146 fn test_events_for_nonexistent_feature() {
147 let db = Database::open_in_memory().unwrap();
148 let conn = db.writer().lock().unwrap();
149
150 let events = get_events(&conn, 999).unwrap();
151 assert_eq!(events.len(), 0);
152 }
153
154 #[test]
155 fn test_event_with_optional_fields() {
156 let db = Database::open_in_memory().unwrap();
157 let conn = db.writer().lock().unwrap();
158
159 let feature = FeatureStore::create(
160 &conn,
161 &flow_core::CreateFeatureInput {
162 name: "Test".to_string(),
163 description: String::new(),
164 priority: Some(1),
165 category: String::new(),
166 steps: vec![],
167 dependencies: vec![],
168 },
169 )
170 .unwrap();
171
172 log_event(&conn, feature.id, "comment", None, None, None, None, "web").unwrap();
174
175 let events = get_events(&conn, feature.id).unwrap();
176 assert_eq!(events.len(), 1);
177 assert_eq!(events[0].event_type, "comment");
178 assert!(events[0].field.is_none());
179 assert!(events[0].agent.is_none());
180 }
181}