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