1use rusqlite::{Connection, Result};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum EventType {
10 SessionCreated,
12 SessionUpdated,
13 SessionPaused,
14 SessionCompleted,
15 SessionDeleted,
16 SessionPathAdded,
17 SessionPathRemoved,
18
19 ItemCreated,
21 ItemUpdated,
22 ItemDeleted,
23
24 IssueCreated,
26 IssueUpdated,
27 IssueClosed,
28 IssueClaimed,
29 IssueReleased,
30 IssueDeleted,
31
32 CheckpointCreated,
34 CheckpointRestored,
35 CheckpointDeleted,
36
37 PlanCreated,
39 PlanUpdated,
40 PlanCompleted,
41
42 MemorySaved,
44 MemoryDeleted,
45
46 ProjectCreated,
48 ProjectUpdated,
49 ProjectDeleted,
50}
51
52impl EventType {
53 #[must_use]
55 pub const fn as_str(&self) -> &'static str {
56 match self {
57 Self::SessionCreated => "session_created",
58 Self::SessionUpdated => "session_updated",
59 Self::SessionPaused => "session_paused",
60 Self::SessionCompleted => "session_completed",
61 Self::SessionDeleted => "session_deleted",
62 Self::SessionPathAdded => "session_path_added",
63 Self::SessionPathRemoved => "session_path_removed",
64 Self::ItemCreated => "item_created",
65 Self::ItemUpdated => "item_updated",
66 Self::ItemDeleted => "item_deleted",
67 Self::IssueCreated => "issue_created",
68 Self::IssueUpdated => "issue_updated",
69 Self::IssueClosed => "issue_closed",
70 Self::IssueClaimed => "issue_claimed",
71 Self::IssueReleased => "issue_released",
72 Self::IssueDeleted => "issue_deleted",
73 Self::CheckpointCreated => "checkpoint_created",
74 Self::CheckpointRestored => "checkpoint_restored",
75 Self::CheckpointDeleted => "checkpoint_deleted",
76 Self::PlanCreated => "plan_created",
77 Self::PlanUpdated => "plan_updated",
78 Self::PlanCompleted => "plan_completed",
79 Self::MemorySaved => "memory_saved",
80 Self::MemoryDeleted => "memory_deleted",
81 Self::ProjectCreated => "project_created",
82 Self::ProjectUpdated => "project_updated",
83 Self::ProjectDeleted => "project_deleted",
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct Event {
91 pub id: i64,
92 pub entity_type: String,
93 pub entity_id: String,
94 pub event_type: EventType,
95 pub actor: String,
96 pub old_value: Option<String>,
97 pub new_value: Option<String>,
98 pub comment: Option<String>,
99 pub created_at: i64,
100}
101
102impl Event {
103 #[must_use]
105 pub fn new(
106 entity_type: &str,
107 entity_id: &str,
108 event_type: EventType,
109 actor: &str,
110 ) -> Self {
111 Self {
112 id: 0,
113 entity_type: entity_type.to_string(),
114 entity_id: entity_id.to_string(),
115 event_type,
116 actor: actor.to_string(),
117 old_value: None,
118 new_value: None,
119 comment: None,
120 created_at: chrono::Utc::now().timestamp_millis(),
121 }
122 }
123
124 #[must_use]
126 pub fn with_values(mut self, old: Option<String>, new: Option<String>) -> Self {
127 self.old_value = old;
128 self.new_value = new;
129 self
130 }
131
132 #[must_use]
134 pub fn with_comment(mut self, comment: &str) -> Self {
135 self.comment = Some(comment.to_string());
136 self
137 }
138}
139
140pub fn insert_event(conn: &Connection, event: &Event) -> Result<i64> {
146 conn.execute(
147 "INSERT INTO events (entity_type, entity_id, event_type, actor, old_value, new_value, comment, created_at)
148 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
149 rusqlite::params![
150 event.entity_type,
151 event.entity_id,
152 event.event_type.as_str(),
153 event.actor,
154 event.old_value,
155 event.new_value,
156 event.comment,
157 event.created_at,
158 ],
159 )?;
160 Ok(conn.last_insert_rowid())
161}
162
163pub fn get_events(
169 conn: &Connection,
170 entity_type: &str,
171 entity_id: &str,
172 limit: Option<u32>,
173) -> Result<Vec<Event>> {
174 let limit = limit.unwrap_or(100);
175 let mut stmt = conn.prepare(
176 "SELECT id, entity_type, entity_id, event_type, actor, old_value, new_value, comment, created_at
177 FROM events
178 WHERE entity_type = ?1 AND entity_id = ?2
179 ORDER BY created_at DESC
180 LIMIT ?3",
181 )?;
182
183 let rows = stmt.query_map(rusqlite::params![entity_type, entity_id, limit], |row| {
184 Ok(Event {
185 id: row.get(0)?,
186 entity_type: row.get(1)?,
187 entity_id: row.get(2)?,
188 event_type: parse_event_type(row.get::<_, String>(3)?.as_str()),
189 actor: row.get(4)?,
190 old_value: row.get(5)?,
191 new_value: row.get(6)?,
192 comment: row.get(7)?,
193 created_at: row.get(8)?,
194 })
195 })?;
196
197 rows.collect()
198}
199
200fn parse_event_type(s: &str) -> EventType {
201 match s {
202 "session_created" => EventType::SessionCreated,
203 "session_updated" => EventType::SessionUpdated,
204 "session_paused" => EventType::SessionPaused,
205 "session_completed" => EventType::SessionCompleted,
206 "session_deleted" => EventType::SessionDeleted,
207 "session_path_added" => EventType::SessionPathAdded,
208 "session_path_removed" => EventType::SessionPathRemoved,
209 "item_created" => EventType::ItemCreated,
210 "item_updated" => EventType::ItemUpdated,
211 "item_deleted" => EventType::ItemDeleted,
212 "issue_created" => EventType::IssueCreated,
213 "issue_updated" => EventType::IssueUpdated,
214 "issue_closed" => EventType::IssueClosed,
215 "issue_claimed" => EventType::IssueClaimed,
216 "issue_released" => EventType::IssueReleased,
217 "issue_deleted" => EventType::IssueDeleted,
218 "checkpoint_created" => EventType::CheckpointCreated,
219 "checkpoint_restored" => EventType::CheckpointRestored,
220 "checkpoint_deleted" => EventType::CheckpointDeleted,
221 "plan_created" => EventType::PlanCreated,
222 "plan_updated" => EventType::PlanUpdated,
223 "plan_completed" => EventType::PlanCompleted,
224 "memory_saved" => EventType::MemorySaved,
225 "memory_deleted" => EventType::MemoryDeleted,
226 "project_created" => EventType::ProjectCreated,
227 "project_updated" => EventType::ProjectUpdated,
228 "project_deleted" => EventType::ProjectDeleted,
229 _ => EventType::SessionUpdated, }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::storage::schema::apply_schema;
237
238 #[test]
239 fn test_event_insert_and_get() {
240 let conn = Connection::open_in_memory().unwrap();
241 apply_schema(&conn).unwrap();
242
243 let event = Event::new("session", "sess_123", EventType::SessionCreated, "test-actor")
244 .with_comment("Test session created");
245
246 let id = insert_event(&conn, &event).unwrap();
247 assert!(id > 0);
248
249 let events = get_events(&conn, "session", "sess_123", Some(10)).unwrap();
250 assert_eq!(events.len(), 1);
251 assert_eq!(events[0].actor, "test-actor");
252 assert_eq!(events[0].comment, Some("Test session created".to_string()));
253 }
254}