Skip to main content

flow_db/
event_log.rs

1use flow_core::Result;
2use rusqlite::Connection;
3use serde::{Deserialize, Serialize};
4
5/// A change event logged for a feature.
6#[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/// Log a change event for a feature.
20pub 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
50/// Get all change events for a feature.
51pub 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        // Create a feature first
96        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 some events
110        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        // Retrieve events
135        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 with minimal fields
171        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}