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:
@@ -1,15 +1,19 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::{
|
||||
AuthService, EventPublisher, MetadataClient, MovieRepository, PasswordHasher,
|
||||
PosterFetcherClient, PosterStorage, UserRepository,
|
||||
AuthService, DiaryRepository, EventPublisher, MetadataClient, MovieRepository,
|
||||
PasswordHasher, PosterFetcherClient, PosterStorage, ReviewRepository, StatsRepository,
|
||||
UserRepository,
|
||||
};
|
||||
|
||||
use crate::config::AppConfig;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppContext {
|
||||
pub repository: Arc<dyn MovieRepository>,
|
||||
pub movie_repository: Arc<dyn MovieRepository>,
|
||||
pub review_repository: Arc<dyn ReviewRepository>,
|
||||
pub diary_repository: Arc<dyn DiaryRepository>,
|
||||
pub stats_repository: Arc<dyn StatsRepository>,
|
||||
pub metadata_client: Arc<dyn MetadataClient>,
|
||||
pub poster_fetcher: Arc<dyn PosterFetcherClient>,
|
||||
pub poster_storage: Arc<dyn PosterStorage>,
|
||||
|
||||
@@ -170,12 +170,9 @@ mod tests {
|
||||
use chrono::NaiveDate;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
models::{DiaryEntry, DiaryFilter, Movie, Review, ReviewHistory, collections::Paginated},
|
||||
models::Movie,
|
||||
ports::{MetadataSearchCriteria, MovieRepository},
|
||||
value_objects::{
|
||||
ExternalMetadataId, MovieId, MovieTitle, PosterUrl, ReleaseYear, ReviewId,
|
||||
},
|
||||
value_objects::{ExternalMetadataId, MovieId, MovieTitle, PosterUrl, ReleaseYear},
|
||||
};
|
||||
|
||||
fn make_cmd(ext_id: Option<&str>, title: Option<&str>, year: Option<u16>) -> LogReviewCommand {
|
||||
@@ -210,155 +207,29 @@ mod tests {
|
||||
|
||||
#[async_trait]
|
||||
impl MovieRepository for RepoWithExternalMovie {
|
||||
async fn get_movie_by_external_id(
|
||||
&self,
|
||||
_: &ExternalMetadataId,
|
||||
) -> Result<Option<Movie>, DomainError> {
|
||||
Ok(Some(self.0.clone()))
|
||||
}
|
||||
async fn get_movie_by_id(&self, _: &MovieId) -> Result<Option<Movie>, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn get_movies_by_title_and_year(
|
||||
&self,
|
||||
_: &MovieTitle,
|
||||
_: &ReleaseYear,
|
||||
) -> Result<Vec<Movie>, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn save_review(&self, _: &Review) -> Result<DomainEvent, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn query_diary(
|
||||
&self,
|
||||
_: &DiaryFilter,
|
||||
) -> Result<Paginated<DiaryEntry>, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn get_review_history(&self, _: &MovieId) -> Result<ReviewHistory, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn get_review_by_id(
|
||||
&self,
|
||||
_: &ReviewId,
|
||||
) -> Result<Option<Review>, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn delete_review(&self, _: &ReviewId) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn query_activity_feed(&self, _: &domain::models::collections::PageParams) -> Result<domain::models::collections::Paginated<domain::models::FeedEntry>, DomainError> { panic!("unexpected") }
|
||||
async fn get_user_stats(&self, _: &domain::value_objects::UserId) -> Result<domain::models::UserStats, DomainError> { panic!("unexpected") }
|
||||
async fn get_user_history(&self, _: &domain::value_objects::UserId) -> Result<Vec<domain::models::DiaryEntry>, DomainError> { panic!("unexpected") }
|
||||
async fn get_user_trends(&self, _: &domain::value_objects::UserId) -> Result<domain::models::UserTrends, DomainError> { panic!("unexpected") }
|
||||
async fn get_movie_by_external_id(&self, _: &ExternalMetadataId) -> Result<Option<Movie>, DomainError> { Ok(Some(self.0.clone())) }
|
||||
async fn get_movie_by_id(&self, _: &MovieId) -> Result<Option<Movie>, DomainError> { panic!("unexpected") }
|
||||
async fn get_movies_by_title_and_year(&self, _: &MovieTitle, _: &ReleaseYear) -> Result<Vec<Movie>, DomainError> { panic!("unexpected") }
|
||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MovieRepository for RepoEmpty {
|
||||
async fn get_movie_by_external_id(
|
||||
&self,
|
||||
_: &ExternalMetadataId,
|
||||
) -> Result<Option<Movie>, DomainError> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn get_movie_by_id(&self, _: &MovieId) -> Result<Option<Movie>, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn get_movies_by_title_and_year(
|
||||
&self,
|
||||
_: &MovieTitle,
|
||||
_: &ReleaseYear,
|
||||
) -> Result<Vec<Movie>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn save_review(&self, _: &Review) -> Result<DomainEvent, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn query_diary(
|
||||
&self,
|
||||
_: &DiaryFilter,
|
||||
) -> Result<Paginated<DiaryEntry>, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn get_review_history(&self, _: &MovieId) -> Result<ReviewHistory, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn get_review_by_id(
|
||||
&self,
|
||||
_: &ReviewId,
|
||||
) -> Result<Option<Review>, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn delete_review(&self, _: &ReviewId) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn query_activity_feed(&self, _: &domain::models::collections::PageParams) -> Result<domain::models::collections::Paginated<domain::models::FeedEntry>, DomainError> { panic!("unexpected") }
|
||||
async fn get_user_stats(&self, _: &domain::value_objects::UserId) -> Result<domain::models::UserStats, DomainError> { panic!("unexpected") }
|
||||
async fn get_user_history(&self, _: &domain::value_objects::UserId) -> Result<Vec<domain::models::DiaryEntry>, DomainError> { panic!("unexpected") }
|
||||
async fn get_user_trends(&self, _: &domain::value_objects::UserId) -> Result<domain::models::UserTrends, DomainError> { panic!("unexpected") }
|
||||
async fn get_movie_by_external_id(&self, _: &ExternalMetadataId) -> Result<Option<Movie>, DomainError> { Ok(None) }
|
||||
async fn get_movie_by_id(&self, _: &MovieId) -> Result<Option<Movie>, DomainError> { panic!("unexpected") }
|
||||
async fn get_movies_by_title_and_year(&self, _: &MovieTitle, _: &ReleaseYear) -> Result<Vec<Movie>, DomainError> { Ok(vec![]) }
|
||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MovieRepository for RepoWithTitleMatch {
|
||||
async fn get_movie_by_external_id(
|
||||
&self,
|
||||
_: &ExternalMetadataId,
|
||||
) -> Result<Option<Movie>, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn get_movie_by_id(&self, _: &MovieId) -> Result<Option<Movie>, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn get_movies_by_title_and_year(
|
||||
&self,
|
||||
_: &MovieTitle,
|
||||
_: &ReleaseYear,
|
||||
) -> Result<Vec<Movie>, DomainError> {
|
||||
Ok(vec![self.0.clone()])
|
||||
}
|
||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn save_review(&self, _: &Review) -> Result<DomainEvent, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn query_diary(
|
||||
&self,
|
||||
_: &DiaryFilter,
|
||||
) -> Result<Paginated<DiaryEntry>, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn get_review_history(&self, _: &MovieId) -> Result<ReviewHistory, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn get_review_by_id(
|
||||
&self,
|
||||
_: &ReviewId,
|
||||
) -> Result<Option<Review>, DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn delete_review(&self, _: &ReviewId) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn query_activity_feed(&self, _: &domain::models::collections::PageParams) -> Result<domain::models::collections::Paginated<domain::models::FeedEntry>, DomainError> { panic!("unexpected") }
|
||||
async fn get_user_stats(&self, _: &domain::value_objects::UserId) -> Result<domain::models::UserStats, DomainError> { panic!("unexpected") }
|
||||
async fn get_user_history(&self, _: &domain::value_objects::UserId) -> Result<Vec<domain::models::DiaryEntry>, DomainError> { panic!("unexpected") }
|
||||
async fn get_user_trends(&self, _: &domain::value_objects::UserId) -> Result<domain::models::UserTrends, DomainError> { panic!("unexpected") }
|
||||
async fn get_movie_by_external_id(&self, _: &ExternalMetadataId) -> Result<Option<Movie>, DomainError> { panic!("unexpected") }
|
||||
async fn get_movie_by_id(&self, _: &MovieId) -> Result<Option<Movie>, DomainError> { panic!("unexpected") }
|
||||
async fn get_movies_by_title_and_year(&self, _: &MovieTitle, _: &ReleaseYear) -> Result<Vec<Movie>, DomainError> { Ok(vec![self.0.clone()]) }
|
||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
}
|
||||
|
||||
struct MetaReturnsMovie(Movie);
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -24,5 +24,5 @@ pub async fn execute(
|
||||
user_id,
|
||||
};
|
||||
|
||||
ctx.repository.query_diary(&filter).await
|
||||
ctx.diary_repository.query_diary(&filter).await
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user