Refactor application context and repository structure

- Updated `AppContext` to include separate repositories for movies, reviews, diaries, and stats.
- Modified use cases to utilize the new repository structure, ensuring that the correct repositories are called for their respective operations.
- Introduced `DiaryRepository` and `StatsRepository` traits to encapsulate diary and statistics-related operations.
- Updated all relevant use cases, handlers, and tests to reflect the changes in repository usage.
- Ensured that panic repositories are updated to implement the new traits for testing purposes.
This commit is contained in:
2026-05-09 18:58:29 +02:00
parent 29a5972c01
commit 89e78a0d1f
19 changed files with 260 additions and 311 deletions

View File

@@ -6,7 +6,7 @@ pub async fn execute(ctx: &AppContext, cmd: DeleteReviewCommand) -> Result<(), D
let requesting_user_id = UserId::from_uuid(cmd.requesting_user_id);
let review = ctx
.repository
.review_repository
.get_review_by_id(&review_id)
.await?
.ok_or_else(|| DomainError::NotFound(format!("review {}", cmd.review_id)))?;
@@ -16,11 +16,11 @@ pub async fn execute(ctx: &AppContext, cmd: DeleteReviewCommand) -> Result<(), D
}
let movie_id = review.movie_id().clone();
ctx.repository.delete_review(&review_id).await?;
ctx.review_repository.delete_review(&review_id).await?;
let history = ctx.repository.get_review_history(&movie_id).await?;
let history = ctx.diary_repository.get_review_history(&movie_id).await?;
if history.viewings().is_empty() {
ctx.repository.delete_movie(&movie_id).await?;
ctx.movie_repository.delete_movie(&movie_id).await?;
}
Ok(())

View File

@@ -2,13 +2,13 @@ use std::sync::Arc;
use domain::{
errors::DomainError,
ports::{DiaryExporter, MovieRepository},
ports::{DiaryExporter, DiaryRepository},
};
use crate::commands::ExportCommand;
pub struct ExportDiary {
repository: Arc<dyn MovieRepository>,
repository: Arc<dyn DiaryRepository>,
exporter: Arc<dyn DiaryExporter>,
}

View File

@@ -9,5 +9,5 @@ pub async fn execute(
query: GetActivityFeedQuery,
) -> Result<Paginated<FeedEntry>, DomainError> {
let page = PageParams::new(query.limit, query.offset)?;
ctx.repository.query_activity_feed(&page).await
ctx.diary_repository.query_activity_feed(&page).await
}

View File

@@ -24,5 +24,5 @@ pub async fn execute(
user_id,
};
ctx.repository.query_diary(&filter).await
ctx.diary_repository.query_diary(&filter).await
}

View File

@@ -13,7 +13,7 @@ pub async fn execute(
) -> Result<(ReviewHistory, Trend), DomainError> {
let movie_id = MovieId::from_uuid(query.movie_id);
let mut history = ctx.repository.get_review_history(&movie_id).await?;
let mut history = ctx.diary_repository.get_review_history(&movie_id).await?;
let trend = ReviewHistoryAnalyzer::rating_trend(&history)?;

View File

@@ -21,28 +21,26 @@ pub async fn execute(
query: GetUserProfileQuery,
) -> Result<UserProfileData, DomainError> {
let user_id = UserId::from_uuid(query.user_id);
let stats = ctx.repository.get_user_stats(&user_id).await?;
let stats = ctx.stats_repository.get_user_stats(&user_id).await?;
match query.view {
ProfileView::History => {
// V1: loads all entries into memory. Personal diaries are bounded in size;
// spec calls for showing every movie grouped by month, so full load is intentional.
let all_entries = ctx.repository.get_user_history(&user_id).await?;
let all_entries = ctx.diary_repository.get_user_history(&user_id).await?;
let history = group_by_month(all_entries);
Ok(UserProfileData { stats, entries: None, history: Some(history), trends: None })
}
ProfileView::Trends => {
let trends = ctx.repository.get_user_trends(&user_id).await?;
let trends = ctx.stats_repository.get_user_trends(&user_id).await?;
Ok(UserProfileData { stats, entries: None, history: None, trends: Some(trends) })
}
ProfileView::Ratings => {
let filter = paged_user_filter(user_id, SortDirection::ByRatingDesc, query.limit, query.offset)?;
let entries = ctx.repository.query_diary(&filter).await?;
let entries = ctx.diary_repository.query_diary(&filter).await?;
Ok(UserProfileData { stats, entries: Some(entries), history: None, trends: None })
}
ProfileView::Recent => {
let filter = paged_user_filter(user_id, SortDirection::Descending, query.limit, query.offset)?;
let entries = ctx.repository.query_diary(&filter).await?;
let entries = ctx.diary_repository.query_diary(&filter).await?;
Ok(UserProfileData { stats, entries: Some(entries), history: None, trends: None })
}
}

View File

@@ -17,15 +17,15 @@ pub async fn execute(ctx: &AppContext, cmd: LogReviewCommand) -> Result<(), Doma
let comment = cmd.comment.clone().map(Comment::new).transpose()?;
let deps = MovieResolverDeps {
repository: ctx.repository.as_ref(),
repository: ctx.movie_repository.as_ref(),
metadata_client: ctx.metadata_client.as_ref(),
};
let (movie, is_new_movie) = MovieResolver::default_pipeline().resolve(&cmd, &deps).await?;
ctx.repository.upsert_movie(&movie).await?;
ctx.movie_repository.upsert_movie(&movie).await?;
let review = Review::new(movie.id().clone(), user_id, rating, comment, cmd.watched_at)?;
let review_event = ctx.repository.save_review(&review).await?;
let review_event = ctx.review_repository.save_review(&review).await?;
publish_events(ctx, &movie, is_new_movie, review_event).await?;

View File

@@ -9,7 +9,7 @@ pub async fn execute(ctx: &AppContext, cmd: SyncPosterCommand) -> Result<(), Dom
let movie_id = MovieId::from_uuid(cmd.movie_id);
let external_metadata_id = ExternalMetadataId::new(cmd.external_metadata_id)?;
let mut movie = match ctx.repository.get_movie_by_id(&movie_id).await? {
let mut movie = match ctx.movie_repository.get_movie_by_id(&movie_id).await? {
Some(m) => m,
None => {
tracing::warn!(
@@ -41,7 +41,7 @@ pub async fn execute(ctx: &AppContext, cmd: SyncPosterCommand) -> Result<(), Dom
.await?;
movie.update_poster(stored_path);
ctx.repository.upsert_movie(&movie).await?;
ctx.movie_repository.upsert_movie(&movie).await?;
Ok(())
}