refactor(ports): CQRS split — UserRepository = UserReader + UserWriter supertrait
This commit is contained in:
@@ -189,7 +189,7 @@ mod tests {
|
|||||||
thought::{Thought, Visibility},
|
thought::{Thought, Visibility},
|
||||||
user::User,
|
user::User,
|
||||||
},
|
},
|
||||||
ports::{SearchPort, ThoughtRepository, UserRepository},
|
ports::{SearchPort, ThoughtRepository, UserWriter},
|
||||||
value_objects::*,
|
value_objects::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::user::PgUserRepository;
|
use crate::user::PgUserRepository;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use domain::ports::UserRepository;
|
use domain::ports::UserWriter;
|
||||||
use domain::{models::user::User, value_objects::*};
|
use domain::{models::user::User, value_objects::*};
|
||||||
|
|
||||||
async fn seed_user(pool: &sqlx::PgPool) -> User {
|
async fn seed_user(pool: &sqlx::PgPool) -> User {
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ mod tests {
|
|||||||
thought::{Thought, Visibility},
|
thought::{Thought, Visibility},
|
||||||
user::User,
|
user::User,
|
||||||
},
|
},
|
||||||
ports::{ThoughtRepository, UserRepository},
|
ports::{ThoughtRepository, UserWriter},
|
||||||
value_objects::*,
|
value_objects::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::user::PgUserRepository;
|
use crate::user::PgUserRepository;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use domain::ports::UserRepository;
|
use domain::ports::UserWriter;
|
||||||
use domain::{
|
use domain::{
|
||||||
models::{notification::NotificationType, user::User},
|
models::{notification::NotificationType, user::User},
|
||||||
value_objects::*,
|
value_objects::*,
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ impl TagRepository for PgTagRepository {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{thought::PgThoughtRepository, user::PgUserRepository};
|
use crate::{thought::PgThoughtRepository, user::PgUserRepository};
|
||||||
use domain::ports::{ThoughtRepository, UserRepository};
|
use domain::ports::{ThoughtRepository, UserWriter};
|
||||||
use domain::{
|
use domain::{
|
||||||
models::{
|
models::{
|
||||||
thought::{Thought, Visibility},
|
thought::{Thought, Visibility},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use domain::{
|
|||||||
thought::{Thought, Visibility},
|
thought::{Thought, Visibility},
|
||||||
user::User,
|
user::User,
|
||||||
},
|
},
|
||||||
ports::{ThoughtRepository, UserRepository},
|
ports::{ThoughtRepository, UserWriter},
|
||||||
value_objects::{Content, Email, PasswordHash, ThoughtId, UserId, Username},
|
value_objects::{Content, Email, PasswordHash, ThoughtId, UserId, Username},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ impl TopFriendRepository for PgTopFriendRepository {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::user::PgUserRepository;
|
use crate::user::PgUserRepository;
|
||||||
use domain::ports::UserRepository;
|
use domain::ports::UserWriter;
|
||||||
use domain::{models::user::User, value_objects::*};
|
use domain::{models::user::User, value_objects::*};
|
||||||
|
|
||||||
async fn seed_user(pool: &sqlx::PgPool, username: &str, email: &str) -> User {
|
async fn seed_user(pool: &sqlx::PgPool, username: &str, email: &str) -> User {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{feed::UserSummary, user::User},
|
models::{feed::UserSummary, user::User},
|
||||||
ports::UserRepository,
|
ports::{UserReader, UserWriter},
|
||||||
value_objects::{Email, PasswordHash, UserId, Username},
|
value_objects::{Email, PasswordHash, UserId, Username},
|
||||||
};
|
};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@@ -58,7 +58,7 @@ pub const USER_SELECT: &str =
|
|||||||
custom_css,local,created_at,updated_at FROM users";
|
custom_css,local,created_at,updated_at FROM users";
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl UserRepository for PgUserRepository {
|
impl UserReader for PgUserRepository {
|
||||||
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
||||||
sqlx::query_as::<_, UserRow>(&format!("{USER_SELECT} WHERE id=$1"))
|
sqlx::query_as::<_, UserRow>(&format!("{USER_SELECT} WHERE id=$1"))
|
||||||
.bind(id.as_uuid())
|
.bind(id.as_uuid())
|
||||||
@@ -86,6 +86,60 @@ impl UserRepository for PgUserRepository {
|
|||||||
.map(|o| o.map(User::from))
|
.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> {
|
async fn save(&self, user: &User) -> Result<(), DomainError> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO users (id,username,email,password_hash,display_name,bio,avatar_url,header_url,custom_css,local,created_at,updated_at)
|
"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()
|
.into_domain()
|
||||||
.map(|_| ())
|
.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)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::thought::Visibility,
|
models::thought::Visibility,
|
||||||
ports::{ActivityPubRepository, OutboundFederationPort, ThoughtRepository, UserRepository},
|
ports::{ActivityPubRepository, OutboundFederationPort, ThoughtRepository, UserReader},
|
||||||
value_objects::ThoughtId,
|
value_objects::ThoughtId,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct FederationEventService {
|
pub struct FederationEventService {
|
||||||
pub thoughts: Arc<dyn ThoughtRepository>,
|
pub thoughts: Arc<dyn ThoughtRepository>,
|
||||||
pub users: Arc<dyn UserRepository>,
|
pub users: Arc<dyn UserReader>,
|
||||||
pub ap: Arc<dyn OutboundFederationPort>,
|
pub ap: Arc<dyn OutboundFederationPort>,
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub ap_repo: Arc<dyn ActivityPubRepository>,
|
pub ap_repo: Arc<dyn ActivityPubRepository>,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::user::User,
|
models::user::User,
|
||||||
ports::{AuthService, EventPublisher, PasswordHasher, UserRepository},
|
ports::{AuthService, EventPublisher, PasswordHasher, UserReader, UserRepository},
|
||||||
value_objects::{Email, UserId, Username},
|
value_objects::{Email, UserId, Username},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ pub struct LoginOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
users: &dyn UserRepository,
|
users: &dyn UserReader,
|
||||||
hasher: &dyn PasswordHasher,
|
hasher: &dyn PasswordHasher,
|
||||||
auth: &dyn AuthService,
|
auth: &dyn AuthService,
|
||||||
input: LoginInput,
|
input: LoginInput,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use domain::{
|
|||||||
},
|
},
|
||||||
ports::{
|
ports::{
|
||||||
ActivityPubRepository, EventPublisher, FederationActionPort, FederationSchedulerPort,
|
ActivityPubRepository, EventPublisher, FederationActionPort, FederationSchedulerPort,
|
||||||
FeedRepository, FollowRepository, RemoteActorConnectionRepository, UserRepository,
|
FeedRepository, FollowRepository, RemoteActorConnectionRepository, UserReader,
|
||||||
},
|
},
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
@@ -61,7 +61,7 @@ pub async fn list_remote_following(
|
|||||||
|
|
||||||
pub async fn remove_remote_following(
|
pub async fn remove_remote_following(
|
||||||
follows: &dyn FollowRepository,
|
follows: &dyn FollowRepository,
|
||||||
users: &dyn UserRepository,
|
users: &dyn UserReader,
|
||||||
federation: &dyn FederationActionPort,
|
federation: &dyn FederationActionPort,
|
||||||
events: &dyn EventPublisher,
|
events: &dyn EventPublisher,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use domain::{
|
|||||||
feed::{FeedEntry, PageParams, Paginated, UserSummary},
|
feed::{FeedEntry, PageParams, Paginated, UserSummary},
|
||||||
user::User,
|
user::User,
|
||||||
},
|
},
|
||||||
ports::{FeedRepository, FollowRepository, TagRepository, UserRepository},
|
ports::{FeedRepository, FollowRepository, TagRepository, UserReader},
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,12 +70,12 @@ pub async fn search(
|
|||||||
feed.search(query, &page, viewer_id).await
|
feed.search(query, &page, viewer_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_users(users: &dyn UserRepository) -> Result<Vec<UserSummary>, DomainError> {
|
pub async fn list_users(users: &dyn UserReader) -> Result<Vec<UserSummary>, DomainError> {
|
||||||
users.list_with_stats().await
|
users.list_with_stats().await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_users_paginated(
|
pub async fn list_users_paginated(
|
||||||
users: &dyn UserRepository,
|
users: &dyn UserReader,
|
||||||
page: PageParams,
|
page: PageParams,
|
||||||
) -> Result<Paginated<UserSummary>, DomainError> {
|
) -> Result<Paginated<UserSummary>, DomainError> {
|
||||||
let all = users.list_with_stats().await?;
|
let all = users.list_with_stats().await?;
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::{top_friend::TopFriend, user::User},
|
models::{top_friend::TopFriend, user::User},
|
||||||
ports::{EventPublisher, TopFriendRepository, UserRepository},
|
ports::{EventPublisher, TopFriendRepository, UserReader, UserWriter},
|
||||||
value_objects::{UserId, Username},
|
value_objects::{UserId, Username},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn get_user(users: &dyn UserRepository, user_id: &UserId) -> Result<User, DomainError> {
|
pub async fn get_user(users: &dyn UserReader, user_id: &UserId) -> Result<User, DomainError> {
|
||||||
users
|
users
|
||||||
.find_by_id(user_id)
|
.find_by_id(user_id)
|
||||||
.await?
|
.await?
|
||||||
@@ -16,7 +16,7 @@ pub async fn get_user(users: &dyn UserRepository, user_id: &UserId) -> Result<Us
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_by_username(
|
pub async fn get_user_by_username(
|
||||||
users: &dyn UserRepository,
|
users: &dyn UserReader,
|
||||||
username: &str,
|
username: &str,
|
||||||
) -> Result<User, DomainError> {
|
) -> Result<User, DomainError> {
|
||||||
let username = Username::new(username).map_err(|_| DomainError::NotFound)?;
|
let username = Username::new(username).map_err(|_| DomainError::NotFound)?;
|
||||||
@@ -28,7 +28,7 @@ pub async fn get_user_by_username(
|
|||||||
|
|
||||||
/// Resolve a path segment that is either a UUID (AP actor URL) or a username.
|
/// Resolve a path segment that is either a UUID (AP actor URL) or a username.
|
||||||
pub async fn get_user_by_id_or_username(
|
pub async fn get_user_by_id_or_username(
|
||||||
users: &dyn UserRepository,
|
users: &dyn UserReader,
|
||||||
id_or_username: &str,
|
id_or_username: &str,
|
||||||
) -> Result<User, DomainError> {
|
) -> Result<User, DomainError> {
|
||||||
if let Ok(uuid) = uuid::Uuid::parse_str(id_or_username) {
|
if let Ok(uuid) = uuid::Uuid::parse_str(id_or_username) {
|
||||||
@@ -43,7 +43,7 @@ pub async fn get_user_by_id_or_username(
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn update_profile(
|
pub async fn update_profile(
|
||||||
users: &dyn UserRepository,
|
users: &dyn UserWriter,
|
||||||
events: &dyn EventPublisher,
|
events: &dyn EventPublisher,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use domain::{
|
|||||||
models::social::{Block, Boost, Follow, FollowState, Like},
|
models::social::{Block, Boost, Follow, FollowState, Like},
|
||||||
ports::{
|
ports::{
|
||||||
BlockRepository, BoostRepository, EventPublisher, FederationActionPort, FollowRepository,
|
BlockRepository, BoostRepository, EventPublisher, FederationActionPort, FollowRepository,
|
||||||
LikeRepository, UserRepository,
|
LikeRepository, UserReader,
|
||||||
},
|
},
|
||||||
value_objects::{BoostId, LikeId, ThoughtId, UserId, Username},
|
value_objects::{BoostId, LikeId, ThoughtId, UserId, Username},
|
||||||
};
|
};
|
||||||
@@ -92,7 +92,7 @@ pub async fn unboost_thought(
|
|||||||
|
|
||||||
pub async fn follow_actor(
|
pub async fn follow_actor(
|
||||||
follows: &dyn FollowRepository,
|
follows: &dyn FollowRepository,
|
||||||
users: &dyn UserRepository,
|
users: &dyn UserReader,
|
||||||
federation: &dyn FederationActionPort,
|
federation: &dyn FederationActionPort,
|
||||||
events: &dyn EventPublisher,
|
events: &dyn EventPublisher,
|
||||||
follower_id: &UserId,
|
follower_id: &UserId,
|
||||||
@@ -139,7 +139,7 @@ pub async fn follow_user(
|
|||||||
|
|
||||||
pub async fn unfollow_actor(
|
pub async fn unfollow_actor(
|
||||||
follows: &dyn FollowRepository,
|
follows: &dyn FollowRepository,
|
||||||
users: &dyn UserRepository,
|
users: &dyn UserReader,
|
||||||
federation: &dyn FederationActionPort,
|
federation: &dyn FederationActionPort,
|
||||||
events: &dyn EventPublisher,
|
events: &dyn EventPublisher,
|
||||||
follower_id: &UserId,
|
follower_id: &UserId,
|
||||||
@@ -212,7 +212,7 @@ pub async fn reject_follow(
|
|||||||
|
|
||||||
pub async fn block_by_username(
|
pub async fn block_by_username(
|
||||||
blocks: &dyn BlockRepository,
|
blocks: &dyn BlockRepository,
|
||||||
users: &dyn UserRepository,
|
users: &dyn UserReader,
|
||||||
events: &dyn EventPublisher,
|
events: &dyn EventPublisher,
|
||||||
blocker_id: &UserId,
|
blocker_id: &UserId,
|
||||||
username: &str,
|
username: &str,
|
||||||
@@ -227,7 +227,7 @@ pub async fn block_by_username(
|
|||||||
|
|
||||||
pub async fn unblock_by_username(
|
pub async fn unblock_by_username(
|
||||||
blocks: &dyn BlockRepository,
|
blocks: &dyn BlockRepository,
|
||||||
users: &dyn UserRepository,
|
users: &dyn UserReader,
|
||||||
events: &dyn EventPublisher,
|
events: &dyn EventPublisher,
|
||||||
blocker_id: &UserId,
|
blocker_id: &UserId,
|
||||||
username: &str,
|
username: &str,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::thought::{Thought, Visibility},
|
models::thought::{Thought, Visibility},
|
||||||
ports::{EventPublisher, TagRepository, ThoughtRepository, UserRepository},
|
ports::{EventPublisher, TagRepository, ThoughtRepository, UserReader},
|
||||||
value_objects::{Content, ThoughtId, UserId},
|
value_objects::{Content, ThoughtId, UserId},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ pub struct CreateThoughtOutput {
|
|||||||
|
|
||||||
pub async fn create_thought(
|
pub async fn create_thought(
|
||||||
thoughts: &dyn ThoughtRepository,
|
thoughts: &dyn ThoughtRepository,
|
||||||
_users: &dyn UserRepository,
|
_users: &dyn UserReader,
|
||||||
tags: &dyn TagRepository,
|
tags: &dyn TagRepository,
|
||||||
events: &dyn EventPublisher,
|
events: &dyn EventPublisher,
|
||||||
input: CreateThoughtInput,
|
input: CreateThoughtInput,
|
||||||
|
|||||||
@@ -45,10 +45,16 @@ pub trait EventConsumer: Send + Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait UserRepository: Send + Sync {
|
pub trait UserReader: Send + Sync {
|
||||||
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError>;
|
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError>;
|
||||||
async fn find_by_username(&self, username: &Username) -> Result<Option<User>, DomainError>;
|
async fn find_by_username(&self, username: &Username) -> Result<Option<User>, DomainError>;
|
||||||
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError>;
|
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError>;
|
||||||
|
async fn list_with_stats(&self) -> Result<Vec<UserSummary>, DomainError>;
|
||||||
|
async fn count(&self) -> Result<i64, DomainError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait UserWriter: Send + Sync {
|
||||||
async fn save(&self, user: &User) -> Result<(), DomainError>;
|
async fn save(&self, user: &User) -> Result<(), DomainError>;
|
||||||
async fn update_profile(
|
async fn update_profile(
|
||||||
&self,
|
&self,
|
||||||
@@ -59,10 +65,13 @@ pub trait UserRepository: Send + Sync {
|
|||||||
header_url: Option<String>,
|
header_url: Option<String>,
|
||||||
custom_css: Option<String>,
|
custom_css: Option<String>,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
async fn list_with_stats(&self) -> Result<Vec<UserSummary>, DomainError>;
|
|
||||||
async fn count(&self) -> Result<i64, DomainError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Combined supertrait — `AppState.users` stays `Arc<dyn UserRepository>`.
|
||||||
|
/// Blanket impl: any type implementing both sub-traits gets `UserRepository` for free.
|
||||||
|
pub trait UserRepository: UserReader + UserWriter {}
|
||||||
|
impl<T: UserReader + UserWriter> UserRepository for T {}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ThoughtRepository: Send + Sync {
|
pub trait ThoughtRepository: Send + Sync {
|
||||||
async fn save(&self, thought: &Thought) -> Result<(), DomainError>;
|
async fn save(&self, thought: &Thought) -> Result<(), DomainError>;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ pub struct TestStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl UserRepository for TestStore {
|
impl UserReader for TestStore {
|
||||||
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.users
|
.users
|
||||||
@@ -73,6 +73,22 @@ impl UserRepository for TestStore {
|
|||||||
.find(|u| u.email.as_str() == email.as_str())
|
.find(|u| u.email.as_str() == email.as_str())
|
||||||
.cloned())
|
.cloned())
|
||||||
}
|
}
|
||||||
|
async fn list_with_stats(&self) -> Result<Vec<UserSummary>, DomainError> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
async fn count(&self) -> Result<i64, DomainError> {
|
||||||
|
Ok(self
|
||||||
|
.users
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.filter(|u| u.local)
|
||||||
|
.count() as i64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl UserWriter for TestStore {
|
||||||
async fn save(&self, user: &User) -> Result<(), DomainError> {
|
async fn save(&self, user: &User) -> Result<(), DomainError> {
|
||||||
let mut g = self.users.lock().unwrap();
|
let mut g = self.users.lock().unwrap();
|
||||||
g.retain(|u| u.id != user.id);
|
g.retain(|u| u.id != user.id);
|
||||||
@@ -103,18 +119,6 @@ impl UserRepository for TestStore {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn list_with_stats(&self) -> Result<Vec<UserSummary>, DomainError> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
async fn count(&self) -> Result<i64, DomainError> {
|
|
||||||
Ok(self
|
|
||||||
.users
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.filter(|u| u.local)
|
|
||||||
.count() as i64)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
Reference in New Issue
Block a user