refactor(movies): EnrichMovieDeps, ReindexSearchDeps, SyncPosterDeps, SearchReindexHandler, EnrichmentStalenessJob

This commit is contained in:
2026-06-11 22:13:25 +02:00
parent 66bd138927
commit 1e62f12903
15 changed files with 224 additions and 125 deletions

View File

@@ -1,6 +1,11 @@
use std::sync::Arc; use std::sync::Arc;
use application::movies::{commands::EnrichMovieCommand, enrich_movie, request_enrichment}; use application::movies::{
commands::EnrichMovieCommand,
deps::EnrichMovieDeps,
enrich_movie,
request_enrichment,
};
use async_trait::async_trait; use async_trait::async_trait;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
@@ -89,13 +94,12 @@ impl EventHandler for MovieEnrichmentHandler {
}; };
self.download_cast_photos(&profile).await; self.download_cast_photos(&profile).await;
enrich_movie::execute( let enrich_deps = EnrichMovieDeps {
&self.movie_repository, movie: self.movie_repository.clone(),
&self.profile_repo, movie_profile: self.profile_repo.clone(),
&self.person_command, person_command: self.person_command.clone(),
&self.search_command, search_command: self.search_command.clone(),
EnrichMovieCommand { movie_id, profile }, };
) enrich_movie::execute(&enrich_deps, EnrichMovieCommand { movie_id, profile }).await
.await
} }
} }

View File

@@ -1,17 +1,27 @@
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use domain::{errors::DomainError, events::DomainEvent, ports::PeriodicJob}; use domain::{
errors::DomainError,
use crate::context::AppContext; events::DomainEvent,
ports::{EventPublisher, MovieProfileRepository, PeriodicJob},
};
pub struct EnrichmentStalenessJob { pub struct EnrichmentStalenessJob {
ctx: AppContext, movie_profile: Arc<dyn MovieProfileRepository>,
event_publisher: Arc<dyn EventPublisher>,
} }
impl EnrichmentStalenessJob { impl EnrichmentStalenessJob {
pub fn new(ctx: AppContext) -> Self { pub fn new(
Self { ctx } movie_profile: Arc<dyn MovieProfileRepository>,
event_publisher: Arc<dyn EventPublisher>,
) -> Self {
Self {
movie_profile,
event_publisher,
}
} }
} }
@@ -22,7 +32,7 @@ impl PeriodicJob for EnrichmentStalenessJob {
} }
async fn run(&self) -> Result<(), DomainError> { async fn run(&self) -> Result<(), DomainError> {
let stale = self.ctx.repos.movie_profile.list_stale().await?; let stale = self.movie_profile.list_stale().await?;
if stale.is_empty() { if stale.is_empty() {
return Ok(()); return Ok(());
} }
@@ -32,7 +42,7 @@ impl PeriodicJob for EnrichmentStalenessJob {
movie_id, movie_id,
external_metadata_id, external_metadata_id,
}; };
self.ctx.services.event_publisher.publish(&event).await?; self.event_publisher.publish(&event).await?;
} }
Ok(()) Ok(())
} }

View File

@@ -0,0 +1,39 @@
use std::sync::Arc;
use domain::ports::{
EventPublisher, MetadataClient, MovieProfileRepository, MovieRepository, ObjectStorage,
PersonCommand, PersonQuery, PosterFetcherClient, SearchCommand,
};
pub struct GetMoviesDeps {
pub movie: Arc<dyn MovieRepository>,
}
pub struct GetMovieProfileDeps {
pub movie_profile: Arc<dyn MovieProfileRepository>,
}
pub struct SyncPosterDeps {
pub movie: Arc<dyn MovieRepository>,
pub movie_profile: Arc<dyn MovieProfileRepository>,
pub metadata: Arc<dyn MetadataClient>,
pub poster_fetcher: Arc<dyn PosterFetcherClient>,
pub object_storage: Arc<dyn ObjectStorage>,
pub event_publisher: Arc<dyn EventPublisher>,
pub search_command: Arc<dyn SearchCommand>,
}
pub struct EnrichMovieDeps {
pub movie: Arc<dyn MovieRepository>,
pub movie_profile: Arc<dyn MovieProfileRepository>,
pub person_command: Arc<dyn PersonCommand>,
pub search_command: Arc<dyn SearchCommand>,
}
pub struct ReindexSearchDeps {
pub movie: Arc<dyn MovieRepository>,
pub movie_profile: Arc<dyn MovieProfileRepository>,
pub search_command: Arc<dyn SearchCommand>,
pub person_command: Arc<dyn PersonCommand>,
pub person_query: Arc<dyn PersonQuery>,
}

View File

@@ -1,38 +1,30 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
models::{CastMember, CrewMember, ExternalPersonId, IndexableDocument, Person, PersonId}, models::{CastMember, CrewMember, ExternalPersonId, IndexableDocument, Person, PersonId},
ports::{MovieProfileRepository, MovieRepository, PersonCommand, SearchCommand},
}; };
use crate::movies::commands::EnrichMovieCommand; use crate::movies::{commands::EnrichMovieCommand, deps::EnrichMovieDeps};
pub async fn execute( pub async fn execute(deps: &EnrichMovieDeps, cmd: EnrichMovieCommand) -> Result<(), DomainError> {
movie_repository: &Arc<dyn MovieRepository>,
profile_repository: &Arc<dyn MovieProfileRepository>,
person_command: &Arc<dyn PersonCommand>,
search_command: &Arc<dyn SearchCommand>,
cmd: EnrichMovieCommand,
) -> Result<(), DomainError> {
// 1. Persist the enriched profile (also handles movie_cast, movie_crew, genres, keywords) // 1. Persist the enriched profile (also handles movie_cast, movie_crew, genres, keywords)
profile_repository.upsert(&cmd.profile).await?; deps.movie_profile.upsert(&cmd.profile).await?;
// 2. Upsert persons extracted from cast + crew (no reads — only upsert) // 2. Upsert persons extracted from cast + crew (no reads — only upsert)
let persons = extract_persons(&cmd.profile.cast, &cmd.profile.crew); let persons = extract_persons(&cmd.profile.cast, &cmd.profile.crew);
if !persons.is_empty() { if !persons.is_empty() {
person_command.upsert_batch(&persons).await?; deps.person_command.upsert_batch(&persons).await?;
} }
// 3. Fetch the movie for the search index document // 3. Fetch the movie for the search index document
let Some(movie) = movie_repository.get_movie_by_id(&cmd.movie_id).await? else { let Some(movie) = deps.movie.get_movie_by_id(&cmd.movie_id).await? else {
tracing::warn!(movie_id = %cmd.movie_id.value(), "enrich_movie: movie not found after profile upsert"); tracing::warn!(movie_id = %cmd.movie_id.value(), "enrich_movie: movie not found after profile upsert");
return Ok(()); return Ok(());
}; };
// 4. Index the movie in search // 4. Index the movie in search
search_command deps.search_command
.index(IndexableDocument::Movie { .index(IndexableDocument::Movie {
id: cmd.movie_id.clone(), id: cmd.movie_id.clone(),
movie: Box::new(movie), movie: Box::new(movie),
@@ -42,7 +34,7 @@ pub async fn execute(
// 5. Index each unique person in search (no reads — persons built from in-memory data) // 5. Index each unique person in search (no reads — persons built from in-memory data)
for person in &persons { for person in &persons {
search_command deps.search_command
.index(IndexableDocument::Person { .index(IndexableDocument::Person {
id: person.id().clone(), id: person.id().clone(),
person: Box::new(person.clone()), person: Box::new(person.clone()),

View File

@@ -5,7 +5,7 @@ use domain::{
}; };
use uuid::Uuid; use uuid::Uuid;
use crate::context::AppContext; use crate::movies::deps::GetMovieProfileDeps;
pub struct GetMovieProfileQuery { pub struct GetMovieProfileQuery {
pub movie_id: Uuid, pub movie_id: Uuid,
@@ -60,11 +60,11 @@ fn resolve_crew(member: &CrewMember) -> CrewMemberWithId {
} }
pub async fn execute( pub async fn execute(
ctx: &AppContext, deps: &GetMovieProfileDeps,
query: GetMovieProfileQuery, query: GetMovieProfileQuery,
) -> Result<Option<MovieProfileResult>, DomainError> { ) -> Result<Option<MovieProfileResult>, DomainError> {
let movie_id = MovieId::from_uuid(query.movie_id); let movie_id = MovieId::from_uuid(query.movie_id);
let profile = ctx.repos.movie_profile.get_by_movie_id(&movie_id).await?; let profile = deps.movie_profile.get_by_movie_id(&movie_id).await?;
Ok(profile.map(|p| { Ok(profile.map(|p| {
let cast = p.cast.iter().map(resolve_cast).collect(); let cast = p.cast.iter().map(resolve_cast).collect();

View File

@@ -4,10 +4,10 @@ use domain::{
models::{MovieFilter, MovieSummary}, models::{MovieFilter, MovieSummary},
}; };
use crate::{context::AppContext, movies::queries::GetMoviesQuery}; use crate::movies::{deps::GetMoviesDeps, queries::GetMoviesQuery};
pub async fn execute( pub async fn execute(
ctx: &AppContext, deps: &GetMoviesDeps,
query: GetMoviesQuery, query: GetMoviesQuery,
) -> Result<Paginated<MovieSummary>, DomainError> { ) -> Result<Paginated<MovieSummary>, DomainError> {
let page = PageParams::new(query.limit, query.offset)?; let page = PageParams::new(query.limit, query.offset)?;
@@ -16,7 +16,7 @@ pub async fn execute(
genre: query.genre, genre: query.genre,
language: query.language, language: query.language,
}; };
ctx.repos.movie.list_movies(&page, &filter).await deps.movie.list_movies(&page, &filter).await
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,4 +1,5 @@
pub mod commands; pub mod commands;
pub mod deps;
pub mod discovery_indexer; pub mod discovery_indexer;
pub mod enrich_movie; pub mod enrich_movie;
pub mod get_movie_profile; pub mod get_movie_profile;

View File

@@ -7,7 +7,7 @@ use domain::{
}; };
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use crate::context::AppContext; use crate::movies::deps::ReindexSearchDeps;
const BATCH_SIZE: u32 = 500; const BATCH_SIZE: u32 = 500;
@@ -17,10 +17,10 @@ pub struct ReindexResult {
pub persons_backfilled: u64, pub persons_backfilled: u64,
} }
pub async fn execute(ctx: &AppContext) -> Result<ReindexResult, DomainError> { pub async fn execute(deps: &ReindexSearchDeps) -> Result<ReindexResult, DomainError> {
let movies_indexed = reindex_movies(ctx).await?; let movies_indexed = reindex_movies(deps).await?;
let persons_backfilled = backfill_persons(ctx).await?; let persons_backfilled = backfill_persons(deps).await?;
let persons_indexed = reindex_persons(ctx).await?; let persons_indexed = reindex_persons(deps).await?;
Ok(ReindexResult { Ok(ReindexResult {
movies_indexed, movies_indexed,
@@ -29,12 +29,11 @@ pub async fn execute(ctx: &AppContext) -> Result<ReindexResult, DomainError> {
}) })
} }
async fn reindex_movies(ctx: &AppContext) -> Result<u64, DomainError> { async fn reindex_movies(deps: &ReindexSearchDeps) -> Result<u64, DomainError> {
let mut count: u64 = 0; let mut count: u64 = 0;
let mut offset: u32 = 0; let mut offset: u32 = 0;
loop { loop {
let page = ctx let page = deps
.repos
.movie .movie
.list_movies( .list_movies(
&PageParams { &PageParams {
@@ -47,10 +46,9 @@ async fn reindex_movies(ctx: &AppContext) -> Result<u64, DomainError> {
for summary in &page.items { for summary in &page.items {
let movie_id = summary.movie.id().clone(); let movie_id = summary.movie.id().clone();
let profile = ctx.repos.movie_profile.get_by_movie_id(&movie_id).await?; let profile = deps.movie_profile.get_by_movie_id(&movie_id).await?;
if let Err(e) = ctx if let Err(e) = deps
.repos
.search_command .search_command
.index(IndexableDocument::Movie { .index(IndexableDocument::Movie {
id: movie_id.clone(), id: movie_id.clone(),
@@ -73,11 +71,10 @@ async fn reindex_movies(ctx: &AppContext) -> Result<u64, DomainError> {
Ok(count) Ok(count)
} }
async fn backfill_persons(ctx: &AppContext) -> Result<u64, DomainError> { async fn backfill_persons(deps: &ReindexSearchDeps) -> Result<u64, DomainError> {
let mut total = 0u64; let mut total = 0u64;
loop { loop {
let (count, has_more) = ctx let (count, has_more) = deps
.repos
.person_command .person_command
.backfill_from_credits_batch(BATCH_SIZE) .backfill_from_credits_batch(BATCH_SIZE)
.await?; .await?;
@@ -90,15 +87,14 @@ async fn backfill_persons(ctx: &AppContext) -> Result<u64, DomainError> {
Ok(total) Ok(total)
} }
async fn reindex_persons(ctx: &AppContext) -> Result<u64, DomainError> { async fn reindex_persons(deps: &ReindexSearchDeps) -> Result<u64, DomainError> {
let mut count: u64 = 0; let mut count: u64 = 0;
let mut offset: u32 = 0; let mut offset: u32 = 0;
loop { loop {
let persons = ctx.repos.person_query.list_page(BATCH_SIZE, offset).await?; let persons = deps.person_query.list_page(BATCH_SIZE, offset).await?;
for person in &persons { for person in &persons {
if let Err(e) = ctx if let Err(e) = deps
.repos
.search_command .search_command
.index(IndexableDocument::Person { .index(IndexableDocument::Person {
id: person.id().clone(), id: person.id().clone(),
@@ -121,14 +117,14 @@ async fn reindex_persons(ctx: &AppContext) -> Result<u64, DomainError> {
} }
pub struct SearchReindexHandler { pub struct SearchReindexHandler {
ctx: AppContext, deps: ReindexSearchDeps,
running: AtomicBool, running: AtomicBool,
} }
impl SearchReindexHandler { impl SearchReindexHandler {
pub fn new(ctx: AppContext) -> Self { pub fn new(deps: ReindexSearchDeps) -> Self {
Self { Self {
ctx, deps,
running: AtomicBool::new(false), running: AtomicBool::new(false),
} }
} }
@@ -147,7 +143,7 @@ impl EventHandler for SearchReindexHandler {
} }
tracing::info!("search reindex started"); tracing::info!("search reindex started");
let result = execute(&self.ctx).await; let result = execute(&self.deps).await;
self.running.store(false, Ordering::SeqCst); self.running.store(false, Ordering::SeqCst);
let r = result?; let r = result?;

View File

@@ -5,12 +5,12 @@ use domain::{
value_objects::{MovieId, PosterPath}, value_objects::{MovieId, PosterPath},
}; };
use crate::{context::AppContext, diary::commands::SyncPosterCommand}; use crate::{diary::commands::SyncPosterCommand, movies::deps::SyncPosterDeps};
pub async fn execute(ctx: &AppContext, cmd: SyncPosterCommand) -> Result<(), DomainError> { pub async fn execute(deps: &SyncPosterDeps, cmd: SyncPosterCommand) -> Result<(), DomainError> {
let movie_id = MovieId::from_uuid(cmd.movie_id); let movie_id = MovieId::from_uuid(cmd.movie_id);
let mut movie = match ctx.repos.movie.get_movie_by_id(&movie_id).await? { let mut movie = match deps.movie.get_movie_by_id(&movie_id).await? {
Some(m) => m, Some(m) => m,
None => { None => {
tracing::warn!( tracing::warn!(
@@ -30,8 +30,7 @@ pub async fn execute(ctx: &AppContext, cmd: SyncPosterCommand) -> Result<(), Dom
})? })?
.clone(); .clone();
let poster_url = match ctx let poster_url = match deps
.services
.metadata .metadata
.get_poster_url(&external_metadata_id) .get_poster_url(&external_metadata_id)
.await .await
@@ -44,20 +43,17 @@ pub async fn execute(ctx: &AppContext, cmd: SyncPosterCommand) -> Result<(), Dom
} }
}; };
let image_bytes = ctx let image_bytes = deps
.services
.poster_fetcher .poster_fetcher
.fetch_poster_bytes(&poster_url) .fetch_poster_bytes(&poster_url)
.await?; .await?;
let stored_path = ctx let stored_path = deps
.services
.object_storage .object_storage
.store(&movie_id.value().to_string(), &image_bytes) .store(&movie_id.value().to_string(), &image_bytes)
.await?; .await?;
if let Err(e) = ctx if let Err(e) = deps
.services
.event_publisher .event_publisher
.publish(&DomainEvent::ImageStored { .publish(&DomainEvent::ImageStored {
key: stored_path.clone(), key: stored_path.clone(),
@@ -70,19 +66,17 @@ pub async fn execute(ctx: &AppContext, cmd: SyncPosterCommand) -> Result<(), Dom
let poster_path = PosterPath::new(stored_path)?; let poster_path = PosterPath::new(stored_path)?;
movie.update_poster(poster_path); movie.update_poster(poster_path);
ctx.repos.movie.upsert_movie(&movie).await?; deps.movie.upsert_movie(&movie).await?;
// Refresh search index so the new poster_path is reflected immediately. // Refresh search index so the new poster_path is reflected immediately.
// Fetch existing profile if available for a complete index document. // Fetch existing profile if available for a complete index document.
let profile = ctx let profile = deps
.repos
.movie_profile .movie_profile
.get_by_movie_id(&movie_id) .get_by_movie_id(&movie_id)
.await .await
.ok() .ok()
.flatten(); .flatten();
if let Err(e) = ctx if let Err(e) = deps
.repos
.search_command .search_command
.index(IndexableDocument::Movie { .index(IndexableDocument::Movie {
id: movie_id.clone(), id: movie_id.clone(),

View File

@@ -11,15 +11,13 @@ use domain::{
value_objects::{MovieId, MovieTitle, ReleaseYear}, value_objects::{MovieId, MovieTitle, ReleaseYear},
}; };
use crate::movies::{commands::EnrichMovieCommand, enrich_movie}; use crate::movies::{commands::EnrichMovieCommand, deps::EnrichMovieDeps, enrich_movie};
#[tokio::test] #[tokio::test]
async fn stores_profile_and_indexes() { async fn stores_profile_and_indexes() {
let movie_repo = InMemoryMovieRepository::new(); let movie_repo = InMemoryMovieRepository::new();
let profile_repo = InMemoryMovieProfileRepository::new(); let profile_repo = InMemoryMovieProfileRepository::new();
let search_cmd: Arc<dyn domain::ports::SearchCommand> = Arc::new(FakeSearchCommand);
// PanicPersonCommand is safe here — empty cast/crew means upsert_batch is never called // PanicPersonCommand is safe here — empty cast/crew means upsert_batch is never called
let person_cmd: Arc<dyn domain::ports::PersonCommand> = Arc::new(PanicPersonCommand);
let movie = Movie::new( let movie = Movie::new(
None, None,
@@ -51,11 +49,15 @@ async fn stores_profile_and_indexes() {
enriched_at: Utc::now(), enriched_at: Utc::now(),
}; };
let deps = EnrichMovieDeps {
movie: movie_repo as Arc<_>,
movie_profile: Arc::clone(&profile_repo) as Arc<_>,
person_command: Arc::new(PanicPersonCommand),
search_command: Arc::new(FakeSearchCommand),
};
enrich_movie::execute( enrich_movie::execute(
&(movie_repo as Arc<_>), &deps,
&(profile_repo.clone() as Arc<_>),
&person_cmd,
&search_cmd,
EnrichMovieCommand { EnrichMovieCommand {
movie_id: movie_id.clone(), movie_id: movie_id.clone(),
profile, profile,
@@ -96,8 +98,6 @@ impl domain::ports::PersonCommand for NoopPersonCommand {
async fn extracts_and_indexes_persons() { async fn extracts_and_indexes_persons() {
let movie_repo = InMemoryMovieRepository::new(); let movie_repo = InMemoryMovieRepository::new();
let profile_repo = InMemoryMovieProfileRepository::new(); let profile_repo = InMemoryMovieProfileRepository::new();
let search_cmd: Arc<dyn domain::ports::SearchCommand> = Arc::new(FakeSearchCommand);
let person_cmd: Arc<dyn domain::ports::PersonCommand> = Arc::new(NoopPersonCommand);
let movie = Movie::new( let movie = Movie::new(
None, None,
@@ -141,11 +141,15 @@ async fn extracts_and_indexes_persons() {
enriched_at: Utc::now(), enriched_at: Utc::now(),
}; };
let deps = EnrichMovieDeps {
movie: movie_repo as Arc<_>,
movie_profile: Arc::clone(&profile_repo) as Arc<_>,
person_command: Arc::new(NoopPersonCommand),
search_command: Arc::new(FakeSearchCommand),
};
enrich_movie::execute( enrich_movie::execute(
&(movie_repo as Arc<_>), &deps,
&(profile_repo.clone() as Arc<_>),
&person_cmd,
&search_cmd,
EnrichMovieCommand { EnrichMovieCommand {
movie_id: movie_id.clone(), movie_id: movie_id.clone(),
profile, profile,

View File

@@ -10,17 +10,19 @@ use domain::{
value_objects::MovieId, value_objects::MovieId,
}; };
use crate::{ use crate::movies::{
movies::get_movie_profile::{self, GetMovieProfileQuery}, deps::GetMovieProfileDeps,
test_helpers::TestContextBuilder, get_movie_profile::{self, GetMovieProfileQuery},
}; };
#[tokio::test] #[tokio::test]
async fn returns_none_when_no_profile() { async fn returns_none_when_no_profile() {
let ctx = TestContextBuilder::new().build(); let deps = GetMovieProfileDeps {
movie_profile: InMemoryMovieProfileRepository::new(),
};
let result = get_movie_profile::execute( let result = get_movie_profile::execute(
&ctx, &deps,
GetMovieProfileQuery { GetMovieProfileQuery {
movie_id: Uuid::new_v4(), movie_id: Uuid::new_v4(),
}, },
@@ -69,12 +71,12 @@ async fn returns_profile_with_cast_and_crew() {
}; };
profile_repo.upsert(&profile).await.unwrap(); profile_repo.upsert(&profile).await.unwrap();
let ctx = TestContextBuilder::new() let deps = GetMovieProfileDeps {
.with_movie_profiles(Arc::clone(&profile_repo) as _) movie_profile: Arc::clone(&profile_repo) as _,
.build(); };
let result = get_movie_profile::execute( let result = get_movie_profile::execute(
&ctx, &deps,
GetMovieProfileQuery { GetMovieProfileQuery {
movie_id: movie_id.value(), movie_id: movie_id.value(),
}, },

View File

@@ -1,14 +1,15 @@
use crate::{ use domain::testing::InMemoryMovieRepository;
movies::{get_movies, queries::GetMoviesQuery},
test_helpers::TestContextBuilder, use crate::movies::{deps::GetMoviesDeps, get_movies, queries::GetMoviesQuery};
};
#[tokio::test] #[tokio::test]
async fn returns_empty_when_no_movies() { async fn returns_empty_when_no_movies() {
let ctx = TestContextBuilder::new().build(); let deps = GetMoviesDeps {
movie: InMemoryMovieRepository::new(),
};
let result = get_movies::execute( let result = get_movies::execute(
&ctx, &deps,
GetMoviesQuery { GetMoviesQuery {
limit: None, limit: None,
offset: None, offset: None,

View File

@@ -6,20 +6,33 @@ use domain::{
errors::DomainError, errors::DomainError,
models::Movie, models::Movie,
ports::{MetadataClient, MovieRepository}, ports::{MetadataClient, MovieRepository},
testing::InMemoryMovieRepository, testing::{InMemoryMovieProfileRepository, InMemoryMovieRepository, NoopEventPublisher, NoopObjectStorage, FakeSearchCommand},
value_objects::{ExternalMetadataId, MovieTitle, PosterUrl, ReleaseYear}, value_objects::{ExternalMetadataId, MovieTitle, PosterUrl, ReleaseYear},
}; };
use crate::{ use crate::{
diary::commands::SyncPosterCommand, movies::sync_poster, test_helpers::TestContextBuilder, diary::commands::SyncPosterCommand,
movies::{deps::SyncPosterDeps, sync_poster},
}; };
fn default_deps() -> SyncPosterDeps {
SyncPosterDeps {
movie: InMemoryMovieRepository::new(),
movie_profile: InMemoryMovieProfileRepository::new(),
metadata: Arc::new(domain::testing::FakeMetadataClient),
poster_fetcher: Arc::new(domain::testing::FakePosterFetcher),
object_storage: Arc::new(NoopObjectStorage),
event_publisher: NoopEventPublisher::new(),
search_command: Arc::new(FakeSearchCommand),
}
}
#[tokio::test] #[tokio::test]
async fn fails_when_movie_not_found() { async fn fails_when_movie_not_found() {
let ctx = TestContextBuilder::new().build(); let deps = default_deps();
let result = sync_poster::execute( let result = sync_poster::execute(
&ctx, &deps,
SyncPosterCommand { SyncPosterCommand {
movie_id: Uuid::new_v4(), movie_id: Uuid::new_v4(),
}, },
@@ -42,11 +55,12 @@ async fn fails_when_no_external_id() {
let movie_id = movie.id().value(); let movie_id = movie.id().value();
movies.upsert_movie(&movie).await.unwrap(); movies.upsert_movie(&movie).await.unwrap();
let ctx = TestContextBuilder::new() let deps = SyncPosterDeps {
.with_movies(Arc::clone(&movies) as _) movie: Arc::clone(&movies) as _,
.build(); ..default_deps()
};
let result = sync_poster::execute(&ctx, SyncPosterCommand { movie_id }).await; let result = sync_poster::execute(&deps, SyncPosterCommand { movie_id }).await;
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -85,12 +99,13 @@ async fn syncs_poster_for_movie_with_external_id() {
let movie_id = movie.id().value(); let movie_id = movie.id().value();
movies.upsert_movie(&movie).await.unwrap(); movies.upsert_movie(&movie).await.unwrap();
let ctx = TestContextBuilder::new() let deps = SyncPosterDeps {
.with_movies(Arc::clone(&movies) as _) movie: Arc::clone(&movies) as _,
.with_metadata_client(Arc::new(FakeMetaWithPoster) as _) metadata: Arc::new(FakeMetaWithPoster) as _,
.build(); ..default_deps()
};
sync_poster::execute(&ctx, SyncPosterCommand { movie_id }) sync_poster::execute(&deps, SyncPosterCommand { movie_id })
.await .await
.unwrap(); .unwrap();

View File

@@ -12,7 +12,12 @@ use application::{
get_movie_social_page, get_review_history, get_movie_social_page, get_review_history,
queries::{GetMovieSocialPageQuery, GetReviewHistoryQuery}, queries::{GetMovieSocialPageQuery, GetReviewHistoryQuery},
}, },
movies::{get_movies, queries::GetMoviesQuery, sync_poster}, movies::{
deps::{GetMovieProfileDeps, GetMoviesDeps, SyncPosterDeps},
get_movies,
queries::GetMoviesQuery,
sync_poster,
},
watchlist::{is_on as is_on_watchlist, queries::IsOnWatchlistQuery}, watchlist::{is_on as is_on_watchlist, queries::IsOnWatchlistQuery},
}; };
use domain::services::review_history::Trend; use domain::services::review_history::Trend;
@@ -47,7 +52,9 @@ pub async fn list_movies(
Query(params): Query<MoviesQueryParams>, Query(params): Query<MoviesQueryParams>,
) -> Result<Json<MoviesResponse>, ApiError> { ) -> Result<Json<MoviesResponse>, ApiError> {
let page = get_movies::execute( let page = get_movies::execute(
&state.app_ctx, &GetMoviesDeps {
movie: state.app_ctx.repos.movie.clone(),
},
GetMoviesQuery { GetMoviesQuery {
limit: params.limit, limit: params.limit,
offset: params.offset, offset: params.offset,
@@ -116,7 +123,19 @@ pub async fn sync_poster(
_user: AuthenticatedUser, _user: AuthenticatedUser,
Path(movie_id): Path<Uuid>, Path(movie_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiError> { ) -> Result<impl IntoResponse, ApiError> {
sync_poster::execute(&state.app_ctx, SyncPosterCommand { movie_id }).await?; sync_poster::execute(
&SyncPosterDeps {
movie: state.app_ctx.repos.movie.clone(),
movie_profile: state.app_ctx.repos.movie_profile.clone(),
metadata: state.app_ctx.services.metadata.clone(),
poster_fetcher: state.app_ctx.services.poster_fetcher.clone(),
object_storage: state.app_ctx.services.object_storage.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
search_command: state.app_ctx.repos.search_command.clone(),
},
SyncPosterCommand { movie_id },
)
.await?;
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
@@ -188,7 +207,14 @@ pub async fn get_movie_profile(
) -> impl IntoResponse { ) -> impl IntoResponse {
use application::movies::get_movie_profile; use application::movies::get_movie_profile;
let query = get_movie_profile::GetMovieProfileQuery { movie_id }; let query = get_movie_profile::GetMovieProfileQuery { movie_id };
match get_movie_profile::execute(&state.app_ctx, query).await { match get_movie_profile::execute(
&GetMovieProfileDeps {
movie_profile: state.app_ctx.repos.movie_profile.clone(),
},
query,
)
.await
{
Ok(Some(result)) => { Ok(Some(result)) => {
let p = result.profile; let p = result.profile;
Json(MovieProfileResponse { Json(MovieProfileResponse {

View File

@@ -9,6 +9,7 @@ use application::{
MovieDiscoveryIndexer, SearchCleanupHandler, SearchReindexHandler, MovieDiscoveryIndexer, SearchCleanupHandler, SearchReindexHandler,
config::AppConfig, config::AppConfig,
context::{AppContext, Repositories, Services}, context::{AppContext, Repositories, Services},
movies::deps::ReindexSearchDeps,
worker::WorkerService, worker::WorkerService,
}; };
use export::ExportAdapter; use export::ExportAdapter;
@@ -155,8 +156,10 @@ async fn main() -> anyhow::Result<()> {
Some(person_enrichment_arc), Some(person_enrichment_arc),
Arc::clone(&ctx.repos.person_command), Arc::clone(&ctx.repos.person_command),
)) as Arc<dyn EventHandler>; )) as Arc<dyn EventHandler>;
let job = Arc::new(application::jobs::EnrichmentStalenessJob::new(ctx.clone())) let job = Arc::new(application::jobs::EnrichmentStalenessJob::new(
as Arc<dyn PeriodicJob>; Arc::clone(&ctx.repos.movie_profile),
Arc::clone(&ctx.services.event_publisher),
)) as Arc<dyn PeriodicJob>;
(Some(handler), Some(person_handler), Some(job)) (Some(handler), Some(person_handler), Some(job))
} }
Err(e) => { Err(e) => {
@@ -234,7 +237,13 @@ async fn main() -> anyhow::Result<()> {
application::wrapup::event_handler::WrapUpEventHandler::new(ctx.clone()), application::wrapup::event_handler::WrapUpEventHandler::new(ctx.clone()),
) as Arc<dyn EventHandler>; ) as Arc<dyn EventHandler>;
let reindex_handler = let reindex_handler =
Arc::new(SearchReindexHandler::new(ctx.clone())) as Arc<dyn EventHandler>; Arc::new(SearchReindexHandler::new(ReindexSearchDeps {
movie: Arc::clone(&ctx.repos.movie),
movie_profile: Arc::clone(&ctx.repos.movie_profile),
search_command: Arc::clone(&ctx.repos.search_command),
person_command: Arc::clone(&ctx.repos.person_command),
person_query: Arc::clone(&ctx.repos.person_query),
})) as Arc<dyn EventHandler>;
let mut h = vec![ let mut h = vec![
poster, poster,
cleanup, cleanup,
@@ -291,7 +300,13 @@ async fn main() -> anyhow::Result<()> {
application::wrapup::event_handler::WrapUpEventHandler::new(ctx.clone()), application::wrapup::event_handler::WrapUpEventHandler::new(ctx.clone()),
) as Arc<dyn EventHandler>; ) as Arc<dyn EventHandler>;
let reindex_handler = let reindex_handler =
Arc::new(SearchReindexHandler::new(ctx.clone())) as Arc<dyn EventHandler>; Arc::new(SearchReindexHandler::new(ReindexSearchDeps {
movie: Arc::clone(&ctx.repos.movie),
movie_profile: Arc::clone(&ctx.repos.movie_profile),
search_command: Arc::clone(&ctx.repos.search_command),
person_command: Arc::clone(&ctx.repos.person_command),
person_query: Arc::clone(&ctx.repos.person_query),
})) as Arc<dyn EventHandler>;
let mut h = vec![ let mut h = vec![
poster, poster,
cleanup, cleanup,