aboutsummaryrefslogtreecommitdiff
path: root/src/infrastructure/persistence/event_repository.rs
diff options
context:
space:
mode:
authorMikkel Thestrup <mikkel@mithe.dk>2026-01-27 17:07:13 +0100
committerMikkel Thestrup <mikkel@mithe.dk>2026-01-27 17:08:24 +0100
commitb35c8cca57811050536a4fa6c1cb5675453ad463 (patch)
tree622bce9ef4021701ab845fd88ff0fc86b47905aa /src/infrastructure/persistence/event_repository.rs
parent4e78fd83349c95711cdee5acc56f248f81ebd25c (diff)
downloadkal-b35c8cca57811050536a4fa6c1cb5675453ad463.tar.gz
kal-b35c8cca57811050536a4fa6c1cb5675453ad463.zip
feat(infrastructure): implement SQLite persistence layer for calendar domain
Add infrastructure layer with SQLite repositories for calendars, events, and recurring events. Implements the repository pattern with proper domain/infrastructure separation. - Add CalendarModel, EventModel, RecurrenceModel, and RecurrenceExceptionModel for database persistence - Implement SqliteCalendarRepository with CRUD operations - Implement SqliteEventRepository with calendar filtering and time range queries - Implement SqliteRecurringEventRepository with exception handling and transactions - Add bidirectional mappers between domain entities and persistence models - Use sqlx query_as for type-safe database queries with FromRow derivation - Support upsert operations for all entities using ON CONFLICT clauses
Diffstat (limited to 'src/infrastructure/persistence/event_repository.rs')
-rw-r--r--src/infrastructure/persistence/event_repository.rs168
1 files changed, 168 insertions, 0 deletions
diff --git a/src/infrastructure/persistence/event_repository.rs b/src/infrastructure/persistence/event_repository.rs
new file mode 100644
index 0000000..cd83e99
--- /dev/null
+++ b/src/infrastructure/persistence/event_repository.rs
@@ -0,0 +1,168 @@
+use async_trait::async_trait;
+use sqlx::SqlitePool;
+use crate::domain::{
+ event::Event,
+ repository::{EventRepository, RepositoryError, Result},
+ value_objects::{CalendarId, EventId, TimeRange},
+};
+use super::{models::EventModel, mappers::EventMapper};
+
+pub struct SqliteEventRepository {
+ pool: SqlitePool,
+}
+
+impl SqliteEventRepository {
+ pub fn new(pool: SqlitePool) -> Self {
+ Self { pool }
+ }
+}
+
+#[async_trait]
+impl EventRepository for SqliteEventRepository {
+ async fn save(&self, event: &Event) -> Result<()> {
+ let model = EventMapper::to_model(event);
+
+ sqlx::query!(
+ r#"
+ INSERT INTO events (
+ id, calendar_id, title, description, starts_at, ends_at,
+ color, is_all_day, is_cancelled, created_at, updated_at
+ )
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
+ ON CONFLICT(id) DO UPDATE SET
+ title = excluded.title,
+ description = excluded.description,
+ starts_at = excluded.starts_at,
+ ends_at = excluded.ends_at,
+ color = excluded.color,
+ is_all_day = excluded.is_all_day,
+ is_cancelled = excluded.is_cancelled,
+ updated_at = excluded.updated_at
+ "#,
+ model.id,
+ model.calendar_id,
+ model.title,
+ model.description,
+ model.starts_at,
+ model.ends_at,
+ model.color,
+ model.is_all_day,
+ model.is_cancelled,
+ model.created_at,
+ model.updated_at,
+ )
+ .execute(&self.pool)
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+
+ Ok(())
+ }
+
+ async fn find_by_id(&self, id: &EventId) -> Result<Option<Event>> {
+ let id_str = id.to_string();
+
+ let model = sqlx::query_as::<_, EventModel>(
+ r#"
+ SELECT id, calendar_id, title, description, starts_at, ends_at,
+ color, is_all_day, is_cancelled, created_at, updated_at
+ FROM events
+ WHERE id = ?1
+ "#
+ )
+ .bind(&id_str)
+ .fetch_optional(&self.pool)
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+
+ match model {
+ Some(m) => {
+ let event = EventMapper::to_domain(m)
+ .map_err(|e| RepositoryError::DatabaseError(e))?;
+ Ok(Some(event))
+ }
+ None => Ok(None),
+ }
+ }
+
+ async fn find_by_calendar(
+ &self,
+ calendar_id: &CalendarId
+ ) -> Result<Vec<Event>> {
+ let calendar_id_str = calendar_id.to_string();
+
+ let models = sqlx::query_as::<_, EventModel>(
+ r#"
+ SELECT id, calendar_id, title, description, starts_at, ends_at,
+ color, is_all_day, is_cancelled, created_at, updated_at
+ FROM events
+ WHERE calendar_id = ?1
+ ORDER BY starts_at
+ "#
+ )
+ .bind(&calendar_id_str)
+ .fetch_all(&self.pool)
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+
+ models
+ .into_iter()
+ .map(|m| EventMapper::to_domain(m)
+ .map_err(|e| RepositoryError::DatabaseError(e)))
+ .collect()
+ }
+
+ async fn find_in_range(
+ &self,
+ calendar_id: &CalendarId,
+ range: &TimeRange,
+ ) -> Result<Vec<Event>> {
+ let calendar_id_str = calendar_id.to_string();
+ let range_start = range.starts_at().to_rfc3339();
+ let range_end = range.ends_at().to_rfc3339();
+
+ let models = sqlx::query_as::<_, EventModel>(
+ r#"
+ SELECT id, calendar_id, title, description, starts_at, ends_at,
+ color, is_all_day, is_cancelled, created_at, updated_at
+ FROM events
+ WHERE calendar_id = ?1
+ AND is_cancelled = 0
+ AND starts_at < ?3
+ AND ends_at > ?2
+ ORDER BY starts_at
+ "#
+ )
+ .bind(&calendar_id_str)
+ .bind(&range_start)
+ .bind(&range_end)
+ .fetch_all(&self.pool)
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+
+ models
+ .into_iter()
+ .map(|m| EventMapper::to_domain(m)
+ .map_err(|e| RepositoryError::DatabaseError(e)))
+ .collect()
+ }
+
+ async fn delete(&self, id: &EventId) -> Result<()> {
+ let id_str = id.to_string();
+
+ let result = sqlx::query!(
+ r#"
+ DELETE FROM events WHERE id = ?1
+ "#,
+ id_str,
+ )
+ .execute(&self.pool)
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+
+ if result.rows_affected() == 0 {
+ return Err(RepositoryError::NotFound);
+ }
+
+ Ok(())
+ }
+}