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