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

@@ -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 }))
}