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:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1608,6 +1608,7 @@ dependencies = [
|
|||||||
"email_address",
|
"email_address",
|
||||||
"futures",
|
"futures",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
ports::{
|
ports::{
|
||||||
GoalRepository, LocalApContentQuery, MovieRepository, ReviewRepository, StatsRepository,
|
GoalRepository, LocalApContentQuery, MovieQuery, ReviewRepository, StatsRepository,
|
||||||
UserFederationSettingsQuery,
|
UserFederationSettingsQuery,
|
||||||
},
|
},
|
||||||
value_objects::{MovieId, ReviewId, UserId},
|
value_objects::{MovieId, ReviewId, UserId},
|
||||||
@@ -21,7 +21,7 @@ pub struct ActivityPubEventHandler {
|
|||||||
ap_service: Arc<ActivityPubService>,
|
ap_service: Arc<ActivityPubService>,
|
||||||
content_query: Arc<dyn LocalApContentQuery>,
|
content_query: Arc<dyn LocalApContentQuery>,
|
||||||
review_repo: Arc<dyn ReviewRepository>,
|
review_repo: Arc<dyn ReviewRepository>,
|
||||||
movie_repo: Arc<dyn MovieRepository>,
|
movie_repo: Arc<dyn MovieQuery>,
|
||||||
goal_repo: Arc<dyn GoalRepository>,
|
goal_repo: Arc<dyn GoalRepository>,
|
||||||
stats_repo: Arc<dyn StatsRepository>,
|
stats_repo: Arc<dyn StatsRepository>,
|
||||||
federation_settings: Arc<dyn UserFederationSettingsQuery>,
|
federation_settings: Arc<dyn UserFederationSettingsQuery>,
|
||||||
@@ -34,7 +34,7 @@ impl ActivityPubEventHandler {
|
|||||||
ap_service: Arc<ActivityPubService>,
|
ap_service: Arc<ActivityPubService>,
|
||||||
content_query: Arc<dyn LocalApContentQuery>,
|
content_query: Arc<dyn LocalApContentQuery>,
|
||||||
review_repo: Arc<dyn ReviewRepository>,
|
review_repo: Arc<dyn ReviewRepository>,
|
||||||
movie_repo: Arc<dyn MovieRepository>,
|
movie_repo: Arc<dyn MovieQuery>,
|
||||||
goal_repo: Arc<dyn GoalRepository>,
|
goal_repo: Arc<dyn GoalRepository>,
|
||||||
stats_repo: Arc<dyn StatsRepository>,
|
stats_repo: Arc<dyn StatsRepository>,
|
||||||
federation_settings: Arc<dyn UserFederationSettingsQuery>,
|
federation_settings: Arc<dyn UserFederationSettingsQuery>,
|
||||||
@@ -108,12 +108,8 @@ impl EventHandler for ActivityPubEventHandler {
|
|||||||
let inbox: url::Url = inbox_url
|
let inbox: url::Url = inbox_url
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|e| DomainError::InfrastructureError(format!("bad inbox URL: {e}")))?;
|
.map_err(|e| DomainError::InfrastructureError(format!("bad inbox URL: {e}")))?;
|
||||||
let activity: serde_json::Value =
|
|
||||||
serde_json::from_str(activity_json).map_err(|e| {
|
|
||||||
DomainError::InfrastructureError(format!("bad activity JSON: {e}"))
|
|
||||||
})?;
|
|
||||||
self.ap_service
|
self.ap_service
|
||||||
.deliver_to_inbox(inbox, activity, *signing_actor_id)
|
.deliver_to_inbox(inbox, activity_json.clone(), *signing_actor_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))
|
.map_err(|e| DomainError::InfrastructureError(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,12 +34,10 @@ impl k_ap::EventPublisher for FederationEventBridge {
|
|||||||
activity,
|
activity,
|
||||||
signing_actor_id,
|
signing_actor_id,
|
||||||
} => {
|
} => {
|
||||||
let json = serde_json::to_string(&activity)
|
|
||||||
.map_err(|e| anyhow::anyhow!("serialize activity: {e}"))?;
|
|
||||||
self.domain_publisher
|
self.domain_publisher
|
||||||
.publish(&DomainEvent::FederationDeliveryRequested {
|
.publish(&DomainEvent::FederationDeliveryRequested {
|
||||||
inbox_url: inbox.to_string(),
|
inbox_url: inbox.to_string(),
|
||||||
activity_json: json,
|
activity_json: activity,
|
||||||
signing_actor_id,
|
signing_actor_id,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ pub struct ActivityPubDeps {
|
|||||||
pub remote_watchlist_repo: std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
|
pub remote_watchlist_repo: std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
|
||||||
pub remote_goal_repo: std::sync::Arc<dyn domain::ports::RemoteGoalRepository>,
|
pub remote_goal_repo: std::sync::Arc<dyn domain::ports::RemoteGoalRepository>,
|
||||||
pub local_ap_content: std::sync::Arc<dyn domain::ports::LocalApContentQuery>,
|
pub local_ap_content: std::sync::Arc<dyn domain::ports::LocalApContentQuery>,
|
||||||
pub movie_repo: std::sync::Arc<dyn domain::ports::MovieRepository>,
|
pub movie_repo: std::sync::Arc<dyn domain::ports::MovieQuery>,
|
||||||
pub review_repo: std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
pub review_repo: std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
||||||
pub diary_repo: std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
pub diary_repo: std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
||||||
pub goal_repo: std::sync::Arc<dyn domain::ports::GoalRepository>,
|
pub goal_repo: std::sync::Arc<dyn domain::ports::GoalRepository>,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use async_trait::async_trait;
|
|||||||
use domain::{
|
use domain::{
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::ReviewSource,
|
models::ReviewSource,
|
||||||
ports::{DiaryRepository, EventPublisher, LocalApContentQuery, MovieRepository},
|
ports::{DiaryRepository, EventPublisher, LocalApContentQuery, MovieQuery},
|
||||||
value_objects::{Comment, ExternalMetadataId, MovieId, Rating, ReviewId, UserId},
|
value_objects::{Comment, ExternalMetadataId, MovieId, Rating, ReviewId, UserId},
|
||||||
};
|
};
|
||||||
use k_ap::{ApContentReader, ApObjectHandler};
|
use k_ap::{ApContentReader, ApObjectHandler};
|
||||||
@@ -16,7 +16,7 @@ use crate::urls::{actor_url, review_url};
|
|||||||
|
|
||||||
pub struct ReviewObjectHandler {
|
pub struct ReviewObjectHandler {
|
||||||
pub content_query: Arc<dyn LocalApContentQuery>,
|
pub content_query: Arc<dyn LocalApContentQuery>,
|
||||||
pub movie_repo: Arc<dyn MovieRepository>,
|
pub movie_repo: Arc<dyn MovieQuery>,
|
||||||
pub diary_repo: Arc<dyn DiaryRepository>,
|
pub diary_repo: Arc<dyn DiaryRepository>,
|
||||||
pub review_store: Arc<dyn RemoteReviewRepository>,
|
pub review_store: Arc<dyn RemoteReviewRepository>,
|
||||||
pub event_publisher: Arc<dyn EventPublisher>,
|
pub event_publisher: Arc<dyn EventPublisher>,
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ pub enum EventPayload {
|
|||||||
},
|
},
|
||||||
FederationDeliveryRequested {
|
FederationDeliveryRequested {
|
||||||
inbox_url: String,
|
inbox_url: String,
|
||||||
activity_json: String,
|
activity_json: serde_json::Value,
|
||||||
signing_actor_id: String,
|
signing_actor_id: String,
|
||||||
},
|
},
|
||||||
WatchEventIngested {
|
WatchEventIngested {
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
ports::{
|
ports::{
|
||||||
EventHandler, EventPublisher, MetadataClient, MovieRepository, ObjectStorage,
|
EventHandler, EventPublisher, MetadataClient, MovieCommand, MovieQuery, ObjectStorage,
|
||||||
PosterFetcherClient,
|
PosterFetcherClient,
|
||||||
},
|
},
|
||||||
value_objects::{ExternalMetadataId, MovieId, PosterPath},
|
value_objects::{ExternalMetadataId, MovieId, PosterPath},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct PosterSyncHandler {
|
pub struct PosterSyncHandler {
|
||||||
movie_repository: Arc<dyn MovieRepository>,
|
movie_command: Arc<dyn MovieCommand>,
|
||||||
|
movie_query: Arc<dyn MovieQuery>,
|
||||||
metadata_client: Arc<dyn MetadataClient>,
|
metadata_client: Arc<dyn MetadataClient>,
|
||||||
poster_fetcher: Arc<dyn PosterFetcherClient>,
|
poster_fetcher: Arc<dyn PosterFetcherClient>,
|
||||||
object_storage: Arc<dyn ObjectStorage>,
|
object_storage: Arc<dyn ObjectStorage>,
|
||||||
@@ -22,7 +23,8 @@ pub struct PosterSyncHandler {
|
|||||||
|
|
||||||
impl PosterSyncHandler {
|
impl PosterSyncHandler {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
movie_repository: Arc<dyn MovieRepository>,
|
movie_command: Arc<dyn MovieCommand>,
|
||||||
|
movie_query: Arc<dyn MovieQuery>,
|
||||||
metadata_client: Arc<dyn MetadataClient>,
|
metadata_client: Arc<dyn MetadataClient>,
|
||||||
poster_fetcher: Arc<dyn PosterFetcherClient>,
|
poster_fetcher: Arc<dyn PosterFetcherClient>,
|
||||||
object_storage: Arc<dyn ObjectStorage>,
|
object_storage: Arc<dyn ObjectStorage>,
|
||||||
@@ -30,7 +32,8 @@ impl PosterSyncHandler {
|
|||||||
max_retries: u32,
|
max_retries: u32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
movie_repository,
|
movie_command,
|
||||||
|
movie_query,
|
||||||
metadata_client,
|
metadata_client,
|
||||||
poster_fetcher,
|
poster_fetcher,
|
||||||
object_storage,
|
object_storage,
|
||||||
@@ -44,7 +47,7 @@ impl PosterSyncHandler {
|
|||||||
movie_id: MovieId,
|
movie_id: MovieId,
|
||||||
external_metadata_id: ExternalMetadataId,
|
external_metadata_id: ExternalMetadataId,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let mut movie = match self.movie_repository.get_movie_by_id(&movie_id).await? {
|
let mut movie = match self.movie_query.get_movie_by_id(&movie_id).await? {
|
||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
None => {
|
None => {
|
||||||
tracing::warn!("Sync cancelled: Movie {} not found", movie_id.value());
|
tracing::warn!("Sync cancelled: Movie {} not found", movie_id.value());
|
||||||
@@ -82,7 +85,7 @@ impl PosterSyncHandler {
|
|||||||
let poster_path = PosterPath::new(stored_path)?;
|
let poster_path = PosterPath::new(stored_path)?;
|
||||||
|
|
||||||
movie.update_poster(poster_path);
|
movie.update_poster(poster_path);
|
||||||
self.movie_repository.upsert_movie(&movie).await?;
|
self.movie_command.upsert_movie(&movie).await?;
|
||||||
|
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.event_publisher
|
.event_publisher
|
||||||
@@ -115,7 +118,7 @@ impl EventHandler for PosterSyncHandler {
|
|||||||
} => {
|
} => {
|
||||||
// Only sync poster if the movie doesn't have one yet
|
// Only sync poster if the movie doesn't have one yet
|
||||||
let already_has_poster = self
|
let already_has_poster = self
|
||||||
.movie_repository
|
.movie_query
|
||||||
.get_movie_by_id(&MovieId::from_uuid(movie_id.value()))
|
.get_movie_by_id(&MovieId::from_uuid(movie_id.value()))
|
||||||
.await?
|
.await?
|
||||||
.map(|m| m.poster_path().is_some())
|
.map(|m| m.poster_path().is_some())
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ pub fn create_profile_fields_repo(
|
|||||||
|
|
||||||
pub struct PostgresWireOutput {
|
pub struct PostgresWireOutput {
|
||||||
pub pool: PgPool,
|
pub pool: PgPool,
|
||||||
pub movie: std::sync::Arc<dyn domain::ports::MovieRepository>,
|
pub movie_command: std::sync::Arc<dyn domain::ports::MovieCommand>,
|
||||||
|
pub movie_query: std::sync::Arc<dyn domain::ports::MovieQuery>,
|
||||||
pub review: std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
pub review: std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
||||||
pub diary: std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
pub diary: std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
||||||
pub stats: std::sync::Arc<dyn domain::ports::StatsRepository>,
|
pub stats: std::sync::Arc<dyn domain::ports::StatsRepository>,
|
||||||
@@ -114,9 +115,12 @@ pub async fn wire(database_url: &str) -> anyhow::Result<PostgresWireOutput> {
|
|||||||
user_settings::PostgresUserSettingsRepository::new(pool.clone()),
|
user_settings::PostgresUserSettingsRepository::new(pool.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let movie_repo = std::sync::Arc::new(PostgresMovieRepository::new(pool.clone()));
|
||||||
|
|
||||||
Ok(PostgresWireOutput {
|
Ok(PostgresWireOutput {
|
||||||
pool: pool.clone(),
|
pool: pool.clone(),
|
||||||
movie: std::sync::Arc::new(PostgresMovieRepository::new(pool.clone())) as _,
|
movie_command: movie_repo.clone() as _,
|
||||||
|
movie_query: movie_repo as _,
|
||||||
review: std::sync::Arc::new(PostgresReviewRepository::new(pool.clone())) as _,
|
review: std::sync::Arc::new(PostgresReviewRepository::new(pool.clone())) as _,
|
||||||
diary: std::sync::Arc::new(PostgresDiaryRepository::new(pool.clone())) as _,
|
diary: std::sync::Arc::new(PostgresDiaryRepository::new(pool.clone())) as _,
|
||||||
stats: std::sync::Arc::new(PostgresStatsRepository::new(pool.clone())) as _,
|
stats: std::sync::Arc::new(PostgresStatsRepository::new(pool.clone())) as _,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use domain::{
|
|||||||
Movie, MovieFilter, MovieSummary,
|
Movie, MovieFilter, MovieSummary,
|
||||||
collections::{PageParams, Paginated},
|
collections::{PageParams, Paginated},
|
||||||
},
|
},
|
||||||
ports::MovieRepository,
|
ports::{MovieCommand, MovieQuery},
|
||||||
value_objects::{ExternalMetadataId, MovieId, MovieTitle, ReleaseYear},
|
value_objects::{ExternalMetadataId, MovieId, MovieTitle, ReleaseYear},
|
||||||
};
|
};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@@ -28,7 +28,51 @@ impl PostgresMovieRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl MovieRepository for PostgresMovieRepository {
|
impl MovieCommand for PostgresMovieRepository {
|
||||||
|
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError> {
|
||||||
|
let id = movie.id().value().to_string();
|
||||||
|
let external_metadata_id = movie.external_metadata_id().map(|e| e.value().to_string());
|
||||||
|
let title = movie.title().value();
|
||||||
|
let release_year = movie.release_year().value() as i64;
|
||||||
|
let director = movie.director();
|
||||||
|
let poster_path = movie.poster_path().map(|p| p.value().to_string());
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO movies (id, external_metadata_id, title, release_year, director, poster_path)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
external_metadata_id = excluded.external_metadata_id,
|
||||||
|
title = excluded.title,
|
||||||
|
release_year = excluded.release_year,
|
||||||
|
director = excluded.director,
|
||||||
|
poster_path = excluded.poster_path",
|
||||||
|
)
|
||||||
|
.bind(&id)
|
||||||
|
.bind(&external_metadata_id)
|
||||||
|
.bind(title)
|
||||||
|
.bind(release_year)
|
||||||
|
.bind(director)
|
||||||
|
.bind(&poster_path)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_err)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError> {
|
||||||
|
let id = movie_id.value().to_string();
|
||||||
|
sqlx::query("DELETE FROM movies WHERE id = $1")
|
||||||
|
.bind(&id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_err)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MovieQuery for PostgresMovieRepository {
|
||||||
async fn get_movie_by_external_id(
|
async fn get_movie_by_external_id(
|
||||||
&self,
|
&self,
|
||||||
external_metadata_id: &ExternalMetadataId,
|
external_metadata_id: &ExternalMetadataId,
|
||||||
@@ -81,47 +125,6 @@ impl MovieRepository for PostgresMovieRepository {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError> {
|
|
||||||
let id = movie.id().value().to_string();
|
|
||||||
let external_metadata_id = movie.external_metadata_id().map(|e| e.value().to_string());
|
|
||||||
let title = movie.title().value();
|
|
||||||
let release_year = movie.release_year().value() as i64;
|
|
||||||
let director = movie.director();
|
|
||||||
let poster_path = movie.poster_path().map(|p| p.value().to_string());
|
|
||||||
|
|
||||||
sqlx::query(
|
|
||||||
"INSERT INTO movies (id, external_metadata_id, title, release_year, director, poster_path)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
|
||||||
external_metadata_id = excluded.external_metadata_id,
|
|
||||||
title = excluded.title,
|
|
||||||
release_year = excluded.release_year,
|
|
||||||
director = excluded.director,
|
|
||||||
poster_path = excluded.poster_path",
|
|
||||||
)
|
|
||||||
.bind(&id)
|
|
||||||
.bind(&external_metadata_id)
|
|
||||||
.bind(title)
|
|
||||||
.bind(release_year)
|
|
||||||
.bind(director)
|
|
||||||
.bind(&poster_path)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Self::map_err)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError> {
|
|
||||||
let id = movie_id.value().to_string();
|
|
||||||
sqlx::query("DELETE FROM movies WHERE id = $1")
|
|
||||||
.bind(&id)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Self::map_err)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn existing_external_ids(
|
async fn existing_external_ids(
|
||||||
&self,
|
&self,
|
||||||
ids: &[ExternalMetadataId],
|
ids: &[ExternalMetadataId],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use async_trait::async_trait;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{PersistedWatchEvent, WatchEvent, WatchEventSource, WatchEventStatus, WebhookToken},
|
models::{PersistedWatchEvent, WatchEvent, WatchEventSource, WatchEventStatus, WebhookToken},
|
||||||
ports::{WatchEventRepository, WebhookTokenRepository},
|
ports::{WatchEventCommand, WatchEventQuery, WebhookTokenRepository},
|
||||||
value_objects::{MovieId, UserId, WatchEventId, WebhookTokenId},
|
value_objects::{MovieId, UserId, WatchEventId, WebhookTokenId},
|
||||||
};
|
};
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row};
|
||||||
@@ -27,7 +27,7 @@ impl PostgresWatchEventRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl WatchEventRepository for PostgresWatchEventRepository {
|
impl WatchEventCommand for PostgresWatchEventRepository {
|
||||||
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError> {
|
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError> {
|
||||||
let id = event.id().value().to_string();
|
let id = event.id().value().to_string();
|
||||||
let user_id = event.user_id().value().to_string();
|
let user_id = event.user_id().value().to_string();
|
||||||
@@ -75,6 +75,41 @@ impl WatchEventRepository for PostgresWatchEventRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_status_batch(
|
||||||
|
&self,
|
||||||
|
ids: &[WatchEventId],
|
||||||
|
status: WatchEventStatus,
|
||||||
|
) -> Result<u64, DomainError> {
|
||||||
|
if ids.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let id_strs: Vec<String> = ids.iter().map(|id| id.value().to_string()).collect();
|
||||||
|
let status_str = status.to_string();
|
||||||
|
let result = sqlx::query("UPDATE watch_events SET status = $1 WHERE id = ANY($2)")
|
||||||
|
.bind(&status_str)
|
||||||
|
.bind(&id_strs)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(map_err)?;
|
||||||
|
Ok(result.rows_affected())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_non_pending_older_than(
|
||||||
|
&self,
|
||||||
|
before: chrono::NaiveDateTime,
|
||||||
|
) -> Result<u64, DomainError> {
|
||||||
|
let result =
|
||||||
|
sqlx::query("DELETE FROM watch_events WHERE status != 'pending' AND created_at < $1")
|
||||||
|
.bind(before)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(map_err)?;
|
||||||
|
Ok(result.rows_affected())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl WatchEventQuery for PostgresWatchEventRepository {
|
||||||
async fn list_pending(&self, user_id: &UserId) -> Result<Vec<WatchEvent>, DomainError> {
|
async fn list_pending(&self, user_id: &UserId) -> Result<Vec<WatchEvent>, DomainError> {
|
||||||
let uid = user_id.value().to_string();
|
let uid = user_id.value().to_string();
|
||||||
|
|
||||||
@@ -135,25 +170,6 @@ impl WatchEventRepository for PostgresWatchEventRepository {
|
|||||||
rows.iter().map(row_to_watch_event).collect()
|
rows.iter().map(row_to_watch_event).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_status_batch(
|
|
||||||
&self,
|
|
||||||
ids: &[WatchEventId],
|
|
||||||
status: WatchEventStatus,
|
|
||||||
) -> Result<u64, DomainError> {
|
|
||||||
if ids.is_empty() {
|
|
||||||
return Ok(0);
|
|
||||||
}
|
|
||||||
let id_strs: Vec<String> = ids.iter().map(|id| id.value().to_string()).collect();
|
|
||||||
let status_str = status.to_string();
|
|
||||||
let result = sqlx::query("UPDATE watch_events SET status = $1 WHERE id = ANY($2)")
|
|
||||||
.bind(&status_str)
|
|
||||||
.bind(&id_strs)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(map_err)?;
|
|
||||||
Ok(result.rows_affected())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_duplicate(
|
async fn find_duplicate(
|
||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
@@ -175,19 +191,6 @@ impl WatchEventRepository for PostgresWatchEventRepository {
|
|||||||
|
|
||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_non_pending_older_than(
|
|
||||||
&self,
|
|
||||||
before: chrono::NaiveDateTime,
|
|
||||||
) -> Result<u64, DomainError> {
|
|
||||||
let result =
|
|
||||||
sqlx::query("DELETE FROM watch_events WHERE status != 'pending' AND created_at < $1")
|
|
||||||
.bind(before)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(map_err)?;
|
|
||||||
Ok(result.rows_affected())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row_to_watch_event(row: &sqlx::postgres::PgRow) -> Result<WatchEvent, DomainError> {
|
fn row_to_watch_event(row: &sqlx::postgres::PgRow) -> Result<WatchEvent, DomainError> {
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ pub async fn migrate(pool: &SqlitePool) -> Result<(), domain::errors::DomainErro
|
|||||||
|
|
||||||
pub struct SqliteWireOutput {
|
pub struct SqliteWireOutput {
|
||||||
pub pool: SqlitePool,
|
pub pool: SqlitePool,
|
||||||
pub movie: std::sync::Arc<dyn domain::ports::MovieRepository>,
|
pub movie_command: std::sync::Arc<dyn domain::ports::MovieCommand>,
|
||||||
|
pub movie_query: std::sync::Arc<dyn domain::ports::MovieQuery>,
|
||||||
pub review: std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
pub review: std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
||||||
pub diary: std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
pub diary: std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
||||||
pub stats: std::sync::Arc<dyn domain::ports::StatsRepository>,
|
pub stats: std::sync::Arc<dyn domain::ports::StatsRepository>,
|
||||||
@@ -119,9 +120,12 @@ pub async fn wire(database_url: &str) -> anyhow::Result<SqliteWireOutput> {
|
|||||||
pool.clone(),
|
pool.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let movie_repo = std::sync::Arc::new(SqliteMovieRepository::new(pool.clone()));
|
||||||
|
|
||||||
Ok(SqliteWireOutput {
|
Ok(SqliteWireOutput {
|
||||||
pool: pool.clone(),
|
pool: pool.clone(),
|
||||||
movie: std::sync::Arc::new(SqliteMovieRepository::new(pool.clone())) as _,
|
movie_command: movie_repo.clone() as _,
|
||||||
|
movie_query: movie_repo as _,
|
||||||
review: std::sync::Arc::new(SqliteReviewRepository::new(pool.clone())) as _,
|
review: std::sync::Arc::new(SqliteReviewRepository::new(pool.clone())) as _,
|
||||||
diary: std::sync::Arc::new(SqliteDiaryRepository::new(pool.clone())) as _,
|
diary: std::sync::Arc::new(SqliteDiaryRepository::new(pool.clone())) as _,
|
||||||
stats: std::sync::Arc::new(SqliteStatsRepository::new(pool.clone())) as _,
|
stats: std::sync::Arc::new(SqliteStatsRepository::new(pool.clone())) as _,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use domain::{
|
|||||||
Movie, MovieFilter, MovieSummary,
|
Movie, MovieFilter, MovieSummary,
|
||||||
collections::{PageParams, Paginated},
|
collections::{PageParams, Paginated},
|
||||||
},
|
},
|
||||||
ports::MovieRepository,
|
ports::{MovieCommand, MovieQuery},
|
||||||
value_objects::{ExternalMetadataId, MovieId, MovieTitle, ReleaseYear},
|
value_objects::{ExternalMetadataId, MovieId, MovieTitle, ReleaseYear},
|
||||||
};
|
};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
@@ -28,7 +28,51 @@ impl SqliteMovieRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl MovieRepository for SqliteMovieRepository {
|
impl MovieCommand for SqliteMovieRepository {
|
||||||
|
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError> {
|
||||||
|
let id = movie.id().value().to_string();
|
||||||
|
let external_metadata_id = movie.external_metadata_id().map(|e| e.value().to_string());
|
||||||
|
let title = movie.title().value();
|
||||||
|
let release_year = movie.release_year().value() as i64;
|
||||||
|
let director = movie.director();
|
||||||
|
let poster_path = movie.poster_path().map(|p| p.value().to_string());
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO movies (id, external_metadata_id, title, release_year, director, poster_path)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
external_metadata_id = excluded.external_metadata_id,
|
||||||
|
title = excluded.title,
|
||||||
|
release_year = excluded.release_year,
|
||||||
|
director = excluded.director,
|
||||||
|
poster_path = excluded.poster_path",
|
||||||
|
)
|
||||||
|
.bind(&id)
|
||||||
|
.bind(&external_metadata_id)
|
||||||
|
.bind(title)
|
||||||
|
.bind(release_year)
|
||||||
|
.bind(director)
|
||||||
|
.bind(&poster_path)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_err)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError> {
|
||||||
|
let id = movie_id.value().to_string();
|
||||||
|
sqlx::query("DELETE FROM movies WHERE id = ?")
|
||||||
|
.bind(&id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_err)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MovieQuery for SqliteMovieRepository {
|
||||||
async fn get_movie_by_external_id(
|
async fn get_movie_by_external_id(
|
||||||
&self,
|
&self,
|
||||||
external_metadata_id: &ExternalMetadataId,
|
external_metadata_id: &ExternalMetadataId,
|
||||||
@@ -81,47 +125,6 @@ impl MovieRepository for SqliteMovieRepository {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError> {
|
|
||||||
let id = movie.id().value().to_string();
|
|
||||||
let external_metadata_id = movie.external_metadata_id().map(|e| e.value().to_string());
|
|
||||||
let title = movie.title().value();
|
|
||||||
let release_year = movie.release_year().value() as i64;
|
|
||||||
let director = movie.director();
|
|
||||||
let poster_path = movie.poster_path().map(|p| p.value().to_string());
|
|
||||||
|
|
||||||
sqlx::query(
|
|
||||||
"INSERT INTO movies (id, external_metadata_id, title, release_year, director, poster_path)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
|
||||||
external_metadata_id = excluded.external_metadata_id,
|
|
||||||
title = excluded.title,
|
|
||||||
release_year = excluded.release_year,
|
|
||||||
director = excluded.director,
|
|
||||||
poster_path = excluded.poster_path",
|
|
||||||
)
|
|
||||||
.bind(&id)
|
|
||||||
.bind(&external_metadata_id)
|
|
||||||
.bind(title)
|
|
||||||
.bind(release_year)
|
|
||||||
.bind(director)
|
|
||||||
.bind(&poster_path)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Self::map_err)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError> {
|
|
||||||
let id = movie_id.value().to_string();
|
|
||||||
sqlx::query("DELETE FROM movies WHERE id = ?")
|
|
||||||
.bind(&id)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Self::map_err)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn existing_external_ids(
|
async fn existing_external_ids(
|
||||||
&self,
|
&self,
|
||||||
ids: &[ExternalMetadataId],
|
ids: &[ExternalMetadataId],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use async_trait::async_trait;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{PersistedWatchEvent, WatchEvent, WatchEventSource, WatchEventStatus, WebhookToken},
|
models::{PersistedWatchEvent, WatchEvent, WatchEventSource, WatchEventStatus, WebhookToken},
|
||||||
ports::{WatchEventRepository, WebhookTokenRepository},
|
ports::{WatchEventCommand, WatchEventQuery, WebhookTokenRepository},
|
||||||
value_objects::{MovieId, UserId, WatchEventId, WebhookTokenId},
|
value_objects::{MovieId, UserId, WatchEventId, WebhookTokenId},
|
||||||
};
|
};
|
||||||
use sqlx::{Row, SqlitePool};
|
use sqlx::{Row, SqlitePool};
|
||||||
@@ -38,7 +38,7 @@ impl SqliteWatchEventRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl WatchEventRepository for SqliteWatchEventRepository {
|
impl WatchEventCommand for SqliteWatchEventRepository {
|
||||||
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError> {
|
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError> {
|
||||||
let id = event.id().value().to_string();
|
let id = event.id().value().to_string();
|
||||||
let user_id = event.user_id().value().to_string();
|
let user_id = event.user_id().value().to_string();
|
||||||
@@ -88,6 +88,44 @@ impl WatchEventRepository for SqliteWatchEventRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_status_batch(
|
||||||
|
&self,
|
||||||
|
ids: &[WatchEventId],
|
||||||
|
status: WatchEventStatus,
|
||||||
|
) -> Result<u64, DomainError> {
|
||||||
|
if ids.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let placeholders: Vec<&str> = ids.iter().map(|_| "?").collect();
|
||||||
|
let sql = format!(
|
||||||
|
"UPDATE watch_events SET status = ? WHERE id IN ({})",
|
||||||
|
placeholders.join(",")
|
||||||
|
);
|
||||||
|
let mut q = sqlx::query(&sql).bind(status.to_string());
|
||||||
|
for id in ids {
|
||||||
|
q = q.bind(id.value().to_string());
|
||||||
|
}
|
||||||
|
let result = q.execute(&self.pool).await.map_err(map_err)?;
|
||||||
|
Ok(result.rows_affected())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_non_pending_older_than(
|
||||||
|
&self,
|
||||||
|
before: chrono::NaiveDateTime,
|
||||||
|
) -> Result<u64, DomainError> {
|
||||||
|
let before_str = datetime_to_str(&before);
|
||||||
|
let result =
|
||||||
|
sqlx::query("DELETE FROM watch_events WHERE status != 'pending' AND created_at < ?")
|
||||||
|
.bind(&before_str)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(map_err)?;
|
||||||
|
Ok(result.rows_affected())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl WatchEventQuery for SqliteWatchEventRepository {
|
||||||
async fn list_pending(&self, user_id: &UserId) -> Result<Vec<WatchEvent>, DomainError> {
|
async fn list_pending(&self, user_id: &UserId) -> Result<Vec<WatchEvent>, DomainError> {
|
||||||
let uid = user_id.value().to_string();
|
let uid = user_id.value().to_string();
|
||||||
|
|
||||||
@@ -141,27 +179,6 @@ impl WatchEventRepository for SqliteWatchEventRepository {
|
|||||||
rows.iter().map(row_to_watch_event).collect()
|
rows.iter().map(row_to_watch_event).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_status_batch(
|
|
||||||
&self,
|
|
||||||
ids: &[WatchEventId],
|
|
||||||
status: WatchEventStatus,
|
|
||||||
) -> Result<u64, DomainError> {
|
|
||||||
if ids.is_empty() {
|
|
||||||
return Ok(0);
|
|
||||||
}
|
|
||||||
let placeholders: Vec<&str> = ids.iter().map(|_| "?").collect();
|
|
||||||
let sql = format!(
|
|
||||||
"UPDATE watch_events SET status = ? WHERE id IN ({})",
|
|
||||||
placeholders.join(",")
|
|
||||||
);
|
|
||||||
let mut q = sqlx::query(&sql).bind(status.to_string());
|
|
||||||
for id in ids {
|
|
||||||
q = q.bind(id.value().to_string());
|
|
||||||
}
|
|
||||||
let result = q.execute(&self.pool).await.map_err(map_err)?;
|
|
||||||
Ok(result.rows_affected())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_duplicate(
|
async fn find_duplicate(
|
||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
@@ -186,20 +203,6 @@ impl WatchEventRepository for SqliteWatchEventRepository {
|
|||||||
|
|
||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_non_pending_older_than(
|
|
||||||
&self,
|
|
||||||
before: chrono::NaiveDateTime,
|
|
||||||
) -> Result<u64, DomainError> {
|
|
||||||
let before_str = datetime_to_str(&before);
|
|
||||||
let result =
|
|
||||||
sqlx::query("DELETE FROM watch_events WHERE status != 'pending' AND created_at < ?")
|
|
||||||
.bind(&before_str)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(map_err)?;
|
|
||||||
Ok(result.rows_affected())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row_to_watch_event(row: &sqlx::sqlite::SqliteRow) -> Result<WatchEvent, DomainError> {
|
fn row_to_watch_event(row: &sqlx::sqlite::SqliteRow) -> Result<WatchEvent, DomainError> {
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ pub struct UserProfileBase {
|
|||||||
pub banner_url: Option<String>,
|
pub banner_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ProfileViewData {
|
||||||
|
Entries { entries: DiaryResponse },
|
||||||
|
History { history: Vec<MonthActivityDto> },
|
||||||
|
Trends { trends: UserTrendsDto },
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct UserProfileResponse {
|
pub struct UserProfileResponse {
|
||||||
pub user_id: Uuid,
|
pub user_id: Uuid,
|
||||||
@@ -87,12 +95,8 @@ pub struct UserProfileResponse {
|
|||||||
pub stats: UserStatsDto,
|
pub stats: UserStatsDto,
|
||||||
pub following_count: usize,
|
pub following_count: usize,
|
||||||
pub followers_count: usize,
|
pub followers_count: usize,
|
||||||
/// Populated for view=recent and view=ratings
|
#[serde(flatten, skip_serializing_if = "Option::is_none")]
|
||||||
pub entries: Option<DiaryResponse>,
|
pub view_data: Option<ProfileViewData>,
|
||||||
/// Populated for view=history
|
|
||||||
pub history: Option<Vec<MonthActivityDto>>,
|
|
||||||
/// Populated for view=trends
|
|
||||||
pub trends: Option<UserTrendsDto>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub goals: Option<Vec<GoalDto>>,
|
pub goals: Option<Vec<GoalDto>>,
|
||||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub async fn execute(deps: &DeleteReviewDeps, cmd: DeleteReviewCommand) -> Resul
|
|||||||
let history = deps.diary.get_review_history(&movie_id).await?;
|
let history = deps.diary.get_review_history(&movie_id).await?;
|
||||||
if history.viewings().is_empty() {
|
if history.viewings().is_empty() {
|
||||||
let poster_path = history.movie().poster_path().cloned();
|
let poster_path = history.movie().poster_path().cloned();
|
||||||
deps.movie.delete_movie(&movie_id).await?;
|
deps.movie_command.delete_movie(&movie_id).await?;
|
||||||
// best-effort: movie is already deleted, so publish failure is non-fatal
|
// best-effort: movie is already deleted, so publish failure is non-fatal
|
||||||
if let Err(e) = deps
|
if let Err(e) = deps
|
||||||
.event_publisher
|
.event_publisher
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
DiaryRepository, EventPublisher, MovieProfileRepository, MovieRepository, ReviewRepository,
|
DiaryRepository, EventPublisher, MovieCommand, MovieProfileRepository, MovieQuery,
|
||||||
SocialQueryPort,
|
ReviewRepository, SocialQueryPort,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
@@ -10,7 +10,7 @@ use crate::config::AppConfig;
|
|||||||
pub struct DeleteReviewDeps {
|
pub struct DeleteReviewDeps {
|
||||||
pub review: Arc<dyn ReviewRepository>,
|
pub review: Arc<dyn ReviewRepository>,
|
||||||
pub diary: Arc<dyn DiaryRepository>,
|
pub diary: Arc<dyn DiaryRepository>,
|
||||||
pub movie: Arc<dyn MovieRepository>,
|
pub movie_command: Arc<dyn MovieCommand>,
|
||||||
pub event_publisher: Arc<dyn EventPublisher>,
|
pub event_publisher: Arc<dyn EventPublisher>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ pub struct EditReviewDeps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct GetMovieSocialPageDeps {
|
pub struct GetMovieSocialPageDeps {
|
||||||
pub movie: Arc<dyn MovieRepository>,
|
pub movie_query: Arc<dyn MovieQuery>,
|
||||||
pub diary: Arc<dyn DiaryRepository>,
|
pub diary: Arc<dyn DiaryRepository>,
|
||||||
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub async fn execute(
|
|||||||
let page = PageParams::new(Some(query.limit), Some(query.offset))?;
|
let page = PageParams::new(Some(query.limit), Some(query.offset))?;
|
||||||
|
|
||||||
let movie = deps
|
let movie = deps
|
||||||
.movie
|
.movie_query
|
||||||
.get_movie_by_id(&movie_id)
|
.get_movie_by_id(&movie_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound(format!("Movie {}", query.movie_id)))?;
|
.ok_or_else(|| DomainError::NotFound(format!("Movie {}", query.movie_id)))?;
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ use async_trait::async_trait;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{MetadataSearchCriteria, Movie},
|
models::{MetadataSearchCriteria, Movie},
|
||||||
ports::{MetadataClient, MovieRepository},
|
ports::{MetadataClient, MovieQuery},
|
||||||
value_objects::{ExternalMetadataId, MovieTitle, ReleaseYear},
|
value_objects::{ExternalMetadataId, MovieTitle, ReleaseYear},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::diary::commands::MovieInput;
|
use crate::diary::commands::MovieInput;
|
||||||
|
|
||||||
pub struct MovieResolverDeps<'a> {
|
pub struct MovieResolverDeps<'a> {
|
||||||
pub repository: &'a dyn MovieRepository,
|
pub repository: &'a dyn MovieQuery,
|
||||||
pub metadata_client: &'a dyn MetadataClient,
|
pub metadata_client: &'a dyn MetadataClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ use domain::{
|
|||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::Review,
|
models::Review,
|
||||||
ports::{
|
ports::{
|
||||||
EventPublisher, MetadataClient, MovieRepository, ReviewRepository, WatchlistRepository,
|
EventPublisher, MetadataClient, MovieCommand, MovieQuery, ReviewRepository,
|
||||||
|
WatchlistRepository,
|
||||||
},
|
},
|
||||||
value_objects::{Comment, Rating, UserId},
|
value_objects::{Comment, Rating, UserId},
|
||||||
};
|
};
|
||||||
@@ -16,7 +17,8 @@ use crate::movies::resolve::resolve_and_persist_movie;
|
|||||||
use crate::ports::ReviewLogger;
|
use crate::ports::ReviewLogger;
|
||||||
|
|
||||||
pub struct DefaultReviewLogger {
|
pub struct DefaultReviewLogger {
|
||||||
movie_repo: Arc<dyn MovieRepository>,
|
movie_command: Arc<dyn MovieCommand>,
|
||||||
|
movie_query: Arc<dyn MovieQuery>,
|
||||||
review_repo: Arc<dyn ReviewRepository>,
|
review_repo: Arc<dyn ReviewRepository>,
|
||||||
watchlist_repo: Arc<dyn WatchlistRepository>,
|
watchlist_repo: Arc<dyn WatchlistRepository>,
|
||||||
metadata_client: Arc<dyn MetadataClient>,
|
metadata_client: Arc<dyn MetadataClient>,
|
||||||
@@ -25,14 +27,16 @@ pub struct DefaultReviewLogger {
|
|||||||
|
|
||||||
impl DefaultReviewLogger {
|
impl DefaultReviewLogger {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
movie_repo: Arc<dyn MovieRepository>,
|
movie_command: Arc<dyn MovieCommand>,
|
||||||
|
movie_query: Arc<dyn MovieQuery>,
|
||||||
review_repo: Arc<dyn ReviewRepository>,
|
review_repo: Arc<dyn ReviewRepository>,
|
||||||
watchlist_repo: Arc<dyn WatchlistRepository>,
|
watchlist_repo: Arc<dyn WatchlistRepository>,
|
||||||
metadata_client: Arc<dyn MetadataClient>,
|
metadata_client: Arc<dyn MetadataClient>,
|
||||||
event_publisher: Arc<dyn EventPublisher>,
|
event_publisher: Arc<dyn EventPublisher>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
movie_repo,
|
movie_command,
|
||||||
|
movie_query,
|
||||||
review_repo,
|
review_repo,
|
||||||
watchlist_repo,
|
watchlist_repo,
|
||||||
metadata_client,
|
metadata_client,
|
||||||
@@ -50,7 +54,8 @@ impl ReviewLogger for DefaultReviewLogger {
|
|||||||
|
|
||||||
let (movie, is_new_movie) = resolve_and_persist_movie(
|
let (movie, is_new_movie) = resolve_and_persist_movie(
|
||||||
&cmd.input,
|
&cmd.input,
|
||||||
self.movie_repo.as_ref(),
|
self.movie_command.as_ref(),
|
||||||
|
self.movie_query.as_ref(),
|
||||||
self.metadata_client.as_ref(),
|
self.metadata_client.as_ref(),
|
||||||
self.event_publisher.as_ref(),
|
self.event_publisher.as_ref(),
|
||||||
)
|
)
|
||||||
@@ -58,7 +63,7 @@ impl ReviewLogger for DefaultReviewLogger {
|
|||||||
|
|
||||||
// Always upsert: even existing movies may have updated metadata
|
// Always upsert: even existing movies may have updated metadata
|
||||||
if !is_new_movie {
|
if !is_new_movie {
|
||||||
self.movie_repo.upsert_movie(&movie).await?;
|
self.movie_command.upsert_movie(&movie).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let review = Review::new(
|
let review = Review::new(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use chrono::Utc;
|
|||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
models::{Movie, Review},
|
models::{Movie, Review},
|
||||||
ports::{MovieRepository, ReviewRepository},
|
ports::{MovieCommand, MovieQuery, ReviewRepository},
|
||||||
testing::{
|
testing::{
|
||||||
FakeDiaryRepository, InMemoryMovieRepository, InMemoryReviewRepository, NoopEventPublisher,
|
FakeDiaryRepository, InMemoryMovieRepository, InMemoryReviewRepository, NoopEventPublisher,
|
||||||
},
|
},
|
||||||
@@ -55,7 +55,7 @@ async fn test_delete_review_removes_it() {
|
|||||||
let deps = DeleteReviewDeps {
|
let deps = DeleteReviewDeps {
|
||||||
review: Arc::clone(&reviews) as _,
|
review: Arc::clone(&reviews) as _,
|
||||||
diary: diary.clone() as _,
|
diary: diary.clone() as _,
|
||||||
movie: Arc::clone(&movies) as _,
|
movie_command: Arc::clone(&movies) as _,
|
||||||
event_publisher: Arc::clone(&events) as _,
|
event_publisher: Arc::clone(&events) as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ async fn test_delete_review_wrong_user_is_unauthorized() {
|
|||||||
let deps = DeleteReviewDeps {
|
let deps = DeleteReviewDeps {
|
||||||
review: Arc::clone(&reviews) as _,
|
review: Arc::clone(&reviews) as _,
|
||||||
diary: diary as _,
|
diary: diary as _,
|
||||||
movie: movies as _,
|
movie_command: movies as _,
|
||||||
event_publisher: Arc::clone(&events) as _,
|
event_publisher: Arc::clone(&events) as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
models::Movie,
|
models::Movie,
|
||||||
ports::MovieRepository,
|
ports::MovieCommand,
|
||||||
testing::{FakeDiaryRepository, InMemoryMovieProfileRepository, InMemoryMovieRepository},
|
testing::{FakeDiaryRepository, InMemoryMovieProfileRepository, InMemoryMovieRepository},
|
||||||
value_objects::{MovieTitle, ReleaseYear},
|
value_objects::{MovieTitle, ReleaseYear},
|
||||||
};
|
};
|
||||||
@@ -17,7 +17,7 @@ use crate::{
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn fails_when_movie_not_found() {
|
async fn fails_when_movie_not_found() {
|
||||||
let deps = GetMovieSocialPageDeps {
|
let deps = GetMovieSocialPageDeps {
|
||||||
movie: InMemoryMovieRepository::new(),
|
movie_query: InMemoryMovieRepository::new(),
|
||||||
diary: FakeDiaryRepository::new() as _,
|
diary: FakeDiaryRepository::new() as _,
|
||||||
movie_profile: InMemoryMovieProfileRepository::new(),
|
movie_profile: InMemoryMovieProfileRepository::new(),
|
||||||
};
|
};
|
||||||
@@ -50,7 +50,7 @@ async fn returns_movie_social_page() {
|
|||||||
movies.upsert_movie(&movie).await.unwrap();
|
movies.upsert_movie(&movie).await.unwrap();
|
||||||
|
|
||||||
let deps = GetMovieSocialPageDeps {
|
let deps = GetMovieSocialPageDeps {
|
||||||
movie: Arc::clone(&movies) as _,
|
movie_query: Arc::clone(&movies) as _,
|
||||||
diary: FakeDiaryRepository::new() as _,
|
diary: FakeDiaryRepository::new() as _,
|
||||||
movie_profile: InMemoryMovieProfileRepository::new(),
|
movie_profile: InMemoryMovieProfileRepository::new(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ use chrono::Utc;
|
|||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
models::Movie,
|
models::Movie,
|
||||||
|
ports::MovieCommand,
|
||||||
value_objects::{MovieTitle, ReleaseYear},
|
value_objects::{MovieTitle, ReleaseYear},
|
||||||
};
|
};
|
||||||
|
|
||||||
use domain::ports::MovieRepository;
|
|
||||||
use domain::testing::{InMemoryMovieRepository, InMemoryReviewRepository, NoopEventPublisher};
|
use domain::testing::{InMemoryMovieRepository, InMemoryReviewRepository, NoopEventPublisher};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -23,6 +23,7 @@ fn build_logger(
|
|||||||
events: &Arc<NoopEventPublisher>,
|
events: &Arc<NoopEventPublisher>,
|
||||||
) -> Arc<dyn crate::ports::ReviewLogger> {
|
) -> Arc<dyn crate::ports::ReviewLogger> {
|
||||||
Arc::new(DefaultReviewLogger::new(
|
Arc::new(DefaultReviewLogger::new(
|
||||||
|
Arc::clone(movies) as _,
|
||||||
Arc::clone(movies) as _,
|
Arc::clone(movies) as _,
|
||||||
Arc::clone(reviews) as _,
|
Arc::clone(reviews) as _,
|
||||||
TestContextBuilder::new().watchlist_repo,
|
TestContextBuilder::new().watchlist_repo,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::diary::commands::MovieInput;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{MetadataSearchCriteria, Movie},
|
models::{MetadataSearchCriteria, Movie},
|
||||||
ports::MovieRepository,
|
ports::MovieQuery,
|
||||||
value_objects::{ExternalMetadataId, MovieId, MovieTitle, PosterUrl, ReleaseYear},
|
value_objects::{ExternalMetadataId, MovieId, MovieTitle, PosterUrl, ReleaseYear},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ struct RepoEmpty;
|
|||||||
struct RepoWithTitleMatch(Movie);
|
struct RepoWithTitleMatch(Movie);
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MovieRepository for RepoWithExternalMovie {
|
impl MovieQuery for RepoWithExternalMovie {
|
||||||
async fn get_movie_by_external_id(
|
async fn get_movie_by_external_id(
|
||||||
&self,
|
&self,
|
||||||
_: &ExternalMetadataId,
|
_: &ExternalMetadataId,
|
||||||
@@ -49,12 +49,6 @@ impl MovieRepository for RepoWithExternalMovie {
|
|||||||
) -> Result<Vec<Movie>, DomainError> {
|
) -> Result<Vec<Movie>, DomainError> {
|
||||||
panic!("unexpected")
|
panic!("unexpected")
|
||||||
}
|
}
|
||||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
|
||||||
panic!("unexpected")
|
|
||||||
}
|
|
||||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
|
||||||
panic!("unexpected")
|
|
||||||
}
|
|
||||||
async fn existing_external_ids(
|
async fn existing_external_ids(
|
||||||
&self,
|
&self,
|
||||||
_: &[ExternalMetadataId],
|
_: &[ExternalMetadataId],
|
||||||
@@ -83,7 +77,7 @@ impl MovieRepository for RepoWithExternalMovie {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MovieRepository for RepoEmpty {
|
impl MovieQuery for RepoEmpty {
|
||||||
async fn get_movie_by_external_id(
|
async fn get_movie_by_external_id(
|
||||||
&self,
|
&self,
|
||||||
_: &ExternalMetadataId,
|
_: &ExternalMetadataId,
|
||||||
@@ -100,12 +94,6 @@ impl MovieRepository for RepoEmpty {
|
|||||||
) -> Result<Vec<Movie>, DomainError> {
|
) -> Result<Vec<Movie>, DomainError> {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
|
||||||
panic!("unexpected")
|
|
||||||
}
|
|
||||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
|
||||||
panic!("unexpected")
|
|
||||||
}
|
|
||||||
async fn existing_external_ids(
|
async fn existing_external_ids(
|
||||||
&self,
|
&self,
|
||||||
_: &[ExternalMetadataId],
|
_: &[ExternalMetadataId],
|
||||||
@@ -134,7 +122,7 @@ impl MovieRepository for RepoEmpty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MovieRepository for RepoWithTitleMatch {
|
impl MovieQuery for RepoWithTitleMatch {
|
||||||
async fn get_movie_by_external_id(
|
async fn get_movie_by_external_id(
|
||||||
&self,
|
&self,
|
||||||
_: &ExternalMetadataId,
|
_: &ExternalMetadataId,
|
||||||
@@ -151,12 +139,6 @@ impl MovieRepository for RepoWithTitleMatch {
|
|||||||
) -> Result<Vec<Movie>, DomainError> {
|
) -> Result<Vec<Movie>, DomainError> {
|
||||||
Ok(vec![self.0.clone()])
|
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")
|
|
||||||
}
|
|
||||||
async fn existing_external_ids(
|
async fn existing_external_ids(
|
||||||
&self,
|
&self,
|
||||||
_: &[ExternalMetadataId],
|
_: &[ExternalMetadataId],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::WatchlistEntry,
|
models::WatchlistEntry,
|
||||||
models::{MetadataSearchCriteria, Movie},
|
models::{MetadataSearchCriteria, Movie},
|
||||||
ports::{MetadataClient, MovieRepository, WatchlistRepository},
|
ports::{MetadataClient, MovieCommand, WatchlistRepository},
|
||||||
testing::{
|
testing::{
|
||||||
FakeMetadataClient, InMemoryMovieRepository, InMemoryReviewRepository,
|
FakeMetadataClient, InMemoryMovieRepository, InMemoryReviewRepository,
|
||||||
InMemoryWatchlistRepository, NoopEventPublisher,
|
InMemoryWatchlistRepository, NoopEventPublisher,
|
||||||
@@ -26,6 +26,7 @@ fn make_logger(
|
|||||||
events: &Arc<NoopEventPublisher>,
|
events: &Arc<NoopEventPublisher>,
|
||||||
) -> DefaultReviewLogger {
|
) -> DefaultReviewLogger {
|
||||||
DefaultReviewLogger::new(
|
DefaultReviewLogger::new(
|
||||||
|
Arc::clone(movies) as _,
|
||||||
Arc::clone(movies) as _,
|
Arc::clone(movies) as _,
|
||||||
Arc::clone(reviews) as _,
|
Arc::clone(reviews) as _,
|
||||||
Arc::clone(watchlist) as _,
|
Arc::clone(watchlist) as _,
|
||||||
@@ -276,6 +277,7 @@ async fn publishes_movie_discovered_for_new_movie_with_external_id() {
|
|||||||
let events = NoopEventPublisher::new();
|
let events = NoopEventPublisher::new();
|
||||||
|
|
||||||
let logger = DefaultReviewLogger::new(
|
let logger = DefaultReviewLogger::new(
|
||||||
|
Arc::clone(&movies) as _,
|
||||||
Arc::clone(&movies) as _,
|
Arc::clone(&movies) as _,
|
||||||
Arc::clone(&reviews) as _,
|
Arc::clone(&reviews) as _,
|
||||||
Arc::clone(&watchlist) as _,
|
Arc::clone(&watchlist) as _,
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::{Goal, GoalType, GoalWithProgress},
|
models::{Goal, GoalType, GoalWithProgress},
|
||||||
ports::{EventPublisher, GoalRepository, StatsRepository},
|
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::commands::CreateGoalCommand;
|
use super::{commands::CreateGoalCommand, deps::GoalCommandDeps};
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
goal: Arc<dyn GoalRepository>,
|
deps: &GoalCommandDeps,
|
||||||
stats: Arc<dyn StatsRepository>,
|
|
||||||
event_publisher: Arc<dyn EventPublisher>,
|
|
||||||
cmd: CreateGoalCommand,
|
cmd: CreateGoalCommand,
|
||||||
) -> Result<GoalWithProgress, DomainError> {
|
) -> Result<GoalWithProgress, DomainError> {
|
||||||
let user_id = UserId::from_uuid(cmd.user_id);
|
let user_id = UserId::from_uuid(cmd.user_id);
|
||||||
|
|
||||||
let existing = goal.find_by_user_and_year(&user_id, cmd.year).await?;
|
let existing = deps.goal.find_by_user_and_year(&user_id, cmd.year).await?;
|
||||||
if existing.is_some() {
|
if existing.is_some() {
|
||||||
return Err(DomainError::ValidationError(
|
return Err(DomainError::ValidationError(
|
||||||
"Goal already exists for this year".into(),
|
"Goal already exists for this year".into(),
|
||||||
@@ -31,11 +26,11 @@ pub async fn execute(
|
|||||||
cmd.target_count,
|
cmd.target_count,
|
||||||
GoalType::Movies,
|
GoalType::Movies,
|
||||||
)?;
|
)?;
|
||||||
goal.save(&g).await?;
|
deps.goal.save(&g).await?;
|
||||||
|
|
||||||
let current_count = stats.count_reviews_in_year(&user_id, cmd.year).await?;
|
let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?;
|
||||||
|
|
||||||
event_publisher
|
deps.event_publisher
|
||||||
.publish(&DomainEvent::GoalCreated {
|
.publish(&DomainEvent::GoalCreated {
|
||||||
goal_id: g.id().clone(),
|
goal_id: g.id().clone(),
|
||||||
user_id,
|
user_id,
|
||||||
|
|||||||
@@ -1,29 +1,26 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
ports::{EventPublisher, GoalRepository},
|
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::commands::DeleteGoalCommand;
|
use super::{commands::DeleteGoalCommand, deps::GoalCommandDeps};
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
goal: Arc<dyn GoalRepository>,
|
deps: &GoalCommandDeps,
|
||||||
event_publisher: Arc<dyn EventPublisher>,
|
|
||||||
cmd: DeleteGoalCommand,
|
cmd: DeleteGoalCommand,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let user_id = UserId::from_uuid(cmd.user_id);
|
let user_id = UserId::from_uuid(cmd.user_id);
|
||||||
|
|
||||||
let g = goal
|
let g = deps
|
||||||
|
.goal
|
||||||
.find_by_user_and_year(&user_id, cmd.year)
|
.find_by_user_and_year(&user_id, cmd.year)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?;
|
.ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?;
|
||||||
|
|
||||||
goal.delete(g.id(), &user_id).await?;
|
deps.goal.delete(g.id(), &user_id).await?;
|
||||||
|
|
||||||
event_publisher
|
deps.event_publisher
|
||||||
.publish(&DomainEvent::GoalDeleted {
|
.publish(&DomainEvent::GoalDeleted {
|
||||||
goal_id: g.id().clone(),
|
goal_id: g.id().clone(),
|
||||||
user_id,
|
user_id,
|
||||||
|
|||||||
14
crates/application/src/goals/deps.rs
Normal file
14
crates/application/src/goals/deps.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use domain::ports::{EventPublisher, GoalRepository, StatsRepository};
|
||||||
|
|
||||||
|
pub struct GoalCommandDeps {
|
||||||
|
pub goal: Arc<dyn GoalRepository>,
|
||||||
|
pub stats: Arc<dyn StatsRepository>,
|
||||||
|
pub event_publisher: Arc<dyn EventPublisher>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GoalQueryDeps {
|
||||||
|
pub goal: Arc<dyn GoalRepository>,
|
||||||
|
pub stats: Arc<dyn StatsRepository>,
|
||||||
|
}
|
||||||
@@ -1,26 +1,22 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::GoalWithProgress,
|
models::GoalWithProgress,
|
||||||
ports::{GoalRepository, StatsRepository},
|
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::queries::GetGoalQuery;
|
use super::{deps::GoalQueryDeps, queries::GetGoalQuery};
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
goal: Arc<dyn GoalRepository>,
|
deps: &GoalQueryDeps,
|
||||||
stats: Arc<dyn StatsRepository>,
|
|
||||||
query: GetGoalQuery,
|
query: GetGoalQuery,
|
||||||
) -> Result<Option<GoalWithProgress>, DomainError> {
|
) -> Result<Option<GoalWithProgress>, DomainError> {
|
||||||
let user_id = UserId::from_uuid(query.user_id);
|
let user_id = UserId::from_uuid(query.user_id);
|
||||||
|
|
||||||
let found = goal.find_by_user_and_year(&user_id, query.year).await?;
|
let found = deps.goal.find_by_user_and_year(&user_id, query.year).await?;
|
||||||
|
|
||||||
let Some(g) = found else { return Ok(None) };
|
let Some(g) = found else { return Ok(None) };
|
||||||
|
|
||||||
let current_count = stats.count_reviews_in_year(&user_id, query.year).await?;
|
let current_count = deps.stats.count_reviews_in_year(&user_id, query.year).await?;
|
||||||
|
|
||||||
Ok(Some(GoalWithProgress {
|
Ok(Some(GoalWithProgress {
|
||||||
goal: g,
|
goal: g,
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::GoalWithProgress,
|
models::GoalWithProgress,
|
||||||
ports::{GoalRepository, StatsRepository},
|
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::queries::ListGoalsQuery;
|
use super::{deps::GoalQueryDeps, queries::ListGoalsQuery};
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
goal: Arc<dyn GoalRepository>,
|
deps: &GoalQueryDeps,
|
||||||
stats: Arc<dyn StatsRepository>,
|
|
||||||
query: ListGoalsQuery,
|
query: ListGoalsQuery,
|
||||||
) -> Result<Vec<GoalWithProgress>, DomainError> {
|
) -> Result<Vec<GoalWithProgress>, DomainError> {
|
||||||
let user_id = UserId::from_uuid(query.user_id);
|
let user_id = UserId::from_uuid(query.user_id);
|
||||||
let goals = goal.list_for_user(&user_id).await?;
|
let goals = deps.goal.list_for_user(&user_id).await?;
|
||||||
|
|
||||||
let mut result = Vec::with_capacity(goals.len());
|
let mut result = Vec::with_capacity(goals.len());
|
||||||
for g in goals {
|
for g in goals {
|
||||||
let current_count = stats.count_reviews_in_year(&user_id, g.year()).await?;
|
let current_count = deps.stats.count_reviews_in_year(&user_id, g.year()).await?;
|
||||||
result.push(GoalWithProgress {
|
result.push(GoalWithProgress {
|
||||||
goal: g,
|
goal: g,
|
||||||
current_count,
|
current_count,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod create;
|
pub mod create;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
|
pub mod deps;
|
||||||
pub mod get;
|
pub mod get;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub mod queries;
|
pub mod queries;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use domain::events::DomainEvent;
|
|||||||
use domain::testing::{FakeStatsRepository, InMemoryGoalRepository, NoopEventPublisher};
|
use domain::testing::{FakeStatsRepository, InMemoryGoalRepository, NoopEventPublisher};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::goals::deps::GoalCommandDeps;
|
||||||
use crate::goals::{commands::CreateGoalCommand, create};
|
use crate::goals::{commands::CreateGoalCommand, create};
|
||||||
use crate::test_helpers::TestContextBuilder;
|
use crate::test_helpers::TestContextBuilder;
|
||||||
|
|
||||||
@@ -12,11 +13,14 @@ async fn creates_goal_and_returns_progress() {
|
|||||||
let goals = InMemoryGoalRepository::new();
|
let goals = InMemoryGoalRepository::new();
|
||||||
let stats = FakeStatsRepository::new();
|
let stats = FakeStatsRepository::new();
|
||||||
let events = NoopEventPublisher::new();
|
let events = NoopEventPublisher::new();
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: Arc::clone(&goals) as _,
|
||||||
|
stats: Arc::clone(&stats) as _,
|
||||||
|
event_publisher: Arc::clone(&events) as _,
|
||||||
|
};
|
||||||
|
|
||||||
let result = create::execute(
|
let result = create::execute(
|
||||||
Arc::clone(&goals) as _,
|
&deps,
|
||||||
Arc::clone(&stats) as _,
|
|
||||||
Arc::clone(&events) as _,
|
|
||||||
CreateGoalCommand {
|
CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -38,11 +42,14 @@ async fn creates_goal_with_review_count() {
|
|||||||
let stats = FakeStatsRepository::new();
|
let stats = FakeStatsRepository::new();
|
||||||
stats.set_review_count(Uuid::nil(), 2025, 5);
|
stats.set_review_count(Uuid::nil(), 2025, 5);
|
||||||
let events = NoopEventPublisher::new();
|
let events = NoopEventPublisher::new();
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: Arc::clone(&goals) as _,
|
||||||
|
stats: Arc::clone(&stats) as _,
|
||||||
|
event_publisher: Arc::clone(&events) as _,
|
||||||
|
};
|
||||||
|
|
||||||
let result = create::execute(
|
let result = create::execute(
|
||||||
Arc::clone(&goals) as _,
|
&deps,
|
||||||
Arc::clone(&stats) as _,
|
|
||||||
Arc::clone(&events) as _,
|
|
||||||
CreateGoalCommand {
|
CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -60,11 +67,14 @@ async fn creates_goal_with_review_count() {
|
|||||||
async fn emits_goal_created_event() {
|
async fn emits_goal_created_event() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
let events = NoopEventPublisher::new();
|
let events = NoopEventPublisher::new();
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
event_publisher: Arc::clone(&events) as _,
|
||||||
|
};
|
||||||
|
|
||||||
create::execute(
|
create::execute(
|
||||||
b.goal_repo.clone(),
|
&deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
Arc::clone(&events) as _,
|
|
||||||
CreateGoalCommand {
|
CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -85,25 +95,21 @@ async fn emits_goal_created_event() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rejects_duplicate_year() {
|
async fn rejects_duplicate_year() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
event_publisher: b.event_publisher.clone(),
|
||||||
|
};
|
||||||
let cmd = CreateGoalCommand {
|
let cmd = CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
target_count: 10,
|
target_count: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
create::execute(
|
create::execute(&deps, cmd).await.unwrap();
|
||||||
b.goal_repo.clone(),
|
|
||||||
b.stats_repo.clone(),
|
|
||||||
b.event_publisher.clone(),
|
|
||||||
cmd,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result = create::execute(
|
let result = create::execute(
|
||||||
b.goal_repo.clone(),
|
&deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
b.event_publisher.clone(),
|
|
||||||
CreateGoalCommand {
|
CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -118,10 +124,13 @@ async fn rejects_duplicate_year() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rejects_year_before_2020() {
|
async fn rejects_year_before_2020() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
event_publisher: b.event_publisher.clone(),
|
||||||
|
};
|
||||||
let result = create::execute(
|
let result = create::execute(
|
||||||
b.goal_repo.clone(),
|
&deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
b.event_publisher.clone(),
|
|
||||||
CreateGoalCommand {
|
CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2019,
|
year: 2019,
|
||||||
@@ -136,10 +145,13 @@ async fn rejects_year_before_2020() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rejects_zero_target() {
|
async fn rejects_zero_target() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
event_publisher: b.event_publisher.clone(),
|
||||||
|
};
|
||||||
let result = create::execute(
|
let result = create::execute(
|
||||||
b.goal_repo.clone(),
|
&deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
b.event_publisher.clone(),
|
|
||||||
CreateGoalCommand {
|
CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::sync::Arc;
|
|||||||
use domain::testing::{FakeStatsRepository, InMemoryGoalRepository, NoopEventPublisher};
|
use domain::testing::{FakeStatsRepository, InMemoryGoalRepository, NoopEventPublisher};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::goals::deps::GoalCommandDeps;
|
||||||
use crate::goals::{
|
use crate::goals::{
|
||||||
commands::{CreateGoalCommand, DeleteGoalCommand},
|
commands::{CreateGoalCommand, DeleteGoalCommand},
|
||||||
create, delete,
|
create, delete,
|
||||||
@@ -14,11 +15,14 @@ async fn deletes_existing_goal() {
|
|||||||
let goals = InMemoryGoalRepository::new();
|
let goals = InMemoryGoalRepository::new();
|
||||||
let stats = FakeStatsRepository::new();
|
let stats = FakeStatsRepository::new();
|
||||||
let events = NoopEventPublisher::new();
|
let events = NoopEventPublisher::new();
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: Arc::clone(&goals) as _,
|
||||||
|
stats: Arc::clone(&stats) as _,
|
||||||
|
event_publisher: Arc::clone(&events) as _,
|
||||||
|
};
|
||||||
|
|
||||||
create::execute(
|
create::execute(
|
||||||
Arc::clone(&goals) as _,
|
&deps,
|
||||||
Arc::clone(&stats) as _,
|
|
||||||
Arc::clone(&events) as _,
|
|
||||||
CreateGoalCommand {
|
CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -30,8 +34,7 @@ async fn deletes_existing_goal() {
|
|||||||
assert_eq!(goals.count(), 1);
|
assert_eq!(goals.count(), 1);
|
||||||
|
|
||||||
delete::execute(
|
delete::execute(
|
||||||
Arc::clone(&goals) as _,
|
&deps,
|
||||||
Arc::clone(&events) as _,
|
|
||||||
DeleteGoalCommand {
|
DeleteGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -46,9 +49,13 @@ async fn deletes_existing_goal() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn fails_when_not_found() {
|
async fn fails_when_not_found() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
event_publisher: b.event_publisher.clone(),
|
||||||
|
};
|
||||||
let result = delete::execute(
|
let result = delete::execute(
|
||||||
b.goal_repo.clone(),
|
&deps,
|
||||||
b.event_publisher.clone(),
|
|
||||||
DeleteGoalCommand {
|
DeleteGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::goals::deps::{GoalCommandDeps, GoalQueryDeps};
|
||||||
use crate::goals::{commands::CreateGoalCommand, create, get, queries::GetGoalQuery};
|
use crate::goals::{commands::CreateGoalCommand, create, get, queries::GetGoalQuery};
|
||||||
use crate::test_helpers::TestContextBuilder;
|
use crate::test_helpers::TestContextBuilder;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn returns_goal_when_exists() {
|
async fn returns_goal_when_exists() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
let cmd_deps = GoalCommandDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
event_publisher: b.event_publisher.clone(),
|
||||||
|
};
|
||||||
|
let query_deps = GoalQueryDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
create::execute(
|
create::execute(
|
||||||
b.goal_repo.clone(),
|
&cmd_deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
b.event_publisher.clone(),
|
|
||||||
CreateGoalCommand {
|
CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -20,8 +29,7 @@ async fn returns_goal_when_exists() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = get::execute(
|
let result = get::execute(
|
||||||
b.goal_repo.clone(),
|
&query_deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
GetGoalQuery {
|
GetGoalQuery {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -37,9 +45,12 @@ async fn returns_goal_when_exists() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn returns_none_when_missing() {
|
async fn returns_none_when_missing() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
let query_deps = GoalQueryDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
};
|
||||||
let result = get::execute(
|
let result = get::execute(
|
||||||
b.goal_repo.clone(),
|
&query_deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
GetGoalQuery {
|
GetGoalQuery {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::goals::deps::{GoalCommandDeps, GoalQueryDeps};
|
||||||
use crate::goals::{commands::CreateGoalCommand, create, list, queries::ListGoalsQuery};
|
use crate::goals::{commands::CreateGoalCommand, create, list, queries::ListGoalsQuery};
|
||||||
use crate::test_helpers::TestContextBuilder;
|
use crate::test_helpers::TestContextBuilder;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn returns_empty_when_no_goals() {
|
async fn returns_empty_when_no_goals() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
let query_deps = GoalQueryDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
};
|
||||||
let result = list::execute(
|
let result = list::execute(
|
||||||
b.goal_repo.clone(),
|
&query_deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
ListGoalsQuery {
|
ListGoalsQuery {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
},
|
},
|
||||||
@@ -22,11 +26,19 @@ async fn returns_empty_when_no_goals() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn returns_all_goals_for_user() {
|
async fn returns_all_goals_for_user() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
let cmd_deps = GoalCommandDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
event_publisher: b.event_publisher.clone(),
|
||||||
|
};
|
||||||
|
let query_deps = GoalQueryDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
for year in [2023, 2024, 2025] {
|
for year in [2023, 2024, 2025] {
|
||||||
create::execute(
|
create::execute(
|
||||||
b.goal_repo.clone(),
|
&cmd_deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
b.event_publisher.clone(),
|
|
||||||
CreateGoalCommand {
|
CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year,
|
year,
|
||||||
@@ -38,8 +50,7 @@ async fn returns_all_goals_for_user() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result = list::execute(
|
let result = list::execute(
|
||||||
b.goal_repo.clone(),
|
&query_deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
ListGoalsQuery {
|
ListGoalsQuery {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::goals::deps::GoalCommandDeps;
|
||||||
use crate::goals::{
|
use crate::goals::{
|
||||||
commands::{CreateGoalCommand, UpdateGoalCommand},
|
commands::{CreateGoalCommand, UpdateGoalCommand},
|
||||||
create, update,
|
create, update,
|
||||||
@@ -9,10 +10,14 @@ use crate::test_helpers::TestContextBuilder;
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn updates_target_count() {
|
async fn updates_target_count() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
event_publisher: b.event_publisher.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
create::execute(
|
create::execute(
|
||||||
b.goal_repo.clone(),
|
&deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
b.event_publisher.clone(),
|
|
||||||
CreateGoalCommand {
|
CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -23,9 +28,7 @@ async fn updates_target_count() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = update::execute(
|
let result = update::execute(
|
||||||
b.goal_repo.clone(),
|
&deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
b.event_publisher.clone(),
|
|
||||||
UpdateGoalCommand {
|
UpdateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -41,10 +44,13 @@ async fn updates_target_count() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn fails_when_goal_not_found() {
|
async fn fails_when_goal_not_found() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
event_publisher: b.event_publisher.clone(),
|
||||||
|
};
|
||||||
let result = update::execute(
|
let result = update::execute(
|
||||||
b.goal_repo.clone(),
|
&deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
b.event_publisher.clone(),
|
|
||||||
UpdateGoalCommand {
|
UpdateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -59,10 +65,14 @@ async fn fails_when_goal_not_found() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rejects_zero_target() {
|
async fn rejects_zero_target() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: b.goal_repo.clone(),
|
||||||
|
stats: b.stats_repo.clone(),
|
||||||
|
event_publisher: b.event_publisher.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
create::execute(
|
create::execute(
|
||||||
b.goal_repo.clone(),
|
&deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
b.event_publisher.clone(),
|
|
||||||
CreateGoalCommand {
|
CreateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
@@ -73,9 +83,7 @@ async fn rejects_zero_target() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = update::execute(
|
let result = update::execute(
|
||||||
b.goal_repo.clone(),
|
&deps,
|
||||||
b.stats_repo.clone(),
|
|
||||||
b.event_publisher.clone(),
|
|
||||||
UpdateGoalCommand {
|
UpdateGoalCommand {
|
||||||
user_id: Uuid::nil(),
|
user_id: Uuid::nil(),
|
||||||
year: 2025,
|
year: 2025,
|
||||||
|
|||||||
@@ -1,34 +1,30 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::GoalWithProgress,
|
models::GoalWithProgress,
|
||||||
ports::{EventPublisher, GoalRepository, StatsRepository},
|
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::commands::UpdateGoalCommand;
|
use super::{commands::UpdateGoalCommand, deps::GoalCommandDeps};
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
goal: Arc<dyn GoalRepository>,
|
deps: &GoalCommandDeps,
|
||||||
stats: Arc<dyn StatsRepository>,
|
|
||||||
event_publisher: Arc<dyn EventPublisher>,
|
|
||||||
cmd: UpdateGoalCommand,
|
cmd: UpdateGoalCommand,
|
||||||
) -> Result<GoalWithProgress, DomainError> {
|
) -> Result<GoalWithProgress, DomainError> {
|
||||||
let user_id = UserId::from_uuid(cmd.user_id);
|
let user_id = UserId::from_uuid(cmd.user_id);
|
||||||
|
|
||||||
let mut g = goal
|
let mut g = deps
|
||||||
|
.goal
|
||||||
.find_by_user_and_year(&user_id, cmd.year)
|
.find_by_user_and_year(&user_id, cmd.year)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?;
|
.ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?;
|
||||||
|
|
||||||
g.update_target(cmd.target_count)?;
|
g.update_target(cmd.target_count)?;
|
||||||
goal.update(&g).await?;
|
deps.goal.update(&g).await?;
|
||||||
|
|
||||||
let current_count = stats.count_reviews_in_year(&user_id, cmd.year).await?;
|
let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?;
|
||||||
|
|
||||||
event_publisher
|
deps.event_publisher
|
||||||
.publish(&DomainEvent::GoalUpdated {
|
.publish(&DomainEvent::GoalUpdated {
|
||||||
goal_id: g.id().clone(),
|
goal_id: g.id().clone(),
|
||||||
user_id,
|
user_id,
|
||||||
|
|||||||
@@ -3,22 +3,21 @@ use std::sync::Arc;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{AnnotatedRow, import::RowResult},
|
models::{AnnotatedRow, import::RowResult},
|
||||||
ports::{DocumentParser, ImportSessionRepository, MovieRepository},
|
ports::MovieQuery,
|
||||||
value_objects::{ExternalMetadataId, ImportSessionId, MovieTitle, ReleaseYear, UserId},
|
value_objects::{ExternalMetadataId, ImportSessionId, MovieTitle, ReleaseYear, UserId},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::import::commands::ApplyImportMappingCommand;
|
use super::{commands::ApplyImportMappingCommand, deps::ApplyMappingDeps};
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
import_session: Arc<dyn ImportSessionRepository>,
|
deps: &ApplyMappingDeps,
|
||||||
document_parser: Arc<dyn DocumentParser>,
|
|
||||||
movie: Arc<dyn MovieRepository>,
|
|
||||||
cmd: ApplyImportMappingCommand,
|
cmd: ApplyImportMappingCommand,
|
||||||
) -> Result<Vec<AnnotatedRow>, DomainError> {
|
) -> Result<Vec<AnnotatedRow>, DomainError> {
|
||||||
let user_id = UserId::from_uuid(cmd.user_id);
|
let user_id = UserId::from_uuid(cmd.user_id);
|
||||||
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
||||||
let mappings = cmd.mappings;
|
let mappings = cmd.mappings;
|
||||||
let mut session = import_session
|
let mut session = deps
|
||||||
|
.import_session
|
||||||
.get(&session_id, &user_id)
|
.get(&session_id, &user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
||||||
@@ -28,20 +27,20 @@ pub async fn execute(
|
|||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| DomainError::ValidationError("session has no parsed file".into()))?;
|
.ok_or_else(|| DomainError::ValidationError("session has no parsed file".into()))?;
|
||||||
|
|
||||||
let mut annotated = document_parser.apply_mapping(&parsed, &mappings);
|
let mut annotated = deps.document_parser.apply_mapping(&parsed, &mappings);
|
||||||
|
|
||||||
mark_duplicates(movie, &mut annotated).await?;
|
mark_duplicates(deps.movie_query.clone(), &mut annotated).await?;
|
||||||
|
|
||||||
session.field_mappings = Some(mappings);
|
session.field_mappings = Some(mappings);
|
||||||
session.row_results = Some(annotated.clone());
|
session.row_results = Some(annotated.clone());
|
||||||
|
|
||||||
import_session.update(&session).await?;
|
deps.import_session.update(&session).await?;
|
||||||
|
|
||||||
Ok(annotated)
|
Ok(annotated)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mark_duplicates(
|
async fn mark_duplicates(
|
||||||
movie: Arc<dyn MovieRepository>,
|
movie: Arc<dyn MovieQuery>,
|
||||||
rows: &mut [AnnotatedRow],
|
rows: &mut [AnnotatedRow],
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let mut ext_ids = Vec::new();
|
let mut ext_ids = Vec::new();
|
||||||
|
|||||||
@@ -1,34 +1,33 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::import::commands::ApplyImportProfileCommand;
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
ports::{ImportProfileRepository, ImportSessionRepository},
|
|
||||||
value_objects::{ImportProfileId, ImportSessionId, UserId},
|
value_objects::{ImportProfileId, ImportSessionId, UserId},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::{commands::ApplyImportProfileCommand, deps::ApplyProfileDeps};
|
||||||
|
|
||||||
/// Copies the profile's field_mappings onto the session. Caller must then invoke
|
/// Copies the profile's field_mappings onto the session. Caller must then invoke
|
||||||
/// apply_import_mapping to regenerate row_results with the new mappings.
|
/// apply_import_mapping to regenerate row_results with the new mappings.
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
import_profile: Arc<dyn ImportProfileRepository>,
|
deps: &ApplyProfileDeps,
|
||||||
import_session: Arc<dyn ImportSessionRepository>,
|
|
||||||
cmd: ApplyImportProfileCommand,
|
cmd: ApplyImportProfileCommand,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let user_id = UserId::from_uuid(cmd.user_id);
|
let user_id = UserId::from_uuid(cmd.user_id);
|
||||||
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
||||||
let profile_id = ImportProfileId::from_uuid(cmd.profile_id);
|
let profile_id = ImportProfileId::from_uuid(cmd.profile_id);
|
||||||
|
|
||||||
let profile = import_profile
|
let profile = deps
|
||||||
|
.import_profile
|
||||||
.get(&profile_id, &user_id)
|
.get(&profile_id, &user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound("import profile".into()))?;
|
.ok_or_else(|| DomainError::NotFound("import profile".into()))?;
|
||||||
let mut session = import_session
|
let mut session = deps
|
||||||
|
.import_session
|
||||||
.get(&session_id, &user_id)
|
.get(&session_id, &user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
||||||
session.field_mappings = Some(profile.field_mappings);
|
session.field_mappings = Some(profile.field_mappings);
|
||||||
session.row_results = None;
|
session.row_results = None;
|
||||||
import_session.update(&session).await
|
deps.import_session.update(&session).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::ImportSession,
|
models::ImportSession,
|
||||||
ports::{DocumentParser, ImportSessionRepository},
|
|
||||||
value_objects::{ImportSessionId, UserId},
|
value_objects::{ImportSessionId, UserId},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::import::commands::CreateImportSessionCommand;
|
use super::{commands::CreateImportSessionCommand, deps::CreateSessionDeps};
|
||||||
|
|
||||||
pub struct CreateSessionResult {
|
pub struct CreateSessionResult {
|
||||||
pub session_id: ImportSessionId,
|
pub session_id: ImportSessionId,
|
||||||
@@ -16,14 +13,14 @@ pub struct CreateSessionResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
import_session: Arc<dyn ImportSessionRepository>,
|
deps: &CreateSessionDeps,
|
||||||
document_parser: Arc<dyn DocumentParser>,
|
|
||||||
cmd: CreateImportSessionCommand,
|
cmd: CreateImportSessionCommand,
|
||||||
) -> Result<CreateSessionResult, DomainError> {
|
) -> Result<CreateSessionResult, DomainError> {
|
||||||
let user_id = UserId::from_uuid(cmd.user_id);
|
let user_id = UserId::from_uuid(cmd.user_id);
|
||||||
import_session.delete_expired_for_user(&user_id).await?;
|
deps.import_session.delete_expired_for_user(&user_id).await?;
|
||||||
|
|
||||||
let parsed = document_parser
|
let parsed = deps
|
||||||
|
.document_parser
|
||||||
.parse(&cmd.bytes, cmd.format)
|
.parse(&cmd.bytes, cmd.format)
|
||||||
.map_err(|e| DomainError::ValidationError(e.to_string()))?;
|
.map_err(|e| DomainError::ValidationError(e.to_string()))?;
|
||||||
|
|
||||||
@@ -34,7 +31,7 @@ pub async fn execute(
|
|||||||
let session_id = session.id.clone();
|
let session_id = session.id.clone();
|
||||||
session.parsed_file = Some(parsed);
|
session.parsed_file = Some(parsed);
|
||||||
|
|
||||||
import_session.create(&session).await?;
|
deps.import_session.create(&session).await?;
|
||||||
|
|
||||||
Ok(CreateSessionResult {
|
Ok(CreateSessionResult {
|
||||||
session_id,
|
session_id,
|
||||||
|
|||||||
31
crates/application/src/import/deps.rs
Normal file
31
crates/application/src/import/deps.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use domain::ports::{DocumentParser, ImportProfileRepository, ImportSessionRepository, MovieQuery};
|
||||||
|
|
||||||
|
use crate::ports::ReviewLogger;
|
||||||
|
|
||||||
|
pub struct CreateSessionDeps {
|
||||||
|
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||||
|
pub document_parser: Arc<dyn DocumentParser>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApplyMappingDeps {
|
||||||
|
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||||
|
pub document_parser: Arc<dyn DocumentParser>,
|
||||||
|
pub movie_query: Arc<dyn MovieQuery>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApplyProfileDeps {
|
||||||
|
pub import_profile: Arc<dyn ImportProfileRepository>,
|
||||||
|
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExecuteImportDeps {
|
||||||
|
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||||
|
pub review_logger: Arc<dyn ReviewLogger>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SaveProfileDeps {
|
||||||
|
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||||
|
pub import_profile: Arc<dyn ImportProfileRepository>,
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ use chrono::NaiveDateTime;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{ImportRow, import::RowResult},
|
models::{ImportRow, import::RowResult},
|
||||||
ports::ImportSessionRepository,
|
|
||||||
value_objects::{ImportSessionId, UserId},
|
value_objects::{ImportSessionId, UserId},
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -12,9 +11,10 @@ use uuid::Uuid;
|
|||||||
use crate::{
|
use crate::{
|
||||||
diary::commands::{LogReviewCommand, MovieInput},
|
diary::commands::{LogReviewCommand, MovieInput},
|
||||||
import::commands::ExecuteImportCommand,
|
import::commands::ExecuteImportCommand,
|
||||||
ports::ReviewLogger,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::deps::ExecuteImportDeps;
|
||||||
|
|
||||||
const CONCURRENCY_LIMIT: usize = 10;
|
const CONCURRENCY_LIMIT: usize = 10;
|
||||||
|
|
||||||
pub struct ImportSummary {
|
pub struct ImportSummary {
|
||||||
@@ -24,14 +24,14 @@ pub struct ImportSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
import_session: Arc<dyn ImportSessionRepository>,
|
deps: &ExecuteImportDeps,
|
||||||
review_logger: Arc<dyn ReviewLogger>,
|
|
||||||
cmd: ExecuteImportCommand,
|
cmd: ExecuteImportCommand,
|
||||||
) -> Result<ImportSummary, DomainError> {
|
) -> Result<ImportSummary, DomainError> {
|
||||||
let user_id = UserId::from_uuid(cmd.user_id);
|
let user_id = UserId::from_uuid(cmd.user_id);
|
||||||
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
||||||
let confirmed_indices = cmd.confirmed_indices;
|
let confirmed_indices = cmd.confirmed_indices;
|
||||||
let session = import_session
|
let session = deps
|
||||||
|
.import_session
|
||||||
.get(&session_id, &user_id)
|
.get(&session_id, &user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
||||||
@@ -59,7 +59,7 @@ pub async fn execute(
|
|||||||
Err(e) => failed.push((idx, e)),
|
Err(e) => failed.push((idx, e)),
|
||||||
Ok(log_cmd) => {
|
Ok(log_cmd) => {
|
||||||
let permit = Arc::clone(&semaphore).acquire_owned().await.unwrap();
|
let permit = Arc::clone(&semaphore).acquire_owned().await.unwrap();
|
||||||
let logger = Arc::clone(&review_logger);
|
let logger = deps.review_logger.clone();
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
let result = logger.log_review(log_cmd).await.map_err(|e| e.to_string());
|
let result = logger.log_review(log_cmd).await.map_err(|e| e.to_string());
|
||||||
drop(permit);
|
drop(permit);
|
||||||
@@ -78,7 +78,7 @@ pub async fn execute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import_session.delete(&session_id).await?;
|
deps.import_session.delete(&session_id).await?;
|
||||||
|
|
||||||
Ok(ImportSummary {
|
Ok(ImportSummary {
|
||||||
imported,
|
imported,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pub mod cleanup;
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod create_session;
|
pub mod create_session;
|
||||||
pub mod delete_profile;
|
pub mod delete_profile;
|
||||||
|
pub mod deps;
|
||||||
pub mod execute;
|
pub mod execute;
|
||||||
pub mod list_profiles;
|
pub mod list_profiles;
|
||||||
pub mod save_profile;
|
pub mod save_profile;
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::import::commands::SaveImportProfileCommand;
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::ImportProfile,
|
models::ImportProfile,
|
||||||
ports::{ImportProfileRepository, ImportSessionRepository},
|
|
||||||
value_objects::{ImportProfileId, ImportSessionId, UserId},
|
value_objects::{ImportProfileId, ImportSessionId, UserId},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::{commands::SaveImportProfileCommand, deps::SaveProfileDeps};
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
import_session: Arc<dyn ImportSessionRepository>,
|
deps: &SaveProfileDeps,
|
||||||
import_profile: Arc<dyn ImportProfileRepository>,
|
|
||||||
cmd: SaveImportProfileCommand,
|
cmd: SaveImportProfileCommand,
|
||||||
) -> Result<ImportProfileId, DomainError> {
|
) -> Result<ImportProfileId, DomainError> {
|
||||||
let user_id = UserId::from_uuid(cmd.user_id);
|
let user_id = UserId::from_uuid(cmd.user_id);
|
||||||
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
||||||
|
|
||||||
let session = import_session
|
let session = deps
|
||||||
|
.import_session
|
||||||
.get(&session_id, &user_id)
|
.get(&session_id, &user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
||||||
@@ -32,7 +30,7 @@ pub async fn execute(
|
|||||||
Utc::now().naive_utc(),
|
Utc::now().naive_utc(),
|
||||||
);
|
);
|
||||||
let id = profile.id.clone();
|
let id = profile.id.clone();
|
||||||
import_profile.save(&profile).await?;
|
deps.import_profile.save(&profile).await?;
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ use domain::{
|
|||||||
AnnotatedRow, Movie,
|
AnnotatedRow, Movie,
|
||||||
import::{ImportRow, ParsedFile, RowResult},
|
import::{ImportRow, ParsedFile, RowResult},
|
||||||
},
|
},
|
||||||
ports::{DocumentParser, MovieRepository},
|
ports::{DocumentParser, MovieCommand},
|
||||||
testing::{InMemoryImportSessionRepository, InMemoryMovieRepository},
|
testing::{InMemoryImportSessionRepository, InMemoryMovieRepository},
|
||||||
value_objects::{ExternalMetadataId, MovieTitle, ReleaseYear},
|
value_objects::{ExternalMetadataId, MovieTitle, ReleaseYear},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::import::deps::{ApplyMappingDeps, CreateSessionDeps};
|
||||||
use crate::import::{
|
use crate::import::{
|
||||||
apply_mapping,
|
apply_mapping,
|
||||||
commands::{ApplyImportMappingCommand, CreateImportSessionCommand},
|
commands::{ApplyImportMappingCommand, CreateImportSessionCommand},
|
||||||
@@ -25,9 +26,13 @@ async fn applies_mapping_to_session() {
|
|||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
let create_deps = CreateSessionDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
document_parser: b.document_parser.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let session = create_session::execute(
|
let session = create_session::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&create_deps,
|
||||||
b.document_parser.clone(),
|
|
||||||
CreateImportSessionCommand {
|
CreateImportSessionCommand {
|
||||||
user_id,
|
user_id,
|
||||||
bytes: b"title\nTest".to_vec(),
|
bytes: b"title\nTest".to_vec(),
|
||||||
@@ -37,10 +42,14 @@ async fn applies_mapping_to_session() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let mapping_deps = ApplyMappingDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
document_parser: b.document_parser.clone(),
|
||||||
|
movie_query: b.movie_query.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let rows = apply_mapping::execute(
|
let rows = apply_mapping::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&mapping_deps,
|
||||||
b.document_parser.clone(),
|
|
||||||
b.movie_repo.clone(),
|
|
||||||
ApplyImportMappingCommand {
|
ApplyImportMappingCommand {
|
||||||
user_id,
|
user_id,
|
||||||
session_id: session.session_id.value(),
|
session_id: session.session_id.value(),
|
||||||
@@ -58,10 +67,14 @@ async fn fails_when_session_not_found() {
|
|||||||
let sessions = InMemoryImportSessionRepository::new();
|
let sessions = InMemoryImportSessionRepository::new();
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
|
||||||
|
let deps = ApplyMappingDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
document_parser: b.document_parser.clone(),
|
||||||
|
movie_query: b.movie_query.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let result = apply_mapping::execute(
|
let result = apply_mapping::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
b.document_parser.clone(),
|
|
||||||
b.movie_repo.clone(),
|
|
||||||
ApplyImportMappingCommand {
|
ApplyImportMappingCommand {
|
||||||
user_id: Uuid::new_v4(),
|
user_id: Uuid::new_v4(),
|
||||||
session_id: Uuid::new_v4(),
|
session_id: Uuid::new_v4(),
|
||||||
@@ -132,9 +145,13 @@ async fn marks_duplicate_by_external_id() {
|
|||||||
|
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
let create_deps = CreateSessionDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
document_parser: Arc::clone(&parser) as _,
|
||||||
|
};
|
||||||
|
|
||||||
let session = create_session::execute(
|
let session = create_session::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&create_deps,
|
||||||
Arc::clone(&parser) as _,
|
|
||||||
CreateImportSessionCommand {
|
CreateImportSessionCommand {
|
||||||
user_id,
|
user_id,
|
||||||
bytes: b"title\nKnown Movie".to_vec(),
|
bytes: b"title\nKnown Movie".to_vec(),
|
||||||
@@ -144,10 +161,14 @@ async fn marks_duplicate_by_external_id() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let mapping_deps = ApplyMappingDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
document_parser: Arc::clone(&parser) as _,
|
||||||
|
movie_query: Arc::clone(&movies) as _,
|
||||||
|
};
|
||||||
|
|
||||||
let rows = apply_mapping::execute(
|
let rows = apply_mapping::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&mapping_deps,
|
||||||
Arc::clone(&parser) as _,
|
|
||||||
Arc::clone(&movies) as _,
|
|
||||||
ApplyImportMappingCommand {
|
ApplyImportMappingCommand {
|
||||||
user_id,
|
user_id,
|
||||||
session_id: session.session_id.value(),
|
session_id: session.session_id.value(),
|
||||||
@@ -185,9 +206,13 @@ async fn marks_duplicate_by_title_and_year() {
|
|||||||
|
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
let create_deps = CreateSessionDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
document_parser: Arc::clone(&parser) as _,
|
||||||
|
};
|
||||||
|
|
||||||
let session = create_session::execute(
|
let session = create_session::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&create_deps,
|
||||||
Arc::clone(&parser) as _,
|
|
||||||
CreateImportSessionCommand {
|
CreateImportSessionCommand {
|
||||||
user_id,
|
user_id,
|
||||||
bytes: b"title\nDuplicate Film".to_vec(),
|
bytes: b"title\nDuplicate Film".to_vec(),
|
||||||
@@ -197,10 +222,14 @@ async fn marks_duplicate_by_title_and_year() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let mapping_deps = ApplyMappingDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
document_parser: Arc::clone(&parser) as _,
|
||||||
|
movie_query: Arc::clone(&movies) as _,
|
||||||
|
};
|
||||||
|
|
||||||
let rows = apply_mapping::execute(
|
let rows = apply_mapping::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&mapping_deps,
|
||||||
Arc::clone(&parser) as _,
|
|
||||||
Arc::clone(&movies) as _,
|
|
||||||
ApplyImportMappingCommand {
|
ApplyImportMappingCommand {
|
||||||
user_id,
|
user_id,
|
||||||
session_id: session.session_id.value(),
|
session_id: session.session_id.value(),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use domain::testing::{InMemoryImportProfileRepository, InMemoryImportSessionRepo
|
|||||||
use domain::value_objects::{ImportProfileId, UserId};
|
use domain::value_objects::{ImportProfileId, UserId};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::import::deps::ApplyProfileDeps;
|
||||||
use crate::import::{apply_profile, commands::ApplyImportProfileCommand};
|
use crate::import::{apply_profile, commands::ApplyImportProfileCommand};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -14,9 +15,13 @@ async fn fails_when_profile_not_found() {
|
|||||||
let profiles = InMemoryImportProfileRepository::new();
|
let profiles = InMemoryImportProfileRepository::new();
|
||||||
let sessions = InMemoryImportSessionRepository::new();
|
let sessions = InMemoryImportSessionRepository::new();
|
||||||
|
|
||||||
|
let deps = ApplyProfileDeps {
|
||||||
|
import_profile: Arc::clone(&profiles) as _,
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
};
|
||||||
|
|
||||||
let result = apply_profile::execute(
|
let result = apply_profile::execute(
|
||||||
Arc::clone(&profiles) as _,
|
&deps,
|
||||||
Arc::clone(&sessions) as _,
|
|
||||||
ApplyImportProfileCommand {
|
ApplyImportProfileCommand {
|
||||||
user_id: Uuid::new_v4(),
|
user_id: Uuid::new_v4(),
|
||||||
session_id: Uuid::new_v4(),
|
session_id: Uuid::new_v4(),
|
||||||
@@ -44,9 +49,13 @@ async fn fails_when_session_not_found() {
|
|||||||
let profile_id = profile.id.clone();
|
let profile_id = profile.id.clone();
|
||||||
profiles.save(&profile).await.unwrap();
|
profiles.save(&profile).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ApplyProfileDeps {
|
||||||
|
import_profile: Arc::clone(&profiles) as _,
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
};
|
||||||
|
|
||||||
let result = apply_profile::execute(
|
let result = apply_profile::execute(
|
||||||
Arc::clone(&profiles) as _,
|
&deps,
|
||||||
Arc::clone(&sessions) as _,
|
|
||||||
ApplyImportProfileCommand {
|
ApplyImportProfileCommand {
|
||||||
user_id,
|
user_id,
|
||||||
session_id: Uuid::new_v4(),
|
session_id: Uuid::new_v4(),
|
||||||
@@ -82,9 +91,13 @@ async fn applies_profile_mappings_to_session() {
|
|||||||
let session_id = session.id.clone();
|
let session_id = session.id.clone();
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ApplyProfileDeps {
|
||||||
|
import_profile: Arc::clone(&profiles) as _,
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
};
|
||||||
|
|
||||||
apply_profile::execute(
|
apply_profile::execute(
|
||||||
Arc::clone(&profiles) as _,
|
&deps,
|
||||||
Arc::clone(&sessions) as _,
|
|
||||||
ApplyImportProfileCommand {
|
ApplyImportProfileCommand {
|
||||||
user_id,
|
user_id,
|
||||||
session_id: session_id.value(),
|
session_id: session_id.value(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use domain::testing::InMemoryImportSessionRepository;
|
use domain::testing::InMemoryImportSessionRepository;
|
||||||
|
|
||||||
|
use crate::import::deps::CreateSessionDeps;
|
||||||
use crate::import::{commands::CreateImportSessionCommand, create_session};
|
use crate::import::{commands::CreateImportSessionCommand, create_session};
|
||||||
use crate::test_helpers::TestContextBuilder;
|
use crate::test_helpers::TestContextBuilder;
|
||||||
|
|
||||||
@@ -12,9 +13,13 @@ async fn creates_session_with_parsed_file() {
|
|||||||
let sessions = InMemoryImportSessionRepository::new();
|
let sessions = InMemoryImportSessionRepository::new();
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
|
|
||||||
|
let deps = CreateSessionDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
document_parser: b.document_parser.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let result = create_session::execute(
|
let result = create_session::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
b.document_parser.clone(),
|
|
||||||
CreateImportSessionCommand {
|
CreateImportSessionCommand {
|
||||||
user_id: Uuid::new_v4(),
|
user_id: Uuid::new_v4(),
|
||||||
bytes: b"col1\nval1".to_vec(),
|
bytes: b"col1\nval1".to_vec(),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use domain::value_objects::UserId;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::import::commands::ExecuteImportCommand;
|
use crate::import::commands::ExecuteImportCommand;
|
||||||
|
use crate::import::deps::ExecuteImportDeps;
|
||||||
use crate::import::execute;
|
use crate::import::execute;
|
||||||
use crate::test_helpers::NoopReviewLogger;
|
use crate::test_helpers::NoopReviewLogger;
|
||||||
|
|
||||||
@@ -50,9 +51,13 @@ async fn imports_confirmed_rows() {
|
|||||||
let sid = session.id.clone();
|
let sid = session.id.clone();
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -76,9 +81,13 @@ async fn skips_unconfirmed_rows() {
|
|||||||
let sid = session.id.clone();
|
let sid = session.id.clone();
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -96,9 +105,13 @@ async fn skips_unconfirmed_rows() {
|
|||||||
async fn fails_when_session_not_found() {
|
async fn fails_when_session_not_found() {
|
||||||
let sessions = InMemoryImportSessionRepository::new();
|
let sessions = InMemoryImportSessionRepository::new();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: Uuid::new_v4(),
|
user_id: Uuid::new_v4(),
|
||||||
session_id: Uuid::new_v4(),
|
session_id: Uuid::new_v4(),
|
||||||
@@ -131,9 +144,13 @@ async fn handles_datetime_format() {
|
|||||||
}]);
|
}]);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -168,9 +185,13 @@ async fn fails_on_invalid_rating() {
|
|||||||
}]);
|
}]);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -205,9 +226,13 @@ async fn fails_on_missing_watched_at() {
|
|||||||
}]);
|
}]);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -242,9 +267,13 @@ async fn imports_row_with_external_metadata_id() {
|
|||||||
}]);
|
}]);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -279,9 +308,13 @@ async fn imports_row_with_director_and_comment() {
|
|||||||
}]);
|
}]);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -316,9 +349,13 @@ async fn handles_space_separated_datetime_format() {
|
|||||||
}]);
|
}]);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -348,9 +385,13 @@ async fn reports_invalid_row_result_errors() {
|
|||||||
}]);
|
}]);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -387,9 +428,13 @@ async fn fails_on_missing_rating() {
|
|||||||
}]);
|
}]);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -425,9 +470,13 @@ async fn fails_on_unparseable_date() {
|
|||||||
}]);
|
}]);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -463,9 +512,13 @@ async fn imports_row_without_release_year() {
|
|||||||
}]);
|
}]);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -489,9 +542,13 @@ async fn deletes_session_after_import() {
|
|||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
assert_eq!(sessions.count(), 1);
|
assert_eq!(sessions.count(), 1);
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
execute::execute(
|
execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
@@ -533,10 +590,14 @@ async fn imports_more_rows_than_concurrency_limit() {
|
|||||||
session.row_results = Some(rows);
|
session.row_results = Some(rows);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = ExecuteImportDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
review_logger: Arc::new(NoopReviewLogger),
|
||||||
|
};
|
||||||
|
|
||||||
let confirmed_indices: Vec<usize> = (0..15).collect();
|
let confirmed_indices: Vec<usize> = (0..15).collect();
|
||||||
let result = execute::execute(
|
let result = execute::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::new(NoopReviewLogger),
|
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use domain::testing::{InMemoryImportProfileRepository, InMemoryImportSessionRepo
|
|||||||
use domain::value_objects::UserId;
|
use domain::value_objects::UserId;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::import::deps::SaveProfileDeps;
|
||||||
use crate::import::{commands::SaveImportProfileCommand, save_profile};
|
use crate::import::{commands::SaveImportProfileCommand, save_profile};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -13,9 +14,13 @@ async fn fails_when_session_not_found() {
|
|||||||
let sessions = InMemoryImportSessionRepository::new();
|
let sessions = InMemoryImportSessionRepository::new();
|
||||||
let profiles = InMemoryImportProfileRepository::new();
|
let profiles = InMemoryImportProfileRepository::new();
|
||||||
|
|
||||||
|
let deps = SaveProfileDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
import_profile: Arc::clone(&profiles) as _,
|
||||||
|
};
|
||||||
|
|
||||||
let result = save_profile::execute(
|
let result = save_profile::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::clone(&profiles) as _,
|
|
||||||
SaveImportProfileCommand {
|
SaveImportProfileCommand {
|
||||||
user_id: Uuid::new_v4(),
|
user_id: Uuid::new_v4(),
|
||||||
session_id: Uuid::new_v4(),
|
session_id: Uuid::new_v4(),
|
||||||
@@ -38,9 +43,13 @@ async fn saves_profile_from_session() {
|
|||||||
session.field_mappings = Some(vec![]);
|
session.field_mappings = Some(vec![]);
|
||||||
sessions.create(&session).await.unwrap();
|
sessions.create(&session).await.unwrap();
|
||||||
|
|
||||||
|
let deps = SaveProfileDeps {
|
||||||
|
import_session: Arc::clone(&sessions) as _,
|
||||||
|
import_profile: Arc::clone(&profiles) as _,
|
||||||
|
};
|
||||||
|
|
||||||
let result = save_profile::execute(
|
let result = save_profile::execute(
|
||||||
Arc::clone(&sessions) as _,
|
&deps,
|
||||||
Arc::clone(&profiles) as _,
|
|
||||||
SaveImportProfileCommand {
|
SaveImportProfileCommand {
|
||||||
user_id,
|
user_id,
|
||||||
session_id: sid.value(),
|
session_id: sid.value(),
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use domain::{errors::DomainError, ports::WatchEventRepository};
|
use domain::{errors::DomainError, ports::WatchEventCommand};
|
||||||
|
|
||||||
pub async fn execute(watch_event: Arc<dyn WatchEventRepository>) -> Result<u64, DomainError> {
|
pub async fn execute(watch_event_command: Arc<dyn WatchEventCommand>) -> Result<u64, DomainError> {
|
||||||
let cutoff = chrono::Utc::now().naive_utc() - Duration::days(30);
|
let cutoff = chrono::Utc::now().naive_utc() - Duration::days(30);
|
||||||
watch_event.delete_non_pending_older_than(cutoff).await
|
watch_event_command.delete_non_pending_older_than(cutoff).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::WatchEventStatus,
|
models::WatchEventStatus,
|
||||||
ports::WatchEventRepository,
|
ports::{WatchEventCommand, WatchEventQuery},
|
||||||
value_objects::{UserId, WatchEventId},
|
value_objects::{UserId, WatchEventId},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -14,7 +14,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
watch_event: Arc<dyn WatchEventRepository>,
|
watch_event_command: Arc<dyn WatchEventCommand>,
|
||||||
|
watch_event_query: Arc<dyn WatchEventQuery>,
|
||||||
review_logger: Arc<dyn ReviewLogger>,
|
review_logger: Arc<dyn ReviewLogger>,
|
||||||
cmd: ConfirmWatchEventsCommand,
|
cmd: ConfirmWatchEventsCommand,
|
||||||
) -> Result<u32, DomainError> {
|
) -> Result<u32, DomainError> {
|
||||||
@@ -23,7 +24,7 @@ pub async fn execute(
|
|||||||
|
|
||||||
for c in cmd.confirmations {
|
for c in cmd.confirmations {
|
||||||
let event_id = WatchEventId::from_uuid(c.watch_event_id);
|
let event_id = WatchEventId::from_uuid(c.watch_event_id);
|
||||||
let event = watch_event
|
let event = watch_event_query
|
||||||
.get_by_id(&event_id)
|
.get_by_id(&event_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound(format!("WatchEvent {}", c.watch_event_id)))?;
|
.ok_or_else(|| DomainError::NotFound(format!("WatchEvent {}", c.watch_event_id)))?;
|
||||||
@@ -61,7 +62,7 @@ pub async fn execute(
|
|||||||
|
|
||||||
review_logger.log_review(review_cmd).await?;
|
review_logger.log_review(review_cmd).await?;
|
||||||
|
|
||||||
watch_event
|
watch_event_command
|
||||||
.update_status(&event_id, WatchEventStatus::Confirmed)
|
.update_status(&event_id, WatchEventStatus::Confirmed)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use domain::ports::{EventPublisher, WatchEventRepository, WebhookTokenRepository};
|
use domain::ports::{EventPublisher, WatchEventCommand, WatchEventQuery, WebhookTokenRepository};
|
||||||
|
|
||||||
pub struct IngestWatchEventDeps {
|
pub struct IngestWatchEventDeps {
|
||||||
pub webhook_token: Arc<dyn WebhookTokenRepository>,
|
pub webhook_token: Arc<dyn WebhookTokenRepository>,
|
||||||
pub watch_event: Arc<dyn WatchEventRepository>,
|
pub watch_event_command: Arc<dyn WatchEventCommand>,
|
||||||
|
pub watch_event_query: Arc<dyn WatchEventQuery>,
|
||||||
pub event_publisher: Arc<dyn EventPublisher>,
|
pub event_publisher: Arc<dyn EventPublisher>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ use std::sync::Arc;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::WatchEventStatus,
|
models::WatchEventStatus,
|
||||||
ports::WatchEventRepository,
|
ports::{WatchEventCommand, WatchEventQuery},
|
||||||
value_objects::{UserId, WatchEventId},
|
value_objects::{UserId, WatchEventId},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::integrations::commands::DismissWatchEventsCommand;
|
use crate::integrations::commands::DismissWatchEventsCommand;
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
watch_event: Arc<dyn WatchEventRepository>,
|
watch_event_command: Arc<dyn WatchEventCommand>,
|
||||||
|
watch_event_query: Arc<dyn WatchEventQuery>,
|
||||||
cmd: DismissWatchEventsCommand,
|
cmd: DismissWatchEventsCommand,
|
||||||
) -> Result<u32, DomainError> {
|
) -> Result<u32, DomainError> {
|
||||||
let user_id = UserId::from_uuid(cmd.user_id);
|
let user_id = UserId::from_uuid(cmd.user_id);
|
||||||
@@ -24,7 +25,7 @@ pub async fn execute(
|
|||||||
.map(|id| WatchEventId::from_uuid(*id))
|
.map(|id| WatchEventId::from_uuid(*id))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let events = watch_event.get_by_ids(&ids).await?;
|
let events = watch_event_query.get_by_ids(&ids).await?;
|
||||||
|
|
||||||
if events.len() != ids.len() {
|
if events.len() != ids.len() {
|
||||||
return Err(DomainError::NotFound(
|
return Err(DomainError::NotFound(
|
||||||
@@ -37,7 +38,7 @@ pub async fn execute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = watch_event
|
let count = watch_event_command
|
||||||
.update_status_batch(&ids, WatchEventStatus::Dismissed)
|
.update_status_batch(&ids, WatchEventStatus::Dismissed)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError, models::WatchEvent, ports::WatchEventRepository, value_objects::UserId,
|
errors::DomainError, models::WatchEvent, ports::WatchEventQuery, value_objects::UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::integrations::queries::GetWatchQueueQuery;
|
use crate::integrations::queries::GetWatchQueueQuery;
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
watch_event: Arc<dyn WatchEventRepository>,
|
watch_event_query: Arc<dyn WatchEventQuery>,
|
||||||
query: GetWatchQueueQuery,
|
query: GetWatchQueueQuery,
|
||||||
) -> Result<Vec<WatchEvent>, DomainError> {
|
) -> Result<Vec<WatchEvent>, DomainError> {
|
||||||
let user_id = UserId::from_uuid(query.user_id);
|
let user_id = UserId::from_uuid(query.user_id);
|
||||||
watch_event.list_pending(&user_id).await
|
watch_event_query.list_pending(&user_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ pub async fn execute(
|
|||||||
if let Some(ref ext_id) = external_metadata_id {
|
if let Some(ref ext_id) = external_metadata_id {
|
||||||
let one_hour_ago = chrono::Utc::now().naive_utc() - Duration::hours(1);
|
let one_hour_ago = chrono::Utc::now().naive_utc() - Duration::hours(1);
|
||||||
if deps
|
if deps
|
||||||
.watch_event
|
.watch_event_query
|
||||||
.find_duplicate(&user_id, ext_id, one_hour_ago)
|
.find_duplicate(&user_id, ext_id, one_hour_ago)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
@@ -49,7 +49,7 @@ pub async fn execute(
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
deps.watch_event.save(&event).await?;
|
deps.watch_event_command.save(&event).await?;
|
||||||
|
|
||||||
let _ = deps
|
let _ = deps
|
||||||
.event_publisher
|
.event_publisher
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use domain::ports::WatchEventRepository;
|
|
||||||
use domain::testing::InMemoryWatchEventRepository;
|
use domain::testing::InMemoryWatchEventRepository;
|
||||||
|
|
||||||
use crate::integrations::cleanup;
|
use crate::integrations::cleanup;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn returns_zero_when_nothing_to_clean() {
|
async fn returns_zero_when_nothing_to_clean() {
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
|
|
||||||
let count = cleanup::execute(watch_events).await.unwrap();
|
let count = cleanup::execute(watch_events).await.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use domain::models::{WatchEvent, WatchEventSource};
|
use domain::models::{WatchEvent, WatchEventSource};
|
||||||
use domain::ports::{MovieRepository, WatchEventRepository};
|
use domain::ports::{MovieCommand, WatchEventCommand};
|
||||||
use domain::testing::{InMemoryWatchEventRepository, NoopEventPublisher};
|
use domain::testing::{InMemoryWatchEventRepository, NoopEventPublisher};
|
||||||
use domain::value_objects::UserId;
|
use domain::value_objects::UserId;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -16,7 +16,7 @@ fn noop_logger() -> Arc<dyn crate::ports::ReviewLogger> {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn confirms_watch_event_via_review_logger() {
|
async fn confirms_watch_event_via_review_logger() {
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
let uid = Uuid::new_v4();
|
let uid = Uuid::new_v4();
|
||||||
|
|
||||||
let event = WatchEvent::new(
|
let event = WatchEvent::new(
|
||||||
@@ -32,7 +32,8 @@ async fn confirms_watch_event_via_review_logger() {
|
|||||||
watch_events.save(&event).await.unwrap();
|
watch_events.save(&event).await.unwrap();
|
||||||
|
|
||||||
let result = confirm::execute(
|
let result = confirm::execute(
|
||||||
Arc::clone(&watch_events),
|
Arc::clone(&watch_events) as _,
|
||||||
|
Arc::clone(&watch_events) as _,
|
||||||
noop_logger(),
|
noop_logger(),
|
||||||
ConfirmWatchEventsCommand {
|
ConfirmWatchEventsCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
@@ -51,10 +52,11 @@ async fn confirms_watch_event_via_review_logger() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn empty_confirmations_returns_zero() {
|
async fn empty_confirmations_returns_zero() {
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
|
|
||||||
let result = confirm::execute(
|
let result = confirm::execute(
|
||||||
Arc::clone(&watch_events),
|
Arc::clone(&watch_events) as _,
|
||||||
|
Arc::clone(&watch_events) as _,
|
||||||
noop_logger(),
|
noop_logger(),
|
||||||
ConfirmWatchEventsCommand {
|
ConfirmWatchEventsCommand {
|
||||||
user_id: Uuid::new_v4(),
|
user_id: Uuid::new_v4(),
|
||||||
@@ -69,7 +71,7 @@ async fn empty_confirmations_returns_zero() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn confirms_event_with_external_metadata_id_and_no_movie_id() {
|
async fn confirms_event_with_external_metadata_id_and_no_movie_id() {
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
let uid = Uuid::new_v4();
|
let uid = Uuid::new_v4();
|
||||||
|
|
||||||
let event = WatchEvent::new(
|
let event = WatchEvent::new(
|
||||||
@@ -85,7 +87,8 @@ async fn confirms_event_with_external_metadata_id_and_no_movie_id() {
|
|||||||
watch_events.save(&event).await.unwrap();
|
watch_events.save(&event).await.unwrap();
|
||||||
|
|
||||||
let result = confirm::execute(
|
let result = confirm::execute(
|
||||||
Arc::clone(&watch_events),
|
Arc::clone(&watch_events) as _,
|
||||||
|
Arc::clone(&watch_events) as _,
|
||||||
noop_logger(),
|
noop_logger(),
|
||||||
ConfirmWatchEventsCommand {
|
ConfirmWatchEventsCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
@@ -104,7 +107,7 @@ async fn confirms_event_with_external_metadata_id_and_no_movie_id() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rejects_other_users_event() {
|
async fn rejects_other_users_event() {
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
let owner = Uuid::new_v4();
|
let owner = Uuid::new_v4();
|
||||||
let intruder = Uuid::new_v4();
|
let intruder = Uuid::new_v4();
|
||||||
|
|
||||||
@@ -121,7 +124,8 @@ async fn rejects_other_users_event() {
|
|||||||
watch_events.save(&event).await.unwrap();
|
watch_events.save(&event).await.unwrap();
|
||||||
|
|
||||||
let result = confirm::execute(
|
let result = confirm::execute(
|
||||||
Arc::clone(&watch_events),
|
Arc::clone(&watch_events) as _,
|
||||||
|
Arc::clone(&watch_events) as _,
|
||||||
noop_logger(),
|
noop_logger(),
|
||||||
ConfirmWatchEventsCommand {
|
ConfirmWatchEventsCommand {
|
||||||
user_id: intruder,
|
user_id: intruder,
|
||||||
@@ -139,10 +143,11 @@ async fn rejects_other_users_event() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn fails_when_event_not_found() {
|
async fn fails_when_event_not_found() {
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
|
|
||||||
let result = confirm::execute(
|
let result = confirm::execute(
|
||||||
Arc::clone(&watch_events),
|
Arc::clone(&watch_events) as _,
|
||||||
|
Arc::clone(&watch_events) as _,
|
||||||
noop_logger(),
|
noop_logger(),
|
||||||
ConfirmWatchEventsCommand {
|
ConfirmWatchEventsCommand {
|
||||||
user_id: Uuid::new_v4(),
|
user_id: Uuid::new_v4(),
|
||||||
@@ -160,7 +165,7 @@ async fn fails_when_event_not_found() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn confirms_event_with_movie_id() {
|
async fn confirms_event_with_movie_id() {
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
let events = NoopEventPublisher::new();
|
let events = NoopEventPublisher::new();
|
||||||
let uid = Uuid::new_v4();
|
let uid = Uuid::new_v4();
|
||||||
let movie_uuid = Uuid::new_v4();
|
let movie_uuid = Uuid::new_v4();
|
||||||
@@ -194,6 +199,7 @@ async fn confirms_event_with_movie_id() {
|
|||||||
let watchlist = domain::testing::InMemoryWatchlistRepository::new();
|
let watchlist = domain::testing::InMemoryWatchlistRepository::new();
|
||||||
let review_logger: Arc<dyn crate::ports::ReviewLogger> =
|
let review_logger: Arc<dyn crate::ports::ReviewLogger> =
|
||||||
Arc::new(crate::diary::review_logger::DefaultReviewLogger::new(
|
Arc::new(crate::diary::review_logger::DefaultReviewLogger::new(
|
||||||
|
Arc::clone(&movies) as _,
|
||||||
Arc::clone(&movies) as _,
|
Arc::clone(&movies) as _,
|
||||||
Arc::clone(&reviews) as _,
|
Arc::clone(&reviews) as _,
|
||||||
Arc::clone(&watchlist) as _,
|
Arc::clone(&watchlist) as _,
|
||||||
@@ -202,7 +208,8 @@ async fn confirms_event_with_movie_id() {
|
|||||||
));
|
));
|
||||||
|
|
||||||
let result = confirm::execute(
|
let result = confirm::execute(
|
||||||
Arc::clone(&watch_events),
|
Arc::clone(&watch_events) as _,
|
||||||
|
Arc::clone(&watch_events) as _,
|
||||||
review_logger,
|
review_logger,
|
||||||
ConfirmWatchEventsCommand {
|
ConfirmWatchEventsCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
@@ -221,7 +228,7 @@ async fn confirms_event_with_movie_id() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn confirms_event_without_movie_id_and_without_external_metadata_id() {
|
async fn confirms_event_without_movie_id_and_without_external_metadata_id() {
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
let uid = Uuid::new_v4();
|
let uid = Uuid::new_v4();
|
||||||
|
|
||||||
let event = WatchEvent::new(
|
let event = WatchEvent::new(
|
||||||
@@ -237,7 +244,8 @@ async fn confirms_event_without_movie_id_and_without_external_metadata_id() {
|
|||||||
watch_events.save(&event).await.unwrap();
|
watch_events.save(&event).await.unwrap();
|
||||||
|
|
||||||
let result = confirm::execute(
|
let result = confirm::execute(
|
||||||
Arc::clone(&watch_events),
|
Arc::clone(&watch_events) as _,
|
||||||
|
Arc::clone(&watch_events) as _,
|
||||||
noop_logger(),
|
noop_logger(),
|
||||||
ConfirmWatchEventsCommand {
|
ConfirmWatchEventsCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
@@ -256,7 +264,7 @@ async fn confirms_event_without_movie_id_and_without_external_metadata_id() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn confirms_multiple_events() {
|
async fn confirms_multiple_events() {
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
let uid = Uuid::new_v4();
|
let uid = Uuid::new_v4();
|
||||||
|
|
||||||
let event1 = WatchEvent::new(
|
let event1 = WatchEvent::new(
|
||||||
@@ -285,7 +293,8 @@ async fn confirms_multiple_events() {
|
|||||||
watch_events.save(&event2).await.unwrap();
|
watch_events.save(&event2).await.unwrap();
|
||||||
|
|
||||||
let result = confirm::execute(
|
let result = confirm::execute(
|
||||||
Arc::clone(&watch_events),
|
Arc::clone(&watch_events) as _,
|
||||||
|
Arc::clone(&watch_events) as _,
|
||||||
noop_logger(),
|
noop_logger(),
|
||||||
ConfirmWatchEventsCommand {
|
ConfirmWatchEventsCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
@@ -311,7 +320,7 @@ async fn confirms_multiple_events() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn confirms_event_without_year() {
|
async fn confirms_event_without_year() {
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
let uid = Uuid::new_v4();
|
let uid = Uuid::new_v4();
|
||||||
|
|
||||||
let event = WatchEvent::new(
|
let event = WatchEvent::new(
|
||||||
@@ -327,7 +336,8 @@ async fn confirms_event_without_year() {
|
|||||||
watch_events.save(&event).await.unwrap();
|
watch_events.save(&event).await.unwrap();
|
||||||
|
|
||||||
let result = confirm::execute(
|
let result = confirm::execute(
|
||||||
Arc::clone(&watch_events),
|
Arc::clone(&watch_events) as _,
|
||||||
|
Arc::clone(&watch_events) as _,
|
||||||
noop_logger(),
|
noop_logger(),
|
||||||
ConfirmWatchEventsCommand {
|
ConfirmWatchEventsCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use domain::models::{WatchEvent, WatchEventSource};
|
use domain::models::{WatchEvent, WatchEventSource};
|
||||||
use domain::ports::WatchEventRepository;
|
use domain::ports::WatchEventCommand;
|
||||||
use domain::testing::InMemoryWatchEventRepository;
|
use domain::testing::InMemoryWatchEventRepository;
|
||||||
use domain::value_objects::UserId;
|
use domain::value_objects::UserId;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -10,10 +10,11 @@ use crate::integrations::{commands::DismissWatchEventsCommand, dismiss};
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dismisses_empty_list_returns_zero() {
|
async fn dismisses_empty_list_returns_zero() {
|
||||||
let events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let events = InMemoryWatchEventRepository::new();
|
||||||
|
|
||||||
let result = dismiss::execute(
|
let result = dismiss::execute(
|
||||||
Arc::clone(&events),
|
Arc::clone(&events) as _,
|
||||||
|
Arc::clone(&events) as _,
|
||||||
DismissWatchEventsCommand {
|
DismissWatchEventsCommand {
|
||||||
user_id: Uuid::new_v4(),
|
user_id: Uuid::new_v4(),
|
||||||
event_ids: vec![],
|
event_ids: vec![],
|
||||||
@@ -27,10 +28,11 @@ async fn dismisses_empty_list_returns_zero() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn fails_when_event_not_found() {
|
async fn fails_when_event_not_found() {
|
||||||
let events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let events = InMemoryWatchEventRepository::new();
|
||||||
|
|
||||||
let result = dismiss::execute(
|
let result = dismiss::execute(
|
||||||
Arc::clone(&events),
|
Arc::clone(&events) as _,
|
||||||
|
Arc::clone(&events) as _,
|
||||||
DismissWatchEventsCommand {
|
DismissWatchEventsCommand {
|
||||||
user_id: Uuid::new_v4(),
|
user_id: Uuid::new_v4(),
|
||||||
event_ids: vec![Uuid::new_v4()],
|
event_ids: vec![Uuid::new_v4()],
|
||||||
@@ -43,7 +45,7 @@ async fn fails_when_event_not_found() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dismisses_existing_events() {
|
async fn dismisses_existing_events() {
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
let uid = Uuid::new_v4();
|
let uid = Uuid::new_v4();
|
||||||
let user_id = UserId::from_uuid(uid);
|
let user_id = UserId::from_uuid(uid);
|
||||||
|
|
||||||
@@ -71,7 +73,8 @@ async fn dismisses_existing_events() {
|
|||||||
watch_events.save(&e2).await.unwrap();
|
watch_events.save(&e2).await.unwrap();
|
||||||
|
|
||||||
let result = dismiss::execute(
|
let result = dismiss::execute(
|
||||||
Arc::clone(&watch_events),
|
Arc::clone(&watch_events) as _,
|
||||||
|
Arc::clone(&watch_events) as _,
|
||||||
DismissWatchEventsCommand {
|
DismissWatchEventsCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
event_ids: vec![id1, id2],
|
event_ids: vec![id1, id2],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use domain::models::{WatchEvent, WatchEventSource};
|
use domain::models::{WatchEvent, WatchEventSource};
|
||||||
use domain::ports::WatchEventRepository;
|
use domain::ports::WatchEventCommand;
|
||||||
use domain::testing::InMemoryWatchEventRepository;
|
use domain::testing::InMemoryWatchEventRepository;
|
||||||
use domain::value_objects::UserId;
|
use domain::value_objects::UserId;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -11,10 +11,10 @@ use crate::integrations::{get_queue, queries::GetWatchQueueQuery};
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn returns_empty_when_no_events() {
|
async fn returns_empty_when_no_events() {
|
||||||
let events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let events = InMemoryWatchEventRepository::new();
|
||||||
|
|
||||||
let result = get_queue::execute(
|
let result = get_queue::execute(
|
||||||
Arc::clone(&events),
|
Arc::clone(&events) as _,
|
||||||
GetWatchQueueQuery {
|
GetWatchQueueQuery {
|
||||||
user_id: Uuid::new_v4(),
|
user_id: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
@@ -27,7 +27,7 @@ async fn returns_empty_when_no_events() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn returns_pending_events() {
|
async fn returns_pending_events() {
|
||||||
let events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let events = InMemoryWatchEventRepository::new();
|
||||||
|
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
let event = WatchEvent::new(
|
let event = WatchEvent::new(
|
||||||
@@ -41,7 +41,7 @@ async fn returns_pending_events() {
|
|||||||
);
|
);
|
||||||
events.save(&event).await.unwrap();
|
events.save(&event).await.unwrap();
|
||||||
|
|
||||||
let result = get_queue::execute(Arc::clone(&events), GetWatchQueueQuery { user_id })
|
let result = get_queue::execute(Arc::clone(&events) as _, GetWatchQueueQuery { user_id })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use domain::models::WatchEventSource;
|
use domain::models::WatchEventSource;
|
||||||
use domain::ports::{EventPublisher, WatchEventRepository, WebhookTokenRepository};
|
use domain::ports::{EventPublisher, WebhookTokenRepository};
|
||||||
use domain::testing::{
|
use domain::testing::{
|
||||||
InMemoryWatchEventRepository, InMemoryWebhookTokenRepository, NoopEventPublisher,
|
InMemoryWatchEventRepository, InMemoryWebhookTokenRepository, NoopEventPublisher,
|
||||||
};
|
};
|
||||||
@@ -30,7 +30,7 @@ impl domain::ports::MediaServerParser for FakeParser {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn ingests_watch_event() {
|
async fn ingests_watch_event() {
|
||||||
let tokens: Arc<dyn WebhookTokenRepository> = InMemoryWebhookTokenRepository::new();
|
let tokens: Arc<dyn WebhookTokenRepository> = InMemoryWebhookTokenRepository::new();
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
let event_publisher: Arc<dyn EventPublisher> = NoopEventPublisher::new();
|
let event_publisher: Arc<dyn EventPublisher> = NoopEventPublisher::new();
|
||||||
|
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
@@ -47,7 +47,8 @@ async fn ingests_watch_event() {
|
|||||||
|
|
||||||
let deps = IngestWatchEventDeps {
|
let deps = IngestWatchEventDeps {
|
||||||
webhook_token: Arc::clone(&tokens),
|
webhook_token: Arc::clone(&tokens),
|
||||||
watch_event: Arc::clone(&watch_events),
|
watch_event_command: Arc::clone(&watch_events) as _,
|
||||||
|
watch_event_query: Arc::clone(&watch_events) as _,
|
||||||
event_publisher: Arc::clone(&event_publisher),
|
event_publisher: Arc::clone(&event_publisher),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,12 +69,13 @@ async fn ingests_watch_event() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rejects_invalid_token() {
|
async fn rejects_invalid_token() {
|
||||||
let tokens: Arc<dyn WebhookTokenRepository> = InMemoryWebhookTokenRepository::new();
|
let tokens: Arc<dyn WebhookTokenRepository> = InMemoryWebhookTokenRepository::new();
|
||||||
let watch_events: Arc<dyn WatchEventRepository> = InMemoryWatchEventRepository::new();
|
let watch_events = InMemoryWatchEventRepository::new();
|
||||||
let event_publisher: Arc<dyn EventPublisher> = NoopEventPublisher::new();
|
let event_publisher: Arc<dyn EventPublisher> = NoopEventPublisher::new();
|
||||||
|
|
||||||
let deps = IngestWatchEventDeps {
|
let deps = IngestWatchEventDeps {
|
||||||
webhook_token: Arc::clone(&tokens),
|
webhook_token: Arc::clone(&tokens),
|
||||||
watch_event: Arc::clone(&watch_events),
|
watch_event_command: Arc::clone(&watch_events) as _,
|
||||||
|
watch_event_query: Arc::clone(&watch_events) as _,
|
||||||
event_publisher: Arc::clone(&event_publisher),
|
event_publisher: Arc::clone(&event_publisher),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::time::Duration;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
ports::{MovieDeduplicator, MovieRepository, ObjectStorage, PeriodicJob},
|
ports::{MovieDeduplicator, MovieQuery, ObjectStorage, PeriodicJob},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::movies::merge_duplicates::{MergeDuplicatesDeps, execute};
|
use crate::movies::merge_duplicates::{MergeDuplicatesDeps, execute};
|
||||||
@@ -15,13 +15,13 @@ pub struct MovieDeduplicationJob {
|
|||||||
|
|
||||||
impl MovieDeduplicationJob {
|
impl MovieDeduplicationJob {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
movie: Arc<dyn MovieRepository>,
|
movie: Arc<dyn MovieQuery>,
|
||||||
deduplicator: Arc<dyn MovieDeduplicator>,
|
deduplicator: Arc<dyn MovieDeduplicator>,
|
||||||
object_storage: Arc<dyn ObjectStorage>,
|
object_storage: Arc<dyn ObjectStorage>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
deps: MergeDuplicatesDeps {
|
deps: MergeDuplicatesDeps {
|
||||||
movie,
|
movie_query: movie,
|
||||||
deduplicator,
|
deduplicator,
|
||||||
object_storage,
|
object_storage,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ use std::time::Duration;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
ports::{PeriodicJob, WatchEventRepository},
|
ports::{PeriodicJob, WatchEventCommand},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct WatchEventCleanupJob {
|
pub struct WatchEventCleanupJob {
|
||||||
watch_event: Arc<dyn WatchEventRepository>,
|
watch_event: Arc<dyn WatchEventCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WatchEventCleanupJob {
|
impl WatchEventCleanupJob {
|
||||||
pub fn new(watch_event: Arc<dyn WatchEventRepository>) -> Self {
|
pub fn new(watch_event: Arc<dyn WatchEventCommand>) -> Self {
|
||||||
Self { watch_event }
|
Self { watch_event }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
EventPublisher, MetadataClient, MovieProfileRepository, MovieRepository, ObjectStorage,
|
EventPublisher, MetadataClient, MovieCommand, MovieProfileRepository, MovieQuery,
|
||||||
PersonCommand, PersonQuery, PosterFetcherClient, SearchCommand,
|
ObjectStorage, PersonCommand, PersonQuery, PosterFetcherClient, SearchCommand,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct SyncPosterDeps {
|
pub struct SyncPosterDeps {
|
||||||
pub movie: Arc<dyn MovieRepository>,
|
pub movie_command: Arc<dyn MovieCommand>,
|
||||||
|
pub movie_query: Arc<dyn MovieQuery>,
|
||||||
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
||||||
pub metadata: Arc<dyn MetadataClient>,
|
pub metadata: Arc<dyn MetadataClient>,
|
||||||
pub poster_fetcher: Arc<dyn PosterFetcherClient>,
|
pub poster_fetcher: Arc<dyn PosterFetcherClient>,
|
||||||
@@ -16,14 +17,14 @@ pub struct SyncPosterDeps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct EnrichMovieDeps {
|
pub struct EnrichMovieDeps {
|
||||||
pub movie: Arc<dyn MovieRepository>,
|
pub movie_query: Arc<dyn MovieQuery>,
|
||||||
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
||||||
pub person_command: Arc<dyn PersonCommand>,
|
pub person_command: Arc<dyn PersonCommand>,
|
||||||
pub search_command: Arc<dyn SearchCommand>,
|
pub search_command: Arc<dyn SearchCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ReindexSearchDeps {
|
pub struct ReindexSearchDeps {
|
||||||
pub movie: Arc<dyn MovieRepository>,
|
pub movie_query: Arc<dyn MovieQuery>,
|
||||||
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
||||||
pub search_command: Arc<dyn SearchCommand>,
|
pub search_command: Arc<dyn SearchCommand>,
|
||||||
pub person_command: Arc<dyn PersonCommand>,
|
pub person_command: Arc<dyn PersonCommand>,
|
||||||
|
|||||||
@@ -5,20 +5,20 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::IndexableDocument,
|
models::IndexableDocument,
|
||||||
ports::{EventHandler, MovieRepository, SearchCommand},
|
ports::{EventHandler, MovieQuery, SearchCommand},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Reacts to `MovieDiscovered` and inserts a bare search index entry immediately,
|
/// Reacts to `MovieDiscovered` and inserts a bare search index entry immediately,
|
||||||
/// so movies are findable before TMDb enrichment runs.
|
/// so movies are findable before TMDb enrichment runs.
|
||||||
/// Enrichment will later overwrite this with the full document (cast, genres, etc.).
|
/// Enrichment will later overwrite this with the full document (cast, genres, etc.).
|
||||||
pub struct MovieDiscoveryIndexer {
|
pub struct MovieDiscoveryIndexer {
|
||||||
movie_repository: Arc<dyn MovieRepository>,
|
movie_repository: Arc<dyn MovieQuery>,
|
||||||
search_command: Arc<dyn SearchCommand>,
|
search_command: Arc<dyn SearchCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MovieDiscoveryIndexer {
|
impl MovieDiscoveryIndexer {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
movie_repository: Arc<dyn MovieRepository>,
|
movie_repository: Arc<dyn MovieQuery>,
|
||||||
search_command: Arc<dyn SearchCommand>,
|
search_command: Arc<dyn SearchCommand>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub async fn execute(deps: &EnrichMovieDeps, cmd: EnrichMovieCommand) -> Result<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fetch the movie for the search index document
|
// 3. Fetch the movie for the search index document
|
||||||
let Some(movie) = deps.movie.get_movie_by_id(&cmd.movie_id).await? else {
|
let Some(movie) = deps.movie_query.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(());
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use domain::{
|
|||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::MovieProfile,
|
models::MovieProfile,
|
||||||
ports::{
|
ports::{
|
||||||
EventHandler, ImageFetcher, MovieEnrichmentClient, MovieProfileRepository, MovieRepository,
|
EventHandler, ImageFetcher, MovieEnrichmentClient, MovieProfileRepository, MovieQuery,
|
||||||
ObjectStorage, PersonCommand, SearchCommand,
|
ObjectStorage, PersonCommand, SearchCommand,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -17,7 +17,7 @@ use crate::movies::{
|
|||||||
|
|
||||||
pub struct MovieEnrichmentHandler {
|
pub struct MovieEnrichmentHandler {
|
||||||
enrichment_client: Arc<dyn MovieEnrichmentClient>,
|
enrichment_client: Arc<dyn MovieEnrichmentClient>,
|
||||||
movie_repository: Arc<dyn MovieRepository>,
|
movie_repository: Arc<dyn MovieQuery>,
|
||||||
profile_repo: Arc<dyn MovieProfileRepository>,
|
profile_repo: Arc<dyn MovieProfileRepository>,
|
||||||
person_command: Arc<dyn PersonCommand>,
|
person_command: Arc<dyn PersonCommand>,
|
||||||
search_command: Arc<dyn SearchCommand>,
|
search_command: Arc<dyn SearchCommand>,
|
||||||
@@ -28,7 +28,7 @@ pub struct MovieEnrichmentHandler {
|
|||||||
impl MovieEnrichmentHandler {
|
impl MovieEnrichmentHandler {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
enrichment_client: Arc<dyn MovieEnrichmentClient>,
|
enrichment_client: Arc<dyn MovieEnrichmentClient>,
|
||||||
movie_repository: Arc<dyn MovieRepository>,
|
movie_repository: Arc<dyn MovieQuery>,
|
||||||
profile_repo: Arc<dyn MovieProfileRepository>,
|
profile_repo: Arc<dyn MovieProfileRepository>,
|
||||||
person_command: Arc<dyn PersonCommand>,
|
person_command: Arc<dyn PersonCommand>,
|
||||||
search_command: Arc<dyn SearchCommand>,
|
search_command: Arc<dyn SearchCommand>,
|
||||||
@@ -92,7 +92,7 @@ impl EventHandler for MovieEnrichmentHandler {
|
|||||||
|
|
||||||
self.download_cast_photos(&profile).await;
|
self.download_cast_photos(&profile).await;
|
||||||
let enrich_deps = EnrichMovieDeps {
|
let enrich_deps = EnrichMovieDeps {
|
||||||
movie: self.movie_repository.clone(),
|
movie_query: self.movie_repository.clone(),
|
||||||
movie_profile: self.profile_repo.clone(),
|
movie_profile: self.profile_repo.clone(),
|
||||||
person_command: self.person_command.clone(),
|
person_command: self.person_command.clone(),
|
||||||
search_command: self.search_command.clone(),
|
search_command: self.search_command.clone(),
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::collections::{PageParams, Paginated},
|
models::collections::{PageParams, Paginated},
|
||||||
models::{MovieFilter, MovieSummary},
|
models::{MovieFilter, MovieSummary},
|
||||||
ports::MovieRepository,
|
ports::MovieQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::movies::queries::GetMoviesQuery;
|
use crate::movies::queries::GetMoviesQuery;
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
movie: Arc<dyn MovieRepository>,
|
movie: Arc<dyn MovieQuery>,
|
||||||
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)?;
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
ports::{MovieDeduplicator, MovieRepository, ObjectStorage},
|
ports::{MovieDeduplicator, MovieQuery, ObjectStorage},
|
||||||
value_objects::MovieId,
|
value_objects::MovieId,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct MergeDuplicatesDeps {
|
pub struct MergeDuplicatesDeps {
|
||||||
pub movie: Arc<dyn MovieRepository>,
|
pub movie_query: Arc<dyn MovieQuery>,
|
||||||
pub deduplicator: Arc<dyn MovieDeduplicator>,
|
pub deduplicator: Arc<dyn MovieDeduplicator>,
|
||||||
pub object_storage: Arc<dyn ObjectStorage>,
|
pub object_storage: Arc<dyn ObjectStorage>,
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ pub struct MergeReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(deps: &MergeDuplicatesDeps) -> Result<MergeReport, DomainError> {
|
pub async fn execute(deps: &MergeDuplicatesDeps) -> Result<MergeReport, DomainError> {
|
||||||
let movies = deps.movie.list_movies_with_external_id().await?;
|
let movies = deps.movie_query.list_movies_with_external_id().await?;
|
||||||
|
|
||||||
let mut pairs_found = 0u64;
|
let mut pairs_found = 0u64;
|
||||||
let mut rows_repointed = 0u64;
|
let mut rows_repointed = 0u64;
|
||||||
@@ -37,7 +37,7 @@ pub async fn execute(deps: &MergeDuplicatesDeps) -> Result<MergeReport, DomainEr
|
|||||||
pairs_found += 1;
|
pairs_found += 1;
|
||||||
|
|
||||||
// Determine which poster will be dropped after merge
|
// Determine which poster will be dropped after merge
|
||||||
let canonical = match deps.movie.get_movie_by_id(&canonical_id).await? {
|
let canonical = match deps.movie_query.get_movie_by_id(&canonical_id).await? {
|
||||||
Some(existing) => existing,
|
Some(existing) => existing,
|
||||||
None => domain::models::Movie::from_persistence(
|
None => domain::models::Movie::from_persistence(
|
||||||
canonical_id,
|
canonical_id,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ async fn reindex_movies(deps: &ReindexSearchDeps) -> Result<u64, DomainError> {
|
|||||||
let mut offset: u32 = 0;
|
let mut offset: u32 = 0;
|
||||||
loop {
|
loop {
|
||||||
let page = deps
|
let page = deps
|
||||||
.movie
|
.movie_query
|
||||||
.list_movies(
|
.list_movies(
|
||||||
&PageParams {
|
&PageParams {
|
||||||
limit: BATCH_SIZE,
|
limit: BATCH_SIZE,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::Movie,
|
models::Movie,
|
||||||
ports::{EventPublisher, MetadataClient, MovieRepository},
|
ports::{EventPublisher, MetadataClient, MovieCommand, MovieQuery},
|
||||||
value_objects::MovieId,
|
value_objects::MovieId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -14,20 +14,21 @@ use crate::diary::movie_resolver::{MovieResolver, MovieResolverDeps};
|
|||||||
/// Returns `(movie, is_new_movie)`.
|
/// Returns `(movie, is_new_movie)`.
|
||||||
pub async fn resolve_and_persist_movie(
|
pub async fn resolve_and_persist_movie(
|
||||||
input: &MovieInput,
|
input: &MovieInput,
|
||||||
movie_repo: &dyn MovieRepository,
|
movie_command: &dyn MovieCommand,
|
||||||
|
movie_query: &dyn MovieQuery,
|
||||||
metadata_client: &dyn MetadataClient,
|
metadata_client: &dyn MetadataClient,
|
||||||
event_publisher: &dyn EventPublisher,
|
event_publisher: &dyn EventPublisher,
|
||||||
) -> Result<(Movie, bool), DomainError> {
|
) -> Result<(Movie, bool), DomainError> {
|
||||||
let (movie, is_new) = if let Some(id) = input.movie_id {
|
let (movie, is_new) = if let Some(id) = input.movie_id {
|
||||||
let movie_id = MovieId::from_uuid(id);
|
let movie_id = MovieId::from_uuid(id);
|
||||||
let movie = movie_repo
|
let movie = movie_query
|
||||||
.get_movie_by_id(&movie_id)
|
.get_movie_by_id(&movie_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound(format!("Movie {id}")))?;
|
.ok_or_else(|| DomainError::NotFound(format!("Movie {id}")))?;
|
||||||
(movie, false)
|
(movie, false)
|
||||||
} else {
|
} else {
|
||||||
let deps = MovieResolverDeps {
|
let deps = MovieResolverDeps {
|
||||||
repository: movie_repo,
|
repository: movie_query,
|
||||||
metadata_client,
|
metadata_client,
|
||||||
};
|
};
|
||||||
MovieResolver::default_pipeline()
|
MovieResolver::default_pipeline()
|
||||||
@@ -36,7 +37,7 @@ pub async fn resolve_and_persist_movie(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if is_new {
|
if is_new {
|
||||||
movie_repo.upsert_movie(&movie).await?;
|
movie_command.upsert_movie(&movie).await?;
|
||||||
if let Some(ext_id) = movie.external_metadata_id() {
|
if let Some(ext_id) = movie.external_metadata_id() {
|
||||||
let _ = event_publisher
|
let _ = event_publisher
|
||||||
.publish(&DomainEvent::MovieDiscovered {
|
.publish(&DomainEvent::MovieDiscovered {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::{diary::commands::SyncPosterCommand, movies::deps::SyncPosterDeps};
|
|||||||
pub async fn execute(deps: &SyncPosterDeps, 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 deps.movie.get_movie_by_id(&movie_id).await? {
|
let mut movie = match deps.movie_query.get_movie_by_id(&movie_id).await? {
|
||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
None => {
|
None => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -59,7 +59,7 @@ pub async fn execute(deps: &SyncPosterDeps, cmd: SyncPosterCommand) -> Result<()
|
|||||||
let poster_path = PosterPath::new(stored_path)?;
|
let poster_path = PosterPath::new(stored_path)?;
|
||||||
|
|
||||||
movie.update_poster(poster_path);
|
movie.update_poster(poster_path);
|
||||||
deps.movie.upsert_movie(&movie).await?;
|
deps.movie_command.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.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use domain::{
|
use domain::{
|
||||||
models::{Movie, MovieProfile},
|
models::{Movie, MovieProfile},
|
||||||
ports::MovieRepository,
|
ports::MovieCommand,
|
||||||
testing::{
|
testing::{
|
||||||
FakeSearchCommand, InMemoryMovieProfileRepository, InMemoryMovieRepository,
|
FakeSearchCommand, InMemoryMovieProfileRepository, InMemoryMovieRepository,
|
||||||
PanicPersonCommand,
|
PanicPersonCommand,
|
||||||
@@ -50,7 +50,7 @@ async fn stores_profile_and_indexes() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let deps = EnrichMovieDeps {
|
let deps = EnrichMovieDeps {
|
||||||
movie: movie_repo as Arc<_>,
|
movie_query: movie_repo as Arc<_>,
|
||||||
movie_profile: Arc::clone(&profile_repo) as Arc<_>,
|
movie_profile: Arc::clone(&profile_repo) as Arc<_>,
|
||||||
person_command: Arc::new(PanicPersonCommand),
|
person_command: Arc::new(PanicPersonCommand),
|
||||||
search_command: Arc::new(FakeSearchCommand),
|
search_command: Arc::new(FakeSearchCommand),
|
||||||
@@ -142,7 +142,7 @@ async fn extracts_and_indexes_persons() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let deps = EnrichMovieDeps {
|
let deps = EnrichMovieDeps {
|
||||||
movie: movie_repo as Arc<_>,
|
movie_query: movie_repo as Arc<_>,
|
||||||
movie_profile: Arc::clone(&profile_repo) as Arc<_>,
|
movie_profile: Arc::clone(&profile_repo) as Arc<_>,
|
||||||
person_command: Arc::new(NoopPersonCommand),
|
person_command: Arc::new(NoopPersonCommand),
|
||||||
search_command: Arc::new(FakeSearchCommand),
|
search_command: Arc::new(FakeSearchCommand),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use uuid::Uuid;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::Movie,
|
models::Movie,
|
||||||
ports::{MetadataClient, MovieRepository},
|
ports::{MetadataClient, MovieCommand, MovieQuery},
|
||||||
testing::{
|
testing::{
|
||||||
FakeSearchCommand, InMemoryMovieProfileRepository, InMemoryMovieRepository,
|
FakeSearchCommand, InMemoryMovieProfileRepository, InMemoryMovieRepository,
|
||||||
NoopEventPublisher, NoopObjectStorage,
|
NoopEventPublisher, NoopObjectStorage,
|
||||||
@@ -20,7 +20,8 @@ use crate::{
|
|||||||
|
|
||||||
fn default_deps() -> SyncPosterDeps {
|
fn default_deps() -> SyncPosterDeps {
|
||||||
SyncPosterDeps {
|
SyncPosterDeps {
|
||||||
movie: InMemoryMovieRepository::new(),
|
movie_command: InMemoryMovieRepository::new(),
|
||||||
|
movie_query: InMemoryMovieRepository::new(),
|
||||||
movie_profile: InMemoryMovieProfileRepository::new(),
|
movie_profile: InMemoryMovieProfileRepository::new(),
|
||||||
metadata: Arc::new(domain::testing::FakeMetadataClient),
|
metadata: Arc::new(domain::testing::FakeMetadataClient),
|
||||||
poster_fetcher: Arc::new(domain::testing::FakePosterFetcher),
|
poster_fetcher: Arc::new(domain::testing::FakePosterFetcher),
|
||||||
@@ -59,7 +60,8 @@ async fn fails_when_no_external_id() {
|
|||||||
movies.upsert_movie(&movie).await.unwrap();
|
movies.upsert_movie(&movie).await.unwrap();
|
||||||
|
|
||||||
let deps = SyncPosterDeps {
|
let deps = SyncPosterDeps {
|
||||||
movie: Arc::clone(&movies) as _,
|
movie_command: Arc::clone(&movies) as _,
|
||||||
|
movie_query: Arc::clone(&movies) as _,
|
||||||
..default_deps()
|
..default_deps()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,7 +105,8 @@ async fn syncs_poster_for_movie_with_external_id() {
|
|||||||
movies.upsert_movie(&movie).await.unwrap();
|
movies.upsert_movie(&movie).await.unwrap();
|
||||||
|
|
||||||
let deps = SyncPosterDeps {
|
let deps = SyncPosterDeps {
|
||||||
movie: Arc::clone(&movies) as _,
|
movie_command: Arc::clone(&movies) as _,
|
||||||
|
movie_query: Arc::clone(&movies) as _,
|
||||||
metadata: Arc::new(FakeMetaWithPoster) as _,
|
metadata: Arc::new(FakeMetaWithPoster) as _,
|
||||||
..default_deps()
|
..default_deps()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ use domain::{
|
|||||||
ports::{
|
ports::{
|
||||||
AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher,
|
AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher,
|
||||||
GoalRepository, ImportProfileRepository, ImportSessionRepository, MetadataClient,
|
GoalRepository, ImportProfileRepository, ImportSessionRepository, MetadataClient,
|
||||||
MovieProfileRepository, MovieRepository, ObjectStorage, PasswordHasher, PersonCommand,
|
MovieCommand, MovieProfileRepository, MovieQuery, ObjectStorage, PasswordHasher,
|
||||||
PersonQuery, PosterFetcherClient, RefreshSessionRepository, ReviewRepository,
|
PersonCommand, PersonQuery, PosterFetcherClient, RefreshSessionRepository,
|
||||||
SearchCommand, SearchPort, StatsRepository, UserProfileFieldsRepository, UserRepository,
|
ReviewRepository, SearchCommand, SearchPort, StatsRepository,
|
||||||
UserSettingsRepository, WatchEventRepository, WatchlistRepository, WebhookTokenRepository,
|
UserProfileFieldsRepository, UserRepository, UserSettingsRepository, WatchEventCommand,
|
||||||
|
WatchEventQuery, WatchlistRepository, WebhookTokenRepository,
|
||||||
WrapUpRepository, WrapUpStatsQuery,
|
WrapUpRepository, WrapUpStatsQuery,
|
||||||
},
|
},
|
||||||
testing::{
|
testing::{
|
||||||
@@ -40,7 +41,8 @@ impl ReviewLogger for NoopReviewLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct TestContextBuilder {
|
pub struct TestContextBuilder {
|
||||||
pub movie_repo: Arc<dyn MovieRepository>,
|
pub movie_command: Arc<dyn MovieCommand>,
|
||||||
|
pub movie_query: Arc<dyn MovieQuery>,
|
||||||
pub review_repo: Arc<dyn ReviewRepository>,
|
pub review_repo: Arc<dyn ReviewRepository>,
|
||||||
pub diary_repo: Arc<dyn DiaryRepository>,
|
pub diary_repo: Arc<dyn DiaryRepository>,
|
||||||
pub diary_exporter: Arc<dyn DiaryExporter>,
|
pub diary_exporter: Arc<dyn DiaryExporter>,
|
||||||
@@ -57,7 +59,8 @@ pub struct TestContextBuilder {
|
|||||||
pub import_profile_repo: Arc<dyn ImportProfileRepository>,
|
pub import_profile_repo: Arc<dyn ImportProfileRepository>,
|
||||||
pub movie_profile_repo: Arc<dyn MovieProfileRepository>,
|
pub movie_profile_repo: Arc<dyn MovieProfileRepository>,
|
||||||
pub watchlist_repo: Arc<dyn WatchlistRepository>,
|
pub watchlist_repo: Arc<dyn WatchlistRepository>,
|
||||||
pub watch_event_repo: Arc<dyn WatchEventRepository>,
|
pub watch_event_command: Arc<dyn WatchEventCommand>,
|
||||||
|
pub watch_event_query: Arc<dyn WatchEventQuery>,
|
||||||
pub webhook_token_repo: Arc<dyn WebhookTokenRepository>,
|
pub webhook_token_repo: Arc<dyn WebhookTokenRepository>,
|
||||||
pub profile_fields_repo: Arc<dyn UserProfileFieldsRepository>,
|
pub profile_fields_repo: Arc<dyn UserProfileFieldsRepository>,
|
||||||
pub person_command: Arc<dyn PersonCommand>,
|
pub person_command: Arc<dyn PersonCommand>,
|
||||||
@@ -83,7 +86,8 @@ impl Default for TestContextBuilder {
|
|||||||
impl TestContextBuilder {
|
impl TestContextBuilder {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
movie_repo: InMemoryMovieRepository::new(),
|
movie_command: InMemoryMovieRepository::new(),
|
||||||
|
movie_query: InMemoryMovieRepository::new(),
|
||||||
review_repo: InMemoryReviewRepository::new(),
|
review_repo: InMemoryReviewRepository::new(),
|
||||||
diary_repo: FakeDiaryRepository::new(),
|
diary_repo: FakeDiaryRepository::new(),
|
||||||
diary_exporter: Arc::new(PanicDiaryExporter),
|
diary_exporter: Arc::new(PanicDiaryExporter),
|
||||||
@@ -100,7 +104,8 @@ impl TestContextBuilder {
|
|||||||
import_profile_repo: InMemoryImportProfileRepository::new(),
|
import_profile_repo: InMemoryImportProfileRepository::new(),
|
||||||
movie_profile_repo: InMemoryMovieProfileRepository::new(),
|
movie_profile_repo: InMemoryMovieProfileRepository::new(),
|
||||||
watchlist_repo: InMemoryWatchlistRepository::new(),
|
watchlist_repo: InMemoryWatchlistRepository::new(),
|
||||||
watch_event_repo: InMemoryWatchEventRepository::new(),
|
watch_event_command: InMemoryWatchEventRepository::new(),
|
||||||
|
watch_event_query: InMemoryWatchEventRepository::new(),
|
||||||
webhook_token_repo: InMemoryWebhookTokenRepository::new(),
|
webhook_token_repo: InMemoryWebhookTokenRepository::new(),
|
||||||
profile_fields_repo: InMemoryProfileFieldsRepo::new(),
|
profile_fields_repo: InMemoryProfileFieldsRepo::new(),
|
||||||
person_command: Arc::new(PanicPersonCommand),
|
person_command: Arc::new(PanicPersonCommand),
|
||||||
@@ -128,8 +133,13 @@ impl TestContextBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_movies(mut self, r: Arc<dyn MovieRepository>) -> Self {
|
pub fn with_movie_command(mut self, r: Arc<dyn MovieCommand>) -> Self {
|
||||||
self.movie_repo = r;
|
self.movie_command = r;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_movie_query(mut self, r: Arc<dyn MovieQuery>) -> Self {
|
||||||
|
self.movie_query = r;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,8 +183,13 @@ impl TestContextBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_watch_events(mut self, r: Arc<dyn WatchEventRepository>) -> Self {
|
pub fn with_watch_event_command(mut self, r: Arc<dyn WatchEventCommand>) -> Self {
|
||||||
self.watch_event_repo = r;
|
self.watch_event_command = r;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_watch_event_query(mut self, r: Arc<dyn WatchEventQuery>) -> Self {
|
||||||
|
self.watch_event_query = r;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,51 @@
|
|||||||
use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId};
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
|
events::DomainEvent,
|
||||||
|
ports::{EventPublisher, ObjectStorage},
|
||||||
|
value_objects::UserId,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::users::{commands::UpdateProfileCommand, deps::UpdateProfileDeps};
|
use crate::users::{commands::UpdateProfileCommand, deps::UpdateProfileDeps};
|
||||||
|
|
||||||
|
async fn upload_image(
|
||||||
|
storage: &dyn ObjectStorage,
|
||||||
|
event_publisher: &dyn EventPublisher,
|
||||||
|
user_id: &UserId,
|
||||||
|
kind: &str,
|
||||||
|
old_path: Option<&str>,
|
||||||
|
new_bytes: Option<Vec<u8>>,
|
||||||
|
content_type: Option<&str>,
|
||||||
|
) -> Result<Option<String>, DomainError> {
|
||||||
|
let Some(bytes) = new_bytes else {
|
||||||
|
return Ok(old_path.map(|s| s.to_string()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let ct = content_type.unwrap_or("");
|
||||||
|
if !["image/jpeg", "image/png", "image/webp"].contains(&ct) {
|
||||||
|
return Err(DomainError::ValidationError(
|
||||||
|
format!("{kind} must be jpeg, png, or webp"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(old) = old_path {
|
||||||
|
let _ = storage.delete(old).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = format!("{kind}/{}", user_id.value());
|
||||||
|
let stored = storage.store(&key, &bytes).await?;
|
||||||
|
|
||||||
|
if let Err(e) = event_publisher
|
||||||
|
.publish(&DomainEvent::ImageStored {
|
||||||
|
key: stored.clone(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!("failed to emit ImageStored for {kind} {stored}: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(stored))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
deps: &UpdateProfileDeps,
|
deps: &UpdateProfileDeps,
|
||||||
cmd: UpdateProfileCommand,
|
cmd: UpdateProfileCommand,
|
||||||
@@ -14,59 +58,30 @@ pub async fn execute(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound("User not found".into()))?;
|
.ok_or_else(|| DomainError::NotFound("User not found".into()))?;
|
||||||
|
|
||||||
// Handle avatar
|
let storage = deps.object_storage.as_ref();
|
||||||
let new_avatar_path = if let Some(bytes) = cmd.avatar_bytes {
|
let events = deps.event_publisher.as_ref();
|
||||||
let content_type = cmd.avatar_content_type.as_deref().unwrap_or("");
|
|
||||||
if !["image/jpeg", "image/png", "image/webp"].contains(&content_type) {
|
|
||||||
return Err(DomainError::ValidationError(
|
|
||||||
"Avatar must be jpeg, png, or webp".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if let Some(old_path) = user.avatar_path() {
|
|
||||||
let _ = deps.object_storage.delete(old_path).await;
|
|
||||||
}
|
|
||||||
let key = format!("avatars/{}", user_id.value());
|
|
||||||
let stored = deps.object_storage.store(&key, &bytes).await?;
|
|
||||||
if let Err(e) = deps
|
|
||||||
.event_publisher
|
|
||||||
.publish(&DomainEvent::ImageStored {
|
|
||||||
key: stored.clone(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::warn!("failed to emit ImageStored for avatar {stored}: {e}");
|
|
||||||
}
|
|
||||||
Some(stored)
|
|
||||||
} else {
|
|
||||||
user.avatar_path().map(|s| s.to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle banner
|
let new_avatar_path = upload_image(
|
||||||
let new_banner_path = if let Some(bytes) = cmd.banner_bytes {
|
storage,
|
||||||
let content_type = cmd.banner_content_type.as_deref().unwrap_or("");
|
events,
|
||||||
if !["image/jpeg", "image/png", "image/webp"].contains(&content_type) {
|
&user_id,
|
||||||
return Err(DomainError::ValidationError(
|
"avatars",
|
||||||
"Banner must be jpeg, png, or webp".into(),
|
user.avatar_path(),
|
||||||
));
|
cmd.avatar_bytes,
|
||||||
}
|
cmd.avatar_content_type.as_deref(),
|
||||||
if let Some(old_path) = user.banner_path() {
|
)
|
||||||
let _ = deps.object_storage.delete(old_path).await;
|
.await?;
|
||||||
}
|
|
||||||
let key = format!("banners/{}", user_id.value());
|
let new_banner_path = upload_image(
|
||||||
let stored = deps.object_storage.store(&key, &bytes).await?;
|
storage,
|
||||||
if let Err(e) = deps
|
events,
|
||||||
.event_publisher
|
&user_id,
|
||||||
.publish(&DomainEvent::ImageStored {
|
"banners",
|
||||||
key: stored.clone(),
|
user.banner_path(),
|
||||||
})
|
cmd.banner_bytes,
|
||||||
.await
|
cmd.banner_content_type.as_deref(),
|
||||||
{
|
)
|
||||||
tracing::warn!("failed to emit ImageStored for banner {stored}: {e}");
|
.await?;
|
||||||
}
|
|
||||||
Some(stored)
|
|
||||||
} else {
|
|
||||||
user.banner_path().map(|s| s.to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
let moved_to = cmd.also_known_as.as_deref().and_then(|new_url| {
|
let moved_to = cmd.also_known_as.as_deref().and_then(|new_url| {
|
||||||
if user.also_known_as().map(|s| s != new_url).unwrap_or(true) {
|
if user.also_known_as().map(|s| s != new_url).unwrap_or(true) {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ pub async fn execute(
|
|||||||
|
|
||||||
let (movie, _is_new) = resolve_and_persist_movie(
|
let (movie, _is_new) = resolve_and_persist_movie(
|
||||||
&cmd.input,
|
&cmd.input,
|
||||||
deps.movie.as_ref(),
|
deps.movie_command.as_ref(),
|
||||||
|
deps.movie_query.as_ref(),
|
||||||
deps.metadata.as_ref(),
|
deps.metadata.as_ref(),
|
||||||
deps.event_publisher.as_ref(),
|
deps.event_publisher.as_ref(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use domain::ports::{EventPublisher, MetadataClient, MovieRepository, WatchlistRepository};
|
use domain::ports::{EventPublisher, MetadataClient, MovieCommand, MovieQuery, WatchlistRepository};
|
||||||
|
|
||||||
pub struct WatchlistAddDeps {
|
pub struct WatchlistAddDeps {
|
||||||
pub movie: Arc<dyn MovieRepository>,
|
pub movie_command: Arc<dyn MovieCommand>,
|
||||||
|
pub movie_query: Arc<dyn MovieQuery>,
|
||||||
pub metadata: Arc<dyn MetadataClient>,
|
pub metadata: Arc<dyn MetadataClient>,
|
||||||
pub watchlist: Arc<dyn WatchlistRepository>,
|
pub watchlist: Arc<dyn WatchlistRepository>,
|
||||||
pub event_publisher: Arc<dyn EventPublisher>,
|
pub event_publisher: Arc<dyn EventPublisher>,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
models::Movie,
|
models::Movie,
|
||||||
ports::MovieRepository,
|
ports::MovieCommand,
|
||||||
testing::{InMemoryMovieRepository, InMemoryWatchlistRepository, NoopEventPublisher},
|
testing::{InMemoryMovieRepository, InMemoryWatchlistRepository, NoopEventPublisher},
|
||||||
value_objects::{MovieTitle, ReleaseYear},
|
value_objects::{MovieTitle, ReleaseYear},
|
||||||
};
|
};
|
||||||
@@ -17,7 +17,8 @@ fn make_deps(
|
|||||||
watchlist: Arc<InMemoryWatchlistRepository>,
|
watchlist: Arc<InMemoryWatchlistRepository>,
|
||||||
) -> WatchlistAddDeps {
|
) -> WatchlistAddDeps {
|
||||||
WatchlistAddDeps {
|
WatchlistAddDeps {
|
||||||
movie: movies,
|
movie_command: Arc::clone(&movies) as _,
|
||||||
|
movie_query: movies,
|
||||||
metadata: Arc::new(domain::testing::FakeMetadataClient),
|
metadata: Arc::new(domain::testing::FakeMetadataClient),
|
||||||
watchlist,
|
watchlist,
|
||||||
event_publisher: NoopEventPublisher::new(),
|
event_publisher: NoopEventPublisher::new(),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ thiserror = { workspace = true }
|
|||||||
bytes = { workspace = true }
|
bytes = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
|
||||||
email_address = "0.2.9"
|
email_address = "0.2.9"
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ pub enum DomainEvent {
|
|||||||
},
|
},
|
||||||
FederationDeliveryRequested {
|
FederationDeliveryRequested {
|
||||||
inbox_url: String,
|
inbox_url: String,
|
||||||
activity_json: String,
|
activity_json: serde_json::Value,
|
||||||
signing_actor_id: uuid::Uuid,
|
signing_actor_id: uuid::Uuid,
|
||||||
},
|
},
|
||||||
WatchEventIngested {
|
WatchEventIngested {
|
||||||
|
|||||||
@@ -12,32 +12,38 @@ pub trait MediaServerParser: Send + Sync {
|
|||||||
-> Result<Option<ParsedPlaybackEvent>, DomainError>;
|
-> Result<Option<ParsedPlaybackEvent>, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write port — mutates watch events.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait WatchEventRepository: Send + Sync {
|
pub trait WatchEventCommand: Send + Sync {
|
||||||
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError>;
|
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError>;
|
||||||
async fn update_status(
|
async fn update_status(
|
||||||
&self,
|
&self,
|
||||||
id: &WatchEventId,
|
id: &WatchEventId,
|
||||||
status: WatchEventStatus,
|
status: WatchEventStatus,
|
||||||
) -> Result<(), DomainError>;
|
) -> 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(
|
async fn update_status_batch(
|
||||||
&self,
|
&self,
|
||||||
ids: &[WatchEventId],
|
ids: &[WatchEventId],
|
||||||
status: WatchEventStatus,
|
status: WatchEventStatus,
|
||||||
) -> Result<u64, DomainError>;
|
) -> 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(
|
async fn find_duplicate(
|
||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
external_id: &str,
|
external_id: &str,
|
||||||
after: NaiveDateTime,
|
after: NaiveDateTime,
|
||||||
) -> Result<bool, DomainError>;
|
) -> Result<bool, DomainError>;
|
||||||
async fn delete_non_pending_older_than(
|
|
||||||
&self,
|
|
||||||
before: NaiveDateTime,
|
|
||||||
) -> Result<u64, DomainError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -9,8 +9,16 @@ use crate::{
|
|||||||
value_objects::{ExternalMetadataId, MovieId, MovieTitle, PosterUrl, ReleaseYear},
|
value_objects::{ExternalMetadataId, MovieId, MovieTitle, PosterUrl, ReleaseYear},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Write port — mutates the movies table.
|
||||||
#[async_trait]
|
#[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(
|
async fn get_movie_by_external_id(
|
||||||
&self,
|
&self,
|
||||||
external_metadata_id: &ExternalMetadataId,
|
external_metadata_id: &ExternalMetadataId,
|
||||||
@@ -21,8 +29,6 @@ pub trait MovieRepository: Send + Sync {
|
|||||||
title: &MovieTitle,
|
title: &MovieTitle,
|
||||||
year: &ReleaseYear,
|
year: &ReleaseYear,
|
||||||
) -> Result<Vec<Movie>, DomainError>;
|
) -> 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(
|
async fn existing_external_ids(
|
||||||
&self,
|
&self,
|
||||||
ids: &[ExternalMetadataId],
|
ids: &[ExternalMetadataId],
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ use crate::{
|
|||||||
},
|
},
|
||||||
ports::{
|
ports::{
|
||||||
GoalRepository, ImportProfileRepository, ImportSessionRepository, MovieProfileRepository,
|
GoalRepository, ImportProfileRepository, ImportSessionRepository, MovieProfileRepository,
|
||||||
MovieRepository, RefreshSessionRepository, ReviewRepository, UserFederationSettingsQuery,
|
MovieCommand, MovieQuery, RefreshSessionRepository, ReviewRepository,
|
||||||
UserProfileFieldsRepository, UserRepository, UserSettingsRepository, WatchEventRepository,
|
UserFederationSettingsQuery, UserProfileFieldsRepository, UserRepository,
|
||||||
|
UserSettingsRepository, WatchEventCommand, WatchEventQuery,
|
||||||
WatchlistRepository, WebhookTokenRepository,
|
WatchlistRepository, WebhookTokenRepository,
|
||||||
},
|
},
|
||||||
value_objects::{
|
value_objects::{
|
||||||
@@ -47,7 +48,23 @@ impl InMemoryMovieRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[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(
|
async fn get_movie_by_external_id(
|
||||||
&self,
|
&self,
|
||||||
external_metadata_id: &ExternalMetadataId,
|
external_metadata_id: &ExternalMetadataId,
|
||||||
@@ -80,19 +97,6 @@ impl MovieRepository for InMemoryMovieRepository {
|
|||||||
.collect())
|
.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(
|
async fn existing_external_ids(
|
||||||
&self,
|
&self,
|
||||||
ids: &[ExternalMetadataId],
|
ids: &[ExternalMetadataId],
|
||||||
@@ -526,7 +530,7 @@ impl InMemoryWatchEventRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl WatchEventRepository for InMemoryWatchEventRepository {
|
impl WatchEventCommand for InMemoryWatchEventRepository {
|
||||||
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError> {
|
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError> {
|
||||||
self.store.lock().unwrap().push(event.clone());
|
self.store.lock().unwrap().push(event.clone());
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -540,6 +544,27 @@ impl WatchEventRepository for InMemoryWatchEventRepository {
|
|||||||
Ok(())
|
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> {
|
async fn list_pending(&self, user_id: &UserId) -> Result<Vec<WatchEvent>, DomainError> {
|
||||||
let store = self.store.lock().unwrap();
|
let store = self.store.lock().unwrap();
|
||||||
Ok(store
|
Ok(store
|
||||||
@@ -566,14 +591,6 @@ impl WatchEventRepository for InMemoryWatchEventRepository {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_status_batch(
|
|
||||||
&self,
|
|
||||||
ids: &[WatchEventId],
|
|
||||||
_status: WatchEventStatus,
|
|
||||||
) -> Result<u64, DomainError> {
|
|
||||||
Ok(ids.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_duplicate(
|
async fn find_duplicate(
|
||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
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 ─────────────────────────────────────────
|
// ── InMemoryImportSessionRepository ─────────────────────────────────────────
|
||||||
|
|||||||
@@ -371,44 +371,56 @@ impl crate::ports::FederatedProfileQuery for PanicFederatedProfileQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PanicWatchEventRepository;
|
pub struct PanicWatchEventCommand;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl crate::ports::WatchEventRepository for PanicWatchEventRepository {
|
impl crate::ports::WatchEventCommand for PanicWatchEventCommand {
|
||||||
async fn save(&self, _: &crate::models::WatchEvent) -> Result<(), DomainError> {
|
async fn save(&self, _: &crate::models::WatchEvent) -> Result<(), DomainError> {
|
||||||
panic!("PanicWatchEventRepository called")
|
panic!("PanicWatchEventCommand called")
|
||||||
}
|
}
|
||||||
async fn update_status(
|
async fn update_status(
|
||||||
&self,
|
&self,
|
||||||
_: &crate::value_objects::WatchEventId,
|
_: &crate::value_objects::WatchEventId,
|
||||||
_: crate::models::WatchEventStatus,
|
_: crate::models::WatchEventStatus,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
panic!("PanicWatchEventRepository called")
|
panic!("PanicWatchEventCommand 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")
|
|
||||||
}
|
}
|
||||||
async fn update_status_batch(
|
async fn update_status_batch(
|
||||||
&self,
|
&self,
|
||||||
_: &[crate::value_objects::WatchEventId],
|
_: &[crate::value_objects::WatchEventId],
|
||||||
_: crate::models::WatchEventStatus,
|
_: crate::models::WatchEventStatus,
|
||||||
) -> Result<u64, DomainError> {
|
) -> 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(
|
async fn find_duplicate(
|
||||||
&self,
|
&self,
|
||||||
@@ -416,13 +428,7 @@ impl crate::ports::WatchEventRepository for PanicWatchEventRepository {
|
|||||||
_: &str,
|
_: &str,
|
||||||
_: chrono::NaiveDateTime,
|
_: chrono::NaiveDateTime,
|
||||||
) -> Result<bool, DomainError> {
|
) -> Result<bool, DomainError> {
|
||||||
panic!("PanicWatchEventRepository called")
|
panic!("PanicWatchEventQuery called")
|
||||||
}
|
|
||||||
async fn delete_non_pending_older_than(
|
|
||||||
&self,
|
|
||||||
_: chrono::NaiveDateTime,
|
|
||||||
) -> Result<u64, DomainError> {
|
|
||||||
panic!("PanicWatchEventRepository called")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ use std::sync::Arc;
|
|||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher,
|
AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher,
|
||||||
FederatedProfileQuery, GoalRepository, ImportProfileRepository, ImportSessionRepository,
|
FederatedProfileQuery, GoalRepository, ImportProfileRepository, ImportSessionRepository,
|
||||||
MetadataClient, MovieProfileRepository, MovieRepository, ObjectStorage, PasswordHasher,
|
MetadataClient, MovieCommand, MovieProfileRepository, MovieQuery, ObjectStorage, PasswordHasher,
|
||||||
PersonCommand, PersonEnrichmentClient, PersonQuery, PosterFetcherClient,
|
PersonCommand, PersonEnrichmentClient, PersonQuery, PosterFetcherClient,
|
||||||
RefreshSessionRepository, RemoteGoalRepository, RemoteWatchlistRepository, ReviewRepository,
|
RefreshSessionRepository, RemoteGoalRepository, RemoteWatchlistRepository, ReviewRepository,
|
||||||
SearchCommand, SearchPort, SocialQueryPort, StatsRepository, UserProfileFieldsRepository,
|
SearchCommand, SearchPort, SocialQueryPort, StatsRepository, UserProfileFieldsRepository,
|
||||||
UserRepository, UserSettingsRepository, WatchEventRepository, WatchlistRepository,
|
UserRepository, UserSettingsRepository, WatchEventCommand, WatchEventQuery,
|
||||||
|
WatchlistRepository,
|
||||||
WebhookTokenRepository, WrapUpRepository, WrapUpStatsQuery,
|
WebhookTokenRepository, WrapUpRepository, WrapUpStatsQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ use application::ports::ReviewLogger;
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Repositories {
|
pub struct Repositories {
|
||||||
pub movie: Arc<dyn MovieRepository>,
|
pub movie_command: Arc<dyn MovieCommand>,
|
||||||
|
pub movie_query: Arc<dyn MovieQuery>,
|
||||||
pub review: Arc<dyn ReviewRepository>,
|
pub review: Arc<dyn ReviewRepository>,
|
||||||
pub diary: Arc<dyn DiaryRepository>,
|
pub diary: Arc<dyn DiaryRepository>,
|
||||||
pub stats: Arc<dyn StatsRepository>,
|
pub stats: Arc<dyn StatsRepository>,
|
||||||
@@ -25,7 +27,8 @@ pub struct Repositories {
|
|||||||
pub import_profile: Arc<dyn ImportProfileRepository>,
|
pub import_profile: Arc<dyn ImportProfileRepository>,
|
||||||
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
||||||
pub watchlist: Arc<dyn WatchlistRepository>,
|
pub watchlist: Arc<dyn WatchlistRepository>,
|
||||||
pub watch_event: Arc<dyn WatchEventRepository>,
|
pub watch_event_command: Arc<dyn WatchEventCommand>,
|
||||||
|
pub watch_event_query: Arc<dyn WatchEventQuery>,
|
||||||
pub webhook_token: Arc<dyn WebhookTokenRepository>,
|
pub webhook_token: Arc<dyn WebhookTokenRepository>,
|
||||||
pub person_command: Arc<dyn PersonCommand>,
|
pub person_command: Arc<dyn PersonCommand>,
|
||||||
pub person_query: Arc<dyn PersonQuery>,
|
pub person_query: Arc<dyn PersonQuery>,
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ use anyhow::Context;
|
|||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
AuthService, LocalApContentQuery, MetadataClient, ObjectStorage, PasswordHasher,
|
AuthService, LocalApContentQuery, MetadataClient, ObjectStorage, PasswordHasher,
|
||||||
PosterFetcherClient, RefreshSessionRepository, UserProfileFieldsRepository,
|
PosterFetcherClient, RefreshSessionRepository, UserProfileFieldsRepository,
|
||||||
WatchEventRepository, WebhookTokenRepository,
|
WatchEventCommand, WatchEventQuery, WebhookTokenRepository,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use infra_wiring::DbPool;
|
pub use infra_wiring::DbPool;
|
||||||
|
|
||||||
pub struct DatabaseOutput {
|
pub struct DatabaseOutput {
|
||||||
pub movie: Arc<dyn domain::ports::MovieRepository>,
|
pub movie_command: Arc<dyn domain::ports::MovieCommand>,
|
||||||
|
pub movie_query: Arc<dyn domain::ports::MovieQuery>,
|
||||||
pub review: Arc<dyn domain::ports::ReviewRepository>,
|
pub review: Arc<dyn domain::ports::ReviewRepository>,
|
||||||
pub diary: Arc<dyn domain::ports::DiaryRepository>,
|
pub diary: Arc<dyn domain::ports::DiaryRepository>,
|
||||||
pub stats: Arc<dyn domain::ports::StatsRepository>,
|
pub stats: Arc<dyn domain::ports::StatsRepository>,
|
||||||
@@ -19,7 +20,8 @@ pub struct DatabaseOutput {
|
|||||||
pub import_profile: Arc<dyn domain::ports::ImportProfileRepository>,
|
pub import_profile: Arc<dyn domain::ports::ImportProfileRepository>,
|
||||||
pub movie_profile: Arc<dyn domain::ports::MovieProfileRepository>,
|
pub movie_profile: Arc<dyn domain::ports::MovieProfileRepository>,
|
||||||
pub watchlist: Arc<dyn domain::ports::WatchlistRepository>,
|
pub watchlist: Arc<dyn domain::ports::WatchlistRepository>,
|
||||||
pub watch_event: Arc<dyn WatchEventRepository>,
|
pub watch_event_command: Arc<dyn WatchEventCommand>,
|
||||||
|
pub watch_event_query: Arc<dyn WatchEventQuery>,
|
||||||
pub webhook_token: Arc<dyn WebhookTokenRepository>,
|
pub webhook_token: Arc<dyn WebhookTokenRepository>,
|
||||||
pub person_command: Arc<dyn domain::ports::PersonCommand>,
|
pub person_command: Arc<dyn domain::ports::PersonCommand>,
|
||||||
pub person_query: Arc<dyn domain::ports::PersonQuery>,
|
pub person_query: Arc<dyn domain::ports::PersonQuery>,
|
||||||
@@ -47,13 +49,13 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result
|
|||||||
let (pc, pq) = postgres::create_person_adapter(w.pool.clone());
|
let (pc, pq) = postgres::create_person_adapter(w.pool.clone());
|
||||||
let (sc, sp) = postgres_search::create_search_adapter(w.pool.clone());
|
let (sc, sp) = postgres_search::create_search_adapter(w.pool.clone());
|
||||||
let pf = postgres::create_profile_fields_repo(w.pool.clone());
|
let pf = postgres::create_profile_fields_repo(w.pool.clone());
|
||||||
let we: Arc<dyn WatchEventRepository> =
|
let we = Arc::new(postgres::PostgresWatchEventRepository::new(w.pool.clone()));
|
||||||
Arc::new(postgres::PostgresWatchEventRepository::new(w.pool.clone()));
|
|
||||||
let wt: Arc<dyn WebhookTokenRepository> = Arc::new(
|
let wt: Arc<dyn WebhookTokenRepository> = Arc::new(
|
||||||
postgres::PostgresWebhookTokenRepository::new(w.pool.clone()),
|
postgres::PostgresWebhookTokenRepository::new(w.pool.clone()),
|
||||||
);
|
);
|
||||||
Ok(DatabaseOutput {
|
Ok(DatabaseOutput {
|
||||||
movie: w.movie,
|
movie_command: w.movie_command,
|
||||||
|
movie_query: w.movie_query,
|
||||||
review: w.review,
|
review: w.review,
|
||||||
diary: w.diary,
|
diary: w.diary,
|
||||||
stats: w.stats,
|
stats: w.stats,
|
||||||
@@ -62,7 +64,8 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result
|
|||||||
import_profile: w.import_profile,
|
import_profile: w.import_profile,
|
||||||
movie_profile: w.movie_profile,
|
movie_profile: w.movie_profile,
|
||||||
watchlist: w.watchlist,
|
watchlist: w.watchlist,
|
||||||
watch_event: we,
|
watch_event_command: we.clone() as _,
|
||||||
|
watch_event_query: we as _,
|
||||||
webhook_token: wt,
|
webhook_token: wt,
|
||||||
person_command: pc,
|
person_command: pc,
|
||||||
person_query: pq,
|
person_query: pq,
|
||||||
@@ -90,12 +93,12 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result
|
|||||||
let (pc, pq) = sqlite::create_person_adapter(w.pool.clone());
|
let (pc, pq) = sqlite::create_person_adapter(w.pool.clone());
|
||||||
let (sc, sp) = sqlite_search::create_search_adapter(w.pool.clone());
|
let (sc, sp) = sqlite_search::create_search_adapter(w.pool.clone());
|
||||||
let pf = sqlite::create_profile_fields_repo(w.pool.clone());
|
let pf = sqlite::create_profile_fields_repo(w.pool.clone());
|
||||||
let we: Arc<dyn WatchEventRepository> =
|
let we = Arc::new(sqlite::SqliteWatchEventRepository::new(w.pool.clone()));
|
||||||
Arc::new(sqlite::SqliteWatchEventRepository::new(w.pool.clone()));
|
|
||||||
let wt: Arc<dyn WebhookTokenRepository> =
|
let wt: Arc<dyn WebhookTokenRepository> =
|
||||||
Arc::new(sqlite::SqliteWebhookTokenRepository::new(w.pool.clone()));
|
Arc::new(sqlite::SqliteWebhookTokenRepository::new(w.pool.clone()));
|
||||||
Ok(DatabaseOutput {
|
Ok(DatabaseOutput {
|
||||||
movie: w.movie,
|
movie_command: w.movie_command,
|
||||||
|
movie_query: w.movie_query,
|
||||||
review: w.review,
|
review: w.review,
|
||||||
diary: w.diary,
|
diary: w.diary,
|
||||||
stats: w.stats,
|
stats: w.stats,
|
||||||
@@ -104,7 +107,8 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result
|
|||||||
import_profile: w.import_profile,
|
import_profile: w.import_profile,
|
||||||
movie_profile: w.movie_profile,
|
movie_profile: w.movie_profile,
|
||||||
watchlist: w.watchlist,
|
watchlist: w.watchlist,
|
||||||
watch_event: we,
|
watch_event_command: we.clone() as _,
|
||||||
|
watch_event_query: we as _,
|
||||||
webhook_token: wt,
|
webhook_token: wt,
|
||||||
person_command: pc,
|
person_command: pc,
|
||||||
person_query: pq,
|
person_query: pq,
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ pub async fn delete_review(
|
|||||||
let deps = DeleteReviewDeps {
|
let deps = DeleteReviewDeps {
|
||||||
review: state.app_ctx.repos.review.clone(),
|
review: state.app_ctx.repos.review.clone(),
|
||||||
diary: state.app_ctx.repos.diary.clone(),
|
diary: state.app_ctx.repos.diary.clone(),
|
||||||
movie: state.app_ctx.repos.movie.clone(),
|
movie_command: state.app_ctx.repos.movie_command.clone(),
|
||||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||||
};
|
};
|
||||||
delete_review::execute(&deps, cmd).await?;
|
delete_review::execute(&deps, cmd).await?;
|
||||||
@@ -277,7 +277,7 @@ pub async fn post_delete_review_html(
|
|||||||
let deps = DeleteReviewDeps {
|
let deps = DeleteReviewDeps {
|
||||||
review: state.app_ctx.repos.review.clone(),
|
review: state.app_ctx.repos.review.clone(),
|
||||||
diary: state.app_ctx.repos.diary.clone(),
|
diary: state.app_ctx.repos.diary.clone(),
|
||||||
movie: state.app_ctx.repos.movie.clone(),
|
movie_command: state.app_ctx.repos.movie_command.clone(),
|
||||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||||
};
|
};
|
||||||
match delete_review::execute(&deps, cmd).await {
|
match delete_review::execute(&deps, cmd).await {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use api_types::{
|
|||||||
CreateGoalRequest, GoalDto, GoalsResponse, UpdateGoalRequest, UpdateUserSettingsRequest,
|
CreateGoalRequest, GoalDto, GoalsResponse, UpdateGoalRequest, UpdateUserSettingsRequest,
|
||||||
UserSettingsDto,
|
UserSettingsDto,
|
||||||
};
|
};
|
||||||
|
use application::goals::deps::{GoalCommandDeps, GoalQueryDeps};
|
||||||
|
|
||||||
// ── Shared mapper ────────────────────────────────────────────────────────────
|
// ── Shared mapper ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -38,9 +39,12 @@ pub async fn list_goals(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
user: AuthenticatedUser,
|
user: AuthenticatedUser,
|
||||||
) -> Result<Json<GoalsResponse>, ApiError> {
|
) -> Result<Json<GoalsResponse>, ApiError> {
|
||||||
|
let deps = GoalQueryDeps {
|
||||||
|
goal: state.app_ctx.repos.goal.clone(),
|
||||||
|
stats: state.app_ctx.repos.stats.clone(),
|
||||||
|
};
|
||||||
let goals = application::goals::list::execute(
|
let goals = application::goals::list::execute(
|
||||||
state.app_ctx.repos.goal.clone(),
|
&deps,
|
||||||
state.app_ctx.repos.stats.clone(),
|
|
||||||
application::goals::queries::ListGoalsQuery {
|
application::goals::queries::ListGoalsQuery {
|
||||||
user_id: user.0.value(),
|
user_id: user.0.value(),
|
||||||
},
|
},
|
||||||
@@ -65,10 +69,13 @@ pub async fn create_goal(
|
|||||||
user: AuthenticatedUser,
|
user: AuthenticatedUser,
|
||||||
Json(req): Json<CreateGoalRequest>,
|
Json(req): Json<CreateGoalRequest>,
|
||||||
) -> Result<Json<GoalDto>, ApiError> {
|
) -> Result<Json<GoalDto>, ApiError> {
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: state.app_ctx.repos.goal.clone(),
|
||||||
|
stats: state.app_ctx.repos.stats.clone(),
|
||||||
|
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||||
|
};
|
||||||
let g = application::goals::create::execute(
|
let g = application::goals::create::execute(
|
||||||
state.app_ctx.repos.goal.clone(),
|
&deps,
|
||||||
state.app_ctx.repos.stats.clone(),
|
|
||||||
state.app_ctx.services.event_publisher.clone(),
|
|
||||||
application::goals::commands::CreateGoalCommand {
|
application::goals::commands::CreateGoalCommand {
|
||||||
user_id: user.0.value(),
|
user_id: user.0.value(),
|
||||||
year: req.year,
|
year: req.year,
|
||||||
@@ -95,10 +102,13 @@ pub async fn update_goal(
|
|||||||
Path(year): Path<u16>,
|
Path(year): Path<u16>,
|
||||||
Json(req): Json<UpdateGoalRequest>,
|
Json(req): Json<UpdateGoalRequest>,
|
||||||
) -> Result<Json<GoalDto>, ApiError> {
|
) -> Result<Json<GoalDto>, ApiError> {
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: state.app_ctx.repos.goal.clone(),
|
||||||
|
stats: state.app_ctx.repos.stats.clone(),
|
||||||
|
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||||
|
};
|
||||||
let g = application::goals::update::execute(
|
let g = application::goals::update::execute(
|
||||||
state.app_ctx.repos.goal.clone(),
|
&deps,
|
||||||
state.app_ctx.repos.stats.clone(),
|
|
||||||
state.app_ctx.services.event_publisher.clone(),
|
|
||||||
application::goals::commands::UpdateGoalCommand {
|
application::goals::commands::UpdateGoalCommand {
|
||||||
user_id: user.0.value(),
|
user_id: user.0.value(),
|
||||||
year,
|
year,
|
||||||
@@ -123,9 +133,13 @@ pub async fn delete_goal(
|
|||||||
user: AuthenticatedUser,
|
user: AuthenticatedUser,
|
||||||
Path(year): Path<u16>,
|
Path(year): Path<u16>,
|
||||||
) -> Result<StatusCode, ApiError> {
|
) -> Result<StatusCode, ApiError> {
|
||||||
|
let deps = GoalCommandDeps {
|
||||||
|
goal: state.app_ctx.repos.goal.clone(),
|
||||||
|
stats: state.app_ctx.repos.stats.clone(),
|
||||||
|
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||||
|
};
|
||||||
application::goals::delete::execute(
|
application::goals::delete::execute(
|
||||||
state.app_ctx.repos.goal.clone(),
|
&deps,
|
||||||
state.app_ctx.services.event_publisher.clone(),
|
|
||||||
application::goals::commands::DeleteGoalCommand {
|
application::goals::commands::DeleteGoalCommand {
|
||||||
user_id: user.0.value(),
|
user_id: user.0.value(),
|
||||||
year,
|
year,
|
||||||
@@ -148,9 +162,12 @@ pub async fn get_user_goals(
|
|||||||
AuthenticatedUser(_viewer): AuthenticatedUser,
|
AuthenticatedUser(_viewer): AuthenticatedUser,
|
||||||
Path(user_id): Path<Uuid>,
|
Path(user_id): Path<Uuid>,
|
||||||
) -> Result<Json<GoalsResponse>, ApiError> {
|
) -> Result<Json<GoalsResponse>, ApiError> {
|
||||||
|
let deps = GoalQueryDeps {
|
||||||
|
goal: state.app_ctx.repos.goal.clone(),
|
||||||
|
stats: state.app_ctx.repos.stats.clone(),
|
||||||
|
};
|
||||||
let goals = application::goals::list::execute(
|
let goals = application::goals::list::execute(
|
||||||
state.app_ctx.repos.goal.clone(),
|
&deps,
|
||||||
state.app_ctx.repos.stats.clone(),
|
|
||||||
application::goals::queries::ListGoalsQuery { user_id },
|
application::goals::queries::ListGoalsQuery { user_id },
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use application::import::{
|
|||||||
DeleteImportProfileCommand, ExecuteImportCommand, SaveImportProfileCommand,
|
DeleteImportProfileCommand, ExecuteImportCommand, SaveImportProfileCommand,
|
||||||
},
|
},
|
||||||
create_session as create_import_session, delete_profile as delete_import_profile,
|
create_session as create_import_session, delete_profile as delete_import_profile,
|
||||||
|
deps::{ApplyMappingDeps, ApplyProfileDeps, CreateSessionDeps, ExecuteImportDeps, SaveProfileDeps},
|
||||||
execute as execute_import, list_profiles as list_import_profiles,
|
execute as execute_import, list_profiles as list_import_profiles,
|
||||||
save_profile as save_import_profile,
|
save_profile as save_import_profile,
|
||||||
};
|
};
|
||||||
@@ -160,8 +161,10 @@ pub async fn post_upload(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match create_import_session::execute(
|
match create_import_session::execute(
|
||||||
state.app_ctx.repos.import_session.clone(),
|
&CreateSessionDeps {
|
||||||
state.app_ctx.services.document_parser.clone(),
|
import_session: state.app_ctx.repos.import_session.clone(),
|
||||||
|
document_parser: state.app_ctx.services.document_parser.clone(),
|
||||||
|
},
|
||||||
CreateImportSessionCommand {
|
CreateImportSessionCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
bytes,
|
bytes,
|
||||||
@@ -250,9 +253,11 @@ pub async fn post_mapping(
|
|||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
match apply_import_mapping::execute(
|
match apply_import_mapping::execute(
|
||||||
state.app_ctx.repos.import_session.clone(),
|
&ApplyMappingDeps {
|
||||||
state.app_ctx.services.document_parser.clone(),
|
import_session: state.app_ctx.repos.import_session.clone(),
|
||||||
state.app_ctx.repos.movie.clone(),
|
document_parser: state.app_ctx.services.document_parser.clone(),
|
||||||
|
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||||
|
},
|
||||||
ApplyImportMappingCommand {
|
ApplyImportMappingCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
session_id: session_id.value(),
|
session_id: session_id.value(),
|
||||||
@@ -346,8 +351,10 @@ pub async fn post_confirm(
|
|||||||
.filter(|n| !n.trim().is_empty());
|
.filter(|n| !n.trim().is_empty());
|
||||||
if let Some(name) = profile_name {
|
if let Some(name) = profile_name {
|
||||||
let _ = save_import_profile::execute(
|
let _ = save_import_profile::execute(
|
||||||
state.app_ctx.repos.import_session.clone(),
|
&SaveProfileDeps {
|
||||||
state.app_ctx.repos.import_profile.clone(),
|
import_session: state.app_ctx.repos.import_session.clone(),
|
||||||
|
import_profile: state.app_ctx.repos.import_profile.clone(),
|
||||||
|
},
|
||||||
SaveImportProfileCommand {
|
SaveImportProfileCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
session_id: session_id.value(),
|
session_id: session_id.value(),
|
||||||
@@ -365,8 +372,10 @@ pub async fn post_confirm(
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
match execute_import::execute(
|
match execute_import::execute(
|
||||||
state.app_ctx.repos.import_session.clone(),
|
&ExecuteImportDeps {
|
||||||
state.app_ctx.services.review_logger.clone(),
|
import_session: state.app_ctx.repos.import_session.clone(),
|
||||||
|
review_logger: state.app_ctx.services.review_logger.clone(),
|
||||||
|
},
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
session_id: session_id.value(),
|
session_id: session_id.value(),
|
||||||
@@ -487,8 +496,10 @@ pub async fn api_post_session(
|
|||||||
_ => FileFormat::Csv,
|
_ => FileFormat::Csv,
|
||||||
};
|
};
|
||||||
let r = create_import_session::execute(
|
let r = create_import_session::execute(
|
||||||
state.app_ctx.repos.import_session.clone(),
|
&CreateSessionDeps {
|
||||||
state.app_ctx.services.document_parser.clone(),
|
import_session: state.app_ctx.repos.import_session.clone(),
|
||||||
|
document_parser: state.app_ctx.services.document_parser.clone(),
|
||||||
|
},
|
||||||
CreateImportSessionCommand {
|
CreateImportSessionCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
bytes,
|
bytes,
|
||||||
@@ -583,9 +594,11 @@ pub async fn api_put_mapping(
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let rows = apply_import_mapping::execute(
|
let rows = apply_import_mapping::execute(
|
||||||
state.app_ctx.repos.import_session.clone(),
|
&ApplyMappingDeps {
|
||||||
state.app_ctx.services.document_parser.clone(),
|
import_session: state.app_ctx.repos.import_session.clone(),
|
||||||
state.app_ctx.repos.movie.clone(),
|
document_parser: state.app_ctx.services.document_parser.clone(),
|
||||||
|
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||||
|
},
|
||||||
ApplyImportMappingCommand {
|
ApplyImportMappingCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
session_id: session_id.value(),
|
session_id: session_id.value(),
|
||||||
@@ -674,8 +687,10 @@ pub async fn api_post_confirm(
|
|||||||
.map(ImportSessionId::from_uuid)
|
.map(ImportSessionId::from_uuid)
|
||||||
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
|
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
|
||||||
let s = execute_import::execute(
|
let s = execute_import::execute(
|
||||||
state.app_ctx.repos.import_session.clone(),
|
&ExecuteImportDeps {
|
||||||
state.app_ctx.services.review_logger.clone(),
|
import_session: state.app_ctx.repos.import_session.clone(),
|
||||||
|
review_logger: state.app_ctx.services.review_logger.clone(),
|
||||||
|
},
|
||||||
ExecuteImportCommand {
|
ExecuteImportCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
session_id: session_id.value(),
|
session_id: session_id.value(),
|
||||||
@@ -739,8 +754,10 @@ pub async fn api_post_profile(
|
|||||||
.map(ImportSessionId::from_uuid)
|
.map(ImportSessionId::from_uuid)
|
||||||
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
|
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
|
||||||
let id = save_import_profile::execute(
|
let id = save_import_profile::execute(
|
||||||
state.app_ctx.repos.import_session.clone(),
|
&SaveProfileDeps {
|
||||||
state.app_ctx.repos.import_profile.clone(),
|
import_session: state.app_ctx.repos.import_session.clone(),
|
||||||
|
import_profile: state.app_ctx.repos.import_profile.clone(),
|
||||||
|
},
|
||||||
SaveImportProfileCommand {
|
SaveImportProfileCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
session_id: session_id.value(),
|
session_id: session_id.value(),
|
||||||
@@ -810,8 +827,10 @@ pub async fn api_apply_profile(
|
|||||||
.map_err(|_| DomainError::ValidationError("invalid profile id".into()))?;
|
.map_err(|_| DomainError::ValidationError("invalid profile id".into()))?;
|
||||||
|
|
||||||
apply_import_profile::execute(
|
apply_import_profile::execute(
|
||||||
state.app_ctx.repos.import_profile.clone(),
|
&ApplyProfileDeps {
|
||||||
state.app_ctx.repos.import_session.clone(),
|
import_profile: state.app_ctx.repos.import_profile.clone(),
|
||||||
|
import_session: state.app_ctx.repos.import_session.clone(),
|
||||||
|
},
|
||||||
ApplyImportProfileCommand {
|
ApplyImportProfileCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
session_id,
|
session_id,
|
||||||
@@ -832,9 +851,11 @@ pub async fn api_apply_profile(
|
|||||||
|
|
||||||
let mappings = session.field_mappings.unwrap_or_default();
|
let mappings = session.field_mappings.unwrap_or_default();
|
||||||
let rows = apply_import_mapping::execute(
|
let rows = apply_import_mapping::execute(
|
||||||
state.app_ctx.repos.import_session.clone(),
|
&ApplyMappingDeps {
|
||||||
state.app_ctx.services.document_parser.clone(),
|
import_session: state.app_ctx.repos.import_session.clone(),
|
||||||
state.app_ctx.repos.movie.clone(),
|
document_parser: state.app_ctx.services.document_parser.clone(),
|
||||||
|
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||||
|
},
|
||||||
ApplyImportMappingCommand {
|
ApplyImportMappingCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
session_id,
|
session_id,
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ pub async fn get_watch_queue_page(
|
|||||||
let query = GetWatchQueueQuery {
|
let query = GetWatchQueueQuery {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
};
|
};
|
||||||
let events = get_watch_queue::execute(state.app_ctx.repos.watch_event.clone(), query)
|
let events = get_watch_queue::execute(state.app_ctx.repos.watch_event_query.clone(), query)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
@@ -173,7 +173,8 @@ pub async fn post_confirm_single(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match confirm_watch_events::execute(
|
match confirm_watch_events::execute(
|
||||||
state.app_ctx.repos.watch_event.clone(),
|
state.app_ctx.repos.watch_event_command.clone(),
|
||||||
|
state.app_ctx.repos.watch_event_query.clone(),
|
||||||
state.app_ctx.services.review_logger.clone(),
|
state.app_ctx.services.review_logger.clone(),
|
||||||
cmd,
|
cmd,
|
||||||
)
|
)
|
||||||
@@ -203,7 +204,7 @@ pub async fn post_dismiss_single(
|
|||||||
event_ids: vec![event_id],
|
event_ids: vec![event_id],
|
||||||
};
|
};
|
||||||
|
|
||||||
match dismiss_watch_events::execute(state.app_ctx.repos.watch_event.clone(), cmd).await {
|
match dismiss_watch_events::execute(state.app_ctx.repos.watch_event_command.clone(), state.app_ctx.repos.watch_event_query.clone(), cmd).await {
|
||||||
Ok(_) => Redirect::to("/watch-queue").into_response(),
|
Ok(_) => Redirect::to("/watch-queue").into_response(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let msg = encode_error(&e.to_string());
|
let msg = encode_error(&e.to_string());
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ 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.repos.movie.clone(),
|
state.app_ctx.repos.movie_query.clone(),
|
||||||
GetMoviesQuery {
|
GetMoviesQuery {
|
||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
offset: params.offset,
|
offset: params.offset,
|
||||||
@@ -122,7 +122,8 @@ pub async fn sync_poster(
|
|||||||
) -> Result<impl IntoResponse, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
sync_poster::execute(
|
sync_poster::execute(
|
||||||
&SyncPosterDeps {
|
&SyncPosterDeps {
|
||||||
movie: state.app_ctx.repos.movie.clone(),
|
movie_command: state.app_ctx.repos.movie_command.clone(),
|
||||||
|
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||||
movie_profile: state.app_ctx.repos.movie_profile.clone(),
|
movie_profile: state.app_ctx.repos.movie_profile.clone(),
|
||||||
metadata: state.app_ctx.services.metadata.clone(),
|
metadata: state.app_ctx.services.metadata.clone(),
|
||||||
poster_fetcher: state.app_ctx.services.poster_fetcher.clone(),
|
poster_fetcher: state.app_ctx.services.poster_fetcher.clone(),
|
||||||
@@ -154,7 +155,7 @@ pub async fn get_movie_detail(
|
|||||||
|
|
||||||
let result = get_movie_social_page::execute(
|
let result = get_movie_social_page::execute(
|
||||||
&GetMovieSocialPageDeps {
|
&GetMovieSocialPageDeps {
|
||||||
movie: state.app_ctx.repos.movie.clone(),
|
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||||
diary: state.app_ctx.repos.diary.clone(),
|
diary: state.app_ctx.repos.diary.clone(),
|
||||||
movie_profile: state.app_ctx.repos.movie_profile.clone(),
|
movie_profile: state.app_ctx.repos.movie_profile.clone(),
|
||||||
},
|
},
|
||||||
@@ -288,7 +289,7 @@ pub async fn get_movie_detail_html(
|
|||||||
|
|
||||||
match get_movie_social_page::execute(
|
match get_movie_social_page::execute(
|
||||||
&GetMovieSocialPageDeps {
|
&GetMovieSocialPageDeps {
|
||||||
movie: state.app_ctx.repos.movie.clone(),
|
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||||
diary: state.app_ctx.repos.diary.clone(),
|
diary: state.app_ctx.repos.diary.clone(),
|
||||||
movie_profile: state.app_ctx.repos.movie_profile.clone(),
|
movie_profile: state.app_ctx.repos.movie_profile.clone(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -269,54 +269,62 @@ pub async fn get_user_profile(
|
|||||||
Err(e) => return crate::errors::domain_error_response(e),
|
Err(e) => return crate::errors::domain_error_response(e),
|
||||||
};
|
};
|
||||||
|
|
||||||
let entries = profile.entries.map(|p| DiaryResponse {
|
let view_data = if let Some(p) = profile.entries {
|
||||||
items: p
|
Some(api_types::ProfileViewData::Entries {
|
||||||
.items
|
entries: DiaryResponse {
|
||||||
.iter()
|
items: p
|
||||||
.map(crate::mappers::movies::entry_to_dto)
|
.items
|
||||||
.collect(),
|
|
||||||
total_count: p.total_count,
|
|
||||||
limit: p.limit,
|
|
||||||
offset: p.offset,
|
|
||||||
});
|
|
||||||
|
|
||||||
let history = profile.history.map(|entries| {
|
|
||||||
application::users::group_by_month(entries)
|
|
||||||
.into_iter()
|
|
||||||
.map(|m| MonthActivityDto {
|
|
||||||
year_month: m.year_month,
|
|
||||||
month_label: m.month_label,
|
|
||||||
count: m.count,
|
|
||||||
entries: m
|
|
||||||
.entries
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(crate::mappers::movies::entry_to_dto)
|
.map(crate::mappers::movies::entry_to_dto)
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
total_count: p.total_count,
|
||||||
.collect()
|
limit: p.limit,
|
||||||
});
|
offset: p.offset,
|
||||||
|
},
|
||||||
let trends = profile.trends.map(|t| UserTrendsDto {
|
})
|
||||||
monthly_ratings: t
|
} else if let Some(h) = profile.history {
|
||||||
.monthly_ratings
|
Some(api_types::ProfileViewData::History {
|
||||||
.into_iter()
|
history: application::users::group_by_month(h)
|
||||||
.map(|r| MonthlyRatingDto {
|
.into_iter()
|
||||||
year_month: r.year_month,
|
.map(|m| MonthActivityDto {
|
||||||
month_label: r.month_label,
|
year_month: m.year_month,
|
||||||
avg_rating: r.avg_rating,
|
month_label: m.month_label,
|
||||||
count: r.count,
|
count: m.count,
|
||||||
})
|
entries: m
|
||||||
.collect(),
|
.entries
|
||||||
top_directors: t
|
.iter()
|
||||||
.top_directors
|
.map(crate::mappers::movies::entry_to_dto)
|
||||||
.into_iter()
|
.collect(),
|
||||||
.map(|d| DirectorStatDto {
|
})
|
||||||
director: d.director,
|
.collect(),
|
||||||
count: d.count,
|
})
|
||||||
})
|
} else if let Some(t) = profile.trends {
|
||||||
.collect(),
|
Some(api_types::ProfileViewData::Trends {
|
||||||
max_director_count: t.max_director_count,
|
trends: UserTrendsDto {
|
||||||
});
|
monthly_ratings: t
|
||||||
|
.monthly_ratings
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| MonthlyRatingDto {
|
||||||
|
year_month: r.year_month,
|
||||||
|
month_label: r.month_label,
|
||||||
|
avg_rating: r.avg_rating,
|
||||||
|
count: r.count,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
top_directors: t
|
||||||
|
.top_directors
|
||||||
|
.into_iter()
|
||||||
|
.map(|d| DirectorStatDto {
|
||||||
|
director: d.director,
|
||||||
|
count: d.count,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
max_director_count: t.max_director_count,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Json(UserProfileResponse {
|
Json(UserProfileResponse {
|
||||||
user_id,
|
user_id,
|
||||||
@@ -339,13 +347,13 @@ pub async fn get_user_profile(
|
|||||||
},
|
},
|
||||||
following_count: profile.following_count,
|
following_count: profile.following_count,
|
||||||
followers_count: profile.followers_count,
|
followers_count: profile.followers_count,
|
||||||
entries,
|
view_data,
|
||||||
history,
|
|
||||||
trends,
|
|
||||||
goals: {
|
goals: {
|
||||||
let goals_list = application::goals::list::execute(
|
let goals_list = application::goals::list::execute(
|
||||||
state.app_ctx.repos.goal.clone(),
|
&application::goals::deps::GoalQueryDeps {
|
||||||
state.app_ctx.repos.stats.clone(),
|
goal: state.app_ctx.repos.goal.clone(),
|
||||||
|
stats: state.app_ctx.repos.stats.clone(),
|
||||||
|
},
|
||||||
application::goals::queries::ListGoalsQuery { user_id },
|
application::goals::queries::ListGoalsQuery { user_id },
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -394,38 +402,46 @@ async fn build_federated_profile_response(
|
|||||||
Err(e) => return crate::errors::domain_error_response(e),
|
Err(e) => return crate::errors::domain_error_response(e),
|
||||||
};
|
};
|
||||||
|
|
||||||
let entries = profile.entries.map(|p| DiaryResponse {
|
let view_data = if let Some(p) = profile.entries {
|
||||||
items: p
|
Some(api_types::ProfileViewData::Entries {
|
||||||
.items
|
entries: DiaryResponse {
|
||||||
.iter()
|
items: p
|
||||||
.map(crate::mappers::movies::entry_to_dto)
|
.items
|
||||||
.collect(),
|
.iter()
|
||||||
total_count: p.total_count,
|
.map(crate::mappers::movies::entry_to_dto)
|
||||||
limit: p.limit,
|
.collect(),
|
||||||
offset: p.offset,
|
total_count: p.total_count,
|
||||||
});
|
limit: p.limit,
|
||||||
|
offset: p.offset,
|
||||||
let trends = profile.trends.map(|t| UserTrendsDto {
|
},
|
||||||
monthly_ratings: t
|
})
|
||||||
.monthly_ratings
|
} else if let Some(t) = profile.trends {
|
||||||
.into_iter()
|
Some(api_types::ProfileViewData::Trends {
|
||||||
.map(|r| MonthlyRatingDto {
|
trends: UserTrendsDto {
|
||||||
year_month: r.year_month,
|
monthly_ratings: t
|
||||||
month_label: r.month_label,
|
.monthly_ratings
|
||||||
avg_rating: r.avg_rating,
|
.into_iter()
|
||||||
count: r.count,
|
.map(|r| MonthlyRatingDto {
|
||||||
})
|
year_month: r.year_month,
|
||||||
.collect(),
|
month_label: r.month_label,
|
||||||
top_directors: t
|
avg_rating: r.avg_rating,
|
||||||
.top_directors
|
count: r.count,
|
||||||
.into_iter()
|
})
|
||||||
.map(|d| DirectorStatDto {
|
.collect(),
|
||||||
director: d.director,
|
top_directors: t
|
||||||
count: d.count,
|
.top_directors
|
||||||
})
|
.into_iter()
|
||||||
.collect(),
|
.map(|d| DirectorStatDto {
|
||||||
max_director_count: t.max_director_count,
|
director: d.director,
|
||||||
});
|
count: d.count,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
max_director_count: t.max_director_count,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let username = fed
|
let username = fed
|
||||||
.display_name
|
.display_name
|
||||||
@@ -449,9 +465,7 @@ async fn build_federated_profile_response(
|
|||||||
},
|
},
|
||||||
following_count: 0,
|
following_count: 0,
|
||||||
followers_count: 0,
|
followers_count: 0,
|
||||||
entries,
|
view_data,
|
||||||
history: None,
|
|
||||||
trends,
|
|
||||||
goals: None,
|
goals: None,
|
||||||
is_federated: true,
|
is_federated: true,
|
||||||
handle: Some(fed.handle),
|
handle: Some(fed.handle),
|
||||||
@@ -735,8 +749,10 @@ pub async fn get_user_profile_html(
|
|||||||
search: params.search.clone(),
|
search: params.search.clone(),
|
||||||
goals: {
|
goals: {
|
||||||
let goals_list = application::goals::list::execute(
|
let goals_list = application::goals::list::execute(
|
||||||
state.app_ctx.repos.goal.clone(),
|
&application::goals::deps::GoalQueryDeps {
|
||||||
state.app_ctx.repos.stats.clone(),
|
goal: state.app_ctx.repos.goal.clone(),
|
||||||
|
stats: state.app_ctx.repos.stats.clone(),
|
||||||
|
},
|
||||||
application::goals::queries::ListGoalsQuery {
|
application::goals::queries::ListGoalsQuery {
|
||||||
user_id: profile_user_uuid,
|
user_id: profile_user_uuid,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ pub async fn post_watchlist_add(
|
|||||||
Json(req): Json<AddToWatchlistRequest>,
|
Json(req): Json<AddToWatchlistRequest>,
|
||||||
) -> Result<impl IntoResponse, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let deps = WatchlistAddDeps {
|
let deps = WatchlistAddDeps {
|
||||||
movie: state.app_ctx.repos.movie.clone(),
|
movie_command: state.app_ctx.repos.movie_command.clone(),
|
||||||
|
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||||
metadata: state.app_ctx.services.metadata.clone(),
|
metadata: state.app_ctx.services.metadata.clone(),
|
||||||
watchlist: state.app_ctx.repos.watchlist.clone(),
|
watchlist: state.app_ctx.repos.watchlist.clone(),
|
||||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||||
@@ -280,7 +281,8 @@ pub async fn post_watchlist_add_html(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let deps = WatchlistAddDeps {
|
let deps = WatchlistAddDeps {
|
||||||
movie: state.app_ctx.repos.movie.clone(),
|
movie_command: state.app_ctx.repos.movie_command.clone(),
|
||||||
|
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||||
metadata: state.app_ctx.services.metadata.clone(),
|
metadata: state.app_ctx.services.metadata.clone(),
|
||||||
watchlist: state.app_ctx.repos.watchlist.clone(),
|
watchlist: state.app_ctx.repos.watchlist.clone(),
|
||||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ async fn run_ingest(
|
|||||||
) -> StatusCode {
|
) -> StatusCode {
|
||||||
let deps = IngestWatchEventDeps {
|
let deps = IngestWatchEventDeps {
|
||||||
webhook_token: state.app_ctx.repos.webhook_token.clone(),
|
webhook_token: state.app_ctx.repos.webhook_token.clone(),
|
||||||
watch_event: state.app_ctx.repos.watch_event.clone(),
|
watch_event_command: state.app_ctx.repos.watch_event_command.clone(),
|
||||||
|
watch_event_query: state.app_ctx.repos.watch_event_query.clone(),
|
||||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||||
};
|
};
|
||||||
match ingest_watch_event::execute(&deps, cmd, parser).await {
|
match ingest_watch_event::execute(&deps, cmd, parser).await {
|
||||||
@@ -250,7 +251,7 @@ pub async fn get_watch_queue(
|
|||||||
let query = GetWatchQueueQuery {
|
let query = GetWatchQueueQuery {
|
||||||
user_id: user.0.value(),
|
user_id: user.0.value(),
|
||||||
};
|
};
|
||||||
let events = get_watch_queue::execute(state.app_ctx.repos.watch_event.clone(), query).await?;
|
let events = get_watch_queue::execute(state.app_ctx.repos.watch_event_query.clone(), query).await?;
|
||||||
|
|
||||||
let dtos = events
|
let dtos = events
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -296,7 +297,8 @@ pub async fn post_confirm_watch_events(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let confirmed = confirm_watch_events::execute(
|
let confirmed = confirm_watch_events::execute(
|
||||||
state.app_ctx.repos.watch_event.clone(),
|
state.app_ctx.repos.watch_event_command.clone(),
|
||||||
|
state.app_ctx.repos.watch_event_query.clone(),
|
||||||
state.app_ctx.services.review_logger.clone(),
|
state.app_ctx.services.review_logger.clone(),
|
||||||
cmd,
|
cmd,
|
||||||
)
|
)
|
||||||
@@ -325,6 +327,6 @@ pub async fn post_dismiss_watch_events(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let dismissed =
|
let dismissed =
|
||||||
dismiss_watch_events::execute(state.app_ctx.repos.watch_event.clone(), cmd).await?;
|
dismiss_watch_events::execute(state.app_ctx.repos.watch_event_command.clone(), state.app_ctx.repos.watch_event_query.clone(), cmd).await?;
|
||||||
Ok(Json(DismissWatchResponse { dismissed }))
|
Ok(Json(DismissWatchResponse { dismissed }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
|||||||
remote_watchlist_repo: remote_watchlist_repo.clone(),
|
remote_watchlist_repo: remote_watchlist_repo.clone(),
|
||||||
remote_goal_repo: Arc::clone(&db.remote_goal),
|
remote_goal_repo: Arc::clone(&db.remote_goal),
|
||||||
local_ap_content: Arc::clone(&ap_content_repo),
|
local_ap_content: Arc::clone(&ap_content_repo),
|
||||||
movie_repo: Arc::clone(&db.movie),
|
movie_repo: Arc::clone(&db.movie_query),
|
||||||
review_repo: Arc::clone(&db.review),
|
review_repo: Arc::clone(&db.review),
|
||||||
diary_repo: Arc::clone(&db.diary),
|
diary_repo: Arc::clone(&db.diary),
|
||||||
goal_repo: Arc::clone(&db.goal),
|
goal_repo: Arc::clone(&db.goal),
|
||||||
@@ -127,7 +127,8 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
|||||||
let ap_router = axum::Router::new();
|
let ap_router = axum::Router::new();
|
||||||
|
|
||||||
let review_logger = Arc::new(application::diary::review_logger::DefaultReviewLogger::new(
|
let review_logger = Arc::new(application::diary::review_logger::DefaultReviewLogger::new(
|
||||||
Arc::clone(&db.movie),
|
Arc::clone(&db.movie_command),
|
||||||
|
Arc::clone(&db.movie_query),
|
||||||
Arc::clone(&db.review),
|
Arc::clone(&db.review),
|
||||||
Arc::clone(&db.watchlist),
|
Arc::clone(&db.watchlist),
|
||||||
Arc::clone(&metadata_client),
|
Arc::clone(&metadata_client),
|
||||||
@@ -136,7 +137,8 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
|||||||
|
|
||||||
let app_ctx = AppContext {
|
let app_ctx = AppContext {
|
||||||
repos: Repositories {
|
repos: Repositories {
|
||||||
movie: db.movie,
|
movie_command: db.movie_command,
|
||||||
|
movie_query: db.movie_query,
|
||||||
review: db.review,
|
review: db.review,
|
||||||
diary: db.diary,
|
diary: db.diary,
|
||||||
stats: db.stats,
|
stats: db.stats,
|
||||||
@@ -145,7 +147,8 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
|||||||
import_profile: db.import_profile,
|
import_profile: db.import_profile,
|
||||||
movie_profile: db.movie_profile,
|
movie_profile: db.movie_profile,
|
||||||
watchlist: db.watchlist,
|
watchlist: db.watchlist,
|
||||||
watch_event: db.watch_event,
|
watch_event_command: db.watch_event_command,
|
||||||
|
watch_event_query: db.watch_event_query,
|
||||||
webhook_token: db.webhook_token,
|
webhook_token: db.webhook_token,
|
||||||
person_command: db.person_command,
|
person_command: db.person_command,
|
||||||
person_query: db.person_query,
|
person_query: db.person_query,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use api_types::{
|
use api_types::{
|
||||||
ProfileFieldDto, ProfileResponse, UpdateProfileFieldsRequest, UserProfileBase,
|
ProfileFieldDto, ProfileResponse, ProfileViewData, UpdateProfileFieldsRequest,
|
||||||
UserProfileResponse, UserStatsDto, UserSummaryDto, UsersResponse,
|
UserProfileBase, UserProfileResponse, UserStatsDto, UserSummaryDto, UsersResponse,
|
||||||
};
|
};
|
||||||
use utoipa::OpenApi;
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ use utoipa::OpenApi;
|
|||||||
UserSummaryDto,
|
UserSummaryDto,
|
||||||
UserProfileBase,
|
UserProfileBase,
|
||||||
UserProfileResponse,
|
UserProfileResponse,
|
||||||
|
ProfileViewData,
|
||||||
UserStatsDto,
|
UserStatsDto,
|
||||||
ProfileResponse,
|
ProfileResponse,
|
||||||
UpdateProfileFieldsRequest,
|
UpdateProfileFieldsRequest,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use domain::{
|
|||||||
collections::{PageParams, Paginated},
|
collections::{PageParams, Paginated},
|
||||||
},
|
},
|
||||||
ports::{
|
ports::{
|
||||||
AuthService, DiaryRepository, EventPublisher, MetadataClient, MovieRepository,
|
AuthService, DiaryRepository, EventPublisher, MetadataClient, MovieCommand, MovieQuery,
|
||||||
ObjectStorage, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient,
|
ObjectStorage, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient,
|
||||||
ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserRepository,
|
ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserRepository,
|
||||||
WatchlistRepository,
|
WatchlistRepository,
|
||||||
@@ -35,7 +35,16 @@ use tower::ServiceExt;
|
|||||||
pub struct Panic;
|
pub struct Panic;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MovieRepository for Panic {
|
impl MovieCommand for Panic {
|
||||||
|
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MovieQuery for Panic {
|
||||||
async fn get_movie_by_external_id(
|
async fn get_movie_by_external_id(
|
||||||
&self,
|
&self,
|
||||||
_: &ExternalMetadataId,
|
_: &ExternalMetadataId,
|
||||||
@@ -52,12 +61,6 @@ impl MovieRepository for Panic {
|
|||||||
) -> Result<Vec<Movie>, DomainError> {
|
) -> Result<Vec<Movie>, DomainError> {
|
||||||
panic!()
|
panic!()
|
||||||
}
|
}
|
||||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
async fn existing_external_ids(
|
async fn existing_external_ids(
|
||||||
&self,
|
&self,
|
||||||
_: &[ExternalMetadataId],
|
_: &[ExternalMetadataId],
|
||||||
@@ -530,7 +533,7 @@ impl domain::ports::RemoteWatchlistRepository for Panic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl domain::ports::WatchEventRepository for Panic {
|
impl domain::ports::WatchEventCommand for Panic {
|
||||||
async fn save(&self, _: &domain::models::WatchEvent) -> Result<(), DomainError> {
|
async fn save(&self, _: &domain::models::WatchEvent) -> Result<(), DomainError> {
|
||||||
panic!()
|
panic!()
|
||||||
}
|
}
|
||||||
@@ -541,6 +544,22 @@ impl domain::ports::WatchEventRepository for Panic {
|
|||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
panic!()
|
panic!()
|
||||||
}
|
}
|
||||||
|
async fn update_status_batch(
|
||||||
|
&self,
|
||||||
|
_: &[domain::value_objects::WatchEventId],
|
||||||
|
_: domain::models::WatchEventStatus,
|
||||||
|
) -> Result<u64, DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
async fn delete_non_pending_older_than(
|
||||||
|
&self,
|
||||||
|
_: chrono::NaiveDateTime,
|
||||||
|
) -> Result<u64, DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl domain::ports::WatchEventQuery for Panic {
|
||||||
async fn list_pending(
|
async fn list_pending(
|
||||||
&self,
|
&self,
|
||||||
_: &domain::value_objects::UserId,
|
_: &domain::value_objects::UserId,
|
||||||
@@ -559,13 +578,6 @@ impl domain::ports::WatchEventRepository for Panic {
|
|||||||
) -> Result<Vec<domain::models::WatchEvent>, DomainError> {
|
) -> Result<Vec<domain::models::WatchEvent>, DomainError> {
|
||||||
panic!()
|
panic!()
|
||||||
}
|
}
|
||||||
async fn update_status_batch(
|
|
||||||
&self,
|
|
||||||
_: &[domain::value_objects::WatchEventId],
|
|
||||||
_: domain::models::WatchEventStatus,
|
|
||||||
) -> Result<u64, DomainError> {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
async fn find_duplicate(
|
async fn find_duplicate(
|
||||||
&self,
|
&self,
|
||||||
_: &domain::value_objects::UserId,
|
_: &domain::value_objects::UserId,
|
||||||
@@ -574,12 +586,6 @@ impl domain::ports::WatchEventRepository for Panic {
|
|||||||
) -> Result<bool, DomainError> {
|
) -> Result<bool, DomainError> {
|
||||||
panic!()
|
panic!()
|
||||||
}
|
}
|
||||||
async fn delete_non_pending_older_than(
|
|
||||||
&self,
|
|
||||||
_: chrono::NaiveDateTime,
|
|
||||||
) -> Result<u64, DomainError> {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl domain::ports::WebhookTokenRepository for Panic {
|
impl domain::ports::WebhookTokenRepository for Panic {
|
||||||
@@ -782,7 +788,8 @@ pub fn make_test_state(auth_service: Arc<dyn AuthService>) -> crate::state::AppS
|
|||||||
crate::state::AppState {
|
crate::state::AppState {
|
||||||
app_ctx: AppContext {
|
app_ctx: AppContext {
|
||||||
repos: Repositories {
|
repos: Repositories {
|
||||||
movie: Arc::clone(&repo) as _,
|
movie_command: Arc::clone(&repo) as _,
|
||||||
|
movie_query: Arc::clone(&repo) as _,
|
||||||
review: Arc::clone(&repo) as _,
|
review: Arc::clone(&repo) as _,
|
||||||
diary: Arc::clone(&repo) as _,
|
diary: Arc::clone(&repo) as _,
|
||||||
stats: Arc::clone(&repo) as _,
|
stats: Arc::clone(&repo) as _,
|
||||||
@@ -791,7 +798,8 @@ pub fn make_test_state(auth_service: Arc<dyn AuthService>) -> crate::state::AppS
|
|||||||
import_profile: Arc::clone(&repo) as _,
|
import_profile: Arc::clone(&repo) as _,
|
||||||
movie_profile: Arc::clone(&repo) as _,
|
movie_profile: Arc::clone(&repo) as _,
|
||||||
watchlist: Arc::clone(&repo) as _,
|
watchlist: Arc::clone(&repo) as _,
|
||||||
watch_event: Arc::clone(&repo) as _,
|
watch_event_command: Arc::clone(&repo) as _,
|
||||||
|
watch_event_query: Arc::clone(&repo) as _,
|
||||||
webhook_token: Arc::clone(&repo) as _,
|
webhook_token: Arc::clone(&repo) as _,
|
||||||
profile_fields: Arc::clone(&repo) as _,
|
profile_fields: Arc::clone(&repo) as _,
|
||||||
person_command: Arc::clone(&repo) as _,
|
person_command: Arc::clone(&repo) as _,
|
||||||
|
|||||||
@@ -445,7 +445,8 @@ async fn test_app() -> Router {
|
|||||||
let state = AppState {
|
let state = AppState {
|
||||||
app_ctx: AppContext {
|
app_ctx: AppContext {
|
||||||
repos: Repositories {
|
repos: Repositories {
|
||||||
movie: Arc::new(SqliteMovieRepository::new(pool.clone())) as _,
|
movie_command: Arc::new(SqliteMovieRepository::new(pool.clone())) as _,
|
||||||
|
movie_query: Arc::new(SqliteMovieRepository::new(pool.clone())) as _,
|
||||||
review: Arc::new(SqliteReviewRepository::new(pool.clone())) as _,
|
review: Arc::new(SqliteReviewRepository::new(pool.clone())) as _,
|
||||||
diary: Arc::new(SqliteDiaryRepository::new(pool.clone())) as _,
|
diary: Arc::new(SqliteDiaryRepository::new(pool.clone())) as _,
|
||||||
stats: Arc::new(SqliteStatsRepository::new(pool.clone())) as _,
|
stats: Arc::new(SqliteStatsRepository::new(pool.clone())) as _,
|
||||||
@@ -454,7 +455,8 @@ async fn test_app() -> Router {
|
|||||||
import_profile: Arc::new(PanicImportProfile),
|
import_profile: Arc::new(PanicImportProfile),
|
||||||
movie_profile: Arc::new(PanicMovieProfile),
|
movie_profile: Arc::new(PanicMovieProfile),
|
||||||
watchlist: Arc::new(PanicWatchlist),
|
watchlist: Arc::new(PanicWatchlist),
|
||||||
watch_event: Arc::new(domain::testing::PanicWatchEventRepository),
|
watch_event_command: Arc::new(domain::testing::PanicWatchEventCommand),
|
||||||
|
watch_event_query: Arc::new(domain::testing::PanicWatchEventQuery),
|
||||||
webhook_token: Arc::new(domain::testing::PanicWebhookTokenRepository),
|
webhook_token: Arc::new(domain::testing::PanicWebhookTokenRepository),
|
||||||
profile_fields: Arc::new(PanicProfileFields),
|
profile_fields: Arc::new(PanicProfileFields),
|
||||||
person_command: Arc::new(PanicPersonCommand),
|
person_command: Arc::new(PanicPersonCommand),
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ use std::sync::Arc;
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
DiaryRepository, GoalRepository, ImageRefCommand, ImageRefQuery, ImportSessionRepository,
|
DiaryRepository, GoalRepository, ImageRefCommand, ImageRefQuery, ImportSessionRepository,
|
||||||
LocalApContentQuery, MovieDeduplicator, MovieProfileRepository, MovieRepository, PersonCommand,
|
LocalApContentQuery, MovieCommand, MovieDeduplicator, MovieProfileRepository, MovieQuery,
|
||||||
PersonQuery, ReviewRepository, SearchCommand, StatsRepository, UserRepository,
|
PersonCommand, PersonQuery, ReviewRepository, SearchCommand, StatsRepository, UserRepository,
|
||||||
WatchEventRepository,
|
WatchEventCommand, WatchEventQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use infra_wiring::DbPool;
|
pub use infra_wiring::DbPool;
|
||||||
|
|
||||||
pub struct WorkerDbOutput {
|
pub struct WorkerDbOutput {
|
||||||
pub movie: Arc<dyn MovieRepository>,
|
pub movie_command: Arc<dyn MovieCommand>,
|
||||||
|
pub movie_query: Arc<dyn MovieQuery>,
|
||||||
pub review: Arc<dyn ReviewRepository>,
|
pub review: Arc<dyn ReviewRepository>,
|
||||||
pub diary: Arc<dyn DiaryRepository>,
|
pub diary: Arc<dyn DiaryRepository>,
|
||||||
pub stats: Arc<dyn StatsRepository>,
|
pub stats: Arc<dyn StatsRepository>,
|
||||||
@@ -19,7 +20,8 @@ pub struct WorkerDbOutput {
|
|||||||
pub user: Arc<dyn UserRepository>,
|
pub user: Arc<dyn UserRepository>,
|
||||||
pub import_session: Arc<dyn ImportSessionRepository>,
|
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||||
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
||||||
pub watch_event: Arc<dyn WatchEventRepository>,
|
pub watch_event_command: Arc<dyn WatchEventCommand>,
|
||||||
|
pub watch_event_query: Arc<dyn WatchEventQuery>,
|
||||||
pub person_command: Arc<dyn PersonCommand>,
|
pub person_command: Arc<dyn PersonCommand>,
|
||||||
pub person_query: Arc<dyn PersonQuery>,
|
pub person_query: Arc<dyn PersonQuery>,
|
||||||
pub search_command: Arc<dyn SearchCommand>,
|
pub search_command: Arc<dyn SearchCommand>,
|
||||||
@@ -46,10 +48,10 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
|
|||||||
let (person_command, person_query) = postgres::create_person_adapter(w.pool.clone());
|
let (person_command, person_query) = postgres::create_person_adapter(w.pool.clone());
|
||||||
let (search_command, _search_port) =
|
let (search_command, _search_port) =
|
||||||
postgres_search::create_search_adapter(w.pool.clone());
|
postgres_search::create_search_adapter(w.pool.clone());
|
||||||
let we: Arc<dyn WatchEventRepository> =
|
let we = Arc::new(postgres::PostgresWatchEventRepository::new(w.pool.clone()));
|
||||||
Arc::new(postgres::PostgresWatchEventRepository::new(w.pool.clone()));
|
|
||||||
Ok(WorkerDbOutput {
|
Ok(WorkerDbOutput {
|
||||||
movie: w.movie,
|
movie_command: w.movie_command,
|
||||||
|
movie_query: w.movie_query,
|
||||||
review: w.review,
|
review: w.review,
|
||||||
diary: w.diary,
|
diary: w.diary,
|
||||||
stats: w.stats,
|
stats: w.stats,
|
||||||
@@ -57,7 +59,8 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
|
|||||||
user: w.user,
|
user: w.user,
|
||||||
import_session: w.import_session,
|
import_session: w.import_session,
|
||||||
movie_profile: w.movie_profile,
|
movie_profile: w.movie_profile,
|
||||||
watch_event: we,
|
watch_event_command: we.clone() as _,
|
||||||
|
watch_event_query: we as _,
|
||||||
person_command,
|
person_command,
|
||||||
person_query,
|
person_query,
|
||||||
search_command,
|
search_command,
|
||||||
@@ -84,10 +87,10 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
|
|||||||
let (person_command, person_query) = sqlite::create_person_adapter(w.pool.clone());
|
let (person_command, person_query) = sqlite::create_person_adapter(w.pool.clone());
|
||||||
let (search_command, _search_port) =
|
let (search_command, _search_port) =
|
||||||
sqlite_search::create_search_adapter(w.pool.clone());
|
sqlite_search::create_search_adapter(w.pool.clone());
|
||||||
let we: Arc<dyn WatchEventRepository> =
|
let we = Arc::new(sqlite::SqliteWatchEventRepository::new(w.pool.clone()));
|
||||||
Arc::new(sqlite::SqliteWatchEventRepository::new(w.pool.clone()));
|
|
||||||
Ok(WorkerDbOutput {
|
Ok(WorkerDbOutput {
|
||||||
movie: w.movie,
|
movie_command: w.movie_command,
|
||||||
|
movie_query: w.movie_query,
|
||||||
review: w.review,
|
review: w.review,
|
||||||
diary: w.diary,
|
diary: w.diary,
|
||||||
stats: w.stats,
|
stats: w.stats,
|
||||||
@@ -95,7 +98,8 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
|
|||||||
user: w.user,
|
user: w.user,
|
||||||
import_session: w.import_session,
|
import_session: w.import_session,
|
||||||
movie_profile: w.movie_profile,
|
movie_profile: w.movie_profile,
|
||||||
watch_event: we,
|
watch_event_command: we.clone() as _,
|
||||||
|
watch_event_query: we as _,
|
||||||
person_command,
|
person_command,
|
||||||
person_query,
|
person_query,
|
||||||
search_command,
|
search_command,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
allow_registration,
|
allow_registration,
|
||||||
) = (
|
) = (
|
||||||
Arc::clone(&db.ap_content),
|
Arc::clone(&db.ap_content),
|
||||||
Arc::clone(&db.movie),
|
Arc::clone(&db.movie_query),
|
||||||
Arc::clone(&db.review),
|
Arc::clone(&db.review),
|
||||||
Arc::clone(&db.diary),
|
Arc::clone(&db.diary),
|
||||||
Arc::clone(&db.goal),
|
Arc::clone(&db.goal),
|
||||||
@@ -76,12 +76,14 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
db::DbPool::Postgres(pool) => postgres_federation::wire(pool.clone()),
|
db::DbPool::Postgres(pool) => postgres_federation::wire(pool.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let movie = db.movie;
|
let movie_command = db.movie_command;
|
||||||
|
let movie_query = db.movie_query;
|
||||||
let deduplicator = db.deduplicator;
|
let deduplicator = db.deduplicator;
|
||||||
let user = db.user;
|
let user = db.user;
|
||||||
let import_session = db.import_session;
|
let import_session = db.import_session;
|
||||||
let movie_profile = db.movie_profile;
|
let movie_profile = db.movie_profile;
|
||||||
let watch_event = db.watch_event;
|
let watch_event_command = db.watch_event_command;
|
||||||
|
let watch_event_query = db.watch_event_query;
|
||||||
let person_command = db.person_command;
|
let person_command = db.person_command;
|
||||||
let person_query = db.person_query;
|
let person_query = db.person_query;
|
||||||
let search_command = db.search_command;
|
let search_command = db.search_command;
|
||||||
@@ -111,7 +113,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let image_fetcher = poster_fetcher::create_image_fetcher()?;
|
let image_fetcher = poster_fetcher::create_image_fetcher()?;
|
||||||
let handler = Arc::new(application::movies::MovieEnrichmentHandler::new(
|
let handler = Arc::new(application::movies::MovieEnrichmentHandler::new(
|
||||||
Arc::clone(&client) as Arc<dyn MovieEnrichmentClient>,
|
Arc::clone(&client) as Arc<dyn MovieEnrichmentClient>,
|
||||||
Arc::clone(&movie),
|
Arc::clone(&movie_query),
|
||||||
Arc::clone(&movie_profile),
|
Arc::clone(&movie_profile),
|
||||||
Arc::clone(&person_command),
|
Arc::clone(&person_command),
|
||||||
Arc::clone(&search_command),
|
Arc::clone(&search_command),
|
||||||
@@ -149,7 +151,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let mut periodic_jobs: Vec<Arc<dyn PeriodicJob>> = vec![
|
let mut periodic_jobs: Vec<Arc<dyn PeriodicJob>> = vec![
|
||||||
Arc::new(application::jobs::MovieDeduplicationJob::new(
|
Arc::new(application::jobs::MovieDeduplicationJob::new(
|
||||||
Arc::clone(&movie),
|
Arc::clone(&movie_query),
|
||||||
Arc::clone(&deduplicator),
|
Arc::clone(&deduplicator),
|
||||||
Arc::clone(&object_storage),
|
Arc::clone(&object_storage),
|
||||||
)),
|
)),
|
||||||
@@ -157,7 +159,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
import_session.clone(),
|
import_session.clone(),
|
||||||
)),
|
)),
|
||||||
Arc::new(application::jobs::WatchEventCleanupJob::new(
|
Arc::new(application::jobs::WatchEventCleanupJob::new(
|
||||||
watch_event.clone(),
|
watch_event_command.clone(),
|
||||||
)),
|
)),
|
||||||
Arc::new(application::jobs::WrapUpAutoGenerateJob::new(
|
Arc::new(application::jobs::WrapUpAutoGenerateJob::new(
|
||||||
Arc::clone(&user),
|
Arc::clone(&user),
|
||||||
@@ -194,7 +196,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let handlers: Vec<Arc<dyn EventHandler>> = {
|
let handlers: Vec<Arc<dyn EventHandler>> = {
|
||||||
let poster = Arc::new(poster_sync::PosterSyncHandler::new(
|
let poster = Arc::new(poster_sync::PosterSyncHandler::new(
|
||||||
Arc::clone(&movie),
|
Arc::clone(&movie_command),
|
||||||
|
Arc::clone(&movie_query),
|
||||||
Arc::clone(&metadata),
|
Arc::clone(&metadata),
|
||||||
Arc::clone(&poster_fetcher),
|
Arc::clone(&poster_fetcher),
|
||||||
Arc::clone(&object_storage),
|
Arc::clone(&object_storage),
|
||||||
@@ -212,7 +215,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
)) as Arc<dyn EventHandler>;
|
)) as Arc<dyn EventHandler>;
|
||||||
|
|
||||||
let discovery_indexer = Arc::new(MovieDiscoveryIndexer::new(
|
let discovery_indexer = Arc::new(MovieDiscoveryIndexer::new(
|
||||||
Arc::clone(&movie),
|
Arc::clone(&movie_query),
|
||||||
Arc::clone(&search_command),
|
Arc::clone(&search_command),
|
||||||
)) as Arc<dyn EventHandler>;
|
)) as Arc<dyn EventHandler>;
|
||||||
|
|
||||||
@@ -223,7 +226,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
)) as Arc<dyn EventHandler>;
|
)) as Arc<dyn EventHandler>;
|
||||||
|
|
||||||
let reindex_handler = Arc::new(SearchReindexHandler::new(ReindexSearchDeps {
|
let reindex_handler = Arc::new(SearchReindexHandler::new(ReindexSearchDeps {
|
||||||
movie: Arc::clone(&movie),
|
movie_query: Arc::clone(&movie_query),
|
||||||
movie_profile: Arc::clone(&movie_profile),
|
movie_profile: Arc::clone(&movie_profile),
|
||||||
search_command: Arc::clone(&search_command),
|
search_command: Arc::clone(&search_command),
|
||||||
person_command: Arc::clone(&person_command),
|
person_command: Arc::clone(&person_command),
|
||||||
|
|||||||
Reference in New Issue
Block a user