feat(postgres): add list_paginated + find_by_ids to PgUserRepository

This commit is contained in:
2026-05-16 11:04:33 +02:00
parent 0222a168db
commit ae8a3dc6ed

View File

@@ -3,11 +3,13 @@ use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
models::{feed::UserSummary, user::User}, models::feed::{PageParams, Paginated, UserSummary},
models::user::User,
ports::{UserReader, UserWriter}, ports::{UserReader, UserWriter},
value_objects::{Email, PasswordHash, UserId, Username}, value_objects::{Email, PasswordHash, UserId, Username},
}; };
use sqlx::PgPool; use sqlx::PgPool;
use std::collections::HashMap;
pub struct PgUserRepository { pub struct PgUserRepository {
pool: PgPool, pool: PgPool,
@@ -136,6 +138,76 @@ impl UserReader for PgUserRepository {
.await .await
.into_domain() .into_domain()
} }
async fn list_paginated(&self, page: PageParams) -> Result<Paginated<UserSummary>, DomainError> {
#[derive(sqlx::FromRow)]
struct Row {
id: uuid::Uuid,
username: String,
display_name: Option<String>,
avatar_url: Option<String>,
bio: Option<String>,
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<HashMap<UserId, User>, DomainError> {
if ids.is_empty() {
return Ok(HashMap::new());
}
let uuids: Vec<uuid::Uuid> = 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] #[async_trait]