perf(dav-collections): indexed UID lookups for single-object operations
Every CalDAV PUT/GET/DELETE of one .ics loaded the ENTIRE calendar — every row including its ical_data — and filtered with .find() in Rust, so importing N events cost O(N²) rows transferred. CardDAV did the exact same in four places (PROPFIND of one .vcf, PUT existence check, GET, DELETE), with three JSONB deserializations per discarded contact. The indexed repo queries (find_event_by_ical_uid, get_contact_by_uid) existed all along with zero callers. Wire them end to end: new `get_event_by_ical_uid` / `get_contact_by_uid` use-case methods (same access checks as list_events / list_contacts, per the service-layer authz rule) exposed through the storage ports and adapters, and the seven handler sites now resolve one row instead of the whole collection. https://claude.ai/code/session_01Dp3oWon5GBMVn4j3QXZdgx
This commit is contained in:
@@ -87,6 +87,14 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
|
||||
) -> Result<CalendarEventDto, DomainError>;
|
||||
async fn delete_event(&self, event_id: &str) -> Result<(), DomainError>;
|
||||
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError>;
|
||||
/// Indexed single-row lookup by iCalendar UID — the CalDAV
|
||||
/// object-resource paths must use this instead of listing the whole
|
||||
/// calendar (every row + its `ical_data`) and filtering client-side.
|
||||
async fn find_event_by_ical_uid(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
ical_uid: &str,
|
||||
) -> Result<Option<CalendarEventDto>, DomainError>;
|
||||
async fn list_events_by_calendar(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
@@ -180,6 +188,15 @@ pub trait CalendarUseCase: Send + Sync + 'static {
|
||||
event_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<CalendarEventDto, DomainError>;
|
||||
/// Resolve one event by its iCalendar UID (the identifier CalDAV
|
||||
/// object resources are addressed by). `Ok(None)` when no event with
|
||||
/// that UID exists in the calendar.
|
||||
async fn get_event_by_ical_uid(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
ical_uid: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Option<CalendarEventDto>, DomainError>;
|
||||
async fn list_events(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
|
||||
@@ -71,6 +71,17 @@ pub trait ContactUseCase: Send + Sync + 'static {
|
||||
async fn delete_contact(&self, contact_id: &str, user_id: Uuid) -> Result<(), DomainError>;
|
||||
async fn get_contact(&self, contact_id: &str, user_id: Uuid)
|
||||
-> Result<ContactDto, DomainError>;
|
||||
/// Resolve one contact by its vCard UID (the identifier CardDAV
|
||||
/// object resources are addressed by) with an indexed single-row
|
||||
/// lookup — instead of listing the whole address book (every row
|
||||
/// with its vCard + JSONB columns) and filtering client-side.
|
||||
/// `Ok(None)` when no contact with that UID exists in the book.
|
||||
async fn get_contact_by_uid(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
uid: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Option<ContactDto>, DomainError>;
|
||||
async fn list_contacts(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
|
||||
@@ -277,6 +277,29 @@ impl CalendarUseCase for CalendarService {
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
async fn get_event_by_ical_uid(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
ical_uid: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Option<CalendarEventDto>, DomainError> {
|
||||
let has_access = self
|
||||
.calendar_storage
|
||||
.check_calendar_access(calendar_id, user_id)
|
||||
.await?;
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
if !has_access && !calendar.is_public {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
"You don't have permission to view events in this calendar",
|
||||
));
|
||||
}
|
||||
self.calendar_storage
|
||||
.find_event_by_ical_uid(calendar_id, ical_uid)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_events(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
|
||||
@@ -780,6 +780,22 @@ impl ContactUseCase for ContactService {
|
||||
Ok(ContactDto::from(contact))
|
||||
}
|
||||
|
||||
async fn get_contact_by_uid(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
uid: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Option<ContactDto>, DomainError> {
|
||||
let id = Uuid::parse_str(address_book_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
|
||||
// Check if user has access to the address book
|
||||
self.check_address_book_access(&id, &user_id).await?;
|
||||
|
||||
let contact = self.contact_repository.get_contact_by_uid(&id, uid).await?;
|
||||
Ok(contact.map(ContactDto::from))
|
||||
}
|
||||
|
||||
async fn list_contacts(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
|
||||
Reference in New Issue
Block a user