Files
Oxicloud/src/infrastructure/adapters/contact_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

248 lines
8.2 KiB
Rust

//! Contact Storage Adapter
//!
//! Implements [`ContactStoragePort`] using the three PostgreSQL
//! repositories (`AddressBookPgRepository`, `ContactPgRepository`,
//! `ContactGroupPgRepository`).
//!
//! **Pure storage port.** No access-control logic, no sharing state,
//! no owner-vs-shared listing carve-outs — every method is plain
//! delegation to a repository. Access decisions live in
//! `AuthorizationEngine`; sharing state lives in
//! `storage.role_grants`. The service layer (`ContactService`) gates
//! each call before reaching through this port.
//!
//! Symmetric with `CalendarStorageAdapter`. Post-Round-3 the
//! pre-existing 1000-line adapter that mixed the use-case impls +
//! bespoke `check_address_book_access` was deleted; this file
//! recreates a much smaller storage-only version.
use std::sync::Arc;
use uuid::Uuid;
use crate::application::ports::carddav_ports::ContactStoragePort;
use crate::common::errors::DomainError;
use crate::domain::entities::contact::{AddressBook, Contact, ContactGroup};
use crate::domain::repositories::address_book_repository::AddressBookRepository;
use crate::domain::repositories::contact_repository::{ContactGroupRepository, ContactRepository};
use crate::infrastructure::repositories::pg::{
AddressBookPgRepository, ContactGroupPgRepository, ContactPgRepository,
};
/// Storage-port adapter bundling the three CardDAV PG repositories.
///
/// Wired in DI once; passed to `ContactService` which layers authz
/// on top and exposes the `AddressBookUseCase` / `ContactUseCase`
/// trait impls the HTTP handlers consume.
pub struct ContactStorageAdapter {
address_book_repository: Arc<AddressBookPgRepository>,
contact_repository: Arc<ContactPgRepository>,
contact_group_repository: Arc<ContactGroupPgRepository>,
}
impl ContactStorageAdapter {
pub fn new(
address_book_repository: Arc<AddressBookPgRepository>,
contact_repository: Arc<ContactPgRepository>,
contact_group_repository: Arc<ContactGroupPgRepository>,
) -> Self {
Self {
address_book_repository,
contact_repository,
contact_group_repository,
}
}
}
impl ContactStoragePort for ContactStorageAdapter {
// ── Address books ────────────────────────────────────────────
async fn create_address_book(
&self,
address_book: AddressBook,
) -> Result<AddressBook, DomainError> {
self.address_book_repository
.create_address_book(address_book)
.await
}
async fn update_address_book(
&self,
address_book: AddressBook,
) -> Result<AddressBook, DomainError> {
self.address_book_repository
.update_address_book(address_book)
.await
}
async fn delete_address_book(&self, id: &Uuid) -> Result<(), DomainError> {
self.address_book_repository.delete_address_book(id).await
}
async fn get_address_book_by_id(&self, id: &Uuid) -> Result<Option<AddressBook>, DomainError> {
self.address_book_repository
.get_address_book_by_id(id)
.await
}
async fn get_address_books_by_ids(
&self,
ids: &[Uuid],
) -> Result<Vec<AddressBook>, DomainError> {
self.address_book_repository
.get_address_books_by_ids(ids)
.await
}
async fn get_public_address_books(&self) -> Result<Vec<AddressBook>, DomainError> {
self.address_book_repository
.get_public_address_books()
.await
}
// ── Contacts ─────────────────────────────────────────────────
async fn create_contact(&self, contact: Contact) -> Result<Contact, DomainError> {
self.contact_repository.create_contact(contact).await
}
async fn update_contact(&self, contact: Contact) -> Result<Contact, DomainError> {
self.contact_repository.update_contact(contact).await
}
async fn delete_contact(&self, id: &Uuid) -> Result<(), DomainError> {
self.contact_repository.delete_contact(id).await
}
async fn get_contact_by_id(&self, id: &Uuid) -> Result<Option<Contact>, DomainError> {
self.contact_repository.get_contact_by_id(id).await
}
async fn get_contact_by_uid(
&self,
address_book_id: &Uuid,
uid: &str,
) -> Result<Option<Contact>, DomainError> {
self.contact_repository
.get_contact_by_uid(address_book_id, uid)
.await
}
async fn get_contacts_by_uids(
&self,
address_book_id: &Uuid,
uids: &[String],
) -> Result<Vec<Contact>, DomainError> {
self.contact_repository
.get_contacts_by_uids(address_book_id, uids)
.await
}
async fn get_contacts_by_address_book(
&self,
address_book_id: &Uuid,
) -> Result<Vec<Contact>, DomainError> {
self.contact_repository
.get_contacts_by_address_book(address_book_id)
.await
}
fn stream_contacts_by_book(
&self,
address_book_id: Uuid,
) -> futures::stream::BoxStream<'static, Result<Contact, DomainError>> {
self.contact_repository
.stream_contacts_by_book(address_book_id)
}
async fn get_contacts_by_address_book_paginated(
&self,
address_book_id: &Uuid,
limit: i64,
offset: i64,
) -> Result<Vec<Contact>, DomainError> {
self.contact_repository
.get_contacts_by_address_book_paginated(address_book_id, limit, offset)
.await
}
async fn search_contacts(
&self,
address_book_id: &Uuid,
query: &str,
) -> Result<Vec<Contact>, DomainError> {
self.contact_repository
.search_contacts(address_book_id, query)
.await
}
// ── Contact groups ───────────────────────────────────────────
async fn create_group(&self, group: ContactGroup) -> Result<ContactGroup, DomainError> {
self.contact_group_repository.create_group(group).await
}
async fn update_group(&self, group: ContactGroup) -> Result<ContactGroup, DomainError> {
self.contact_group_repository.update_group(group).await
}
async fn delete_group(&self, id: &Uuid) -> Result<(), DomainError> {
self.contact_group_repository.delete_group(id).await
}
async fn get_group_by_id(&self, id: &Uuid) -> Result<Option<ContactGroup>, DomainError> {
self.contact_group_repository.get_group_by_id(id).await
}
async fn get_groups_by_address_book(
&self,
address_book_id: &Uuid,
) -> Result<Vec<ContactGroup>, DomainError> {
self.contact_group_repository
.get_groups_by_address_book(address_book_id)
.await
}
// ── Group membership ─────────────────────────────────────────
async fn add_contact_to_group(
&self,
group_id: &Uuid,
contact_id: &Uuid,
) -> Result<(), DomainError> {
self.contact_group_repository
.add_contact_to_group(group_id, contact_id)
.await
}
async fn remove_contact_from_group(
&self,
group_id: &Uuid,
contact_id: &Uuid,
) -> Result<(), DomainError> {
self.contact_group_repository
.remove_contact_from_group(group_id, contact_id)
.await
}
async fn get_contacts_in_group(&self, group_id: &Uuid) -> Result<Vec<Contact>, DomainError> {
self.contact_group_repository
.get_contacts_in_group(group_id)
.await
}
async fn count_contacts_in_group(&self, group_id: &Uuid) -> Result<i64, DomainError> {
self.contact_group_repository
.count_contacts_in_group(group_id)
.await
}
async fn get_groups_for_contact(
&self,
contact_id: &Uuid,
) -> Result<Vec<ContactGroup>, DomainError> {
self.contact_group_repository
.get_groups_for_contact(contact_id)
.await
}
}