refactor(ports): CQRS split — UserRepository = UserReader + UserWriter supertrait
This commit is contained in:
@@ -189,7 +189,7 @@ mod tests {
|
||||
thought::{Thought, Visibility},
|
||||
user::User,
|
||||
},
|
||||
ports::{SearchPort, ThoughtRepository, UserRepository},
|
||||
ports::{SearchPort, ThoughtRepository, UserWriter},
|
||||
value_objects::*,
|
||||
};
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::user::PgUserRepository;
|
||||
use chrono::Utc;
|
||||
use domain::ports::UserRepository;
|
||||
use domain::ports::UserWriter;
|
||||
use domain::{models::user::User, value_objects::*};
|
||||
|
||||
async fn seed_user(pool: &sqlx::PgPool) -> User {
|
||||
|
||||
@@ -338,7 +338,7 @@ mod tests {
|
||||
thought::{Thought, Visibility},
|
||||
user::User,
|
||||
},
|
||||
ports::{ThoughtRepository, UserRepository},
|
||||
ports::{ThoughtRepository, UserWriter},
|
||||
value_objects::*,
|
||||
};
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::user::PgUserRepository;
|
||||
use chrono::Utc;
|
||||
use domain::ports::UserRepository;
|
||||
use domain::ports::UserWriter;
|
||||
use domain::{
|
||||
models::{notification::NotificationType, user::User},
|
||||
value_objects::*,
|
||||
|
||||
@@ -132,7 +132,7 @@ impl TagRepository for PgTagRepository {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{thought::PgThoughtRepository, user::PgUserRepository};
|
||||
use domain::ports::{ThoughtRepository, UserRepository};
|
||||
use domain::ports::{ThoughtRepository, UserWriter};
|
||||
use domain::{
|
||||
models::{
|
||||
thought::{Thought, Visibility},
|
||||
|
||||
@@ -4,7 +4,7 @@ use domain::{
|
||||
thought::{Thought, Visibility},
|
||||
user::User,
|
||||
},
|
||||
ports::{ThoughtRepository, UserRepository},
|
||||
ports::{ThoughtRepository, UserWriter},
|
||||
value_objects::{Content, Email, PasswordHash, ThoughtId, UserId, Username},
|
||||
};
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ impl TopFriendRepository for PgTopFriendRepository {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::user::PgUserRepository;
|
||||
use domain::ports::UserRepository;
|
||||
use domain::ports::UserWriter;
|
||||
use domain::{models::user::User, value_objects::*};
|
||||
|
||||
async fn seed_user(pool: &sqlx::PgPool, username: &str, email: &str) -> User {
|
||||
|
||||
@@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{feed::UserSummary, user::User},
|
||||
ports::UserRepository,
|
||||
ports::{UserReader, UserWriter},
|
||||
value_objects::{Email, PasswordHash, UserId, Username},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
@@ -58,7 +58,7 @@ pub const USER_SELECT: &str =
|
||||
custom_css,local,created_at,updated_at FROM users";
|
||||
|
||||
#[async_trait]
|
||||
impl UserRepository for PgUserRepository {
|
||||
impl UserReader for PgUserRepository {
|
||||
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
||||
sqlx::query_as::<_, UserRow>(&format!("{USER_SELECT} WHERE id=$1"))
|
||||
.bind(id.as_uuid())
|
||||
@@ -86,6 +86,60 @@ impl UserRepository for PgUserRepository {
|
||||
.map(|o| o.map(User::from))
|
||||
}
|
||||
|
||||
async fn list_with_stats(&self) -> Result<Vec<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,
|
||||
}
|
||||
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
|
||||
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",
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.into_domain()?;
|
||||
|
||||
Ok(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())
|
||||
}
|
||||
|
||||
async fn count(&self) -> Result<i64, DomainError> {
|
||||
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM users WHERE local = true")
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.into_domain()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserWriter for PgUserRepository {
|
||||
async fn save(&self, user: &User) -> Result<(), DomainError> {
|
||||
sqlx::query(
|
||||
"INSERT INTO users (id,username,email,password_hash,display_name,bio,avatar_url,header_url,custom_css,local,created_at,updated_at)
|
||||
@@ -139,57 +193,6 @@ impl UserRepository for PgUserRepository {
|
||||
.into_domain()
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn list_with_stats(&self) -> Result<Vec<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,
|
||||
}
|
||||
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
|
||||
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",
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.into_domain()?;
|
||||
|
||||
Ok(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())
|
||||
}
|
||||
|
||||
async fn count(&self) -> Result<i64, DomainError> {
|
||||
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM users WHERE local = true")
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.into_domain()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user