refactoring hexagonal and clean architecture

This commit is contained in:
Diocrafts
2026-02-08 13:40:23 +01:00
parent 3e4fb67c19
commit a82faa5eaf
101 changed files with 7433 additions and 9721 deletions
@@ -27,28 +27,28 @@ impl AddressBookRepository for AddressBookPgRepository {
RETURNING id, name, owner_id, description, color, is_public, created_at, updated_at
"#
)
.bind(address_book.id)
.bind(&address_book.name)
.bind(&address_book.owner_id)
.bind(&address_book.description)
.bind(&address_book.color)
.bind(address_book.is_public)
.bind(address_book.created_at)
.bind(address_book.updated_at)
.bind(address_book.id())
.bind(address_book.name())
.bind(address_book.owner_id())
.bind(address_book.description())
.bind(address_book.color())
.bind(address_book.is_public())
.bind(address_book.created_at())
.bind(address_book.updated_at())
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to create address book: {}", e)))?;
Ok(AddressBook {
id: row.get("id"),
name: row.get("name"),
owner_id: row.get("owner_id"),
description: row.get("description"),
color: row.get("color"),
is_public: row.get("is_public"),
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
})
Ok(AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
))
}
async fn update_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook> {
@@ -61,26 +61,26 @@ impl AddressBookRepository for AddressBookPgRepository {
RETURNING id, name, owner_id, description, color, is_public, created_at, updated_at
"#
)
.bind(&address_book.name)
.bind(&address_book.description)
.bind(&address_book.color)
.bind(address_book.is_public)
.bind(address_book.name())
.bind(address_book.description())
.bind(address_book.color())
.bind(address_book.is_public())
.bind(now)
.bind(address_book.id)
.bind(address_book.id())
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to update address book: {}", e)))?;
Ok(AddressBook {
id: row.get("id"),
name: row.get("name"),
owner_id: row.get("owner_id"),
description: row.get("description"),
color: row.get("color"),
is_public: row.get("is_public"),
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
})
Ok(AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
))
}
async fn delete_address_book(&self, id: &Uuid) -> AddressBookRepositoryResult<()> {
@@ -111,16 +111,16 @@ impl AddressBookRepository for AddressBookPgRepository {
.await
.map_err(|e| DomainError::database_error(format!("Failed to get address book by id: {}", e)))?;
let result = maybe_row.map(|row| AddressBook {
id: row.get("id"),
name: row.get("name"),
owner_id: row.get("owner_id"),
description: row.get("description"),
color: row.get("color"),
is_public: row.get("is_public"),
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
});
let result = maybe_row.map(|row| AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
));
Ok(result)
}
@@ -140,16 +140,16 @@ impl AddressBookRepository for AddressBookPgRepository {
.map_err(|e| DomainError::database_error(format!("Failed to get address books by owner: {}", e)))?;
let result = rows.into_iter()
.map(|row| AddressBook {
id: row.get("id"),
name: row.get("name"),
owner_id: row.get("owner_id"),
description: row.get("description"),
color: row.get("color"),
is_public: row.get("is_public"),
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
})
.map(|row| AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
))
.collect();
Ok(result)
@@ -171,16 +171,16 @@ impl AddressBookRepository for AddressBookPgRepository {
.map_err(|e| DomainError::database_error(format!("Failed to get shared address books: {}", e)))?;
let result = rows.into_iter()
.map(|row| AddressBook {
id: row.get("id"),
name: row.get("name"),
owner_id: row.get("owner_id"),
description: row.get("description"),
color: row.get("color"),
is_public: row.get("is_public"),
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
})
.map(|row| AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
))
.collect();
Ok(result)
@@ -200,16 +200,16 @@ impl AddressBookRepository for AddressBookPgRepository {
.map_err(|e| DomainError::database_error(format!("Failed to get public address books: {}", e)))?;
let result = rows.into_iter()
.map(|row| AddressBook {
id: row.get("id"),
name: row.get("name"),
owner_id: row.get("owner_id"),
description: row.get("description"),
color: row.get("color"),
is_public: row.get("is_public"),
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
})
.map(|row| AddressBook::from_raw(
row.get("id"),
row.get("name"),
row.get("owner_id"),
row.get("description"),
row.get("color"),
row.get("is_public"),
row.get("created_at"),
row.get("updated_at"),
))
.collect();
Ok(result)
@@ -23,9 +23,9 @@ impl ContactPgRepository {
impl ContactRepository for ContactPgRepository {
async fn create_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact> {
// 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_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);
@@ -48,26 +48,26 @@ impl ContactRepository for ContactPgRepository {
birthday, anniversary, vcard, etag, created_at, updated_at
"#
)
.bind(contact.id)
.bind(contact.address_book_id)
.bind(&contact.uid)
.bind(&contact.full_name)
.bind(&contact.first_name)
.bind(&contact.last_name)
.bind(&contact.nickname)
.bind(contact.id())
.bind(contact.address_book_id())
.bind(contact.uid())
.bind(contact.full_name_owned())
.bind(contact.first_name_owned())
.bind(contact.last_name_owned())
.bind(contact.nickname_owned())
.bind(email_json)
.bind(phone_json)
.bind(address_json)
.bind(&contact.organization)
.bind(&contact.title)
.bind(&contact.notes)
.bind(&contact.photo_url)
.bind(contact.birthday)
.bind(contact.anniversary)
.bind(&contact.vcard)
.bind(&contact.etag)
.bind(contact.created_at)
.bind(contact.updated_at)
.bind(contact.organization_owned())
.bind(contact.title_owned())
.bind(contact.notes_owned())
.bind(contact.photo_url_owned())
.bind(contact.birthday().copied())
.bind(contact.anniversary().copied())
.bind(contact.vcard())
.bind(contact.etag())
.bind(*contact.created_at())
.bind(*contact.updated_at())
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to create contact: {}", e)))?;
@@ -80,9 +80,9 @@ impl ContactRepository for ContactPgRepository {
async fn update_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact> {
let now = Utc::now();
// 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_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);
@@ -90,7 +90,7 @@ impl ContactRepository for ContactPgRepository {
// Create a clone of the contact with the updated timestamp
let mut updated_contact = contact.clone();
updated_contact.updated_at = now;
updated_contact.set_updated_at(now);
let _row = sqlx::query(
r#"
@@ -119,23 +119,23 @@ impl ContactRepository for ContactPgRepository {
birthday, anniversary, vcard, etag, created_at, updated_at
"#
)
.bind(&updated_contact.full_name)
.bind(&updated_contact.first_name)
.bind(&updated_contact.last_name)
.bind(&updated_contact.nickname)
.bind(updated_contact.full_name_owned())
.bind(updated_contact.first_name_owned())
.bind(updated_contact.last_name_owned())
.bind(updated_contact.nickname_owned())
.bind(email_json)
.bind(phone_json)
.bind(address_json)
.bind(&updated_contact.organization)
.bind(&updated_contact.title)
.bind(&updated_contact.notes)
.bind(&updated_contact.photo_url)
.bind(updated_contact.birthday)
.bind(updated_contact.anniversary)
.bind(&updated_contact.vcard)
.bind(&updated_contact.etag)
.bind(updated_contact.organization_owned())
.bind(updated_contact.title_owned())
.bind(updated_contact.notes_owned())
.bind(updated_contact.photo_url_owned())
.bind(updated_contact.birthday().copied())
.bind(updated_contact.anniversary().copied())
.bind(updated_contact.vcard())
.bind(updated_contact.etag())
.bind(now)
.bind(updated_contact.id)
.bind(updated_contact.id())
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to update contact: {}", e)))?;
@@ -0,0 +1,130 @@
use std::sync::Arc;
use async_trait::async_trait;
use sqlx::{PgPool, Row};
use tracing::error;
use uuid::Uuid;
use crate::application::dtos::favorites_dto::FavoriteItemDto;
use crate::application::ports::favorites_ports::FavoritesRepositoryPort;
use crate::common::errors::{Result, DomainError, ErrorKind};
/// Implementación PostgreSQL del puerto de persistencia de favoritos.
pub struct FavoritesPgRepository {
db_pool: Arc<PgPool>,
}
impl FavoritesPgRepository {
pub fn new(db_pool: Arc<PgPool>) -> Self {
Self { db_pool }
}
}
#[async_trait]
impl FavoritesRepositoryPort for FavoritesPgRepository {
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>> {
let user_uuid = Uuid::parse_str(user_id)?;
let rows = sqlx::query(
r#"
SELECT
id::TEXT AS "id",
user_id::TEXT AS "user_id",
item_id AS "item_id",
item_type AS "item_type",
created_at AS "created_at"
FROM auth.user_favorites
WHERE user_id = $1::TEXT
ORDER BY created_at DESC
"#,
)
.bind(user_uuid)
.fetch_all(&*self.db_pool)
.await
.map_err(|e| {
error!("Database error fetching favorites: {}", e);
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to fetch favorites: {}", e))
})?;
let favorites = rows
.iter()
.map(|row| FavoriteItemDto {
id: row.get("id"),
user_id: row.get("user_id"),
item_id: row.get("item_id"),
item_type: row.get("item_type"),
created_at: row.get("created_at"),
})
.collect();
Ok(favorites)
}
async fn add_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
let user_uuid = Uuid::parse_str(user_id)?;
sqlx::query(
r#"
INSERT INTO auth.user_favorites (user_id, item_id, item_type)
VALUES ($1::TEXT, $2, $3)
ON CONFLICT (user_id, item_id, item_type) DO NOTHING
"#,
)
.bind(user_uuid)
.bind(item_id)
.bind(item_type)
.execute(&*self.db_pool)
.await
.map_err(|e| {
error!("Database error adding favorite: {}", e);
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to add to favorites: {}", e))
})?;
Ok(())
}
async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
let user_uuid = Uuid::parse_str(user_id)?;
let result = sqlx::query(
r#"
DELETE FROM auth.user_favorites
WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3
"#,
)
.bind(user_uuid)
.bind(item_id)
.bind(item_type)
.execute(&*self.db_pool)
.await
.map_err(|e| {
error!("Database error removing favorite: {}", e);
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to remove from favorites: {}", e))
})?;
Ok(result.rows_affected() > 0)
}
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
let user_uuid = Uuid::parse_str(user_id)?;
let row = sqlx::query(
r#"
SELECT EXISTS (
SELECT 1 FROM auth.user_favorites
WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3
) AS "is_favorite"
"#,
)
.bind(user_uuid)
.bind(item_id)
.bind(item_type)
.fetch_one(&*self.db_pool)
.await
.map_err(|e| {
error!("Database error checking favorite status: {}", e);
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to check favorite status: {}", e))
})?;
Ok(row.try_get("is_favorite").unwrap_or(false))
}
}
@@ -3,6 +3,8 @@ mod calendar_pg_repository;
mod calendar_event_pg_repository;
mod contact_pg_repository;
mod contact_persistence_dto;
mod favorites_pg_repository;
mod recent_items_pg_repository;
mod session_pg_repository;
mod transaction_utils;
mod user_pg_repository;
@@ -12,5 +14,7 @@ pub use calendar_pg_repository::CalendarPgRepository;
pub use calendar_event_pg_repository::CalendarEventPgRepository;
pub use contact_pg_repository::ContactPgRepository;
pub use contact_persistence_dto::*;
pub use favorites_pg_repository::FavoritesPgRepository;
pub use recent_items_pg_repository::RecentItemsPgRepository;
pub use session_pg_repository::SessionPgRepository;
pub use user_pg_repository::UserPgRepository;
@@ -0,0 +1,155 @@
use std::sync::Arc;
use async_trait::async_trait;
use sqlx::{PgPool, Row};
use tracing::error;
use uuid::Uuid;
use crate::application::dtos::recent_dto::RecentItemDto;
use crate::application::ports::recent_ports::RecentItemsRepositoryPort;
use crate::common::errors::{Result, DomainError, ErrorKind};
/// Implementación PostgreSQL del puerto de persistencia de elementos recientes.
pub struct RecentItemsPgRepository {
db_pool: Arc<PgPool>,
}
impl RecentItemsPgRepository {
pub fn new(db_pool: Arc<PgPool>) -> Self {
Self { db_pool }
}
}
#[async_trait]
impl RecentItemsRepositoryPort for RecentItemsPgRepository {
async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result<Vec<RecentItemDto>> {
let user_uuid = Uuid::parse_str(user_id)?;
let rows = sqlx::query(
r#"
SELECT
id::TEXT AS "id",
user_id::TEXT AS "user_id",
item_id AS "item_id",
item_type AS "item_type",
accessed_at AS "accessed_at"
FROM auth.user_recent_files
WHERE user_id = $1::TEXT
ORDER BY accessed_at DESC
LIMIT $2
"#,
)
.bind(user_uuid)
.bind(limit)
.fetch_all(&*self.db_pool)
.await
.map_err(|e| {
error!("Database error fetching recent items: {}", e);
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to fetch recent items: {}", e))
})?;
let items = rows
.iter()
.map(|row| RecentItemDto {
id: row.get("id"),
user_id: row.get("user_id"),
item_id: row.get("item_id"),
item_type: row.get("item_type"),
accessed_at: row.get("accessed_at"),
})
.collect();
Ok(items)
}
async fn upsert_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
let user_uuid = Uuid::parse_str(user_id)?;
sqlx::query(
r#"
INSERT INTO auth.user_recent_files (user_id, item_id, item_type, accessed_at)
VALUES ($1::TEXT, $2, $3, CURRENT_TIMESTAMP)
ON CONFLICT (user_id, item_id, item_type)
DO UPDATE SET accessed_at = CURRENT_TIMESTAMP
"#,
)
.bind(user_uuid)
.bind(item_id)
.bind(item_type)
.execute(&*self.db_pool)
.await
.map_err(|e| {
error!("Database error upserting recent item access: {}", e);
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to record item access: {}", e))
})?;
Ok(())
}
async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
let user_uuid = Uuid::parse_str(user_id)?;
let result = sqlx::query(
r#"
DELETE FROM auth.user_recent_files
WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3
"#,
)
.bind(user_uuid)
.bind(item_id)
.bind(item_type)
.execute(&*self.db_pool)
.await
.map_err(|e| {
error!("Database error removing recent item: {}", e);
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to remove recent item: {}", e))
})?;
Ok(result.rows_affected() > 0)
}
async fn clear_all(&self, user_id: &str) -> Result<()> {
let user_uuid = Uuid::parse_str(user_id)?;
sqlx::query(
r#"
DELETE FROM auth.user_recent_files
WHERE user_id = $1::TEXT
"#,
)
.bind(user_uuid)
.execute(&*self.db_pool)
.await
.map_err(|e| {
error!("Database error clearing recent items: {}", e);
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to clear recent items: {}", e))
})?;
Ok(())
}
async fn prune(&self, user_id: &str, max_items: i32) -> Result<()> {
let user_uuid = Uuid::parse_str(user_id)?;
sqlx::query(
r#"
DELETE FROM auth.user_recent_files
WHERE id IN (
SELECT id FROM auth.user_recent_files
WHERE user_id = $1::TEXT
ORDER BY accessed_at DESC
OFFSET $2
)
"#,
)
.bind(user_uuid)
.bind(max_items)
.execute(&*self.db_pool)
.await
.map_err(|e| {
error!("Database error pruning old recent items: {}", e);
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to prune recent items: {}", e))
})?;
Ok(())
}
}
@@ -66,8 +66,8 @@ impl SessionRepository for SessionPgRepository {
.bind(session_clone.user_id())
.bind(session_clone.refresh_token())
.bind(session_clone.expires_at())
.bind(&session_clone.ip_address)
.bind(&session_clone.user_agent)
.bind(session_clone.ip_address())
.bind(session_clone.user_agent())
.bind(session_clone.created_at())
.bind(session_clone.is_revoked())
.execute(&mut **tx)
@@ -120,16 +120,16 @@ impl SessionRepository for SessionPgRepository {
.await
.map_err(Self::map_sqlx_error)?;
Ok(Session {
id: row.get("id"),
user_id: row.get("user_id"),
refresh_token: row.get("refresh_token"),
expires_at: row.get("expires_at"),
ip_address: row.get("ip_address"),
user_agent: row.get("user_agent"),
created_at: row.get("created_at"),
revoked: row.get("revoked"),
})
Ok(Session::from_raw(
row.get("id"),
row.get("user_id"),
row.get("refresh_token"),
row.get("expires_at"),
row.get("ip_address"),
row.get("user_agent"),
row.get("created_at"),
row.get("revoked"),
))
}
/// Obtiene una sesión por token de actualización
@@ -148,16 +148,16 @@ impl SessionRepository for SessionPgRepository {
.await
.map_err(Self::map_sqlx_error)?;
Ok(Session {
id: row.get("id"),
user_id: row.get("user_id"),
refresh_token: row.get("refresh_token"),
expires_at: row.get("expires_at"),
ip_address: row.get("ip_address"),
user_agent: row.get("user_agent"),
created_at: row.get("created_at"),
revoked: row.get("revoked"),
})
Ok(Session::from_raw(
row.get("id"),
row.get("user_id"),
row.get("refresh_token"),
row.get("expires_at"),
row.get("ip_address"),
row.get("user_agent"),
row.get("created_at"),
row.get("revoked"),
))
}
/// Obtiene todas las sesiones de un usuario
@@ -179,16 +179,16 @@ impl SessionRepository for SessionPgRepository {
let sessions = rows.into_iter()
.map(|row| {
Session {
id: row.get("id"),
user_id: row.get("user_id"),
refresh_token: row.get("refresh_token"),
expires_at: row.get("expires_at"),
ip_address: row.get("ip_address"),
user_agent: row.get("user_agent"),
created_at: row.get("created_at"),
revoked: row.get("revoked"),
}
Session::from_raw(
row.get("id"),
row.get("user_id"),
row.get("refresh_token"),
row.get("expires_at"),
row.get("ip_address"),
row.get("user_agent"),
row.get("created_at"),
row.get("revoked"),
)
})
.collect();