From b35c8cca57811050536a4fa6c1cb5675453ad463 Mon Sep 17 00:00:00 2001 From: Mikkel Thestrup Date: Tue, 27 Jan 2026 17:07:13 +0100 Subject: 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 --- .../persistence/calendar_repository.rs | 119 +++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/infrastructure/persistence/calendar_repository.rs (limited to 'src/infrastructure/persistence/calendar_repository.rs') diff --git a/src/infrastructure/persistence/calendar_repository.rs b/src/infrastructure/persistence/calendar_repository.rs new file mode 100644 index 0000000..d0b758d --- /dev/null +++ b/src/infrastructure/persistence/calendar_repository.rs @@ -0,0 +1,119 @@ +use async_trait::async_trait; +use sqlx::SqlitePool; +use crate::{ + domain::{ + calendar::Calendar, + repository::{CalendarRepository, RepositoryError, Result}, + value_objects::CalendarId, + }, + infrastructure::persistence::models::CalendarModel +}; +use super::mappers::CalendarMapper; + +pub struct SqliteCalendarRepository { + pool: SqlitePool, +} + +impl SqliteCalendarRepository { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } +} + +#[async_trait] +impl CalendarRepository for SqliteCalendarRepository { + async fn save(&self, calendar: &Calendar) -> Result<()> { + let model = CalendarMapper::to_model(calendar); + + sqlx::query!( + r#" + INSERT INTO calendars ( + id, name, description, is_archived, + created_at, updated_at + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + description = excluded.description, + is_archived = excluded.is_archived, + updated_at = excluded.updated_at + "#, + model.id, + model.name, + model.description, + model.is_archived, + 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: &CalendarId) -> Result> { + let id_str = id.to_string(); + + let model = sqlx::query_as::<_, CalendarModel>( + r#" + SELECT id, name, description, is_archived, created_at, updated_at + FROM calendars + WHERE id = ?1 + "# + ) + .bind(&id_str) + .fetch_optional(&self.pool) + .await + .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?; + + match model { + Some(m) => { + let calendar = CalendarMapper::to_domain(m) + .map_err(|e| RepositoryError::DatabaseError(e))?; + Ok(Some(calendar)) + } + None => Ok(None), + } + } + + async fn find_all_active(&self) -> Result> { + let models = sqlx::query_as::<_, CalendarModel>( + r#" + SELECT id, name, description, is_archived, created_at, updated_at + FROM calendars + WHERE is_archived = 0 + ORDER BY name + "# + ) + .fetch_all(&self.pool) + .await + .map_err(|e| RepositoryError::DatabaseError(e.to_string()))?; + + models + .into_iter() + .map(|m| CalendarMapper::to_domain(m) + .map_err(|e| RepositoryError::DatabaseError(e))) + .collect() + } + + async fn delete(&self, id: &CalendarId) -> Result<()> { + let id_str = id.to_string(); + + let result = sqlx::query!( + r#" + DELETE FROM calendars 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(()) + } +} -- cgit v1.2.3-70-g09d2