use crate::db::PgPool; use async_trait::async_trait; use chrono::{DateTime, Utc}; use domain::{ errors::DomainError, ports::UserRepository, value_objects::{Email, PasswordHash, SystemId}, }; use uuid::Uuid; #[derive(sqlx::FromRow)] struct UserRow { id: Uuid, username: String, email: String, password_hash: String, created_at: DateTime, } impl TryFrom for domain::entities::User { type Error = DomainError; fn try_from(r: UserRow) -> Result { Ok(Self { id: SystemId::from_uuid(r.id), username: r.username, email: Email::new(r.email)?, password_hash: PasswordHash::from_hash(r.password_hash), created_at: r.created_at, }) } } pub struct PostgresUserRepository { pool: PgPool, } impl PostgresUserRepository { pub fn new(pool: PgPool) -> Self { Self { pool } } } #[async_trait] impl UserRepository for PostgresUserRepository { async fn find_by_id( &self, id: &SystemId, ) -> Result, DomainError> { let row = sqlx::query_as::<_, UserRow>( "SELECT id, username, email, password_hash, created_at FROM users WHERE id = $1", ) .bind(*id.as_uuid()) .fetch_optional(&self.pool) .await .map_err(|e| DomainError::Internal(e.to_string()))?; row.map(TryInto::try_into).transpose() } async fn find_by_email( &self, email: &Email, ) -> Result, DomainError> { let row = sqlx::query_as::<_, UserRow>( "SELECT id, username, email, password_hash, created_at FROM users WHERE email = $1", ) .bind(email.as_str()) .fetch_optional(&self.pool) .await .map_err(|e| DomainError::Internal(e.to_string()))?; row.map(TryInto::try_into).transpose() } async fn find_by_username( &self, username: &str, ) -> Result, DomainError> { let row = sqlx::query_as::<_, UserRow>( "SELECT id, username, email, password_hash, created_at FROM users WHERE username = $1", ) .bind(username) .fetch_optional(&self.pool) .await .map_err(|e| DomainError::Internal(e.to_string()))?; row.map(TryInto::try_into).transpose() } async fn save(&self, user: &domain::entities::User) -> Result<(), DomainError> { sqlx::query_as::<_, UserRow>( "INSERT INTO users (id, username, email, password_hash, created_at) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username, email = EXCLUDED.email, password_hash = EXCLUDED.password_hash RETURNING id, username, email, password_hash, created_at", ) .bind(*user.id.as_uuid()) .bind(&user.username) .bind(user.email.as_str()) .bind(user.password_hash.as_str()) .bind(user.created_at) .fetch_one(&self.pool) .await .map_err(|e| DomainError::Internal(e.to_string()))?; Ok(()) } async fn delete(&self, id: &SystemId) -> Result<(), DomainError> { sqlx::query("DELETE FROM users WHERE id = $1") .bind(*id.as_uuid()) .execute(&self.pool) .await .map_err(|e| DomainError::Internal(e.to_string()))?; Ok(()) } }