refactor: remaining MEDIUM — CQRS splits, DI Deps, profile dedup, event Value, response enum

M1: MovieRepository→MovieCommand/MovieQuery, WatchEventRepository→
WatchEventCommand/WatchEventQuery
M2: goals/ and import/ use Deps structs
M7: extract upload_image helper in update_profile
M8: FederationDeliveryRequested activity_json String→serde_json::Value
M11: UserProfileResponse uses ProfileViewData enum
This commit is contained in:
2026-07-10 03:50:43 +02:00
parent 12da356a40
commit dee013c7eb
99 changed files with 1262 additions and 896 deletions

View File

@@ -74,7 +74,7 @@ pub enum DomainEvent {
},
FederationDeliveryRequested {
inbox_url: String,
activity_json: String,
activity_json: serde_json::Value,
signing_actor_id: uuid::Uuid,
},
WatchEventIngested {

View File

@@ -12,32 +12,38 @@ pub trait MediaServerParser: Send + Sync {
-> Result<Option<ParsedPlaybackEvent>, DomainError>;
}
/// Write port — mutates watch events.
#[async_trait]
pub trait WatchEventRepository: Send + Sync {
pub trait WatchEventCommand: Send + Sync {
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError>;
async fn update_status(
&self,
id: &WatchEventId,
status: WatchEventStatus,
) -> Result<(), DomainError>;
async fn list_pending(&self, user_id: &UserId) -> Result<Vec<WatchEvent>, DomainError>;
async fn get_by_id(&self, id: &WatchEventId) -> Result<Option<WatchEvent>, DomainError>;
async fn get_by_ids(&self, ids: &[WatchEventId]) -> Result<Vec<WatchEvent>, DomainError>;
async fn update_status_batch(
&self,
ids: &[WatchEventId],
status: WatchEventStatus,
) -> Result<u64, DomainError>;
async fn delete_non_pending_older_than(
&self,
before: NaiveDateTime,
) -> Result<u64, DomainError>;
}
/// Read port — queries watch events. No mutations.
#[async_trait]
pub trait WatchEventQuery: Send + Sync {
async fn list_pending(&self, user_id: &UserId) -> Result<Vec<WatchEvent>, DomainError>;
async fn get_by_id(&self, id: &WatchEventId) -> Result<Option<WatchEvent>, DomainError>;
async fn get_by_ids(&self, ids: &[WatchEventId]) -> Result<Vec<WatchEvent>, DomainError>;
async fn find_duplicate(
&self,
user_id: &UserId,
external_id: &str,
after: NaiveDateTime,
) -> Result<bool, DomainError>;
async fn delete_non_pending_older_than(
&self,
before: NaiveDateTime,
) -> Result<u64, DomainError>;
}
#[async_trait]

View File

@@ -9,8 +9,16 @@ use crate::{
value_objects::{ExternalMetadataId, MovieId, MovieTitle, PosterUrl, ReleaseYear},
};
/// Write port — mutates the movies table.
#[async_trait]
pub trait MovieRepository: Send + Sync {
pub trait MovieCommand: Send + Sync {
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError>;
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError>;
}
/// Read port — queries movies. No mutations.
#[async_trait]
pub trait MovieQuery: Send + Sync {
async fn get_movie_by_external_id(
&self,
external_metadata_id: &ExternalMetadataId,
@@ -21,8 +29,6 @@ pub trait MovieRepository: Send + Sync {
title: &MovieTitle,
year: &ReleaseYear,
) -> Result<Vec<Movie>, DomainError>;
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError>;
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError>;
async fn existing_external_ids(
&self,
ids: &[ExternalMetadataId],

View File

@@ -18,8 +18,9 @@ use crate::{
},
ports::{
GoalRepository, ImportProfileRepository, ImportSessionRepository, MovieProfileRepository,
MovieRepository, RefreshSessionRepository, ReviewRepository, UserFederationSettingsQuery,
UserProfileFieldsRepository, UserRepository, UserSettingsRepository, WatchEventRepository,
MovieCommand, MovieQuery, RefreshSessionRepository, ReviewRepository,
UserFederationSettingsQuery, UserProfileFieldsRepository, UserRepository,
UserSettingsRepository, WatchEventCommand, WatchEventQuery,
WatchlistRepository, WebhookTokenRepository,
},
value_objects::{
@@ -47,7 +48,23 @@ impl InMemoryMovieRepository {
}
#[async_trait]
impl MovieRepository for InMemoryMovieRepository {
impl MovieCommand for InMemoryMovieRepository {
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError> {
self.store
.lock()
.unwrap()
.insert(movie.id().value(), movie.clone());
Ok(())
}
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError> {
self.store.lock().unwrap().remove(&movie_id.value());
Ok(())
}
}
#[async_trait]
impl MovieQuery for InMemoryMovieRepository {
async fn get_movie_by_external_id(
&self,
external_metadata_id: &ExternalMetadataId,
@@ -80,19 +97,6 @@ impl MovieRepository for InMemoryMovieRepository {
.collect())
}
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError> {
self.store
.lock()
.unwrap()
.insert(movie.id().value(), movie.clone());
Ok(())
}
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError> {
self.store.lock().unwrap().remove(&movie_id.value());
Ok(())
}
async fn existing_external_ids(
&self,
ids: &[ExternalMetadataId],
@@ -526,7 +530,7 @@ impl InMemoryWatchEventRepository {
}
#[async_trait]
impl WatchEventRepository for InMemoryWatchEventRepository {
impl WatchEventCommand for InMemoryWatchEventRepository {
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError> {
self.store.lock().unwrap().push(event.clone());
Ok(())
@@ -540,6 +544,27 @@ impl WatchEventRepository for InMemoryWatchEventRepository {
Ok(())
}
async fn update_status_batch(
&self,
ids: &[WatchEventId],
_status: WatchEventStatus,
) -> Result<u64, DomainError> {
Ok(ids.len() as u64)
}
async fn delete_non_pending_older_than(
&self,
before: NaiveDateTime,
) -> Result<u64, DomainError> {
let mut store = self.store.lock().unwrap();
let before_len = store.len();
store.retain(|e| *e.status() == WatchEventStatus::Pending || *e.created_at() >= before);
Ok((before_len - store.len()) as u64)
}
}
#[async_trait]
impl WatchEventQuery for InMemoryWatchEventRepository {
async fn list_pending(&self, user_id: &UserId) -> Result<Vec<WatchEvent>, DomainError> {
let store = self.store.lock().unwrap();
Ok(store
@@ -566,14 +591,6 @@ impl WatchEventRepository for InMemoryWatchEventRepository {
.collect())
}
async fn update_status_batch(
&self,
ids: &[WatchEventId],
_status: WatchEventStatus,
) -> Result<u64, DomainError> {
Ok(ids.len() as u64)
}
async fn find_duplicate(
&self,
user_id: &UserId,
@@ -588,15 +605,6 @@ impl WatchEventRepository for InMemoryWatchEventRepository {
}))
}
async fn delete_non_pending_older_than(
&self,
before: NaiveDateTime,
) -> Result<u64, DomainError> {
let mut store = self.store.lock().unwrap();
let before_len = store.len();
store.retain(|e| *e.status() == WatchEventStatus::Pending || *e.created_at() >= before);
Ok((before_len - store.len()) as u64)
}
}
// ── InMemoryImportSessionRepository ─────────────────────────────────────────

View File

@@ -371,44 +371,56 @@ impl crate::ports::FederatedProfileQuery for PanicFederatedProfileQuery {
}
}
pub struct PanicWatchEventRepository;
pub struct PanicWatchEventCommand;
#[async_trait]
impl crate::ports::WatchEventRepository for PanicWatchEventRepository {
impl crate::ports::WatchEventCommand for PanicWatchEventCommand {
async fn save(&self, _: &crate::models::WatchEvent) -> Result<(), DomainError> {
panic!("PanicWatchEventRepository called")
panic!("PanicWatchEventCommand called")
}
async fn update_status(
&self,
_: &crate::value_objects::WatchEventId,
_: crate::models::WatchEventStatus,
) -> Result<(), DomainError> {
panic!("PanicWatchEventRepository called")
}
async fn list_pending(
&self,
_: &UserId,
) -> Result<Vec<crate::models::WatchEvent>, DomainError> {
panic!("PanicWatchEventRepository called")
}
async fn get_by_id(
&self,
_: &crate::value_objects::WatchEventId,
) -> Result<Option<crate::models::WatchEvent>, DomainError> {
panic!("PanicWatchEventRepository called")
}
async fn get_by_ids(
&self,
_: &[crate::value_objects::WatchEventId],
) -> Result<Vec<crate::models::WatchEvent>, DomainError> {
panic!("PanicWatchEventRepository called")
panic!("PanicWatchEventCommand called")
}
async fn update_status_batch(
&self,
_: &[crate::value_objects::WatchEventId],
_: crate::models::WatchEventStatus,
) -> Result<u64, DomainError> {
panic!("PanicWatchEventRepository called")
panic!("PanicWatchEventCommand called")
}
async fn delete_non_pending_older_than(
&self,
_: chrono::NaiveDateTime,
) -> Result<u64, DomainError> {
panic!("PanicWatchEventCommand called")
}
}
pub struct PanicWatchEventQuery;
#[async_trait]
impl crate::ports::WatchEventQuery for PanicWatchEventQuery {
async fn list_pending(
&self,
_: &UserId,
) -> Result<Vec<crate::models::WatchEvent>, DomainError> {
panic!("PanicWatchEventQuery called")
}
async fn get_by_id(
&self,
_: &crate::value_objects::WatchEventId,
) -> Result<Option<crate::models::WatchEvent>, DomainError> {
panic!("PanicWatchEventQuery called")
}
async fn get_by_ids(
&self,
_: &[crate::value_objects::WatchEventId],
) -> Result<Vec<crate::models::WatchEvent>, DomainError> {
panic!("PanicWatchEventQuery called")
}
async fn find_duplicate(
&self,
@@ -416,13 +428,7 @@ impl crate::ports::WatchEventRepository for PanicWatchEventRepository {
_: &str,
_: chrono::NaiveDateTime,
) -> Result<bool, DomainError> {
panic!("PanicWatchEventRepository called")
}
async fn delete_non_pending_older_than(
&self,
_: chrono::NaiveDateTime,
) -> Result<u64, DomainError> {
panic!("PanicWatchEventRepository called")
panic!("PanicWatchEventQuery called")
}
}