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.
20#[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
51/// Get all change events for a feature.
52pub 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        // Create a feature first
98        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 some events
112        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        // Retrieve events
137        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 with minimal fields
173        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}