1use jiff::Zoned;
6use sqlx::SqlitePool;
7
8#[derive(Debug, Clone)]
9pub struct Calendars {
10 pool: SqlitePool,
11}
12
13impl Calendars {
14 pub fn new(pool: SqlitePool) -> Self {
15 Self { pool }
16 }
17
18 pub async fn upsert(&self, calendar: CalendarRecord) -> Result<(), sqlx::Error> {
19 const SQL: &str = "\
20INSERT INTO calendars (id, name, kind, priority, enabled, created_at, updated_at)
21VALUES (?, ?, ?, ?, ?, ?, ?)
22ON CONFLICT(id) DO UPDATE SET
23 name = excluded.name,
24 kind = excluded.kind,
25 priority = excluded.priority,
26 enabled = excluded.enabled,
27 updated_at = excluded.updated_at;
28";
29
30 sqlx::query(SQL)
31 .bind(&calendar.id)
32 .bind(&calendar.name)
33 .bind(&calendar.kind)
34 .bind(calendar.priority)
35 .bind(calendar.enabled)
36 .bind(&calendar.created_at)
37 .bind(&calendar.updated_at)
38 .execute(&self.pool)
39 .await?;
40
41 Ok(())
42 }
43
44 pub async fn get(&self, id: &str) -> Result<Option<CalendarRecord>, sqlx::Error> {
45 const SQL: &str = "\
46SELECT id, name, kind, priority, enabled, created_at, updated_at
47FROM calendars
48WHERE id = ?;
49";
50
51 sqlx::query_as(SQL)
52 .bind(id)
53 .fetch_optional(&self.pool)
54 .await
55 }
56
57 pub async fn list(&self) -> Result<Vec<CalendarRecord>, sqlx::Error> {
58 const SQL: &str = "\
59SELECT id, name, kind, priority, enabled, created_at, updated_at
60FROM calendars
61ORDER BY priority ASC;
62";
63
64 sqlx::query_as(SQL).fetch_all(&self.pool).await
65 }
66
67 pub async fn list_enabled(&self) -> Result<Vec<CalendarRecord>, sqlx::Error> {
68 const SQL: &str = "\
69SELECT id, name, kind, priority, enabled, created_at, updated_at
70FROM calendars
71WHERE enabled = 1
72ORDER BY priority ASC;
73";
74
75 sqlx::query_as(SQL).fetch_all(&self.pool).await
76 }
77
78 pub async fn set_enabled(&self, id: &str, enabled: bool) -> Result<(), sqlx::Error> {
79 const SQL: &str = "\
80UPDATE calendars
81SET enabled = ?, updated_at = ?
82WHERE id = ?;
83";
84
85 let now = Zoned::now().strftime("%Y-%m-%dT%H:%M:%S%.f%:z").to_string();
86
87 sqlx::query(SQL)
88 .bind(enabled)
89 .bind(&now)
90 .bind(id)
91 .execute(&self.pool)
92 .await?;
93
94 Ok(())
95 }
96
97 pub async fn delete(&self, id: &str) -> Result<(), sqlx::Error> {
98 const SQL: &str = "DELETE FROM calendars WHERE id = ?;";
99
100 sqlx::query(SQL).bind(id).execute(&self.pool).await?;
101
102 Ok(())
103 }
104}
105
106#[derive(Debug, Clone, sqlx::FromRow)]
108pub struct CalendarRecord {
109 pub id: String,
111 pub name: String,
113 pub kind: String,
115 pub priority: i32,
117 pub enabled: bool,
119 pub created_at: String,
121 pub updated_at: String,
123}
124
125impl CalendarRecord {
126 #[must_use]
128 pub fn new(id: String, name: String, kind: String, priority: i32, enabled: bool) -> Self {
129 let now = Zoned::now().strftime("%Y-%m-%dT%H:%M:%S%.f%:z").to_string();
130 Self {
131 id,
132 name,
133 kind,
134 priority,
135 enabled,
136 created_at: now.clone(),
137 updated_at: now,
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 async fn setup_test_db() -> crate::db::Db {
147 crate::db::Db::open(None)
148 .await
149 .expect("Failed to create test database")
150 }
151
152 #[tokio::test]
153 async fn calendars_insert_inserts_new_calendar() {
154 let db = setup_test_db().await;
155
156 let calendar = CalendarRecord::new(
157 "test-calendar".to_string(),
158 "Test Calendar".to_string(),
159 "local".to_string(),
160 0,
161 true,
162 );
163
164 db.calendars
165 .upsert(calendar)
166 .await
167 .expect("Failed to insert calendar");
168
169 let retrieved = db
170 .calendars
171 .get("test-calendar")
172 .await
173 .expect("Failed to get calendar")
174 .expect("Calendar not found");
175
176 assert_eq!(retrieved.id, "test-calendar");
177 assert_eq!(retrieved.name, "Test Calendar");
178 assert_eq!(retrieved.kind, "local");
179 assert_eq!(retrieved.priority, 0);
180 assert!(retrieved.enabled);
181 }
182
183 #[tokio::test]
184 async fn calendars_upsert_updates_existing_calendar() {
185 let db = setup_test_db().await;
186
187 let calendar = CalendarRecord::new(
188 "test-calendar".to_string(),
189 "Original Name".to_string(),
190 "local".to_string(),
191 0,
192 true,
193 );
194 db.calendars.upsert(calendar).await.unwrap();
195
196 let updated = CalendarRecord::new(
197 "test-calendar".to_string(),
198 "Updated Name".to_string(),
199 "caldav".to_string(),
200 5,
201 false,
202 );
203 db.calendars.upsert(updated).await.unwrap();
204
205 let retrieved = db.calendars.get("test-calendar").await.unwrap().unwrap();
206 assert_eq!(retrieved.name, "Updated Name");
207 assert_eq!(retrieved.kind, "caldav");
208 assert_eq!(retrieved.priority, 5);
209 assert!(!retrieved.enabled);
210 }
211
212 #[tokio::test]
213 async fn calendars_list_returns_all_calendars() {
214 let db = setup_test_db().await;
215
216 let calendars = db.calendars.list().await.unwrap();
218 assert!(!calendars.is_empty());
219
220 for i in 1..=3 {
222 let calendar = CalendarRecord::new(
223 format!("calendar-{i}"),
224 format!("Calendar {i}"),
225 "local".to_string(),
226 i,
227 true,
228 );
229 db.calendars.upsert(calendar).await.unwrap();
230 }
231
232 let calendars = db.calendars.list().await.unwrap();
233 assert!(calendars.len() >= 4); }
235
236 #[tokio::test]
237 async fn calendars_list_enabled_returns_only_enabled() {
238 let db = setup_test_db().await;
239
240 let enabled = CalendarRecord::new(
242 "enabled-cal".to_string(),
243 "Enabled".to_string(),
244 "local".to_string(),
245 0,
246 true,
247 );
248 db.calendars.upsert(enabled).await.unwrap();
249
250 let disabled = CalendarRecord::new(
251 "disabled-cal".to_string(),
252 "Disabled".to_string(),
253 "local".to_string(),
254 1,
255 false,
256 );
257 db.calendars.upsert(disabled).await.unwrap();
258
259 let enabled_calendars = db.calendars.list_enabled().await.unwrap();
260 let ids: Vec<&str> = enabled_calendars.iter().map(|c| c.id.as_str()).collect();
261 assert!(ids.contains(&"enabled-cal"));
262 assert!(!ids.contains(&"disabled-cal"));
263 }
264
265 #[tokio::test]
266 async fn calendars_set_enabled_toggles_enabled_flag() {
267 let db = setup_test_db().await;
268
269 let calendar = CalendarRecord::new(
270 "test-cal".to_string(),
271 "Test".to_string(),
272 "local".to_string(),
273 0,
274 true,
275 );
276 db.calendars.upsert(calendar).await.unwrap();
277
278 db.calendars.set_enabled("test-cal", false).await.unwrap();
279
280 let retrieved = db.calendars.get("test-cal").await.unwrap().unwrap();
281 assert!(!retrieved.enabled);
282
283 db.calendars.set_enabled("test-cal", true).await.unwrap();
284
285 let retrieved = db.calendars.get("test-cal").await.unwrap().unwrap();
286 assert!(retrieved.enabled);
287 }
288
289 #[tokio::test]
290 async fn calendars_delete_removes_calendar() {
291 let db = setup_test_db().await;
292
293 let calendar = CalendarRecord::new(
294 "to-delete".to_string(),
295 "To Delete".to_string(),
296 "local".to_string(),
297 0,
298 true,
299 );
300 db.calendars.upsert(calendar).await.unwrap();
301
302 db.calendars.delete("to-delete").await.unwrap();
303
304 let retrieved = db.calendars.get("to-delete").await.unwrap();
305 assert!(retrieved.is_none());
306 }
307
308 #[tokio::test]
309 async fn calendars_list_orders_by_priority() {
310 let db = setup_test_db().await;
311
312 db.calendars.delete("default").await.ok();
314
315 for (priority, name) in [(2, "Second"), (0, "First"), (1, "Middle")] {
317 let calendar = CalendarRecord::new(
318 name.to_lowercase(),
319 name.to_string(),
320 "local".to_string(),
321 priority,
322 true,
323 );
324 db.calendars.upsert(calendar).await.unwrap();
325 }
326
327 let calendars = db.calendars.list().await.unwrap();
328 let names: Vec<&str> = calendars.iter().map(|c| c.name.as_str()).collect();
329 assert_eq!(names, vec!["First", "Middle", "Second"]);
330 }
331}