diff --git a/crates/adapters/activitypub/src/event_handler.rs b/crates/adapters/activitypub/src/event_handler.rs index 0d18589..6864055 100644 --- a/crates/adapters/activitypub/src/event_handler.rs +++ b/crates/adapters/activitypub/src/event_handler.rs @@ -5,7 +5,7 @@ use domain::{ errors::DomainError, events::DomainEvent, ports::{ - GoalRepository, LocalApContentQuery, MovieQuery, ReviewRepository, StatsRepository, + GoalQuery, LocalApContentQuery, MovieQuery, ReviewRepository, StatsRepository, UserFederationSettingsQuery, }, value_objects::{MovieId, ReviewId, UserId}, @@ -22,7 +22,7 @@ pub struct ActivityPubEventHandler { content_query: Arc, review_repo: Arc, movie_repo: Arc, - goal_repo: Arc, + goal_repo: Arc, stats_repo: Arc, federation_settings: Arc, base_url: String, @@ -35,7 +35,7 @@ impl ActivityPubEventHandler { content_query: Arc, review_repo: Arc, movie_repo: Arc, - goal_repo: Arc, + goal_repo: Arc, stats_repo: Arc, federation_settings: Arc, base_url: String, diff --git a/crates/adapters/activitypub/src/goal_handler.rs b/crates/adapters/activitypub/src/goal_handler.rs index 9d85c5d..e6dd1cf 100644 --- a/crates/adapters/activitypub/src/goal_handler.rs +++ b/crates/adapters/activitypub/src/goal_handler.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use chrono::DateTime; use domain::{ models::RemoteGoalEntry, - ports::{GoalRepository, RemoteGoalRepository}, + ports::{GoalQuery, RemoteGoalRepository}, value_objects::UserId, }; use k_ap::{ApContentReader, ApObjectHandler}; @@ -15,7 +15,7 @@ use crate::urls::{actor_url, goal_url}; pub struct GoalObjectHandler { pub remote_goal_repo: Arc, - pub goal_repo: Arc, + pub goal_repo: Arc, pub base_url: String, } diff --git a/crates/adapters/activitypub/src/lib.rs b/crates/adapters/activitypub/src/lib.rs index f82d63e..8c9ea3d 100644 --- a/crates/adapters/activitypub/src/lib.rs +++ b/crates/adapters/activitypub/src/lib.rs @@ -53,8 +53,8 @@ pub struct ActivityPubDeps { pub local_ap_content: std::sync::Arc, pub movie_repo: std::sync::Arc, pub review_repo: std::sync::Arc, - pub diary_repo: std::sync::Arc, - pub goal_repo: std::sync::Arc, + pub diary_repo: std::sync::Arc, + pub goal_repo: std::sync::Arc, pub stats_repo: std::sync::Arc, pub user_repo: std::sync::Arc, pub federation_settings: std::sync::Arc, diff --git a/crates/adapters/activitypub/src/review_handler.rs b/crates/adapters/activitypub/src/review_handler.rs index 4a5853e..b1e7d45 100644 --- a/crates/adapters/activitypub/src/review_handler.rs +++ b/crates/adapters/activitypub/src/review_handler.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use domain::{ events::DomainEvent, models::ReviewSource, - ports::{DiaryRepository, EventPublisher, LocalApContentQuery, MovieQuery}, + ports::{DiaryQuery, EventPublisher, LocalApContentQuery, MovieQuery}, value_objects::{Comment, ExternalMetadataId, MovieId, Rating, ReviewId, UserId}, }; use k_ap::{ApContentReader, ApObjectHandler}; @@ -17,7 +17,7 @@ use crate::urls::{actor_url, review_url}; pub struct ReviewObjectHandler { pub content_query: Arc, pub movie_repo: Arc, - pub diary_repo: Arc, + pub diary_repo: Arc, pub review_store: Arc, pub event_publisher: Arc, pub base_url: String, diff --git a/crates/adapters/postgres/src/diary.rs b/crates/adapters/postgres/src/diary.rs index 541d688..010a601 100644 --- a/crates/adapters/postgres/src/diary.rs +++ b/crates/adapters/postgres/src/diary.rs @@ -5,7 +5,7 @@ use domain::{ DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, ReviewSortBy, collections::{PageParams, Paginated}, }, - ports::DiaryRepository, + ports::DiaryQuery, value_objects::{MovieId, UserId}, }; use futures::stream::BoxStream; @@ -202,7 +202,7 @@ impl PostgresDiaryRepository { } #[async_trait] -impl DiaryRepository for PostgresDiaryRepository { +impl DiaryQuery for PostgresDiaryRepository { async fn query_diary( &self, filter: &DiaryFilter, diff --git a/crates/adapters/postgres/src/goals.rs b/crates/adapters/postgres/src/goals.rs index 8ad4fde..fd45ee7 100644 --- a/crates/adapters/postgres/src/goals.rs +++ b/crates/adapters/postgres/src/goals.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use domain::{ errors::DomainError, models::{Goal, GoalType}, - ports::GoalRepository, + ports::{GoalCommand, GoalQuery}, value_objects::{GoalId, UserId}, }; use sqlx::{PgPool, Row}; @@ -25,7 +25,7 @@ impl PostgresGoalRepository { } #[async_trait] -impl GoalRepository for PostgresGoalRepository { +impl GoalCommand for PostgresGoalRepository { async fn save(&self, goal: &Goal) -> Result<(), DomainError> { let id = goal.id().value().to_string(); let user_id = goal.user_id().value().to_string(); @@ -84,7 +84,10 @@ impl GoalRepository for PostgresGoalRepository { } Ok(()) } +} +#[async_trait] +impl GoalQuery for PostgresGoalRepository { async fn find_by_user_and_year( &self, user_id: &UserId, diff --git a/crates/adapters/postgres/src/lib.rs b/crates/adapters/postgres/src/lib.rs index e51790e..d11e177 100644 --- a/crates/adapters/postgres/src/lib.rs +++ b/crates/adapters/postgres/src/lib.rs @@ -82,7 +82,7 @@ pub struct PostgresWireOutput { pub movie_command: std::sync::Arc, pub movie_query: std::sync::Arc, pub review: std::sync::Arc, - pub diary: std::sync::Arc, + pub diary: std::sync::Arc, pub stats: std::sync::Arc, pub user: std::sync::Arc, pub import_session: std::sync::Arc, @@ -92,7 +92,8 @@ pub struct PostgresWireOutput { pub ap_content: std::sync::Arc, pub wrapup_repo: std::sync::Arc, pub wrapup_stats: std::sync::Arc, - pub goal: std::sync::Arc, + pub goal_command: std::sync::Arc, + pub goal_query: std::sync::Arc, pub user_settings: std::sync::Arc, pub federation_settings: std::sync::Arc, pub remote_goal: std::sync::Arc, @@ -134,7 +135,8 @@ pub async fn wire(database_url: &str) -> anyhow::Result { ap_content: std::sync::Arc::new(PostgresApContentQuery::new(pool.clone())) as _, wrapup_repo: std::sync::Arc::new(PostgresWrapUpRepository::new(pool.clone())) as _, wrapup_stats: std::sync::Arc::new(PostgresWrapUpStatsQuery::new(pool.clone())) as _, - goal: std::sync::Arc::new(goals::PostgresGoalRepository::new(pool.clone())) as _, + goal_command: std::sync::Arc::new(goals::PostgresGoalRepository::new(pool.clone())) as _, + goal_query: std::sync::Arc::new(goals::PostgresGoalRepository::new(pool.clone())) as _, user_settings: std::sync::Arc::clone(&user_settings_repo) as _, federation_settings: user_settings_repo as _, remote_goal: std::sync::Arc::new(postgres_federation::PostgresRemoteGoalRepository::new( diff --git a/crates/adapters/sqlite/src/diary.rs b/crates/adapters/sqlite/src/diary.rs index 0dc760d..e0ce51c 100644 --- a/crates/adapters/sqlite/src/diary.rs +++ b/crates/adapters/sqlite/src/diary.rs @@ -5,7 +5,7 @@ use domain::{ DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, ReviewSortBy, collections::{PageParams, Paginated}, }, - ports::DiaryRepository, + ports::DiaryQuery, value_objects::{MovieId, UserId}, }; use futures::stream::BoxStream; @@ -181,7 +181,7 @@ impl SqliteDiaryRepository { } #[async_trait] -impl DiaryRepository for SqliteDiaryRepository { +impl DiaryQuery for SqliteDiaryRepository { async fn query_diary( &self, filter: &DiaryFilter, diff --git a/crates/adapters/sqlite/src/goals.rs b/crates/adapters/sqlite/src/goals.rs index 42bc852..30bd37e 100644 --- a/crates/adapters/sqlite/src/goals.rs +++ b/crates/adapters/sqlite/src/goals.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use domain::{ errors::DomainError, models::{Goal, GoalType}, - ports::GoalRepository, + ports::{GoalCommand, GoalQuery}, value_objects::{GoalId, UserId}, }; use sqlx::{Row, SqlitePool}; @@ -23,7 +23,7 @@ impl SqliteGoalRepository { } #[async_trait] -impl GoalRepository for SqliteGoalRepository { +impl GoalCommand for SqliteGoalRepository { async fn save(&self, goal: &Goal) -> Result<(), DomainError> { let id = goal.id().value().to_string(); let user_id = goal.user_id().value().to_string(); @@ -82,7 +82,10 @@ impl GoalRepository for SqliteGoalRepository { } Ok(()) } +} +#[async_trait] +impl GoalQuery for SqliteGoalRepository { async fn find_by_user_and_year( &self, user_id: &UserId, diff --git a/crates/adapters/sqlite/src/lib.rs b/crates/adapters/sqlite/src/lib.rs index 5146e22..b29756e 100644 --- a/crates/adapters/sqlite/src/lib.rs +++ b/crates/adapters/sqlite/src/lib.rs @@ -78,7 +78,7 @@ pub struct SqliteWireOutput { pub movie_command: std::sync::Arc, pub movie_query: std::sync::Arc, pub review: std::sync::Arc, - pub diary: std::sync::Arc, + pub diary: std::sync::Arc, pub stats: std::sync::Arc, pub user: std::sync::Arc, pub import_session: std::sync::Arc, @@ -88,7 +88,8 @@ pub struct SqliteWireOutput { pub ap_content: std::sync::Arc, pub wrapup_repo: std::sync::Arc, pub wrapup_stats: std::sync::Arc, - pub goal: std::sync::Arc, + pub goal_command: std::sync::Arc, + pub goal_query: std::sync::Arc, pub user_settings: std::sync::Arc, pub federation_settings: std::sync::Arc, pub remote_goal: std::sync::Arc, @@ -137,7 +138,8 @@ pub async fn wire(database_url: &str) -> anyhow::Result { ap_content: std::sync::Arc::new(SqliteApContentQuery::new(pool.clone())) as _, wrapup_repo: std::sync::Arc::new(SqliteWrapUpRepository::new(pool.clone())) as _, wrapup_stats: std::sync::Arc::new(SqliteWrapUpStatsQuery::new(pool.clone())) as _, - goal: std::sync::Arc::new(goals::SqliteGoalRepository::new(pool.clone())) as _, + goal_command: std::sync::Arc::new(goals::SqliteGoalRepository::new(pool.clone())) as _, + goal_query: std::sync::Arc::new(goals::SqliteGoalRepository::new(pool.clone())) as _, user_settings: std::sync::Arc::clone(&user_settings_repo) as _, federation_settings: user_settings_repo as _, remote_goal: std::sync::Arc::new(sqlite_federation::SqliteRemoteGoalRepository::new( diff --git a/crates/adapters/sqlite/src/tests/diary.rs b/crates/adapters/sqlite/src/tests/diary.rs index d20692e..78c6660 100644 --- a/crates/adapters/sqlite/src/tests/diary.rs +++ b/crates/adapters/sqlite/src/tests/diary.rs @@ -2,7 +2,7 @@ use super::*; use domain::{ models::collections::PageParams, models::{FeedSortBy, FollowingFilter}, - ports::DiaryRepository, + ports::DiaryQuery, }; use sqlx::SqlitePool; diff --git a/crates/application/src/diary/deps.rs b/crates/application/src/diary/deps.rs index 2fe3aee..93d3271 100644 --- a/crates/application/src/diary/deps.rs +++ b/crates/application/src/diary/deps.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use domain::ports::{ - DiaryRepository, EventPublisher, MovieCommand, MovieProfileRepository, MovieQuery, + DiaryQuery, EventPublisher, MovieCommand, MovieProfileRepository, MovieQuery, ReviewRepository, SocialQueryPort, }; @@ -9,7 +9,7 @@ use crate::config::AppConfig; pub struct DeleteReviewDeps { pub review: Arc, - pub diary: Arc, + pub diary: Arc, pub movie_command: Arc, pub event_publisher: Arc, } @@ -21,12 +21,12 @@ pub struct EditReviewDeps { pub struct GetMovieSocialPageDeps { pub movie_query: Arc, - pub diary: Arc, + pub diary: Arc, pub movie_profile: Arc, } pub struct GetActivityFeedDeps { - pub diary: Arc, + pub diary: Arc, pub social_query: Arc, pub config: AppConfig, } diff --git a/crates/application/src/diary/export_diary.rs b/crates/application/src/diary/export_diary.rs index 4c80322..a04223e 100644 --- a/crates/application/src/diary/export_diary.rs +++ b/crates/application/src/diary/export_diary.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use bytes::Bytes; use domain::{ errors::DomainError, - ports::{DiaryExporter, DiaryRepository}, + ports::{DiaryExporter, DiaryQuery}, value_objects::UserId, }; use futures::stream::BoxStream; @@ -11,7 +11,7 @@ use futures::stream::BoxStream; use crate::diary::queries::ExportQuery; pub fn execute( - diary: &Arc, + diary: &Arc, diary_exporter: &Arc, query: ExportQuery, ) -> BoxStream<'static, Result> { diff --git a/crates/application/src/diary/get_diary.rs b/crates/application/src/diary/get_diary.rs index 68693ef..bc1cc05 100644 --- a/crates/application/src/diary/get_diary.rs +++ b/crates/application/src/diary/get_diary.rs @@ -6,14 +6,14 @@ use domain::{ DiaryEntry, DiaryFilter, ReviewSortBy, collections::{PageParams, Paginated}, }, - ports::DiaryRepository, + ports::DiaryQuery, value_objects::{MovieId, UserId}, }; use crate::diary::queries::GetDiaryQuery; pub async fn execute( - diary: &Arc, + diary: &Arc, query: GetDiaryQuery, ) -> Result, DomainError> { let page = PageParams::new(query.limit, query.offset)?; diff --git a/crates/application/src/diary/get_review_history.rs b/crates/application/src/diary/get_review_history.rs index e57541e..4782c55 100644 --- a/crates/application/src/diary/get_review_history.rs +++ b/crates/application/src/diary/get_review_history.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use domain::{ errors::DomainError, models::ReviewHistory, - ports::DiaryRepository, + ports::DiaryQuery, services::review_history::{ReviewHistoryAnalyzer, Trend}, value_objects::MovieId, }; @@ -11,7 +11,7 @@ use domain::{ use crate::diary::queries::GetReviewHistoryQuery; pub async fn execute( - diary: &Arc, + diary: &Arc, query: GetReviewHistoryQuery, ) -> Result<(ReviewHistory, Trend), DomainError> { let movie_id = MovieId::from_uuid(query.movie_id); diff --git a/crates/application/src/diary/tests/delete_review.rs b/crates/application/src/diary/tests/delete_review.rs index 565d12b..39b60cf 100644 --- a/crates/application/src/diary/tests/delete_review.rs +++ b/crates/application/src/diary/tests/delete_review.rs @@ -6,7 +6,7 @@ use domain::{ models::{Movie, Review}, ports::{MovieCommand, MovieQuery, ReviewRepository}, testing::{ - FakeDiaryRepository, InMemoryMovieRepository, InMemoryReviewRepository, NoopEventPublisher, + FakeDiaryQuery, InMemoryMovieRepository, InMemoryReviewRepository, NoopEventPublisher, }, value_objects::{MovieId, MovieTitle, Rating, ReleaseYear, UserId}, }; @@ -41,7 +41,7 @@ fn make_review(movie_id: MovieId, user_id: UserId) -> Review { async fn test_delete_review_removes_it() { let movies = InMemoryMovieRepository::new(); let reviews = InMemoryReviewRepository::new(); - let diary = FakeDiaryRepository::new(); + let diary = FakeDiaryQuery::new(); let events = NoopEventPublisher::new(); let movie = make_movie(); @@ -79,7 +79,7 @@ async fn test_delete_review_removes_it() { #[tokio::test] async fn test_delete_review_wrong_user_is_unauthorized() { let reviews = InMemoryReviewRepository::new(); - let diary = FakeDiaryRepository::new(); + let diary = FakeDiaryQuery::new(); let movies = InMemoryMovieRepository::new(); let events = NoopEventPublisher::new(); diff --git a/crates/application/src/diary/tests/get_activity_feed.rs b/crates/application/src/diary/tests/get_activity_feed.rs index 50fcea9..e677d61 100644 --- a/crates/application/src/diary/tests/get_activity_feed.rs +++ b/crates/application/src/diary/tests/get_activity_feed.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use async_trait::async_trait; use domain::errors::DomainError; -use domain::testing::{FakeDiaryRepository, NoopSocialQueryPort}; +use domain::testing::{FakeDiaryQuery, NoopSocialQueryPort}; use crate::{ config::AppConfig, diary::deps::GetActivityFeedDeps, diary::get_activity_feed, @@ -11,7 +11,7 @@ use crate::{ fn default_deps() -> GetActivityFeedDeps { GetActivityFeedDeps { - diary: FakeDiaryRepository::new() as _, + diary: FakeDiaryQuery::new() as _, social_query: Arc::new(NoopSocialQueryPort), config: TestContextBuilder::new().config, } @@ -112,7 +112,7 @@ async fn following_filter_parses_local_and_remote_urls() { let social = Arc::new(FakeSocialWithFollowing(following_urls)); let deps = GetActivityFeedDeps { - diary: FakeDiaryRepository::new() as _, + diary: FakeDiaryQuery::new() as _, social_query: social as _, config: AppConfig { allow_registration: true, diff --git a/crates/application/src/diary/tests/get_diary.rs b/crates/application/src/diary/tests/get_diary.rs index 47fcaf8..c6a0041 100644 --- a/crates/application/src/diary/tests/get_diary.rs +++ b/crates/application/src/diary/tests/get_diary.rs @@ -1,11 +1,11 @@ -use domain::testing::FakeDiaryRepository; +use domain::testing::FakeDiaryQuery; use std::sync::Arc; use crate::{diary::get_diary, diary::queries::GetDiaryQuery}; #[tokio::test] async fn returns_empty_page() { - let diary = FakeDiaryRepository::new() as Arc; + let diary = FakeDiaryQuery::new() as Arc; let result = get_diary::execute( &diary, diff --git a/crates/application/src/diary/tests/get_movie_social_page.rs b/crates/application/src/diary/tests/get_movie_social_page.rs index 394d03d..c378067 100644 --- a/crates/application/src/diary/tests/get_movie_social_page.rs +++ b/crates/application/src/diary/tests/get_movie_social_page.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use domain::{ models::Movie, ports::MovieCommand, - testing::{FakeDiaryRepository, InMemoryMovieProfileRepository, InMemoryMovieRepository}, + testing::{FakeDiaryQuery, InMemoryMovieProfileRepository, InMemoryMovieRepository}, value_objects::{MovieTitle, ReleaseYear}, }; @@ -18,7 +18,7 @@ use crate::{ async fn fails_when_movie_not_found() { let deps = GetMovieSocialPageDeps { movie_query: InMemoryMovieRepository::new(), - diary: FakeDiaryRepository::new() as _, + diary: FakeDiaryQuery::new() as _, movie_profile: InMemoryMovieProfileRepository::new(), }; @@ -51,7 +51,7 @@ async fn returns_movie_social_page() { let deps = GetMovieSocialPageDeps { movie_query: Arc::clone(&movies) as _, - diary: FakeDiaryRepository::new() as _, + diary: FakeDiaryQuery::new() as _, movie_profile: InMemoryMovieProfileRepository::new(), }; diff --git a/crates/application/src/diary/tests/get_review_history.rs b/crates/application/src/diary/tests/get_review_history.rs index da7043c..2e09e68 100644 --- a/crates/application/src/diary/tests/get_review_history.rs +++ b/crates/application/src/diary/tests/get_review_history.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use domain::{ models::Movie, - ports::DiaryRepository, + ports::DiaryQuery, services::review_history::Trend, value_objects::{MovieTitle, ReleaseYear}, }; @@ -20,9 +20,9 @@ async fn returns_empty_history() { ); let movie_id = movie.id().value(); - let diary = domain::testing::FakeDiaryRepository::new(); + let diary = domain::testing::FakeDiaryQuery::new(); diary.seed_history(movie, vec![]); - let diary: Arc = diary; + let diary: Arc = diary; let (history, trend) = get_review_history::execute(&diary, GetReviewHistoryQuery { movie_id }) .await diff --git a/crates/application/src/goals/create.rs b/crates/application/src/goals/create.rs index bf8b9a9..f9b0336 100644 --- a/crates/application/src/goals/create.rs +++ b/crates/application/src/goals/create.rs @@ -13,7 +13,7 @@ pub async fn execute( ) -> Result { let user_id = UserId::from_uuid(cmd.user_id); - let existing = deps.goal.find_by_user_and_year(&user_id, cmd.year).await?; + let existing = deps.goal_query.find_by_user_and_year(&user_id, cmd.year).await?; if existing.is_some() { return Err(DomainError::ValidationError( "Goal already exists for this year".into(), @@ -26,7 +26,7 @@ pub async fn execute( cmd.target_count, GoalType::Movies, )?; - deps.goal.save(&g).await?; + deps.goal_command.save(&g).await?; let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?; diff --git a/crates/application/src/goals/delete.rs b/crates/application/src/goals/delete.rs index 526cbec..ab25278 100644 --- a/crates/application/src/goals/delete.rs +++ b/crates/application/src/goals/delete.rs @@ -6,12 +6,12 @@ pub async fn execute(deps: &GoalCommandDeps, cmd: DeleteGoalCommand) -> Result<( let user_id = UserId::from_uuid(cmd.user_id); let g = deps - .goal + .goal_query .find_by_user_and_year(&user_id, cmd.year) .await? .ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?; - deps.goal.delete(g.id(), &user_id).await?; + deps.goal_command.delete(g.id(), &user_id).await?; deps.event_publisher .publish(&DomainEvent::GoalDeleted { diff --git a/crates/application/src/goals/deps.rs b/crates/application/src/goals/deps.rs index d181399..cfe4b9a 100644 --- a/crates/application/src/goals/deps.rs +++ b/crates/application/src/goals/deps.rs @@ -1,14 +1,15 @@ use std::sync::Arc; -use domain::ports::{EventPublisher, GoalRepository, StatsRepository}; +use domain::ports::{EventPublisher, GoalCommand, GoalQuery, StatsRepository}; pub struct GoalCommandDeps { - pub goal: Arc, + pub goal_command: Arc, + pub goal_query: Arc, pub stats: Arc, pub event_publisher: Arc, } pub struct GoalQueryDeps { - pub goal: Arc, + pub goal_query: Arc, pub stats: Arc, } diff --git a/crates/application/src/goals/get.rs b/crates/application/src/goals/get.rs index 4fe7bbe..6e8db79 100644 --- a/crates/application/src/goals/get.rs +++ b/crates/application/src/goals/get.rs @@ -9,7 +9,7 @@ pub async fn execute( let user_id = UserId::from_uuid(query.user_id); let found = deps - .goal + .goal_query .find_by_user_and_year(&user_id, query.year) .await?; diff --git a/crates/application/src/goals/list.rs b/crates/application/src/goals/list.rs index b793b3b..10cbeac 100644 --- a/crates/application/src/goals/list.rs +++ b/crates/application/src/goals/list.rs @@ -7,7 +7,7 @@ pub async fn execute( query: ListGoalsQuery, ) -> Result, DomainError> { let user_id = UserId::from_uuid(query.user_id); - let goals = deps.goal.list_for_user(&user_id).await?; + let goals = deps.goal_query.list_for_user(&user_id).await?; let mut result = Vec::with_capacity(goals.len()); for g in goals { diff --git a/crates/application/src/goals/tests/create.rs b/crates/application/src/goals/tests/create.rs index cb762c4..7a9f978 100644 --- a/crates/application/src/goals/tests/create.rs +++ b/crates/application/src/goals/tests/create.rs @@ -14,7 +14,8 @@ async fn creates_goal_and_returns_progress() { let stats = FakeStatsRepository::new(); let events = NoopEventPublisher::new(); let deps = GoalCommandDeps { - goal: Arc::clone(&goals) as _, + goal_command: Arc::clone(&goals) as _, + goal_query: Arc::clone(&goals) as _, stats: Arc::clone(&stats) as _, event_publisher: Arc::clone(&events) as _, }; @@ -43,7 +44,8 @@ async fn creates_goal_with_review_count() { stats.set_review_count(Uuid::nil(), 2025, 5); let events = NoopEventPublisher::new(); let deps = GoalCommandDeps { - goal: Arc::clone(&goals) as _, + goal_command: Arc::clone(&goals) as _, + goal_query: Arc::clone(&goals) as _, stats: Arc::clone(&stats) as _, event_publisher: Arc::clone(&events) as _, }; @@ -68,7 +70,8 @@ async fn emits_goal_created_event() { let b = TestContextBuilder::new(); let events = NoopEventPublisher::new(); let deps = GoalCommandDeps { - goal: b.goal_repo.clone(), + goal_command: b.goal_command.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), event_publisher: Arc::clone(&events) as _, }; @@ -96,7 +99,8 @@ async fn emits_goal_created_event() { async fn rejects_duplicate_year() { let b = TestContextBuilder::new(); let deps = GoalCommandDeps { - goal: b.goal_repo.clone(), + goal_command: b.goal_command.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; @@ -125,7 +129,8 @@ async fn rejects_duplicate_year() { async fn rejects_year_before_2020() { let b = TestContextBuilder::new(); let deps = GoalCommandDeps { - goal: b.goal_repo.clone(), + goal_command: b.goal_command.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; @@ -146,7 +151,8 @@ async fn rejects_year_before_2020() { async fn rejects_zero_target() { let b = TestContextBuilder::new(); let deps = GoalCommandDeps { - goal: b.goal_repo.clone(), + goal_command: b.goal_command.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; diff --git a/crates/application/src/goals/tests/delete.rs b/crates/application/src/goals/tests/delete.rs index 7f82ac7..408cbdf 100644 --- a/crates/application/src/goals/tests/delete.rs +++ b/crates/application/src/goals/tests/delete.rs @@ -16,7 +16,8 @@ async fn deletes_existing_goal() { let stats = FakeStatsRepository::new(); let events = NoopEventPublisher::new(); let deps = GoalCommandDeps { - goal: Arc::clone(&goals) as _, + goal_command: Arc::clone(&goals) as _, + goal_query: Arc::clone(&goals) as _, stats: Arc::clone(&stats) as _, event_publisher: Arc::clone(&events) as _, }; @@ -50,7 +51,8 @@ async fn deletes_existing_goal() { async fn fails_when_not_found() { let b = TestContextBuilder::new(); let deps = GoalCommandDeps { - goal: b.goal_repo.clone(), + goal_command: b.goal_command.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; diff --git a/crates/application/src/goals/tests/get.rs b/crates/application/src/goals/tests/get.rs index 42c983d..8ffb37b 100644 --- a/crates/application/src/goals/tests/get.rs +++ b/crates/application/src/goals/tests/get.rs @@ -8,12 +8,13 @@ use crate::test_helpers::TestContextBuilder; async fn returns_goal_when_exists() { let b = TestContextBuilder::new(); let cmd_deps = GoalCommandDeps { - goal: b.goal_repo.clone(), + goal_command: b.goal_command.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; let query_deps = GoalQueryDeps { - goal: b.goal_repo.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), }; @@ -46,7 +47,7 @@ async fn returns_goal_when_exists() { async fn returns_none_when_missing() { let b = TestContextBuilder::new(); let query_deps = GoalQueryDeps { - goal: b.goal_repo.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), }; let result = get::execute( diff --git a/crates/application/src/goals/tests/list.rs b/crates/application/src/goals/tests/list.rs index b320f17..3edb380 100644 --- a/crates/application/src/goals/tests/list.rs +++ b/crates/application/src/goals/tests/list.rs @@ -8,7 +8,7 @@ use crate::test_helpers::TestContextBuilder; async fn returns_empty_when_no_goals() { let b = TestContextBuilder::new(); let query_deps = GoalQueryDeps { - goal: b.goal_repo.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), }; let result = list::execute( @@ -27,12 +27,13 @@ async fn returns_empty_when_no_goals() { async fn returns_all_goals_for_user() { let b = TestContextBuilder::new(); let cmd_deps = GoalCommandDeps { - goal: b.goal_repo.clone(), + goal_command: b.goal_command.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; let query_deps = GoalQueryDeps { - goal: b.goal_repo.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), }; diff --git a/crates/application/src/goals/tests/update.rs b/crates/application/src/goals/tests/update.rs index 5cec9db..e973ee5 100644 --- a/crates/application/src/goals/tests/update.rs +++ b/crates/application/src/goals/tests/update.rs @@ -11,7 +11,8 @@ use crate::test_helpers::TestContextBuilder; async fn updates_target_count() { let b = TestContextBuilder::new(); let deps = GoalCommandDeps { - goal: b.goal_repo.clone(), + goal_command: b.goal_command.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; @@ -45,7 +46,8 @@ async fn updates_target_count() { async fn fails_when_goal_not_found() { let b = TestContextBuilder::new(); let deps = GoalCommandDeps { - goal: b.goal_repo.clone(), + goal_command: b.goal_command.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; @@ -66,7 +68,8 @@ async fn fails_when_goal_not_found() { async fn rejects_zero_target() { let b = TestContextBuilder::new(); let deps = GoalCommandDeps { - goal: b.goal_repo.clone(), + goal_command: b.goal_command.clone(), + goal_query: b.goal_query.clone(), stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; diff --git a/crates/application/src/goals/update.rs b/crates/application/src/goals/update.rs index 16e6df9..f91db40 100644 --- a/crates/application/src/goals/update.rs +++ b/crates/application/src/goals/update.rs @@ -11,13 +11,13 @@ pub async fn execute( let user_id = UserId::from_uuid(cmd.user_id); let mut g = deps - .goal + .goal_query .find_by_user_and_year(&user_id, cmd.year) .await? .ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?; g.update_target(cmd.target_count)?; - deps.goal.update(&g).await?; + deps.goal_command.update(&g).await?; let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?; diff --git a/crates/application/src/test_helpers.rs b/crates/application/src/test_helpers.rs index 44eb2ae..cae06fa 100644 --- a/crates/application/src/test_helpers.rs +++ b/crates/application/src/test_helpers.rs @@ -5,8 +5,8 @@ use domain::testing::{ }; use domain::{ ports::{ - AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher, - GoalRepository, ImportProfileRepository, ImportSessionRepository, MetadataClient, + AuthService, DiaryExporter, DiaryQuery, DocumentParser, EventPublisher, + GoalCommand, GoalQuery, ImportProfileRepository, ImportSessionRepository, MetadataClient, MovieCommand, MovieProfileRepository, MovieQuery, ObjectStorage, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient, RefreshSessionRepository, ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserProfileFieldsRepository, @@ -14,7 +14,7 @@ use domain::{ WatchlistRepository, WebhookTokenRepository, WrapUpRepository, WrapUpStatsQuery, }, testing::{ - FakeAuthService, FakeDiaryRepository, FakeDocumentParser, FakeMetadataClient, + FakeAuthService, FakeDiaryQuery, FakeDocumentParser, FakeMetadataClient, FakePasswordHasher, FakePersonQuery, FakePosterFetcher, FakeSearchCommand, FakeSearchPort, FakeStatsRepository, InMemoryImportProfileRepository, InMemoryImportSessionRepository, InMemoryMovieProfileRepository, InMemoryMovieRepository, InMemoryProfileFieldsRepo, @@ -43,7 +43,7 @@ pub struct TestContextBuilder { pub movie_command: Arc, pub movie_query: Arc, pub review_repo: Arc, - pub diary_repo: Arc, + pub diary_repo: Arc, pub diary_exporter: Arc, pub document_parser: Arc, pub stats_repo: Arc, @@ -68,7 +68,8 @@ pub struct TestContextBuilder { pub search_command: Arc, pub wrapup_stats: Arc, pub wrapup_repo: Arc, - pub goal_repo: Arc, + pub goal_command: Arc, + pub goal_query: Arc, pub user_settings_repo: Arc, pub review_logger: Arc, pub social_query: Arc, @@ -84,11 +85,14 @@ impl Default for TestContextBuilder { impl TestContextBuilder { pub fn new() -> Self { + let movies = InMemoryMovieRepository::new(); + let watch_events = InMemoryWatchEventRepository::new(); + let goals = InMemoryGoalRepository::new(); Self { - movie_command: InMemoryMovieRepository::new(), - movie_query: InMemoryMovieRepository::new(), + movie_command: Arc::clone(&movies) as _, + movie_query: movies as _, review_repo: InMemoryReviewRepository::new(), - diary_repo: FakeDiaryRepository::new(), + diary_repo: FakeDiaryQuery::new(), diary_exporter: Arc::new(PanicDiaryExporter), document_parser: Arc::new(FakeDocumentParser), stats_repo: FakeStatsRepository::new(), @@ -103,8 +107,8 @@ impl TestContextBuilder { import_profile_repo: InMemoryImportProfileRepository::new(), movie_profile_repo: InMemoryMovieProfileRepository::new(), watchlist_repo: InMemoryWatchlistRepository::new(), - watch_event_command: InMemoryWatchEventRepository::new(), - watch_event_query: InMemoryWatchEventRepository::new(), + watch_event_command: Arc::clone(&watch_events) as _, + watch_event_query: watch_events as _, webhook_token_repo: InMemoryWebhookTokenRepository::new(), profile_fields_repo: InMemoryProfileFieldsRepo::new(), person_command: Arc::new(PanicPersonCommand), @@ -113,7 +117,8 @@ impl TestContextBuilder { search_command: Arc::new(FakeSearchCommand), wrapup_stats: InMemoryWrapUpStatsQuery::new(), wrapup_repo: InMemoryWrapUpRepository::new(), - goal_repo: InMemoryGoalRepository::new(), + goal_command: Arc::clone(&goals) as _, + goal_query: goals as _, user_settings_repo: InMemoryUserSettingsRepository::new(), review_logger: Arc::new(NoopReviewLogger), social_query: Arc::new(NoopSocialQueryPort), @@ -157,7 +162,7 @@ impl TestContextBuilder { self } - pub fn with_diary(mut self, r: Arc) -> Self { + pub fn with_diary(mut self, r: Arc) -> Self { self.diary_repo = r; self } @@ -172,8 +177,13 @@ impl TestContextBuilder { self } - pub fn with_goal(mut self, r: Arc) -> Self { - self.goal_repo = r; + pub fn with_goal_command(mut self, r: Arc) -> Self { + self.goal_command = r; + self + } + + pub fn with_goal_query(mut self, r: Arc) -> Self { + self.goal_query = r; self } diff --git a/crates/application/src/users/deps.rs b/crates/application/src/users/deps.rs index c57b864..c0fb987 100644 --- a/crates/application/src/users/deps.rs +++ b/crates/application/src/users/deps.rs @@ -1,13 +1,13 @@ use std::sync::Arc; use domain::ports::{ - DiaryRepository, EventPublisher, ObjectStorage, SocialQueryPort, StatsRepository, + DiaryQuery, EventPublisher, ObjectStorage, SocialQueryPort, StatsRepository, UserRepository, }; pub struct GetProfileDeps { pub stats: Arc, - pub diary: Arc, + pub diary: Arc, pub social_query: Arc, } diff --git a/crates/domain/src/ports/diary.rs b/crates/domain/src/ports/diary.rs index bdef1ef..5c742f4 100644 --- a/crates/domain/src/ports/diary.rs +++ b/crates/domain/src/ports/diary.rs @@ -11,7 +11,7 @@ use crate::{ }; #[async_trait] -pub trait DiaryRepository: Send + Sync { +pub trait DiaryQuery: Send + Sync { async fn query_diary(&self, filter: &DiaryFilter) -> Result, DomainError>; async fn query_activity_feed( diff --git a/crates/domain/src/ports/goals.rs b/crates/domain/src/ports/goals.rs index 5fac75a..c7691f4 100644 --- a/crates/domain/src/ports/goals.rs +++ b/crates/domain/src/ports/goals.rs @@ -7,10 +7,14 @@ use crate::{ }; #[async_trait] -pub trait GoalRepository: Send + Sync { +pub trait GoalCommand: Send + Sync { async fn save(&self, goal: &Goal) -> Result<(), DomainError>; async fn update(&self, goal: &Goal) -> Result<(), DomainError>; async fn delete(&self, id: &GoalId, user_id: &UserId) -> Result<(), DomainError>; +} + +#[async_trait] +pub trait GoalQuery: Send + Sync { async fn find_by_user_and_year( &self, user_id: &UserId, diff --git a/crates/domain/src/ports/mod.rs b/crates/domain/src/ports/mod.rs index 2d951c6..71d6dc0 100644 --- a/crates/domain/src/ports/mod.rs +++ b/crates/domain/src/ports/mod.rs @@ -9,6 +9,7 @@ pub mod import; pub mod jobs; pub mod media_server; pub mod movie; +pub mod noop; pub mod person; pub mod rss; pub mod search; diff --git a/crates/domain/src/ports/noop.rs b/crates/domain/src/ports/noop.rs new file mode 100644 index 0000000..0323d8f --- /dev/null +++ b/crates/domain/src/ports/noop.rs @@ -0,0 +1,65 @@ +use async_trait::async_trait; + +use crate::{errors::DomainError, value_objects::UserId}; + +// ── NoopRemoteWatchlistRepository ───────────────────────────────────────────── + +/// Stub used when federation is disabled — every operation is a no-op. +pub struct NoopRemoteWatchlistRepository; + +#[async_trait] +impl super::RemoteWatchlistRepository for NoopRemoteWatchlistRepository { + async fn save(&self, _: crate::models::RemoteWatchlistEntry) -> Result<(), DomainError> { + Ok(()) + } + async fn remove_by_ap_id(&self, _: &str, _: &str) -> Result<(), DomainError> { + Ok(()) + } + async fn get_by_actor_url( + &self, + _: &str, + ) -> Result, DomainError> { + Ok(vec![]) + } + async fn remove_all_by_actor(&self, _: &str) -> Result<(), DomainError> { + Ok(()) + } + async fn get_by_derived_uuid( + &self, + _: uuid::Uuid, + ) -> Result, DomainError> { + Ok(vec![]) + } +} + +// ── NoopSocialQueryPort ─────────────────────────────────────────────────────── + +/// Stub used when federation is disabled — returns empty results. +pub struct NoopSocialQueryPort; + +#[async_trait] +impl super::SocialQueryPort for NoopSocialQueryPort { + async fn get_accepted_following_urls( + &self, + _: &UserId, + ) -> Result, DomainError> { + Ok(vec![]) + } + async fn list_all_followed_remote_actors( + &self, + ) -> Result, DomainError> { + Ok(vec![]) + } + async fn count_following(&self, _: &UserId) -> Result { + Ok(0) + } + async fn count_accepted_followers(&self, _: &UserId) -> Result { + Ok(0) + } + async fn get_pending_followers( + &self, + _: &UserId, + ) -> Result, DomainError> { + Ok(vec![]) + } +} diff --git a/crates/domain/src/testing/fakes.rs b/crates/domain/src/testing/fakes.rs index 3f61655..97428a5 100644 --- a/crates/domain/src/testing/fakes.rs +++ b/crates/domain/src/testing/fakes.rs @@ -16,7 +16,7 @@ use crate::{ collections::{PageParams, Paginated}, }, ports::{ - AuthService, DiaryRepository, DocumentParser, MetadataClient, MovieEnrichmentClient, + AuthService, DiaryQuery, DocumentParser, MetadataClient, MovieEnrichmentClient, PasswordHasher, PersonQuery, PosterFetcherClient, SearchCommand, SearchPort, StatsRepository, }, @@ -81,13 +81,13 @@ impl MetadataClient for FakeMetadataClient { } } -// ── FakeDiaryRepository ─────────────────────────────────────────────────────── +// ── FakeDiaryQuery ──────────────────────────────────────────────────────────── -pub struct FakeDiaryRepository { +pub struct FakeDiaryQuery { histories: Mutex)>>, } -impl FakeDiaryRepository { +impl FakeDiaryQuery { pub fn new() -> Arc { Arc::new(Self { histories: Mutex::new(HashMap::new()), @@ -103,7 +103,7 @@ impl FakeDiaryRepository { } #[async_trait] -impl DiaryRepository for FakeDiaryRepository { +impl DiaryQuery for FakeDiaryQuery { async fn query_diary( &self, _filter: &DiaryFilter, diff --git a/crates/domain/src/testing/in_memory.rs b/crates/domain/src/testing/in_memory.rs index 3c7359a..515294c 100644 --- a/crates/domain/src/testing/in_memory.rs +++ b/crates/domain/src/testing/in_memory.rs @@ -17,7 +17,7 @@ use crate::{ collections::{PageParams, Paginated}, }, ports::{ - GoalRepository, ImportProfileRepository, ImportSessionRepository, MovieCommand, + GoalCommand, GoalQuery, ImportProfileRepository, ImportSessionRepository, MovieCommand, MovieProfileRepository, MovieQuery, RefreshSessionRepository, ReviewRepository, UserFederationSettingsQuery, UserProfileFieldsRepository, UserRepository, UserSettingsRepository, WatchEventCommand, WatchEventQuery, WatchlistRepository, @@ -357,7 +357,7 @@ impl InMemoryGoalRepository { } #[async_trait] -impl GoalRepository for InMemoryGoalRepository { +impl GoalCommand for InMemoryGoalRepository { async fn save(&self, goal: &Goal) -> Result<(), DomainError> { self.store .lock() @@ -383,7 +383,10 @@ impl GoalRepository for InMemoryGoalRepository { self.store.lock().unwrap().remove(&id.value()); Ok(()) } +} +#[async_trait] +impl GoalQuery for InMemoryGoalRepository { async fn find_by_user_and_year( &self, user_id: &UserId, diff --git a/crates/domain/src/testing/noops.rs b/crates/domain/src/testing/noops.rs index 0be1df1..3c443a8 100644 --- a/crates/domain/src/testing/noops.rs +++ b/crates/domain/src/testing/noops.rs @@ -62,78 +62,17 @@ impl ObjectStorage for NoopObjectStorage { } } -// ── NoopRemoteWatchlistRepository ───────────────────────────────────────────── +// Re-export production noop types so test code that imports from +// `domain::testing` keeps compiling without changes. +pub use crate::ports::noop::NoopRemoteWatchlistRepository; +pub use crate::ports::noop::NoopSocialQueryPort; -pub struct NoopRemoteWatchlistRepository; +// ── NoopGoalCommand ─────────────────────────────────────────────────────────── + +pub struct NoopGoalCommand; #[async_trait] -impl crate::ports::RemoteWatchlistRepository for NoopRemoteWatchlistRepository { - async fn save(&self, _: crate::models::RemoteWatchlistEntry) -> Result<(), DomainError> { - Ok(()) - } - async fn remove_by_ap_id(&self, _: &str, _: &str) -> Result<(), DomainError> { - Ok(()) - } - async fn get_by_actor_url( - &self, - _: &str, - ) -> Result, DomainError> { - Ok(vec![]) - } - async fn remove_all_by_actor(&self, _: &str) -> Result<(), DomainError> { - Ok(()) - } - async fn get_by_derived_uuid( - &self, - _: uuid::Uuid, - ) -> Result, DomainError> { - Ok(vec![]) - } -} - -// ── NoopSocialQueryPort ─────────────────────────────────────────────────────── - -pub struct NoopSocialQueryPort; - -#[async_trait] -impl crate::ports::SocialQueryPort for NoopSocialQueryPort { - async fn get_accepted_following_urls( - &self, - _: &crate::value_objects::UserId, - ) -> Result, DomainError> { - Ok(vec![]) - } - async fn list_all_followed_remote_actors( - &self, - ) -> Result, DomainError> { - Ok(vec![]) - } - async fn count_following( - &self, - _: &crate::value_objects::UserId, - ) -> Result { - Ok(0) - } - async fn count_accepted_followers( - &self, - _: &crate::value_objects::UserId, - ) -> Result { - Ok(0) - } - async fn get_pending_followers( - &self, - _: &crate::value_objects::UserId, - ) -> Result, DomainError> { - Ok(vec![]) - } -} - -// ── NoopGoalRepository ──────────────────────────────────────────────────────── - -pub struct NoopGoalRepository; - -#[async_trait] -impl crate::ports::GoalRepository for NoopGoalRepository { +impl crate::ports::GoalCommand for NoopGoalCommand { async fn save(&self, _: &crate::models::Goal) -> Result<(), DomainError> { Ok(()) } @@ -147,6 +86,14 @@ impl crate::ports::GoalRepository for NoopGoalRepository { ) -> Result<(), DomainError> { Ok(()) } +} + +// ── NoopGoalQuery ───────────────────────────────────────────────────────────── + +pub struct NoopGoalQuery; + +#[async_trait] +impl crate::ports::GoalQuery for NoopGoalQuery { async fn find_by_user_and_year( &self, _: &UserId, diff --git a/crates/domain/src/testing/panics.rs b/crates/domain/src/testing/panics.rs index 7233daf..17f96b8 100644 --- a/crates/domain/src/testing/panics.rs +++ b/crates/domain/src/testing/panics.rs @@ -11,7 +11,7 @@ use crate::{ collections::{PageParams, Paginated}, }, ports::{ - DiaryExporter, DiaryRepository, DocumentParser, ImportProfileRepository, + DiaryExporter, DiaryQuery, DocumentParser, ImportProfileRepository, ImportSessionRepository, MovieProfileRepository, PersonCommand, PersonQuery, PosterFetcherClient, RefreshSessionRepository, SearchCommand, SearchPort, StatsRepository, UserProfileFieldsRepository, @@ -19,20 +19,20 @@ use crate::{ value_objects::{ImportProfileId, ImportSessionId, MovieId, PosterUrl, UserId}, }; -// ── PanicDiaryRepository ────────────────────────────────────────────────────── +// ── PanicDiaryQuery ─────────────────────────────────────────────────────────── -pub struct PanicDiaryRepository; +pub struct PanicDiaryQuery; #[async_trait] -impl DiaryRepository for PanicDiaryRepository { +impl DiaryQuery for PanicDiaryQuery { async fn query_diary(&self, _: &DiaryFilter) -> Result, DomainError> { - panic!("PanicDiaryRepository called") + panic!("PanicDiaryQuery called") } async fn query_activity_feed( &self, _: &PageParams, ) -> Result, DomainError> { - panic!("PanicDiaryRepository called") + panic!("PanicDiaryQuery called") } async fn query_activity_feed_filtered( &self, @@ -41,32 +41,32 @@ impl DiaryRepository for PanicDiaryRepository { _: Option<&str>, _: Option<&FollowingFilter>, ) -> Result, DomainError> { - panic!("PanicDiaryRepository called") + panic!("PanicDiaryQuery called") } async fn get_review_history(&self, _: &MovieId) -> Result { - panic!("PanicDiaryRepository called") + panic!("PanicDiaryQuery called") } async fn get_user_history(&self, _: &UserId) -> Result, DomainError> { - panic!("PanicDiaryRepository called") + panic!("PanicDiaryQuery called") } fn stream_user_history( &self, _: UserId, ) -> futures::stream::BoxStream<'static, Result> { - panic!("PanicDiaryRepository called") + panic!("PanicDiaryQuery called") } async fn get_movie_stats(&self, _: &MovieId) -> Result { - panic!("PanicDiaryRepository called") + panic!("PanicDiaryQuery called") } async fn get_movie_social_feed( &self, _: &MovieId, _: &PageParams, ) -> Result, DomainError> { - panic!("PanicDiaryRepository called") + panic!("PanicDiaryQuery called") } async fn count_local_posts(&self) -> Result { - panic!("PanicDiaryRepository called") + panic!("PanicDiaryQuery called") } } diff --git a/crates/presentation/Cargo.toml b/crates/presentation/Cargo.toml index f1a263c..2184bb2 100644 --- a/crates/presentation/Cargo.toml +++ b/crates/presentation/Cargo.toml @@ -45,7 +45,7 @@ async-trait = { workspace = true } futures = { workspace = true } api-types = { workspace = true } -domain = { workspace = true, features = ["test-helpers"] } +domain = { workspace = true } application = { workspace = true } auth = { workspace = true } metadata = { workspace = true } diff --git a/crates/presentation/src/context.rs b/crates/presentation/src/context.rs index 5217dae..5d61bda 100644 --- a/crates/presentation/src/context.rs +++ b/crates/presentation/src/context.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use domain::ports::{ - AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher, - FederatedProfileQuery, GoalRepository, ImportProfileRepository, ImportSessionRepository, + AuthService, DiaryExporter, DiaryQuery, DocumentParser, EventPublisher, + FederatedProfileQuery, GoalCommand, GoalQuery, ImportProfileRepository, ImportSessionRepository, MetadataClient, MovieCommand, MovieProfileRepository, MovieQuery, ObjectStorage, PasswordHasher, PersonCommand, PersonEnrichmentClient, PersonQuery, PosterFetcherClient, RefreshSessionRepository, RemoteGoalRepository, RemoteWatchlistRepository, ReviewRepository, @@ -19,7 +19,7 @@ pub struct Repositories { pub movie_command: Arc, pub movie_query: Arc, pub review: Arc, - pub diary: Arc, + pub diary: Arc, pub stats: Arc, pub user: Arc, pub import_session: Arc, @@ -38,7 +38,8 @@ pub struct Repositories { pub social_query: Arc, pub wrapup_stats: Arc, pub wrapup_repo: Arc, - pub goal: Arc, + pub goal_command: Arc, + pub goal_query: Arc, pub user_settings: Arc, pub remote_goal: Arc, pub refresh_session: Arc, diff --git a/crates/presentation/src/factory.rs b/crates/presentation/src/factory.rs index a78a261..f201e5a 100644 --- a/crates/presentation/src/factory.rs +++ b/crates/presentation/src/factory.rs @@ -13,7 +13,7 @@ pub struct DatabaseOutput { pub movie_command: Arc, pub movie_query: Arc, pub review: Arc, - pub diary: Arc, + pub diary: Arc, pub stats: Arc, pub user: Arc, pub import_session: Arc, @@ -31,7 +31,8 @@ pub struct DatabaseOutput { pub ap_content: Arc, pub wrapup_stats: Arc, pub wrapup_repo: Arc, - pub goal: Arc, + pub goal_command: Arc, + pub goal_query: Arc, pub user_settings: Arc, pub federation_settings: std::sync::Arc, pub remote_goal: Arc, @@ -75,7 +76,8 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result ap_content: w.ap_content, wrapup_stats: w.wrapup_stats, wrapup_repo: w.wrapup_repo, - goal: w.goal, + goal_command: w.goal_command, + goal_query: w.goal_query, user_settings: w.user_settings, federation_settings: w.federation_settings, remote_goal: w.remote_goal, @@ -118,7 +120,8 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result ap_content: w.ap_content, wrapup_stats: w.wrapup_stats, wrapup_repo: w.wrapup_repo, - goal: w.goal, + goal_command: w.goal_command, + goal_query: w.goal_query, user_settings: w.user_settings, federation_settings: w.federation_settings, remote_goal: w.remote_goal, diff --git a/crates/presentation/src/handlers/goals.rs b/crates/presentation/src/handlers/goals.rs index c9f6c07..9a02c7b 100644 --- a/crates/presentation/src/handlers/goals.rs +++ b/crates/presentation/src/handlers/goals.rs @@ -40,7 +40,7 @@ pub async fn list_goals( user: AuthenticatedUser, ) -> Result, ApiError> { let deps = GoalQueryDeps { - goal: state.app_ctx.repos.goal.clone(), + goal_query: state.app_ctx.repos.goal_query.clone(), stats: state.app_ctx.repos.stats.clone(), }; let goals = application::goals::list::execute( @@ -70,7 +70,8 @@ pub async fn create_goal( Json(req): Json, ) -> Result, ApiError> { let deps = GoalCommandDeps { - goal: state.app_ctx.repos.goal.clone(), + goal_command: state.app_ctx.repos.goal_command.clone(), + goal_query: state.app_ctx.repos.goal_query.clone(), stats: state.app_ctx.repos.stats.clone(), event_publisher: state.app_ctx.services.event_publisher.clone(), }; @@ -103,7 +104,8 @@ pub async fn update_goal( Json(req): Json, ) -> Result, ApiError> { let deps = GoalCommandDeps { - goal: state.app_ctx.repos.goal.clone(), + goal_command: state.app_ctx.repos.goal_command.clone(), + goal_query: state.app_ctx.repos.goal_query.clone(), stats: state.app_ctx.repos.stats.clone(), event_publisher: state.app_ctx.services.event_publisher.clone(), }; @@ -134,7 +136,8 @@ pub async fn delete_goal( Path(year): Path, ) -> Result { let deps = GoalCommandDeps { - goal: state.app_ctx.repos.goal.clone(), + goal_command: state.app_ctx.repos.goal_command.clone(), + goal_query: state.app_ctx.repos.goal_query.clone(), stats: state.app_ctx.repos.stats.clone(), event_publisher: state.app_ctx.services.event_publisher.clone(), }; @@ -163,7 +166,7 @@ pub async fn get_user_goals( Path(user_id): Path, ) -> Result, ApiError> { let deps = GoalQueryDeps { - goal: state.app_ctx.repos.goal.clone(), + goal_query: state.app_ctx.repos.goal_query.clone(), stats: state.app_ctx.repos.stats.clone(), }; let goals = application::goals::list::execute( diff --git a/crates/presentation/src/handlers/users.rs b/crates/presentation/src/handlers/users.rs index 9f34624..15f8bbb 100644 --- a/crates/presentation/src/handlers/users.rs +++ b/crates/presentation/src/handlers/users.rs @@ -351,7 +351,7 @@ pub async fn get_user_profile( goals: { let goals_list = application::goals::list::execute( &application::goals::deps::GoalQueryDeps { - goal: state.app_ctx.repos.goal.clone(), + goal_query: state.app_ctx.repos.goal_query.clone(), stats: state.app_ctx.repos.stats.clone(), }, application::goals::queries::ListGoalsQuery { user_id }, @@ -531,6 +531,107 @@ pub async fn get_user_by_username( } } +// ── Profile helpers (private) ─────────────────────────────────────────────── + +struct PaginationInfo { + offset: u32, + has_more: bool, + limit: u32, + page_items: Vec, +} + +fn compute_pagination( + entries: Option<&domain::models::collections::Paginated>, +) -> PaginationInfo { + let (offset, has_more, limit) = entries + .map(|e| { + let has_more = (e.offset as u64).saturating_add(e.limit as u64) < e.total_count; + (e.offset, has_more, e.limit) + }) + .unwrap_or((0, false, super::DEFAULT_PAGE_LIMIT)); + let total = entries.map(|e| e.total_count as u32).unwrap_or(0); + let total_pages = total + .saturating_add(limit.saturating_sub(1)) + .checked_div(limit) + .unwrap_or(1); + let current_page = offset.checked_div(limit).unwrap_or(0); + let page_items = build_page_items(total_pages, current_page); + PaginationInfo { + offset, + has_more, + limit, + page_items, + } +} + +struct StatsDisplay { + avg_rating: String, + favorite_director: String, + most_active_month: String, +} + +fn build_stats_display(stats: &domain::models::UserStats) -> StatsDisplay { + StatsDisplay { + avg_rating: stats + .avg_rating + .map(|r| format!("{:.1}", r)) + .unwrap_or_else(|| "\u{2014}".to_string()), + favorite_director: stats + .favorite_director + .clone() + .unwrap_or_else(|| "\u{2014}".to_string()), + most_active_month: stats + .most_active_month + .clone() + .unwrap_or_else(|| "\u{2014}".to_string()), + } +} + +fn build_monthly_rating_rows<'a>( + trends: Option<&'a domain::models::UserTrends>, +) -> Vec> { + trends + .map(|t| { + t.monthly_ratings + .iter() + .map(|r| MonthlyRatingRow { + rating: r, + bar_height_px: bar_height_px(r.avg_rating), + }) + .collect() + }) + .unwrap_or_default() +} + +async fn fetch_profile_goals( + state: &AppState, + user_id: Uuid, +) -> Vec { + let goals_list = application::goals::list::execute( + &application::goals::deps::GoalQueryDeps { + goal_query: state.app_ctx.repos.goal_query.clone(), + stats: state.app_ctx.repos.stats.clone(), + }, + application::goals::queries::ListGoalsQuery { + user_id, + }, + ) + .await + .unwrap_or_default(); + goals_list + .iter() + .map(|g| template_askama::GoalViewData { + year: g.goal.year(), + target_count: g.goal.target_count(), + current_count: g.current_count, + percentage: g.percentage().round(), + is_complete: g.is_complete(), + }) + .collect() +} + +// ── Handler ───────────────────────────────────────────────────────────────── + pub async fn get_user_profile_html( OptionalCookieUser(user_id): OptionalCookieUser, State(state): State, @@ -634,60 +735,16 @@ pub async fn get_user_profile_html( }; match application::users::get_profile::execute(&html_profile_deps, query).await { Ok(profile) => { - let (offset, has_more, limit) = profile - .entries - .as_ref() - .map(|e| { - let has_more = (e.offset as u64).saturating_add(e.limit as u64) < e.total_count; - (e.offset, has_more, e.limit) - }) - .unwrap_or((0, false, super::DEFAULT_PAGE_LIMIT)); + let pag = compute_pagination(profile.entries.as_ref()); if !is_own_profile { ctx.page_rss_url = Some(format!("/users/{}/feed.rss", profile_user_uuid)); } let email = profile_user.email().value().to_string(); let display_name = email.split('@').next().unwrap_or("?").to_string(); - let avg_rating_display = profile - .stats - .avg_rating - .map(|r| format!("{:.1}", r)) - .unwrap_or_else(|| "\u{2014}".to_string()); - let favorite_director_display = profile - .stats - .favorite_director - .clone() - .unwrap_or_else(|| "\u{2014}".to_string()); - let most_active_month_display = profile - .stats - .most_active_month - .clone() - .unwrap_or_else(|| "\u{2014}".to_string()); + let stats_disp = build_stats_display(&profile.stats); let history = profile.history.map(application::users::group_by_month); let heatmap = history.as_deref().map(build_heatmap).unwrap_or_default(); - let monthly_rating_rows: Vec> = profile - .trends - .as_ref() - .map(|t| { - t.monthly_ratings - .iter() - .map(|r| MonthlyRatingRow { - rating: r, - bar_height_px: bar_height_px(r.avg_rating), - }) - .collect() - }) - .unwrap_or_default(); - let total = profile - .entries - .as_ref() - .map(|e| e.total_count as u32) - .unwrap_or(0); - let total_pages = total - .saturating_add(limit.saturating_sub(1)) - .checked_div(limit) - .unwrap_or(1); - let current_page = offset.checked_div(limit).unwrap_or(0); - let page_items = build_page_items(total_pages, current_page); + let monthly_rating_rows = build_monthly_rating_rows(profile.trends.as_ref()); let pending_followers: Vec = profile .pending_followers .iter() @@ -703,43 +760,44 @@ pub async fn get_user_profile_html( profile_user_id: profile_user_uuid, profile_url, stats: &profile.stats, - avg_rating_display, - favorite_director_display, - most_active_month_display, + avg_rating_display: stats_disp.avg_rating, + favorite_director_display: stats_disp.favorite_director, + most_active_month_display: stats_disp.most_active_month, view: profile_view.as_str(), entries: profile.entries.as_ref(), - current_offset: offset, - has_more, - limit, + current_offset: pag.offset, + has_more: pag.has_more, + limit: pag.limit, history: history.as_ref(), trends: profile.trends.as_ref(), monthly_rating_rows, heatmap, - page_items, + page_items: pag.page_items, sort_by: sort_by_str.to_string(), }); let mut resp = response.into_response(); resp.headers_mut().remove("x-frame-options"); resp } else { + let goals = fetch_profile_goals(&state, profile_user_uuid).await; render_page(ProfileTemplate { ctx: &ctx, profile_display_name: display_name, profile_user_id: profile_user_uuid, stats: &profile.stats, - avg_rating_display, - favorite_director_display, - most_active_month_display, + avg_rating_display: stats_disp.avg_rating, + favorite_director_display: stats_disp.favorite_director, + most_active_month_display: stats_disp.most_active_month, view: profile_view.as_str(), entries: profile.entries.as_ref(), - current_offset: offset, - has_more, - limit, + current_offset: pag.offset, + has_more: pag.has_more, + limit: pag.limit, history: history.as_ref(), trends: profile.trends.as_ref(), monthly_rating_rows, heatmap, - page_items, + page_items: pag.page_items, is_own_profile, error: params.error, following_count: profile.following_count, @@ -747,29 +805,7 @@ pub async fn get_user_profile_html( pending_followers, sort_by: sort_by_str.to_string(), search: params.search.clone(), - goals: { - let goals_list = application::goals::list::execute( - &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, - }, - ) - .await - .unwrap_or_default(); - goals_list - .iter() - .map(|g| template_askama::GoalViewData { - year: g.goal.year(), - target_count: g.goal.target_count(), - current_count: g.current_count, - percentage: g.percentage().round(), - is_complete: g.is_complete(), - }) - .collect() - }, + goals, }) .into_response() } diff --git a/crates/presentation/src/main.rs b/crates/presentation/src/main.rs index 0463a37..2b761e8 100644 --- a/crates/presentation/src/main.rs +++ b/crates/presentation/src/main.rs @@ -100,7 +100,7 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> { 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), + goal_repo: Arc::clone(&db.goal_query), stats_repo: Arc::clone(&db.stats), user_repo: Arc::clone(&db.user), federation_settings: std::sync::Arc::clone(&db.federation_settings), @@ -158,14 +158,15 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> { #[cfg(feature = "federation")] remote_watchlist: remote_watchlist_repo, #[cfg(not(feature = "federation"))] - remote_watchlist: Arc::new(domain::testing::NoopRemoteWatchlistRepository), + remote_watchlist: Arc::new(domain::ports::noop::NoopRemoteWatchlistRepository), #[cfg(feature = "federation")] social_query: social_query.clone(), #[cfg(not(feature = "federation"))] - social_query: Arc::new(domain::testing::NoopSocialQueryPort), + social_query: Arc::new(domain::ports::noop::NoopSocialQueryPort), wrapup_stats: db.wrapup_stats, wrapup_repo: db.wrapup_repo, - goal: db.goal, + goal_command: db.goal_command, + goal_query: db.goal_query, user_settings: db.user_settings, remote_goal: db.remote_goal, refresh_session: db.refresh_session, diff --git a/crates/presentation/src/tests/extractors.rs b/crates/presentation/src/tests/extractors.rs index 948018f..7555770 100644 --- a/crates/presentation/src/tests/extractors.rs +++ b/crates/presentation/src/tests/extractors.rs @@ -17,7 +17,7 @@ use domain::{ collections::{PageParams, Paginated}, }, ports::{ - AuthService, DiaryRepository, EventPublisher, MetadataClient, MovieCommand, MovieQuery, + AuthService, DiaryQuery, EventPublisher, MetadataClient, MovieCommand, MovieQuery, ObjectStorage, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient, ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserRepository, WatchlistRepository, @@ -106,7 +106,7 @@ impl ReviewRepository for Panic { } } #[async_trait::async_trait] -impl DiaryRepository for Panic { +impl DiaryQuery for Panic { async fn query_diary(&self, _: &DiaryFilter) -> Result, DomainError> { panic!() } @@ -682,7 +682,7 @@ impl domain::ports::WrapUpRepository for Panic { } #[async_trait::async_trait] -impl domain::ports::GoalRepository for Panic { +impl domain::ports::GoalCommand for Panic { async fn save(&self, _: &domain::models::Goal) -> Result<(), DomainError> { panic!() } @@ -696,6 +696,10 @@ impl domain::ports::GoalRepository for Panic { ) -> Result<(), DomainError> { panic!() } +} + +#[async_trait::async_trait] +impl domain::ports::GoalQuery for Panic { async fn find_by_user_and_year( &self, _: &domain::value_objects::UserId, @@ -810,7 +814,8 @@ pub fn make_test_state(auth_service: Arc) -> crate::state::AppS social_query: Arc::clone(&repo) as _, wrapup_stats: Arc::clone(&repo) as _, wrapup_repo: Arc::clone(&repo) as _, - goal: Arc::clone(&repo) as _, + goal_command: Arc::clone(&repo) as _, + goal_query: Arc::clone(&repo) as _, user_settings: Arc::clone(&repo) as _, remote_goal: Arc::clone(&repo) as _, refresh_session: Arc::clone(&repo) as _, diff --git a/crates/presentation/tests/api_test.rs b/crates/presentation/tests/api_test.rs index a602b82..d48eeb9 100644 --- a/crates/presentation/tests/api_test.rs +++ b/crates/presentation/tests/api_test.rs @@ -467,7 +467,8 @@ async fn test_app() -> Router { social_query: Arc::new(PanicSocialQuery), wrapup_stats: Arc::new(domain::testing::PanicWrapUpStatsQuery) as _, wrapup_repo: Arc::new(domain::testing::PanicWrapUpRepository) as _, - goal: Arc::new(domain::testing::NoopGoalRepository), + goal_command: Arc::new(domain::testing::NoopGoalCommand), + goal_query: Arc::new(domain::testing::NoopGoalQuery), user_settings: Arc::new(domain::testing::NoopUserSettingsRepository), remote_goal: Arc::new(domain::testing::NoopRemoteGoalRepository), refresh_session: Arc::new(domain::testing::PanicRefreshSessionRepository), diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index 0c7dea5..30bf4da 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -13,7 +13,7 @@ sqlite-federation = ["sqlite", "dep:sqlite-federation", "dep:activitypub", "fede postgres-federation = ["postgres", "dep:postgres-federation", "dep:activitypub", "federation"] [dependencies] -domain = { workspace = true, features = ["test-helpers"] } +domain = { workspace = true } application = { workspace = true } tokio = { workspace = true } anyhow = { workspace = true } diff --git a/crates/worker/src/db.rs b/crates/worker/src/db.rs index 56e0706..2885aa6 100644 --- a/crates/worker/src/db.rs +++ b/crates/worker/src/db.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::Context; use domain::ports::{ - DiaryRepository, GoalRepository, ImageRefCommand, ImageRefQuery, ImportSessionRepository, + DiaryQuery, GoalCommand, GoalQuery, ImageRefCommand, ImageRefQuery, ImportSessionRepository, LocalApContentQuery, MovieCommand, MovieDeduplicator, MovieProfileRepository, MovieQuery, PersonCommand, PersonQuery, ReviewRepository, SearchCommand, StatsRepository, UserRepository, WatchEventCommand, WatchEventQuery, @@ -14,9 +14,10 @@ pub struct WorkerDbOutput { pub movie_command: Arc, pub movie_query: Arc, pub review: Arc, - pub diary: Arc, + pub diary: Arc, pub stats: Arc, - pub goal: Arc, + pub _goal_command: Arc, + pub goal_query: Arc, pub user: Arc, pub import_session: Arc, pub movie_profile: Arc, @@ -55,7 +56,8 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result anyhow::Result anyhow::Result<()> { Arc::clone(&db.movie_query), Arc::clone(&db.review), Arc::clone(&db.diary), - Arc::clone(&db.goal), + Arc::clone(&db.goal_query), Arc::clone(&db.stats), Arc::clone(&db.user), app_config.base_url.clone(),