feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user