c51af68432
Benchmark-gated (benches/ROUND10.md; every change carries a BEFORE/AFTER harness with equivalence/safety gates — two designs were rejected or rewritten by their own benches before adoption): - Auth hot path: TokenClaims/CurrentUser display fields to Arc<str>, role to inline SmolStr end-to-end (Bearer, cookie, Basic-auth cache) — 4→1 allocs per authenticated request, 3→0 per warm DAV request; JWT Encoding/Decoding/Validation built once. - Cold shared-album herd: leader-inline parent batching in PgAclEngine (+ cascade try_get_with single-flight) — 100→2 parent queries per 100-thumb cold herd, herd wall 1.9x, sequential + warm paths unchanged, all ROUND8/9 safety gates plus new herd-equivalence gates. - Query-shape pack: share download double-fetch 2→1 (2.18x), contact-group COUNT(*) 14.9x, save_faces UNNEST 3.9x, playlist reorder UNNEST 63.7x (now atomic), search files∥folders join! 1.45x, move drive-lookup join! 2.14x, trash partial (drive_id, trashed_at) indexes, CalDAV event-gate narrow read, favorites/recents binary-decode port, dead count_files removed. - NC surface: preview + avatar honour If-None-Match (e2e: 5 KB and 197 KB → 0 bytes per revalidation), avatar WebP→PNG transcode memoised, PROPFIND/trashbin integer+date emits on stack formatters, folder-header enrichment join!, chunk-PUT retry stat folded into create_new open. - common::fmt integer rendering rewritten on the std 2-digit LUT after the round's own bench caught the div-loop losing to to_string (16.1 ns vs 22.5; speeds every prior-round call site). - Micro-pack: WebDAV scope probe borrow-only, ShareService base_url snapshot, cookie_secure OnceLock, Arc'd AES-GCM cipher, stack request-id, tantivy analyzer clone dropped. - SPA: search stale-guard + AbortController (10→1 completed round-trips, stale-clobber gone), getFolder in-flight dedup, gridColumns matchMedia hoist (10k→0 style reads). Backend: cargo fmt + clippy -D warnings clean, 524 tests green. Frontend: npm run check clean, 301 vitest green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DdM7V7M3QPW7HEHg3gLov
533 lines
17 KiB
Rust
533 lines
17 KiB
Rust
//! 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<CalendarPgRepository>,
|
|
event_repository: Arc<CalendarEventPgRepository>,
|
|
}
|
|
|
|
impl CalendarStorageAdapter {
|
|
/// Creates a new CalendarStorageAdapter with the given repositories
|
|
pub fn new(
|
|
calendar_repository: Arc<CalendarPgRepository>,
|
|
event_repository: Arc<CalendarEventPgRepository>,
|
|
) -> Self {
|
|
Self {
|
|
calendar_repository,
|
|
event_repository,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CalendarStoragePort for CalendarStorageAdapter {
|
|
// Calendar operations
|
|
|
|
async fn create_calendar(
|
|
&self,
|
|
dto: CreateCalendarDto,
|
|
owner_id: Uuid,
|
|
) -> Result<CalendarDto, DomainError> {
|
|
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<CalendarDto, DomainError> {
|
|
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<CalendarDto, DomainError> {
|
|
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<Vec<CalendarDto>, 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<Vec<CalendarDto>, 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<Vec<CalendarDto>, 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<Option<String>, 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<HashMap<String, String>, 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<CalendarEventDto, DomainError> {
|
|
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<CalendarEventDto, DomainError> {
|
|
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<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,
|
|
update: UpdateEventDto,
|
|
) -> Result<CalendarEventDto, DomainError> {
|
|
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<CalendarEventDto, DomainError> {
|
|
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 calendar_id_for_event(&self, event_id: &str) -> Result<String, DomainError> {
|
|
let uuid = Uuid::parse_str(event_id).map_err(|_| {
|
|
DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format")
|
|
})?;
|
|
|
|
let calendar_id = self
|
|
.event_repository
|
|
.find_calendar_id_by_event_id(&uuid)
|
|
.await?;
|
|
Ok(calendar_id.to_string())
|
|
}
|
|
|
|
async fn find_event_by_ical_uid(
|
|
&self,
|
|
calendar_id: &str,
|
|
ical_uid: &str,
|
|
) -> Result<Option<CalendarEventDto>, 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<Vec<CalendarEventDto>, 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<Vec<CalendarEventDto>, 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<CalendarEventDto, DomainError>> {
|
|
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<Vec<CalendarEventDto>, 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<Utc>,
|
|
end: &DateTime<Utc>,
|
|
) -> Result<Vec<CalendarEventDto>, 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
|
|
}
|