Files
Oxicloud/src/infrastructure/adapters/calendar_storage_adapter.rs
T
Claude c51af68432 perf: round 10 — auth alloc purge, parent-herd batching, query-shape pack, NC 304s
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
2026-07-18 20:33:50 +00:00

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
}