2026-02-14 01:29:34 +01:00
|
|
|
//! Contact Storage Adapter
|
|
|
|
|
//!
|
2026-07-06 00:41:22 +02:00
|
|
|
//! 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.
|
2026-02-14 01:29:34 +01:00
|
|
|
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
use crate::application::ports::carddav_ports::ContactStoragePort;
|
|
|
|
|
use crate::common::errors::DomainError;
|
|
|
|
|
use crate::domain::entities::contact::{AddressBook, Contact, ContactGroup};
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::domain::repositories::address_book_repository::AddressBookRepository;
|
|
|
|
|
use crate::domain::repositories::contact_repository::{ContactGroupRepository, ContactRepository};
|
2026-07-06 00:41:22 +02:00
|
|
|
use crate::infrastructure::repositories::pg::{
|
|
|
|
|
AddressBookPgRepository, ContactGroupPgRepository, ContactPgRepository,
|
|
|
|
|
};
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
/// 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.
|
2026-02-14 01:29:34 +01:00
|
|
|
pub struct ContactStorageAdapter {
|
2026-03-03 15:36:42 +00:00
|
|
|
address_book_repository: Arc<AddressBookPgRepository>,
|
|
|
|
|
contact_repository: Arc<ContactPgRepository>,
|
2026-07-06 00:41:22 +02:00
|
|
|
contact_group_repository: Arc<ContactGroupPgRepository>,
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ContactStorageAdapter {
|
|
|
|
|
pub fn new(
|
2026-03-03 15:36:42 +00:00
|
|
|
address_book_repository: Arc<AddressBookPgRepository>,
|
|
|
|
|
contact_repository: Arc<ContactPgRepository>,
|
2026-07-06 00:41:22 +02:00
|
|
|
contact_group_repository: Arc<ContactGroupPgRepository>,
|
2026-02-14 01:29:34 +01:00
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
address_book_repository,
|
|
|
|
|
contact_repository,
|
2026-07-06 00:41:22 +02:00
|
|
|
contact_group_repository,
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
impl ContactStoragePort for ContactStorageAdapter {
|
|
|
|
|
// ── Address books ────────────────────────────────────────────
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
async fn create_address_book(
|
|
|
|
|
&self,
|
2026-07-06 00:41:22 +02:00
|
|
|
address_book: AddressBook,
|
|
|
|
|
) -> Result<AddressBook, DomainError> {
|
|
|
|
|
self.address_book_repository
|
2026-02-14 01:29:34 +01:00
|
|
|
.create_address_book(address_book)
|
2026-07-06 00:41:22 +02:00
|
|
|
.await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn update_address_book(
|
|
|
|
|
&self,
|
2026-07-06 00:41:22 +02:00
|
|
|
address_book: AddressBook,
|
|
|
|
|
) -> Result<AddressBook, DomainError> {
|
2026-02-14 01:29:34 +01:00
|
|
|
self.address_book_repository
|
2026-07-06 00:41:22 +02:00
|
|
|
.update_address_book(address_book)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn delete_address_book(&self, id: &Uuid) -> Result<(), DomainError> {
|
|
|
|
|
self.address_book_repository.delete_address_book(id).await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn get_address_book_by_id(&self, id: &Uuid) -> Result<Option<AddressBook>, DomainError> {
|
2026-02-14 01:29:34 +01:00
|
|
|
self.address_book_repository
|
2026-07-06 00:41:22 +02:00
|
|
|
.get_address_book_by_id(id)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 13:48:37 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn get_public_address_books(&self) -> Result<Vec<AddressBook>, DomainError> {
|
2026-02-14 01:29:34 +01:00
|
|
|
self.address_book_repository
|
2026-07-06 00:41:22 +02:00
|
|
|
.get_public_address_books()
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
// ── Contacts ─────────────────────────────────────────────────
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn create_contact(&self, contact: Contact) -> Result<Contact, DomainError> {
|
|
|
|
|
self.contact_repository.create_contact(contact).await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn update_contact(&self, contact: Contact) -> Result<Contact, DomainError> {
|
|
|
|
|
self.contact_repository.update_contact(contact).await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn delete_contact(&self, id: &Uuid) -> Result<(), DomainError> {
|
|
|
|
|
self.contact_repository.delete_contact(id).await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn get_contact_by_id(&self, id: &Uuid) -> Result<Option<Contact>, DomainError> {
|
|
|
|
|
self.contact_repository.get_contact_by_id(id).await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-10 09:52:28 +00:00
|
|
|
async fn get_contact_by_uid(
|
|
|
|
|
&self,
|
2026-07-06 00:41:22 +02:00
|
|
|
address_book_id: &Uuid,
|
2026-06-10 09:52:28 +00:00
|
|
|
uid: &str,
|
2026-07-06 00:41:22 +02:00
|
|
|
) -> Result<Option<Contact>, DomainError> {
|
|
|
|
|
self.contact_repository
|
|
|
|
|
.get_contact_by_uid(address_book_id, uid)
|
|
|
|
|
.await
|
2026-06-10 09:52:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-11 09:21:35 +00:00
|
|
|
async fn get_contacts_by_uids(
|
2026-02-14 01:29:34 +01:00
|
|
|
&self,
|
2026-07-06 00:41:22 +02:00
|
|
|
address_book_id: &Uuid,
|
2026-06-11 09:21:35 +00:00
|
|
|
uids: &[String],
|
2026-07-06 00:41:22 +02:00
|
|
|
) -> Result<Vec<Contact>, DomainError> {
|
|
|
|
|
self.contact_repository
|
|
|
|
|
.get_contacts_by_uids(address_book_id, uids)
|
|
|
|
|
.await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn get_contacts_by_address_book(
|
2026-06-11 09:21:35 +00:00
|
|
|
&self,
|
2026-07-06 00:41:22 +02:00
|
|
|
address_book_id: &Uuid,
|
|
|
|
|
) -> Result<Vec<Contact>, DomainError> {
|
|
|
|
|
self.contact_repository
|
|
|
|
|
.get_contacts_by_address_book(address_book_id)
|
|
|
|
|
.await
|
|
|
|
|
}
|
2026-06-11 09:21:35 +00:00
|
|
|
|
2026-07-18 09:03:33 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
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
|
2026-06-11 09:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
async fn search_contacts(
|
|
|
|
|
&self,
|
2026-07-06 00:41:22 +02:00
|
|
|
address_book_id: &Uuid,
|
2026-02-14 01:29:34 +01:00
|
|
|
query: &str,
|
2026-07-06 00:41:22 +02:00
|
|
|
) -> Result<Vec<Contact>, DomainError> {
|
|
|
|
|
self.contact_repository
|
|
|
|
|
.search_contacts(address_book_id, query)
|
|
|
|
|
.await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
// ── Contact groups ───────────────────────────────────────────
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn create_group(&self, group: ContactGroup) -> Result<ContactGroup, DomainError> {
|
|
|
|
|
self.contact_group_repository.create_group(group).await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn update_group(&self, group: ContactGroup) -> Result<ContactGroup, DomainError> {
|
|
|
|
|
self.contact_group_repository.update_group(group).await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn delete_group(&self, id: &Uuid) -> Result<(), DomainError> {
|
|
|
|
|
self.contact_group_repository.delete_group(id).await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn get_group_by_id(&self, id: &Uuid) -> Result<Option<ContactGroup>, DomainError> {
|
|
|
|
|
self.contact_group_repository.get_group_by_id(id).await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn get_groups_by_address_book(
|
2026-02-14 01:29:34 +01:00
|
|
|
&self,
|
2026-07-06 00:41:22 +02:00
|
|
|
address_book_id: &Uuid,
|
|
|
|
|
) -> Result<Vec<ContactGroup>, DomainError> {
|
|
|
|
|
self.contact_group_repository
|
|
|
|
|
.get_groups_by_address_book(address_book_id)
|
|
|
|
|
.await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
// ── Group membership ─────────────────────────────────────────
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
async fn add_contact_to_group(
|
|
|
|
|
&self,
|
2026-07-06 00:41:22 +02:00
|
|
|
group_id: &Uuid,
|
|
|
|
|
contact_id: &Uuid,
|
2026-02-14 01:29:34 +01:00
|
|
|
) -> Result<(), DomainError> {
|
2026-07-06 00:41:22 +02:00
|
|
|
self.contact_group_repository
|
|
|
|
|
.add_contact_to_group(group_id, contact_id)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn remove_contact_from_group(
|
|
|
|
|
&self,
|
2026-07-06 00:41:22 +02:00
|
|
|
group_id: &Uuid,
|
|
|
|
|
contact_id: &Uuid,
|
2026-02-14 01:29:34 +01:00
|
|
|
) -> Result<(), DomainError> {
|
2026-07-06 00:41:22 +02:00
|
|
|
self.contact_group_repository
|
|
|
|
|
.remove_contact_from_group(group_id, contact_id)
|
2026-02-14 01:29:34 +01:00
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
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
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-18 20:33:50 +00:00
|
|
|
async fn count_contacts_in_group(&self, group_id: &Uuid) -> Result<i64, DomainError> {
|
|
|
|
|
self.contact_group_repository
|
|
|
|
|
.count_contacts_in_group(group_id)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 00:41:22 +02:00
|
|
|
async fn get_groups_for_contact(
|
2026-02-14 01:29:34 +01:00
|
|
|
&self,
|
2026-07-06 00:41:22 +02:00
|
|
|
contact_id: &Uuid,
|
|
|
|
|
) -> Result<Vec<ContactGroup>, DomainError> {
|
|
|
|
|
self.contact_group_repository
|
|
|
|
|
.get_groups_for_contact(contact_id)
|
|
|
|
|
.await
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
}
|