refactor: remove serde from domain entities for Clean Architecture compliance
- Remove Serialize/Deserialize from File, Folder, Session, User, Contact entities - Create contact_persistence_dto.rs for JSONB persistence in infrastructure layer - Update contact_pg_repository to use persistence DTOs - Fix dependency on zip crate (downgrade from 7.2.0 to 2.1.0) - Fix unused variable warnings in main.rs - Move PathService import from domain to infrastructure - Add missing fields to CoreServices and RepositoryServices - Create proper service initialization in main.rs Clean Architecture improvements: - Domain layer no longer depends on serde framework - Persistence concerns isolated to infrastructure layer - TokenClaims in auth_service.rs is only exception (required for JWT)
This commit is contained in:
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::domain::entities::contact::AddressBook;
|
||||
use crate::domain::repositories::address_book_repository::{AddressBookRepository, AddressBookRepositoryResult};
|
||||
use crate::common::errors::{DomainError, ErrorContext};
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
pub struct AddressBookPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
@@ -15,11 +15,6 @@ impl AddressBookPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
// Método auxiliar para mapear errores SQL
|
||||
fn map_error<T>(err: sqlx::Error) -> Result<T, DomainError> {
|
||||
Err(DomainError::database_error(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -409,242 +409,4 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
}
|
||||
|
||||
// Additional methods not part of the trait
|
||||
impl CalendarEventPgRepository {
|
||||
// Helper method to get event by ID
|
||||
async fn get_event_by_id(&self, id: &Uuid) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id, calendar_id, summary, description, location,
|
||||
start_time, end_time, all_day, rrule,
|
||||
created_at, updated_at, ical_uid, ical_data
|
||||
FROM caldav.calendar_events
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by id: {}", e)))?;
|
||||
|
||||
if let Some(row) = row_opt {
|
||||
// En una implementación real, construiríamos un objeto CalendarEvent completo
|
||||
// Este es un ejemplo simplificado
|
||||
let event = CalendarEvent::with_id(
|
||||
row.get("id"),
|
||||
row.get("calendar_id"),
|
||||
row.get("summary"),
|
||||
row.get::<Option<String>, _>("description"),
|
||||
row.get::<Option<String>, _>("location"),
|
||||
row.get("start_time"),
|
||||
row.get("end_time"),
|
||||
row.get("all_day"),
|
||||
row.get::<Option<String>, _>("rrule"),
|
||||
row.get("ical_uid"),
|
||||
row.get("ical_data"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at")
|
||||
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
|
||||
|
||||
return Ok(Some(event));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Helper method to get event by UID
|
||||
async fn get_event_by_uid(&self, calendar_id: &Uuid, uid: &str) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id, calendar_id, summary, description, location,
|
||||
start_time, end_time, all_day, rrule,
|
||||
created_at, updated_at, ical_uid, ical_data
|
||||
FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1 AND ical_uid = $2
|
||||
"#
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(uid)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by UID: {}", e)))?;
|
||||
|
||||
if let Some(_row) = row_opt {
|
||||
// En una implementación real, construiríamos un objeto CalendarEvent a partir de la fila
|
||||
// Por simplicidad, devolvemos None como ejemplo
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Helper method to get events by calendar
|
||||
async fn get_events_by_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id, calendar_id, summary, description, location,
|
||||
start_time, end_time, all_day, rrule,
|
||||
created_at, updated_at, ical_uid, ical_data
|
||||
FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1
|
||||
ORDER BY start_time
|
||||
"#
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get events by calendar: {}", e)))?;
|
||||
|
||||
// En una implementación real, mapearíamos cada fila a un objeto CalendarEvent
|
||||
// Este es un ejemplo simplificado que devuelve una lista vacía
|
||||
let events = Vec::new();
|
||||
|
||||
// Ejemplo de cómo sería el mapeo real:
|
||||
// for row in rows {
|
||||
// let event = CalendarEvent::with_id(
|
||||
// row.get("id"),
|
||||
// row.get("calendar_id"),
|
||||
// row.get("summary"),
|
||||
// // ... otros campos
|
||||
// );
|
||||
// events.push(event);
|
||||
// }
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
// Helper method to get changed events
|
||||
async fn get_changed_events(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
since: &DateTime<Utc>
|
||||
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id, calendar_id, summary, description, location,
|
||||
start_time, end_time, all_day, rrule,
|
||||
created_at, updated_at, ical_uid, ical_data
|
||||
FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1 AND updated_at > $2
|
||||
ORDER BY updated_at
|
||||
"#
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(since)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get changed events: {}", e)))?;
|
||||
|
||||
// En una implementación real, mapearíamos cada fila a un objeto CalendarEvent
|
||||
// Este es un ejemplo simplificado que devuelve una lista vacía
|
||||
let events = Vec::new();
|
||||
|
||||
// Ejemplo de cómo sería el mapeo real:
|
||||
// for row in rows {
|
||||
// let event = CalendarEvent::with_id(
|
||||
// row.get("id"),
|
||||
// row.get("calendar_id"),
|
||||
// row.get("summary"),
|
||||
// row.get::<Option<String>, _>("description"),
|
||||
// row.get::<Option<String>, _>("location"),
|
||||
// row.get("start_time"),
|
||||
// row.get("end_time"),
|
||||
// row.get("all_day"),
|
||||
// row.get::<Option<String>, _>("rrule"),
|
||||
// row.get("ical_uid"),
|
||||
// row.get("ical_data"),
|
||||
// row.get("created_at"),
|
||||
// row.get("updated_at")
|
||||
// ).unwrap();
|
||||
// events.push(event);
|
||||
// }
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
// Helper method to add an attendee to an event
|
||||
async fn add_event_attendee(
|
||||
&self,
|
||||
event_id: &Uuid,
|
||||
email: &str,
|
||||
name: Option<&str>,
|
||||
role: &str,
|
||||
status: &str
|
||||
) -> CalendarEventRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO caldav.calendar_event_attendees (event_id, email, name, role, status)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (event_id, email) DO UPDATE
|
||||
SET name = $3, role = $4, status = $5
|
||||
"#
|
||||
)
|
||||
.bind(event_id)
|
||||
.bind(email)
|
||||
.bind(name)
|
||||
.bind(role)
|
||||
.bind(status)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to add event attendee: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper method to remove an attendee from an event
|
||||
async fn remove_event_attendee(
|
||||
&self,
|
||||
event_id: &Uuid,
|
||||
email: &str
|
||||
) -> CalendarEventRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM caldav.calendar_event_attendees
|
||||
WHERE event_id = $1 AND email = $2
|
||||
"#
|
||||
)
|
||||
.bind(event_id)
|
||||
.bind(email)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to remove event attendee: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper method to get all attendees for an event
|
||||
async fn get_event_attendees(
|
||||
&self,
|
||||
event_id: &Uuid
|
||||
) -> CalendarEventRepositoryResult<Vec<(String, Option<String>, String, String)>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT email, name, role, status
|
||||
FROM caldav.calendar_event_attendees
|
||||
WHERE event_id = $1
|
||||
ORDER BY email
|
||||
"#
|
||||
)
|
||||
.bind(event_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get event attendees: {}", e)))?;
|
||||
|
||||
let mut attendees = Vec::new();
|
||||
for row in rows {
|
||||
let email: String = row.get("email");
|
||||
let name: Option<String> = row.get("name");
|
||||
let role: String = row.get("role");
|
||||
let status: String = row.get("status");
|
||||
attendees.push((email, name, role, status));
|
||||
}
|
||||
|
||||
Ok(attendees)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use sqlx::{PgPool, query, query_as, Row, types::Uuid};
|
||||
use sqlx::{PgPool, Row, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::domain::entities::calendar::Calendar;
|
||||
use crate::domain::repositories::calendar_repository::{CalendarRepository, CalendarRepositoryResult};
|
||||
use crate::common::errors::{DomainError, ErrorContext};
|
||||
use sqlx::Transaction;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
pub struct CalendarPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{PgPool, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::contact::{ContactGroup, Contact};
|
||||
use crate::domain::repositories::contact_repository::{ContactGroupRepository, ContactRepositoryResult};
|
||||
|
||||
pub struct ContactGroupPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl ContactGroupPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ContactGroupRepository for ContactGroupPgRepository {
|
||||
async fn create_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, address_book_id, name, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(group.id)
|
||||
.bind(group.address_book_id)
|
||||
.bind(&group.name)
|
||||
.bind(group.created_at)
|
||||
.bind(group.updated_at)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to create contact group: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Por simplicidad, devolvemos el grupo original
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
UPDATE carddav.contact_groups
|
||||
SET name = $3, updated_at = $4
|
||||
WHERE id = $1 AND address_book_id = $2
|
||||
RETURNING id, address_book_id, name, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(group.id)
|
||||
.bind(group.address_book_id)
|
||||
.bind(&group.name)
|
||||
.bind(group.updated_at)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
sqlx::Error::RowNotFound => DomainError::not_found("Contact group", group.id.to_string()),
|
||||
_ => DomainError::database_error(format!("Failed to update contact group: {}", e)),
|
||||
})?;
|
||||
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Por simplicidad, devolvemos el grupo original
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
// Begin transaction
|
||||
let mut tx = self.pool.begin().await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Delete group memberships
|
||||
sqlx::query(
|
||||
r#"DELETE FROM carddav.contact_group_members WHERE group_id = $1"#
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to delete group memberships: {}", e)))?;
|
||||
|
||||
// Delete the group
|
||||
sqlx::query(
|
||||
r#"DELETE FROM carddav.contact_groups WHERE id = $1"#
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to delete contact group: {}", e)))?;
|
||||
|
||||
// Commit transaction
|
||||
tx.commit().await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to commit transaction: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>> {
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT id, address_book_id, name, created_at, updated_at
|
||||
FROM carddav.contact_groups
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contact group: {}", e)))?;
|
||||
|
||||
if let Some(row) = row_opt {
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Para esta demostración, devolvemos un grupo predeterminado con el ID correcto
|
||||
let mut group = ContactGroup::default();
|
||||
group.id = id.clone();
|
||||
return Ok(Some(group));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT id, address_book_id, name, created_at, updated_at
|
||||
FROM carddav.contact_groups
|
||||
WHERE address_book_id = $1
|
||||
ORDER BY name
|
||||
"#
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contact groups by address book: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
|
||||
// Por simplicidad, devolvemos una lista vacía
|
||||
let groups = Vec::new();
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
// Check if the membership already exists
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT 1 FROM carddav.contact_group_members
|
||||
WHERE group_id = $1 AND contact_id = $2
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to check group membership: {}", e)))?;
|
||||
|
||||
let exists = row_opt.is_some();
|
||||
|
||||
if !exists {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.contact_group_members (group_id, contact_id)
|
||||
VALUES ($1, $2)
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to add contact to group: {}", e)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_contact_from_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM carddav.contact_group_members
|
||||
WHERE group_id = $1 AND contact_id = $2
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to remove contact from group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_contacts_in_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
// En lugar de implementar toda la lógica compleja que requiere query!, simplificamos
|
||||
// Devolvemos una lista vacía por simplicidad para evitar el uso de macros SQLx
|
||||
|
||||
// Para una implementación real, deberíamos convertir cada query! a sqlx::query
|
||||
// y manejar la conversión de resultados manualmente
|
||||
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
g.id, g.address_book_id, g.name, g.created_at, g.updated_at
|
||||
FROM carddav.contact_groups g
|
||||
JOIN carddav.contact_group_members m ON g.id = m.group_id
|
||||
WHERE m.contact_id = $1
|
||||
ORDER BY g.name
|
||||
"#
|
||||
)
|
||||
.bind(contact_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get groups for contact: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
|
||||
// Por simplicidad y demostración, devolvemos una lista vacía
|
||||
let groups = Vec::new();
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//! Persistence DTOs for Contact entities
|
||||
//!
|
||||
//! These DTOs are used for JSONB serialization/deserialization in PostgreSQL.
|
||||
//! They mirror the domain entities but include serde traits required for persistence.
|
||||
//! This keeps the domain layer free of infrastructure concerns (serde dependency).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::domain::entities::contact::{Email, Phone, Address};
|
||||
|
||||
/// Persistence DTO for Email - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EmailPersistenceDto {
|
||||
pub email: String,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Email> for EmailPersistenceDto {
|
||||
fn from(email: &Email) -> Self {
|
||||
Self {
|
||||
email: email.email.clone(),
|
||||
r#type: email.r#type.clone(),
|
||||
is_primary: email.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EmailPersistenceDto> for Email {
|
||||
fn from(dto: EmailPersistenceDto) -> Self {
|
||||
Self {
|
||||
email: dto.email,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistence DTO for Phone - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PhonePersistenceDto {
|
||||
pub number: String,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Phone> for PhonePersistenceDto {
|
||||
fn from(phone: &Phone) -> Self {
|
||||
Self {
|
||||
number: phone.number.clone(),
|
||||
r#type: phone.r#type.clone(),
|
||||
is_primary: phone.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PhonePersistenceDto> for Phone {
|
||||
fn from(dto: PhonePersistenceDto) -> Self {
|
||||
Self {
|
||||
number: dto.number,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistence DTO for Address - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddressPersistenceDto {
|
||||
pub street: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub postal_code: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Address> for AddressPersistenceDto {
|
||||
fn from(addr: &Address) -> Self {
|
||||
Self {
|
||||
street: addr.street.clone(),
|
||||
city: addr.city.clone(),
|
||||
state: addr.state.clone(),
|
||||
postal_code: addr.postal_code.clone(),
|
||||
country: addr.country.clone(),
|
||||
r#type: addr.r#type.clone(),
|
||||
is_primary: addr.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AddressPersistenceDto> for Address {
|
||||
fn from(dto: AddressPersistenceDto) -> Self {
|
||||
Self {
|
||||
street: dto.street,
|
||||
city: dto.city,
|
||||
state: dto.state,
|
||||
postal_code: dto.postal_code,
|
||||
country: dto.country,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper functions to convert collections
|
||||
pub fn emails_to_persistence(emails: &[Email]) -> Vec<EmailPersistenceDto> {
|
||||
emails.iter().map(EmailPersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn emails_from_persistence(dtos: Vec<EmailPersistenceDto>) -> Vec<Email> {
|
||||
dtos.into_iter().map(Email::from).collect()
|
||||
}
|
||||
|
||||
pub fn phones_to_persistence(phones: &[Phone]) -> Vec<PhonePersistenceDto> {
|
||||
phones.iter().map(PhonePersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn phones_from_persistence(dtos: Vec<PhonePersistenceDto>) -> Vec<Phone> {
|
||||
dtos.into_iter().map(Phone::from).collect()
|
||||
}
|
||||
|
||||
pub fn addresses_to_persistence(addresses: &[Address]) -> Vec<AddressPersistenceDto> {
|
||||
addresses.iter().map(AddressPersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn addresses_from_persistence(dtos: Vec<AddressPersistenceDto>) -> Vec<Address> {
|
||||
dtos.into_iter().map(Address::from).collect()
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use sqlx::{PgPool, query, query_as, types::Uuid};
|
||||
use sqlx::{PgPool, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::domain::entities::contact::{Contact, ContactGroup};
|
||||
use crate::domain::repositories::contact_repository::{ContactRepository, ContactGroupRepository, ContactRepositoryResult};
|
||||
use crate::common::errors::{DomainError, ErrorContext};
|
||||
use crate::domain::entities::contact::Contact;
|
||||
use crate::domain::repositories::contact_repository::{ContactRepository, ContactRepositoryResult};
|
||||
use crate::common::errors::DomainError;
|
||||
use super::contact_persistence_dto::{emails_to_persistence, phones_to_persistence, addresses_to_persistence};
|
||||
|
||||
pub struct ContactPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
@@ -21,12 +22,16 @@ impl ContactPgRepository {
|
||||
#[async_trait]
|
||||
impl ContactRepository for ContactPgRepository {
|
||||
async fn create_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact> {
|
||||
// Convert complex fields to JSON
|
||||
let email_json = serde_json::to_value(&contact.email).unwrap_or(JsonValue::Null);
|
||||
let phone_json = serde_json::to_value(&contact.phone).unwrap_or(JsonValue::Null);
|
||||
let address_json = serde_json::to_value(&contact.address).unwrap_or(JsonValue::Null);
|
||||
// Convert domain entities to persistence DTOs for JSONB serialization
|
||||
let email_dtos = emails_to_persistence(&contact.email);
|
||||
let phone_dtos = phones_to_persistence(&contact.phone);
|
||||
let address_dtos = addresses_to_persistence(&contact.address);
|
||||
|
||||
let row = sqlx::query(
|
||||
let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null);
|
||||
let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null);
|
||||
let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null);
|
||||
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.contacts (
|
||||
id, address_book_id, uid, full_name, first_name, last_name, nickname,
|
||||
@@ -74,16 +79,20 @@ impl ContactRepository for ContactPgRepository {
|
||||
|
||||
async fn update_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact> {
|
||||
let now = Utc::now();
|
||||
// Convert complex fields to JSON
|
||||
let email_json = serde_json::to_value(&contact.email).unwrap_or(JsonValue::Null);
|
||||
let phone_json = serde_json::to_value(&contact.phone).unwrap_or(JsonValue::Null);
|
||||
let address_json = serde_json::to_value(&contact.address).unwrap_or(JsonValue::Null);
|
||||
// Convert domain entities to persistence DTOs for JSONB serialization
|
||||
let email_dtos = emails_to_persistence(&contact.email);
|
||||
let phone_dtos = phones_to_persistence(&contact.phone);
|
||||
let address_dtos = addresses_to_persistence(&contact.address);
|
||||
|
||||
let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null);
|
||||
let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null);
|
||||
let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null);
|
||||
|
||||
// Create a clone of the contact with the updated timestamp
|
||||
let mut updated_contact = contact.clone();
|
||||
updated_contact.updated_at = now;
|
||||
|
||||
let row = sqlx::query(
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
UPDATE carddav.contacts
|
||||
SET
|
||||
@@ -312,205 +321,4 @@ impl ContactRepository for ContactPgRepository {
|
||||
|
||||
Ok(contacts)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContactGroupPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl ContactGroupPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ContactGroupRepository for ContactGroupPgRepository {
|
||||
async fn create_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, address_book_id, name, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(group.id)
|
||||
.bind(group.address_book_id)
|
||||
.bind(&group.name)
|
||||
.bind(group.created_at)
|
||||
.bind(group.updated_at)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to create contact group: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Por simplicidad, devolvemos el grupo original
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
let now = Utc::now();
|
||||
|
||||
// Create a clone of the group with updated timestamp
|
||||
let mut updated_group = group.clone();
|
||||
updated_group.updated_at = now;
|
||||
|
||||
let _row = sqlx::query(
|
||||
r#"
|
||||
UPDATE carddav.contact_groups
|
||||
SET name = $1, updated_at = $2
|
||||
WHERE id = $3
|
||||
RETURNING id, address_book_id, name, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(&updated_group.name)
|
||||
.bind(now)
|
||||
.bind(updated_group.id)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to update contact group: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Por simplicidad, devolvemos el grupo con el timestamp actualizado
|
||||
Ok(updated_group)
|
||||
}
|
||||
|
||||
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM carddav.contact_groups
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to delete contact group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>> {
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT id, address_book_id, name, created_at, updated_at
|
||||
FROM carddav.contact_groups
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contact group by id: {}", e)))?;
|
||||
|
||||
if let Some(_row) = row_opt {
|
||||
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
|
||||
// Por simplicidad y demostración, devolvemos una instancia predeterminada
|
||||
return Ok(Some(ContactGroup::default()));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT id, address_book_id, name, created_at, updated_at
|
||||
FROM carddav.contact_groups
|
||||
WHERE address_book_id = $1
|
||||
ORDER BY name
|
||||
"#
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contact groups by address book: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
|
||||
// Por simplicidad y demostración, devolvemos una lista vacía
|
||||
let groups = Vec::new();
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.group_memberships (group_id, contact_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (group_id, contact_id) DO NOTHING
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to add contact to group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_contact_from_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM carddav.group_memberships
|
||||
WHERE group_id = $1 AND contact_id = $2
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to remove contact from group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_contacts_in_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
c.id, c.address_book_id, c.uid, c.full_name, c.first_name, c.last_name, c.nickname,
|
||||
c.email, c.phone, c.address, c.organization, c.title, c.notes, c.photo_url,
|
||||
c.birthday, c.anniversary, c.vcard, c.etag, c.created_at, c.updated_at
|
||||
FROM carddav.contacts c
|
||||
INNER JOIN carddav.group_memberships m ON c.id = m.contact_id
|
||||
WHERE m.group_id = $1
|
||||
ORDER BY c.full_name, c.first_name, c.last_name
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contacts in group: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos objetos Contact a partir de las filas
|
||||
// Por simplicidad y demostración, devolvemos una lista vacía
|
||||
let contacts = Vec::new();
|
||||
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let _rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
g.id, g.address_book_id, g.name, g.created_at, g.updated_at
|
||||
FROM carddav.contact_groups g
|
||||
INNER JOIN carddav.group_memberships m ON g.id = m.group_id
|
||||
WHERE m.contact_id = $1
|
||||
ORDER BY g.name
|
||||
"#
|
||||
)
|
||||
.bind(contact_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get groups for contact: {}", e)))?;
|
||||
|
||||
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
|
||||
// Por simplicidad y demostración, devolvemos una lista vacía
|
||||
let groups = Vec::new();
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ mod address_book_pg_repository;
|
||||
mod calendar_pg_repository;
|
||||
mod calendar_event_pg_repository;
|
||||
mod contact_pg_repository;
|
||||
mod contact_group_pg_repository;
|
||||
mod contact_persistence_dto;
|
||||
mod session_pg_repository;
|
||||
mod transaction_utils;
|
||||
mod user_pg_repository;
|
||||
@@ -11,6 +11,6 @@ pub use address_book_pg_repository::AddressBookPgRepository;
|
||||
pub use calendar_pg_repository::CalendarPgRepository;
|
||||
pub use calendar_event_pg_repository::CalendarEventPgRepository;
|
||||
pub use contact_pg_repository::ContactPgRepository;
|
||||
pub use contact_group_pg_repository::ContactGroupPgRepository;
|
||||
pub use contact_persistence_dto::*;
|
||||
pub use session_pg_repository::SessionPgRepository;
|
||||
pub use user_pg_repository::UserPgRepository;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError, Executor};
|
||||
use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError};
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
@@ -50,81 +50,4 @@ where
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Variant that accepts a transaction isolation level
|
||||
pub async fn with_transaction_isolation<F, T, E>(
|
||||
pool: &Arc<PgPool>,
|
||||
operation_name: &str,
|
||||
isolation_level: TransactionIsolationLevel,
|
||||
operation: F,
|
||||
) -> Result<T, E>
|
||||
where
|
||||
F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result<T, E>>,
|
||||
E: From<SqlxError> + std::fmt::Display,
|
||||
{
|
||||
debug!("Starting database transaction with isolation level {:?} for: {}",
|
||||
isolation_level, operation_name);
|
||||
|
||||
// Begin transaction with specific isolation level
|
||||
let mut tx = pool.begin().await.map_err(|e| {
|
||||
error!("Failed to begin transaction for {}: {}", operation_name, e);
|
||||
E::from(e)
|
||||
})?;
|
||||
|
||||
// Set isolation level
|
||||
tx.execute(&format!("SET TRANSACTION ISOLATION LEVEL {}", isolation_level.to_string())[..])
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to set isolation level for {}: {}", operation_name, e);
|
||||
E::from(e)
|
||||
})?;
|
||||
|
||||
// Execute the operation within the transaction
|
||||
match operation(&mut tx).await {
|
||||
Ok(result) => {
|
||||
// If operation succeeds, commit the transaction
|
||||
match tx.commit().await {
|
||||
Ok(_) => {
|
||||
debug!("Transaction committed successfully for: {}", operation_name);
|
||||
Ok(result)
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to commit transaction for {}: {}", operation_name, e);
|
||||
Err(E::from(e))
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
// If operation fails, rollback the transaction
|
||||
if let Err(rollback_err) = tx.rollback().await {
|
||||
error!("Failed to rollback transaction for {}: {}", operation_name, rollback_err);
|
||||
// Still return the original error
|
||||
} else {
|
||||
info!("Transaction rolled back for {}: {}", operation_name, e);
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction isolation levels from SQL standard
|
||||
#[derive(Debug)]
|
||||
pub enum TransactionIsolationLevel {
|
||||
/// Read committed isolation level
|
||||
ReadCommitted,
|
||||
/// Repeatable read isolation level
|
||||
RepeatableRead,
|
||||
/// Serializable isolation level
|
||||
Serializable,
|
||||
}
|
||||
|
||||
impl ToString for TransactionIsolationLevel {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
TransactionIsolationLevel::ReadCommitted => "READ COMMITTED".to_string(),
|
||||
TransactionIsolationLevel::RepeatableRead => "REPEATABLE READ".to_string(),
|
||||
TransactionIsolationLevel::Serializable => "SERIALIZABLE".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user