aboutsummaryrefslogtreecommitdiff
path: root/src/infrastructure/persistence/recurring_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/recurring_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/recurring_event_repository.rs')
-rw-r--r--src/infrastructure/persistence/recurring_event_repository.rs174
1 files changed, 174 insertions, 0 deletions
diff --git a/src/infrastructure/persistence/recurring_event_repository.rs b/src/infrastructure/persistence/recurring_event_repository.rs
new file mode 100644
index 0000000..af5753a
--- /dev/null
+++ b/src/infrastructure/persistence/recurring_event_repository.rs
@@ -0,0 +1,174 @@
+use async_trait::async_trait;
+use sqlx::SqlitePool;
+use crate::domain::{
+ recurrence::RecurringEvent,
+ repository::{RecurringEventRepository, RepositoryError, Result},
+ value_objects::CalendarId,
+};
+use super::{
+ models::{RecurrenceModel, RecurrenceExceptionModel},
+ mappers::RecurrenceMapper,
+};
+
+pub struct SqliteRecurringEventRepository {
+ pool: SqlitePool,
+}
+
+impl SqliteRecurringEventRepository {
+ pub fn new(pool: SqlitePool) -> Self {
+ Self { pool }
+ }
+}
+
+#[async_trait]
+impl RecurringEventRepository for SqliteRecurringEventRepository {
+ async fn save(&self, event: &RecurringEvent) -> Result<()> {
+ let mut tx = self.pool.begin()
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+
+ let model = RecurrenceMapper::to_model(event);
+
+ sqlx::query!(
+ r#"
+ INSERT INTO recurrences (
+ id, calendar_id, title, description, starts_at, ends_at,
+ frequency, interval, until, color, is_all_day, is_cancelled,
+ created_at, updated_at
+ )
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)
+ ON CONFLICT(id) DO UPDATE SET
+ title = excluded.title,
+ description = excluded.description,
+ starts_at = excluded.starts_at,
+ ends_at = excluded.ends_at,
+ frequency = excluded.frequency,
+ interval = excluded.interval,
+ until = excluded.until,
+ 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.frequency,
+ model.interval,
+ model.until,
+ model.color,
+ model.is_all_day,
+ model.is_cancelled,
+ model.created_at,
+ model.updated_at,
+ )
+ .execute(&mut *tx)
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+
+ sqlx::query!(
+ r#"
+ DELETE FROM recurrence_exceptions WHERE recurrence_id = ?1
+ "#,
+ model.id,
+ )
+ .execute(&mut *tx)
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+
+ for exception in event.exceptions().values() {
+ let ex_model = RecurrenceMapper::exception_to_model(
+ exception,
+ event.id(),
+ );
+
+ sqlx::query!(
+ r#"
+ INSERT INTO recurrence_exceptions (
+ recurrence_id, original_starts_at, new_starts_at,
+ new_ends_at, is_cancelled
+ )
+ VALUES (?1, ?2, ?3, ?4, ?5)
+ "#,
+ ex_model.recurrence_id,
+ ex_model.original_starts_at,
+ ex_model.new_starts_at,
+ ex_model.new_ends_at,
+ ex_model.is_cancelled,
+ )
+ .execute(&mut *tx)
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+ }
+
+ tx.commit()
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+
+ Ok(())
+ }
+
+ async fn find_by_calendar(&self, calendar_id: &CalendarId) -> Result<Vec<RecurringEvent>> {
+ let calendar_id_str = calendar_id.to_string();
+
+ let models = sqlx::query_as::<_, RecurrenceModel>(
+ r#"
+ SELECT id, calendar_id, title, description, starts_at, ends_at,
+ frequency, interval, until, color, is_all_day, is_cancelled,
+ created_at, updated_at
+ FROM recurrences
+ 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()))?;
+
+ let mut result = Vec::new();
+
+ for model in models {
+ let exceptions = sqlx::query_as::<_, RecurrenceExceptionModel>(
+ r#"
+ SELECT id, recurrence_id, original_starts_at, new_starts_at,
+ new_ends_at, is_cancelled
+ FROM recurrence_exceptions
+ WHERE recurrence_id = ?1
+ "#
+ )
+ .bind(&model.id)
+ .fetch_all(&self.pool)
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+
+ let event = RecurrenceMapper::to_domain(model, exceptions)
+ .map_err(|e| RepositoryError::DatabaseError(e))?;
+
+ result.push(event);
+ }
+
+ Ok(result)
+ }
+
+ async fn delete(&self, id: &str) -> Result<()> {
+ let result = sqlx::query!(
+ r#"
+ DELETE FROM recurrences WHERE id = ?1
+ "#,
+ id,
+ )
+ .execute(&self.pool)
+ .await
+ .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
+
+ if result.rows_affected() == 0 {
+ return Err(RepositoryError::NotFound);
+ }
+
+ Ok(())
+ }
+}