Files
thoughts/crates/domain/src/ports.rs

157 lines
7.3 KiB
Rust

use async_trait::async_trait;
use crate::{
errors::DomainError,
events::{DomainEvent, EventEnvelope},
models::{
api_key::ApiKey,
feed::{FeedEntry, PageParams, Paginated, UserSummary},
notification::Notification,
remote_actor::RemoteActor,
social::{Block, Boost, Follow, FollowState, Like},
tag::Tag,
thought::Thought,
top_friend::TopFriend,
user::User,
},
value_objects::{ApiKeyId, Content, Email, NotificationId, PasswordHash, ThoughtId, UserId, Username},
};
pub struct GeneratedToken { pub token: String, pub user_id: UserId }
#[async_trait]
pub trait AuthService: Send + Sync {
fn generate_token(&self, user_id: &UserId) -> Result<GeneratedToken, DomainError>;
fn validate_token(&self, token: &str) -> Result<UserId, DomainError>;
}
#[async_trait]
pub trait PasswordHasher: Send + Sync {
async fn hash(&self, plain: &str) -> Result<PasswordHash, DomainError>;
async fn verify(&self, plain: &str, hash: &PasswordHash) -> Result<bool, DomainError>;
}
#[async_trait]
pub trait EventPublisher: Send + Sync {
async fn publish(&self, event: &DomainEvent) -> Result<(), DomainError>;
}
pub trait EventConsumer: Send + Sync {
fn consume(&self) -> futures::stream::BoxStream<'_, Result<EventEnvelope, DomainError>>;
}
#[async_trait]
pub trait UserRepository: Send + Sync {
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_email(&self, email: &Email) -> Result<Option<User>, DomainError>;
async fn save(&self, user: &User) -> Result<(), DomainError>;
async fn update_profile(&self, user_id: &UserId, display_name: Option<String>, bio: Option<String>, avatar_url: Option<String>, header_url: Option<String>, custom_css: Option<String>) -> Result<(), DomainError>;
async fn list_with_stats(&self) -> Result<Vec<UserSummary>, DomainError>;
}
#[async_trait]
pub trait ThoughtRepository: Send + Sync {
async fn save(&self, thought: &Thought) -> Result<(), DomainError>;
async fn find_by_id(&self, id: &ThoughtId) -> Result<Option<Thought>, DomainError>;
async fn delete(&self, id: &ThoughtId, user_id: &UserId) -> Result<(), DomainError>;
async fn update_content(&self, id: &ThoughtId, content: &Content) -> Result<(), DomainError>;
async fn get_thread(&self, id: &ThoughtId) -> Result<Vec<Thought>, DomainError>;
async fn list_by_user(&self, user_id: &UserId, page: &PageParams) -> Result<Paginated<FeedEntry>, DomainError>;
}
#[async_trait]
pub trait LikeRepository: Send + Sync {
async fn save(&self, like: &Like) -> Result<(), DomainError>;
async fn delete(&self, user_id: &UserId, thought_id: &ThoughtId) -> Result<(), DomainError>;
async fn find(&self, user_id: &UserId, thought_id: &ThoughtId) -> Result<Option<Like>, DomainError>;
async fn count_for_thought(&self, thought_id: &ThoughtId) -> Result<i64, DomainError>;
}
#[async_trait]
pub trait BoostRepository: Send + Sync {
async fn save(&self, boost: &Boost) -> Result<(), DomainError>;
async fn delete(&self, user_id: &UserId, thought_id: &ThoughtId) -> Result<(), DomainError>;
async fn find(&self, user_id: &UserId, thought_id: &ThoughtId) -> Result<Option<Boost>, DomainError>;
async fn count_for_thought(&self, thought_id: &ThoughtId) -> Result<i64, DomainError>;
}
#[async_trait]
pub trait FollowRepository: Send + Sync {
async fn save(&self, follow: &Follow) -> Result<(), DomainError>;
async fn delete(&self, follower_id: &UserId, following_id: &UserId) -> Result<(), DomainError>;
async fn find(&self, follower_id: &UserId, following_id: &UserId) -> Result<Option<Follow>, DomainError>;
async fn update_state(&self, follower_id: &UserId, following_id: &UserId, state: &FollowState) -> Result<(), DomainError>;
async fn list_followers(&self, user_id: &UserId, page: &PageParams) -> Result<Paginated<User>, DomainError>;
async fn list_following(&self, user_id: &UserId, page: &PageParams) -> Result<Paginated<User>, DomainError>;
async fn get_accepted_following_ids(&self, user_id: &UserId) -> Result<Vec<UserId>, DomainError>;
}
#[async_trait]
pub trait BlockRepository: Send + Sync {
async fn save(&self, block: &Block) -> Result<(), DomainError>;
async fn delete(&self, blocker_id: &UserId, blocked_id: &UserId) -> Result<(), DomainError>;
async fn exists(&self, blocker_id: &UserId, blocked_id: &UserId) -> Result<bool, DomainError>;
}
#[async_trait]
pub trait TagRepository: Send + Sync {
async fn find_or_create(&self, name: &str) -> Result<Tag, DomainError>;
async fn attach_to_thought(&self, thought_id: &ThoughtId, tag_id: i32) -> Result<(), DomainError>;
async fn detach_from_thought(&self, thought_id: &ThoughtId) -> Result<(), DomainError>;
async fn list_for_thought(&self, thought_id: &ThoughtId) -> Result<Vec<Tag>, DomainError>;
async fn list_thoughts_by_tag(&self, tag_name: &str, page: &PageParams) -> Result<Paginated<Thought>, DomainError>;
}
#[async_trait]
pub trait ApiKeyRepository: Send + Sync {
async fn save(&self, key: &ApiKey) -> Result<(), DomainError>;
async fn find_by_hash(&self, key_hash: &str) -> Result<Option<ApiKey>, DomainError>;
async fn list_for_user(&self, user_id: &UserId) -> Result<Vec<ApiKey>, DomainError>;
async fn delete(&self, id: &ApiKeyId, user_id: &UserId) -> Result<(), DomainError>;
}
#[async_trait]
pub trait TopFriendRepository: Send + Sync {
async fn set_top_friends(&self, user_id: &UserId, friends: Vec<(UserId, i16)>) -> Result<(), DomainError>;
async fn list_for_user(&self, user_id: &UserId) -> Result<Vec<(TopFriend, User)>, DomainError>;
}
#[async_trait]
pub trait NotificationRepository: Send + Sync {
async fn save(&self, n: &Notification) -> Result<(), DomainError>;
async fn list_for_user(&self, user_id: &UserId, page: &PageParams) -> Result<Paginated<Notification>, DomainError>;
async fn mark_read(&self, id: &NotificationId, user_id: &UserId) -> Result<(), DomainError>;
async fn mark_all_read(&self, user_id: &UserId) -> Result<(), DomainError>;
}
#[async_trait]
pub trait RemoteActorRepository: Send + Sync {
async fn upsert(&self, actor: &RemoteActor) -> Result<(), DomainError>;
async fn find_by_url(&self, url: &str) -> Result<Option<RemoteActor>, DomainError>;
}
#[async_trait]
pub trait FeedRepository: Send + Sync {
async fn home_feed(&self, following_ids: &[UserId], page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>;
async fn public_feed(&self, page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>;
async fn search(&self, query: &str, page: &PageParams, viewer_id: Option<&UserId>) -> Result<Paginated<FeedEntry>, DomainError>;
}
#[async_trait]
pub trait SearchPort: Send + Sync {
/// Full-text search over public thoughts, ranked by trigram similarity.
async fn search_thoughts(
&self,
query: &str,
page: &PageParams,
viewer_id: Option<&UserId>,
) -> Result<Paginated<FeedEntry>, DomainError>;
/// Search users by username or display_name, ranked by trigram similarity.
async fn search_users(
&self,
query: &str,
page: &PageParams,
) -> Result<Paginated<User>, DomainError>;
}