diff --git a/crates/adapters/postgres/src/user.rs b/crates/adapters/postgres/src/user.rs index 7d26fd8..9019b16 100644 --- a/crates/adapters/postgres/src/user.rs +++ b/crates/adapters/postgres/src/user.rs @@ -3,11 +3,13 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use domain::{ errors::DomainError, - models::{feed::UserSummary, user::User}, + models::feed::{PageParams, Paginated, UserSummary}, + models::user::User, ports::{UserReader, UserWriter}, value_objects::{Email, PasswordHash, UserId, Username}, }; use sqlx::PgPool; +use std::collections::HashMap; pub struct PgUserRepository { pool: PgPool, @@ -136,6 +138,76 @@ impl UserReader for PgUserRepository { .await .into_domain() } + + async fn list_paginated(&self, page: PageParams) -> Result, DomainError> { + #[derive(sqlx::FromRow)] + struct Row { + id: uuid::Uuid, + username: String, + display_name: Option, + avatar_url: Option, + bio: Option, + thought_count: i64, + follower_count: i64, + following_count: i64, + total: i64, + } + let rows = sqlx::query_as::<_, Row>( + "SELECT u.id, u.username, u.display_name, u.avatar_url, u.bio, + COUNT(DISTINCT t.id) AS thought_count, + COUNT(DISTINCT f1.follower_id) AS follower_count, + COUNT(DISTINCT f2.following_id) AS following_count, + COUNT(*) OVER() AS total + FROM users u + LEFT JOIN thoughts t ON t.user_id=u.id AND t.local=true + LEFT JOIN follows f1 ON f1.following_id=u.id AND f1.state='accepted' + LEFT JOIN follows f2 ON f2.follower_id=u.id AND f2.state='accepted' + WHERE u.local=true + GROUP BY u.id + ORDER BY u.username + LIMIT $1 OFFSET $2", + ) + .bind(page.limit()) + .bind(page.offset()) + .fetch_all(&self.pool) + .await + .into_domain()?; + + let total = rows.first().map(|r| r.total).unwrap_or(0); + let items = rows + .into_iter() + .map(|r| UserSummary { + id: UserId::from_uuid(r.id), + username: r.username, + display_name: r.display_name, + avatar_url: r.avatar_url, + bio: r.bio, + thought_count: r.thought_count, + follower_count: r.follower_count, + following_count: r.following_count, + }) + .collect(); + Ok(Paginated { items, total, page: page.page, per_page: page.per_page }) + } + + async fn find_by_ids(&self, ids: &[UserId]) -> Result, DomainError> { + if ids.is_empty() { + return Ok(HashMap::new()); + } + let uuids: Vec = ids.iter().map(|id| id.as_uuid()).collect(); + let rows = sqlx::query_as::<_, UserRow>( + &format!("{USER_SELECT} WHERE id = ANY($1)") + ) + .bind(&uuids[..]) + .fetch_all(&self.pool) + .await + .into_domain()?; + + Ok(rows.into_iter().map(|r| { + let user = User::from(r); + (user.id.clone(), user) + }).collect()) + } } #[async_trait]