//! Calendar Storage Adapter //! //! This adapter implements the `CalendarStoragePort` application port using //! the `CalendarRepository` and `CalendarEventRepository` domain repositories. //! It bridges the gap between the application layer and the infrastructure layer. use chrono::{DateTime, Utc}; use std::collections::HashMap; use std::sync::Arc; use uuid::Uuid; use crate::application::dtos::calendar_dto::{ CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto, UpdateCalendarDto, UpdateEventDto, }; use crate::application::ports::calendar_ports::{CalendarStoragePort, UpsertEventsResult}; use crate::common::errors::{DomainError, ErrorKind}; use crate::domain::entities::calendar::Calendar; use crate::domain::entities::calendar_event::CalendarEvent; use crate::domain::repositories::calendar_event_repository::CalendarEventRepository; use crate::domain::repositories::calendar_repository::CalendarRepository; use crate::infrastructure::repositories::pg::CalendarEventPgRepository; use crate::infrastructure::repositories::pg::CalendarPgRepository; /// Adapter that implements CalendarStoragePort using domain repositories pub struct CalendarStorageAdapter { calendar_repository: Arc, event_repository: Arc, } impl CalendarStorageAdapter { /// Creates a new CalendarStorageAdapter with the given repositories pub fn new( calendar_repository: Arc, event_repository: Arc, ) -> Self { Self { calendar_repository, event_repository, } } } impl CalendarStoragePort for CalendarStorageAdapter { // Calendar operations async fn create_calendar( &self, dto: CreateCalendarDto, owner_id: Uuid, ) -> Result { let calendar = Calendar::new(dto.name, owner_id, dto.description, dto.color)?; let created = self.calendar_repository.create_calendar(calendar).await?; Ok(CalendarDto::from(created)) } async fn update_calendar( &self, calendar_id: &str, update: UpdateCalendarDto, ) -> Result { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", ) })?; let mut calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?; if let Some(name) = update.name { calendar.update_name(name)?; } if let Some(description) = update.description { calendar.update_description(Some(description)); } if let Some(color) = update.color { calendar.update_color(Some(color))?; } let updated = self.calendar_repository.update_calendar(calendar).await?; Ok(CalendarDto::from(updated)) } async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", ) })?; // First delete all events in the calendar self.event_repository .delete_all_events_in_calendar(&uuid) .await?; // Then delete the calendar itself self.calendar_repository.delete_calendar(&uuid).await } async fn get_calendar(&self, calendar_id: &str) -> Result { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", ) })?; let calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?; Ok(CalendarDto::from(calendar)) } async fn get_calendars_by_ids(&self, ids: &[Uuid]) -> Result, DomainError> { let calendars = self.calendar_repository.find_calendars_by_ids(ids).await?; Ok(calendars.into_iter().map(CalendarDto::from).collect()) } async fn list_calendars_by_owner( &self, owner_id: Uuid, ) -> Result, DomainError> { let calendars = self .calendar_repository .list_calendars_by_owner(owner_id) .await?; Ok(calendars.into_iter().map(CalendarDto::from).collect()) } async fn list_public_calendars( &self, limit: i64, offset: i64, ) -> Result, DomainError> { let calendars = self .calendar_repository .list_public_calendars(limit, offset) .await?; Ok(calendars.into_iter().map(CalendarDto::from).collect()) } // Calendar properties async fn set_calendar_property( &self, calendar_id: &str, property_name: &str, property_value: &str, ) -> Result<(), DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", ) })?; self.calendar_repository .set_calendar_property(&uuid, property_name, property_value) .await } async fn get_calendar_property( &self, calendar_id: &str, property_name: &str, ) -> Result, DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", ) })?; self.calendar_repository .get_calendar_property(&uuid, property_name) .await } async fn get_calendar_properties( &self, calendar_id: &str, ) -> Result, DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", ) })?; self.calendar_repository .get_calendar_properties(&uuid) .await } // Event operations async fn create_event(&self, dto: CreateEventDto) -> Result { let calendar_id = Uuid::parse_str(&dto.calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Event", "Invalid calendar ID format", ) })?; // Verify calendar exists and user has access let _calendar = self .calendar_repository .find_calendar_by_id(&calendar_id) .await?; // Generate basic iCal data let ical_data = format!( "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//OxiCloud//EN\nBEGIN:VEVENT\nUID:{}@oxicloud\nDTSTAMP:{}\nDTSTART:{}\nDTEND:{}\nSUMMARY:{}\nEND:VEVENT\nEND:VCALENDAR", uuid::Uuid::new_v4(), chrono::Utc::now().format("%Y%m%dT%H%M%SZ"), dto.start_time.format("%Y%m%dT%H%M%SZ"), dto.end_time.format("%Y%m%dT%H%M%SZ"), dto.summary ); let event = CalendarEvent::new( calendar_id, dto.summary, dto.description, dto.location, dto.start_time, dto.end_time, dto.all_day.unwrap_or(false), dto.rrule, ical_data, )?; let created = self.event_repository.create_event(event).await?; Ok(CalendarEventDto::from(created)) } async fn create_event_from_ical( &self, dto: CreateEventICalDto, ) -> Result { let calendar_id = Uuid::parse_str(&dto.calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Event", "Invalid calendar ID format", ) })?; // Verify calendar exists let _calendar = self .calendar_repository .find_calendar_by_id(&calendar_id) .await?; // Parse iCal data and create event let event = CalendarEvent::from_ical(calendar_id, dto.ical_data.clone())?; let created = self.event_repository.create_event(event).await?; Ok(CalendarEventDto::from(created)) } async fn upsert_ical_events( &self, dto: CreateEventICalDto, ) -> Result { let calendar_id = Uuid::parse_str(&dto.calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Event", "Invalid calendar ID format", ) })?; // Verify calendar exists before touching the events table. let _calendar = self .calendar_repository .find_calendar_by_id(&calendar_id) .await?; // Split the body into one CalendarEvent per VEVENT. A body // with zero VEVENTs (or only VTODOs / VJOURNALs) returns // InvalidInput here — which the handler layer maps to 400. let parsed = CalendarEvent::parse_all_events(calendar_id, &dto.ical_data)?; let mut out = Vec::with_capacity(parsed.len()); let mut any_inserted = false; for event in parsed { let ical_uid = event.ical_uid().to_string(); // Existing row lookup routes on the master/exception split. // Master: (calendar_id, ical_uid) WHERE recurrence_id IS NULL // Exception: (calendar_id, ical_uid, recurrence_id) let existing = match event.recurrence_id().copied() { Some(rid) => { self.event_repository .find_event_by_ical_uid_and_recurrence_id(&calendar_id, &ical_uid, &rid) .await? } None => { self.event_repository .find_event_by_ical_uid(&calendar_id, &ical_uid) .await? } }; // Delete-then-insert keeps the DB-level partial unique // indexes happy and matches the pre-#528 update semantics // of the single-event path (fresh row id per replace, // ETag changes on update). if let Some(existing_event) = existing { self.event_repository .delete_event(existing_event.id()) .await?; } else { any_inserted = true; } let created = self.event_repository.create_event(event).await?; out.push(CalendarEventDto::from(created)); } Ok(UpsertEventsResult { events: out, any_inserted, }) } async fn update_event( &self, event_id: &str, update: UpdateEventDto, ) -> Result { let uuid = Uuid::parse_str(event_id).map_err(|_| { DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format") })?; let mut event = self.event_repository.find_event_by_id(&uuid).await?; if let Some(summary) = update.summary { event.update_summary(summary)?; } if let Some(description) = update.description { event.update_description(Some(description)); } if let Some(location) = update.location { event.update_location(Some(location)); } if let Some(start_time) = update.start_time { if let Some(end_time) = update.end_time { event.update_time_range(start_time, end_time)?; } else { event.update_time_range(start_time, *event.end_time())?; } } else if let Some(end_time) = update.end_time { event.update_time_range(*event.start_time(), end_time)?; } if let Some(all_day) = update.all_day { event.update_all_day(all_day); } if let Some(rrule) = update.rrule { event.update_rrule(Some(rrule))?; } let updated = self.event_repository.update_event(event).await?; Ok(CalendarEventDto::from(updated)) } async fn delete_event(&self, event_id: &str) -> Result<(), DomainError> { let uuid = Uuid::parse_str(event_id).map_err(|_| { DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format") })?; self.event_repository.delete_event(&uuid).await } async fn get_event(&self, event_id: &str) -> Result { let uuid = Uuid::parse_str(event_id).map_err(|_| { DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format") })?; let event = self.event_repository.find_event_by_id(&uuid).await?; Ok(CalendarEventDto::from(event)) } async fn find_event_by_ical_uid( &self, calendar_id: &str, ical_uid: &str, ) -> Result, DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", ) })?; let event = self .event_repository .find_event_by_ical_uid(&uuid, ical_uid) .await?; Ok(event.map(CalendarEventDto::from)) } async fn find_events_by_ical_uids( &self, calendar_id: &str, ical_uids: &[String], ) -> Result, DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", ) })?; let events = self .event_repository .find_events_by_ical_uids(&uuid, ical_uids) .await?; Ok(events.into_iter().map(CalendarEventDto::from).collect()) } async fn list_events_by_calendar( &self, calendar_id: &str, ) -> Result, DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", ) })?; let events = self.event_repository.list_events_by_calendar(&uuid).await?; Ok(events.into_iter().map(CalendarEventDto::from).collect()) } fn stream_events_uid_order( &self, calendar_id: &str, ) -> futures::stream::BoxStream<'static, Result> { use futures::StreamExt; let uuid = match Uuid::parse_str(calendar_id) { Ok(u) => u, Err(_) => { return Box::pin(futures::stream::once(async { Err(DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", )) })); } }; Box::pin( self.event_repository .stream_events_uid_order(uuid) .map(|r| r.map(CalendarEventDto::from)), ) } async fn list_events_by_calendar_paginated( &self, calendar_id: &str, limit: i64, offset: i64, ) -> Result, DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", ) })?; let events = self .event_repository .list_events_by_calendar_paginated(&uuid, limit, offset) .await?; Ok(events.into_iter().map(CalendarEventDto::from).collect()) } async fn get_events_in_time_range( &self, calendar_id: &str, start: &DateTime, end: &DateTime, ) -> Result, DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format", ) })?; let events = self .event_repository .get_events_in_time_range(&uuid, start, end) .await?; Ok(events.into_iter().map(CalendarEventDto::from).collect()) } } #[cfg(test)] mod tests { // Tests would go here using mock repositories }