fix(528): pass3: PUT with RECURRENCE-ID
This commit is contained in:
@@ -13,7 +13,7 @@ use crate::application::dtos::calendar_dto::{
|
||||
CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto,
|
||||
UpdateCalendarDto, UpdateEventDto,
|
||||
};
|
||||
use crate::application::ports::calendar_ports::CalendarStoragePort;
|
||||
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;
|
||||
@@ -262,6 +262,73 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
||||
Ok(CalendarEventDto::from(created))
|
||||
}
|
||||
|
||||
async fn upsert_ical_events(
|
||||
&self,
|
||||
dto: CreateEventICalDto,
|
||||
) -> Result<UpsertEventsResult, DomainError> {
|
||||
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,
|
||||
|
||||
@@ -389,6 +389,66 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_event_by_ical_uid_and_recurrence_id(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
ical_uid: &str,
|
||||
recurrence_id: &DateTime<Utc>,
|
||||
) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
|
||||
// Uses idx_calendar_events_exception_unique — the partial
|
||||
// unique index on (calendar_id, ical_uid, recurrence_id)
|
||||
// WHERE recurrence_id IS NOT NULL — for the exact-match seek.
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id, calendar_id, summary, description, location,
|
||||
start_time, end_time, all_day, rrule,
|
||||
created_at, updated_at, ical_uid, ical_data, recurrence_id
|
||||
FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1
|
||||
AND ical_uid = $2
|
||||
AND recurrence_id = $3
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(ical_uid)
|
||||
.bind(recurrence_id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!(
|
||||
"Failed to get calendar event exception by UID+RECURRENCE-ID: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
match row_opt {
|
||||
Some(row) => {
|
||||
let mut event = CalendarEvent::with_id(
|
||||
row.get("id"),
|
||||
row.get("calendar_id"),
|
||||
row.get("summary"),
|
||||
row.get::<Option<String>, _>("description"),
|
||||
row.get::<Option<String>, _>("location"),
|
||||
row.get("start_time"),
|
||||
row.get("end_time"),
|
||||
row.get("all_day"),
|
||||
row.get::<Option<String>, _>("rrule"),
|
||||
row.get("ical_uid"),
|
||||
row.get("ical_data"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Error creating calendar event: {}", e))
|
||||
})?;
|
||||
event.set_recurrence_id(row.get::<Option<DateTime<Utc>>, _>("recurrence_id"));
|
||||
Ok(Some(event))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_events_by_ical_uids(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
|
||||
Reference in New Issue
Block a user