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

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

View File

@@ -3,11 +3,12 @@ use std::sync::Arc;
use domain::ports::{
AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher,
FederatedProfileQuery, GoalRepository, ImportProfileRepository, ImportSessionRepository,
MetadataClient, MovieProfileRepository, MovieRepository, ObjectStorage, PasswordHasher,
MetadataClient, MovieCommand, MovieProfileRepository, MovieQuery, ObjectStorage, PasswordHasher,
PersonCommand, PersonEnrichmentClient, PersonQuery, PosterFetcherClient,
RefreshSessionRepository, RemoteGoalRepository, RemoteWatchlistRepository, ReviewRepository,
SearchCommand, SearchPort, SocialQueryPort, StatsRepository, UserProfileFieldsRepository,
UserRepository, UserSettingsRepository, WatchEventRepository, WatchlistRepository,
UserRepository, UserSettingsRepository, WatchEventCommand, WatchEventQuery,
WatchlistRepository,
WebhookTokenRepository, WrapUpRepository, WrapUpStatsQuery,
};
@@ -16,7 +17,8 @@ use application::ports::ReviewLogger;
#[derive(Clone)]
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 diary: Arc<dyn DiaryRepository>,
pub stats: Arc<dyn StatsRepository>,
@@ -25,7 +27,8 @@ pub struct Repositories {
pub import_profile: Arc<dyn ImportProfileRepository>,
pub movie_profile: Arc<dyn MovieProfileRepository>,
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 person_command: Arc<dyn PersonCommand>,
pub person_query: Arc<dyn PersonQuery>,

View File

@@ -4,13 +4,14 @@ use anyhow::Context;
use domain::ports::{
AuthService, LocalApContentQuery, MetadataClient, ObjectStorage, PasswordHasher,
PosterFetcherClient, RefreshSessionRepository, UserProfileFieldsRepository,
WatchEventRepository, WebhookTokenRepository,
WatchEventCommand, WatchEventQuery, WebhookTokenRepository,
};
pub use infra_wiring::DbPool;
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 diary: Arc<dyn domain::ports::DiaryRepository>,
pub stats: Arc<dyn domain::ports::StatsRepository>,
@@ -19,7 +20,8 @@ pub struct DatabaseOutput {
pub import_profile: Arc<dyn domain::ports::ImportProfileRepository>,
pub movie_profile: Arc<dyn domain::ports::MovieProfileRepository>,
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 person_command: Arc<dyn domain::ports::PersonCommand>,
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 (sc, sp) = postgres_search::create_search_adapter(w.pool.clone());
let pf = postgres::create_profile_fields_repo(w.pool.clone());
let we: Arc<dyn WatchEventRepository> =
Arc::new(postgres::PostgresWatchEventRepository::new(w.pool.clone()));
let we = Arc::new(postgres::PostgresWatchEventRepository::new(w.pool.clone()));
let wt: Arc<dyn WebhookTokenRepository> = Arc::new(
postgres::PostgresWebhookTokenRepository::new(w.pool.clone()),
);
Ok(DatabaseOutput {
movie: w.movie,
movie_command: w.movie_command,
movie_query: w.movie_query,
review: w.review,
diary: w.diary,
stats: w.stats,
@@ -62,7 +64,8 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result
import_profile: w.import_profile,
movie_profile: w.movie_profile,
watchlist: w.watchlist,
watch_event: we,
watch_event_command: we.clone() as _,
watch_event_query: we as _,
webhook_token: wt,
person_command: pc,
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 (sc, sp) = sqlite_search::create_search_adapter(w.pool.clone());
let pf = sqlite::create_profile_fields_repo(w.pool.clone());
let we: Arc<dyn WatchEventRepository> =
Arc::new(sqlite::SqliteWatchEventRepository::new(w.pool.clone()));
let we = Arc::new(sqlite::SqliteWatchEventRepository::new(w.pool.clone()));
let wt: Arc<dyn WebhookTokenRepository> =
Arc::new(sqlite::SqliteWebhookTokenRepository::new(w.pool.clone()));
Ok(DatabaseOutput {
movie: w.movie,
movie_command: w.movie_command,
movie_query: w.movie_query,
review: w.review,
diary: w.diary,
stats: w.stats,
@@ -104,7 +107,8 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result
import_profile: w.import_profile,
movie_profile: w.movie_profile,
watchlist: w.watchlist,
watch_event: we,
watch_event_command: we.clone() as _,
watch_event_query: we as _,
webhook_token: wt,
person_command: pc,
person_query: pq,

View File

@@ -106,7 +106,7 @@ pub async fn delete_review(
let deps = DeleteReviewDeps {
review: state.app_ctx.repos.review.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(),
};
delete_review::execute(&deps, cmd).await?;
@@ -277,7 +277,7 @@ pub async fn post_delete_review_html(
let deps = DeleteReviewDeps {
review: state.app_ctx.repos.review.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(),
};
match delete_review::execute(&deps, cmd).await {

View File

@@ -10,6 +10,7 @@ use api_types::{
CreateGoalRequest, GoalDto, GoalsResponse, UpdateGoalRequest, UpdateUserSettingsRequest,
UserSettingsDto,
};
use application::goals::deps::{GoalCommandDeps, GoalQueryDeps};
// ── Shared mapper ────────────────────────────────────────────────────────────
@@ -38,9 +39,12 @@ pub async fn list_goals(
State(state): State<AppState>,
user: AuthenticatedUser,
) -> 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(
state.app_ctx.repos.goal.clone(),
state.app_ctx.repos.stats.clone(),
&deps,
application::goals::queries::ListGoalsQuery {
user_id: user.0.value(),
},
@@ -65,10 +69,13 @@ pub async fn create_goal(
user: AuthenticatedUser,
Json(req): Json<CreateGoalRequest>,
) -> 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(
state.app_ctx.repos.goal.clone(),
state.app_ctx.repos.stats.clone(),
state.app_ctx.services.event_publisher.clone(),
&deps,
application::goals::commands::CreateGoalCommand {
user_id: user.0.value(),
year: req.year,
@@ -95,10 +102,13 @@ pub async fn update_goal(
Path(year): Path<u16>,
Json(req): Json<UpdateGoalRequest>,
) -> 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(
state.app_ctx.repos.goal.clone(),
state.app_ctx.repos.stats.clone(),
state.app_ctx.services.event_publisher.clone(),
&deps,
application::goals::commands::UpdateGoalCommand {
user_id: user.0.value(),
year,
@@ -123,9 +133,13 @@ pub async fn delete_goal(
user: AuthenticatedUser,
Path(year): Path<u16>,
) -> 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(
state.app_ctx.repos.goal.clone(),
state.app_ctx.services.event_publisher.clone(),
&deps,
application::goals::commands::DeleteGoalCommand {
user_id: user.0.value(),
year,
@@ -148,9 +162,12 @@ pub async fn get_user_goals(
AuthenticatedUser(_viewer): AuthenticatedUser,
Path(user_id): Path<Uuid>,
) -> 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(
state.app_ctx.repos.goal.clone(),
state.app_ctx.repos.stats.clone(),
&deps,
application::goals::queries::ListGoalsQuery { user_id },
)
.await?;

View File

@@ -19,6 +19,7 @@ use application::import::{
DeleteImportProfileCommand, ExecuteImportCommand, SaveImportProfileCommand,
},
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,
save_profile as save_import_profile,
};
@@ -160,8 +161,10 @@ pub async fn post_upload(
};
match create_import_session::execute(
state.app_ctx.repos.import_session.clone(),
state.app_ctx.services.document_parser.clone(),
&CreateSessionDeps {
import_session: state.app_ctx.repos.import_session.clone(),
document_parser: state.app_ctx.services.document_parser.clone(),
},
CreateImportSessionCommand {
user_id: user_id.value(),
bytes,
@@ -250,9 +253,11 @@ pub async fn post_mapping(
.into_response();
}
match apply_import_mapping::execute(
state.app_ctx.repos.import_session.clone(),
state.app_ctx.services.document_parser.clone(),
state.app_ctx.repos.movie.clone(),
&ApplyMappingDeps {
import_session: state.app_ctx.repos.import_session.clone(),
document_parser: state.app_ctx.services.document_parser.clone(),
movie_query: state.app_ctx.repos.movie_query.clone(),
},
ApplyImportMappingCommand {
user_id: user_id.value(),
session_id: session_id.value(),
@@ -346,8 +351,10 @@ pub async fn post_confirm(
.filter(|n| !n.trim().is_empty());
if let Some(name) = profile_name {
let _ = save_import_profile::execute(
state.app_ctx.repos.import_session.clone(),
state.app_ctx.repos.import_profile.clone(),
&SaveProfileDeps {
import_session: state.app_ctx.repos.import_session.clone(),
import_profile: state.app_ctx.repos.import_profile.clone(),
},
SaveImportProfileCommand {
user_id: user_id.value(),
session_id: session_id.value(),
@@ -365,8 +372,10 @@ pub async fn post_confirm(
.collect();
match execute_import::execute(
state.app_ctx.repos.import_session.clone(),
state.app_ctx.services.review_logger.clone(),
&ExecuteImportDeps {
import_session: state.app_ctx.repos.import_session.clone(),
review_logger: state.app_ctx.services.review_logger.clone(),
},
ExecuteImportCommand {
user_id: user_id.value(),
session_id: session_id.value(),
@@ -487,8 +496,10 @@ pub async fn api_post_session(
_ => FileFormat::Csv,
};
let r = create_import_session::execute(
state.app_ctx.repos.import_session.clone(),
state.app_ctx.services.document_parser.clone(),
&CreateSessionDeps {
import_session: state.app_ctx.repos.import_session.clone(),
document_parser: state.app_ctx.services.document_parser.clone(),
},
CreateImportSessionCommand {
user_id: user_id.value(),
bytes,
@@ -583,9 +594,11 @@ pub async fn api_put_mapping(
.collect();
let rows = apply_import_mapping::execute(
state.app_ctx.repos.import_session.clone(),
state.app_ctx.services.document_parser.clone(),
state.app_ctx.repos.movie.clone(),
&ApplyMappingDeps {
import_session: state.app_ctx.repos.import_session.clone(),
document_parser: state.app_ctx.services.document_parser.clone(),
movie_query: state.app_ctx.repos.movie_query.clone(),
},
ApplyImportMappingCommand {
user_id: user_id.value(),
session_id: session_id.value(),
@@ -674,8 +687,10 @@ pub async fn api_post_confirm(
.map(ImportSessionId::from_uuid)
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
let s = execute_import::execute(
state.app_ctx.repos.import_session.clone(),
state.app_ctx.services.review_logger.clone(),
&ExecuteImportDeps {
import_session: state.app_ctx.repos.import_session.clone(),
review_logger: state.app_ctx.services.review_logger.clone(),
},
ExecuteImportCommand {
user_id: user_id.value(),
session_id: session_id.value(),
@@ -739,8 +754,10 @@ pub async fn api_post_profile(
.map(ImportSessionId::from_uuid)
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
let id = save_import_profile::execute(
state.app_ctx.repos.import_session.clone(),
state.app_ctx.repos.import_profile.clone(),
&SaveProfileDeps {
import_session: state.app_ctx.repos.import_session.clone(),
import_profile: state.app_ctx.repos.import_profile.clone(),
},
SaveImportProfileCommand {
user_id: user_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()))?;
apply_import_profile::execute(
state.app_ctx.repos.import_profile.clone(),
state.app_ctx.repos.import_session.clone(),
&ApplyProfileDeps {
import_profile: state.app_ctx.repos.import_profile.clone(),
import_session: state.app_ctx.repos.import_session.clone(),
},
ApplyImportProfileCommand {
user_id: user_id.value(),
session_id,
@@ -832,9 +851,11 @@ pub async fn api_apply_profile(
let mappings = session.field_mappings.unwrap_or_default();
let rows = apply_import_mapping::execute(
state.app_ctx.repos.import_session.clone(),
state.app_ctx.services.document_parser.clone(),
state.app_ctx.repos.movie.clone(),
&ApplyMappingDeps {
import_session: state.app_ctx.repos.import_session.clone(),
document_parser: state.app_ctx.services.document_parser.clone(),
movie_query: state.app_ctx.repos.movie_query.clone(),
},
ApplyImportMappingCommand {
user_id: user_id.value(),
session_id,

View File

@@ -135,7 +135,7 @@ pub async fn get_watch_queue_page(
let query = GetWatchQueueQuery {
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
.unwrap_or_default();
@@ -173,7 +173,8 @@ pub async fn post_confirm_single(
};
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(),
cmd,
)
@@ -203,7 +204,7 @@ pub async fn post_dismiss_single(
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(),
Err(e) => {
let msg = encode_error(&e.to_string());

View File

@@ -48,7 +48,7 @@ pub async fn list_movies(
Query(params): Query<MoviesQueryParams>,
) -> Result<Json<MoviesResponse>, ApiError> {
let page = get_movies::execute(
state.app_ctx.repos.movie.clone(),
state.app_ctx.repos.movie_query.clone(),
GetMoviesQuery {
limit: params.limit,
offset: params.offset,
@@ -122,7 +122,8 @@ pub async fn sync_poster(
) -> Result<impl IntoResponse, ApiError> {
sync_poster::execute(
&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(),
metadata: state.app_ctx.services.metadata.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(
&GetMovieSocialPageDeps {
movie: state.app_ctx.repos.movie.clone(),
movie_query: state.app_ctx.repos.movie_query.clone(),
diary: state.app_ctx.repos.diary.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(
&GetMovieSocialPageDeps {
movie: state.app_ctx.repos.movie.clone(),
movie_query: state.app_ctx.repos.movie_query.clone(),
diary: state.app_ctx.repos.diary.clone(),
movie_profile: state.app_ctx.repos.movie_profile.clone(),
},

View File

@@ -269,54 +269,62 @@ pub async fn get_user_profile(
Err(e) => return crate::errors::domain_error_response(e),
};
let entries = profile.entries.map(|p| DiaryResponse {
items: p
.items
.iter()
.map(crate::mappers::movies::entry_to_dto)
.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
let view_data = if let Some(p) = profile.entries {
Some(api_types::ProfileViewData::Entries {
entries: DiaryResponse {
items: p
.items
.iter()
.map(crate::mappers::movies::entry_to_dto)
.collect(),
})
.collect()
});
let trends = profile.trends.map(|t| 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,
});
total_count: p.total_count,
limit: p.limit,
offset: p.offset,
},
})
} else if let Some(h) = profile.history {
Some(api_types::ProfileViewData::History {
history: application::users::group_by_month(h)
.into_iter()
.map(|m| MonthActivityDto {
year_month: m.year_month,
month_label: m.month_label,
count: m.count,
entries: m
.entries
.iter()
.map(crate::mappers::movies::entry_to_dto)
.collect(),
})
.collect(),
})
} else if let Some(t) = profile.trends {
Some(api_types::ProfileViewData::Trends {
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 {
user_id,
@@ -339,13 +347,13 @@ pub async fn get_user_profile(
},
following_count: profile.following_count,
followers_count: profile.followers_count,
entries,
history,
trends,
view_data,
goals: {
let goals_list = application::goals::list::execute(
state.app_ctx.repos.goal.clone(),
state.app_ctx.repos.stats.clone(),
&application::goals::deps::GoalQueryDeps {
goal: state.app_ctx.repos.goal.clone(),
stats: state.app_ctx.repos.stats.clone(),
},
application::goals::queries::ListGoalsQuery { user_id },
)
.await
@@ -394,38 +402,46 @@ async fn build_federated_profile_response(
Err(e) => return crate::errors::domain_error_response(e),
};
let entries = profile.entries.map(|p| DiaryResponse {
items: p
.items
.iter()
.map(crate::mappers::movies::entry_to_dto)
.collect(),
total_count: p.total_count,
limit: p.limit,
offset: p.offset,
});
let trends = profile.trends.map(|t| 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,
});
let view_data = if let Some(p) = profile.entries {
Some(api_types::ProfileViewData::Entries {
entries: DiaryResponse {
items: p
.items
.iter()
.map(crate::mappers::movies::entry_to_dto)
.collect(),
total_count: p.total_count,
limit: p.limit,
offset: p.offset,
},
})
} else if let Some(t) = profile.trends {
Some(api_types::ProfileViewData::Trends {
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
};
let username = fed
.display_name
@@ -449,9 +465,7 @@ async fn build_federated_profile_response(
},
following_count: 0,
followers_count: 0,
entries,
history: None,
trends,
view_data,
goals: None,
is_federated: true,
handle: Some(fed.handle),
@@ -735,8 +749,10 @@ pub async fn get_user_profile_html(
search: params.search.clone(),
goals: {
let goals_list = application::goals::list::execute(
state.app_ctx.repos.goal.clone(),
state.app_ctx.repos.stats.clone(),
&application::goals::deps::GoalQueryDeps {
goal: state.app_ctx.repos.goal.clone(),
stats: state.app_ctx.repos.stats.clone(),
},
application::goals::queries::ListGoalsQuery {
user_id: profile_user_uuid,
},

View File

@@ -95,7 +95,8 @@ pub async fn post_watchlist_add(
Json(req): Json<AddToWatchlistRequest>,
) -> Result<impl IntoResponse, ApiError> {
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(),
watchlist: state.app_ctx.repos.watchlist.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
@@ -280,7 +281,8 @@ pub async fn post_watchlist_add_html(
};
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(),
watchlist: state.app_ctx.repos.watchlist.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),

View File

@@ -129,7 +129,8 @@ async fn run_ingest(
) -> StatusCode {
let deps = IngestWatchEventDeps {
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(),
};
match ingest_watch_event::execute(&deps, cmd, parser).await {
@@ -250,7 +251,7 @@ pub async fn get_watch_queue(
let query = GetWatchQueueQuery {
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
.into_iter()
@@ -296,7 +297,8 @@ pub async fn post_confirm_watch_events(
};
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(),
cmd,
)
@@ -325,6 +327,6 @@ pub async fn post_dismiss_watch_events(
};
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 }))
}

View File

@@ -97,7 +97,7 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
remote_watchlist_repo: remote_watchlist_repo.clone(),
remote_goal_repo: Arc::clone(&db.remote_goal),
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),
diary_repo: Arc::clone(&db.diary),
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 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.watchlist),
Arc::clone(&metadata_client),
@@ -136,7 +137,8 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
let app_ctx = AppContext {
repos: Repositories {
movie: db.movie,
movie_command: db.movie_command,
movie_query: db.movie_query,
review: db.review,
diary: db.diary,
stats: db.stats,
@@ -145,7 +147,8 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
import_profile: db.import_profile,
movie_profile: db.movie_profile,
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,
person_command: db.person_command,
person_query: db.person_query,

View File

@@ -1,6 +1,6 @@
use api_types::{
ProfileFieldDto, ProfileResponse, UpdateProfileFieldsRequest, UserProfileBase,
UserProfileResponse, UserStatsDto, UserSummaryDto, UsersResponse,
ProfileFieldDto, ProfileResponse, ProfileViewData, UpdateProfileFieldsRequest,
UserProfileBase, UserProfileResponse, UserStatsDto, UserSummaryDto, UsersResponse,
};
use utoipa::OpenApi;
@@ -18,6 +18,7 @@ use utoipa::OpenApi;
UserSummaryDto,
UserProfileBase,
UserProfileResponse,
ProfileViewData,
UserStatsDto,
ProfileResponse,
UpdateProfileFieldsRequest,

View File

@@ -17,7 +17,7 @@ use domain::{
collections::{PageParams, Paginated},
},
ports::{
AuthService, DiaryRepository, EventPublisher, MetadataClient, MovieRepository,
AuthService, DiaryRepository, EventPublisher, MetadataClient, MovieCommand, MovieQuery,
ObjectStorage, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient,
ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserRepository,
WatchlistRepository,
@@ -35,7 +35,16 @@ use tower::ServiceExt;
pub struct Panic;
#[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(
&self,
_: &ExternalMetadataId,
@@ -52,12 +61,6 @@ impl MovieRepository for Panic {
) -> Result<Vec<Movie>, DomainError> {
panic!()
}
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
panic!()
}
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
panic!()
}
async fn existing_external_ids(
&self,
_: &[ExternalMetadataId],
@@ -530,7 +533,7 @@ impl domain::ports::RemoteWatchlistRepository for Panic {
}
#[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> {
panic!()
}
@@ -541,6 +544,22 @@ impl domain::ports::WatchEventRepository for Panic {
) -> Result<(), DomainError> {
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(
&self,
_: &domain::value_objects::UserId,
@@ -559,13 +578,6 @@ impl domain::ports::WatchEventRepository for Panic {
) -> Result<Vec<domain::models::WatchEvent>, DomainError> {
panic!()
}
async fn update_status_batch(
&self,
_: &[domain::value_objects::WatchEventId],
_: domain::models::WatchEventStatus,
) -> Result<u64, DomainError> {
panic!()
}
async fn find_duplicate(
&self,
_: &domain::value_objects::UserId,
@@ -574,12 +586,6 @@ impl domain::ports::WatchEventRepository for Panic {
) -> Result<bool, DomainError> {
panic!()
}
async fn delete_non_pending_older_than(
&self,
_: chrono::NaiveDateTime,
) -> Result<u64, DomainError> {
panic!()
}
}
#[async_trait::async_trait]
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 {
app_ctx: AppContext {
repos: Repositories {
movie: Arc::clone(&repo) as _,
movie_command: Arc::clone(&repo) as _,
movie_query: Arc::clone(&repo) as _,
review: Arc::clone(&repo) as _,
diary: 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 _,
movie_profile: 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 _,
profile_fields: Arc::clone(&repo) as _,
person_command: Arc::clone(&repo) as _,