diff --git a/crates/adapters/postgres/src/diary.rs b/crates/adapters/postgres/src/diary.rs index fceeadf..541d688 100644 --- a/crates/adapters/postgres/src/diary.rs +++ b/crates/adapters/postgres/src/diary.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use domain::{ errors::DomainError, models::{ - DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, SortDirection, + DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, ReviewSortBy, collections::{PageParams, Paginated}, }, ports::DiaryRepository, @@ -45,15 +45,15 @@ impl PostgresDiaryRepository { async fn fetch_all_diary_rows( &self, - sort: &SortDirection, + sort: &ReviewSortBy, limit: i64, offset: i64, ) -> Result, DomainError> { let order = match sort { - SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", - SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", - SortDirection::Ascending => "r.watched_at ASC", - SortDirection::Descending => "r.watched_at DESC", + ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC", + ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC", + ReviewSortBy::Ascending => "r.watched_at ASC", + ReviewSortBy::Descending => "r.watched_at DESC", }; let sql = format!( "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, @@ -79,15 +79,15 @@ impl PostgresDiaryRepository { async fn fetch_movie_diary_rows( &self, movie_id: &str, - sort: &SortDirection, + sort: &ReviewSortBy, limit: i64, offset: i64, ) -> Result, DomainError> { let order = match sort { - SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", - SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", - SortDirection::Ascending => "r.watched_at ASC", - SortDirection::Descending => "r.watched_at DESC", + ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC", + ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC", + ReviewSortBy::Ascending => "r.watched_at ASC", + ReviewSortBy::Descending => "r.watched_at DESC", }; let sql = format!( "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, @@ -144,7 +144,7 @@ impl PostgresDiaryRepository { async fn fetch_user_diary_rows( &self, user_id: &str, - sort: &SortDirection, + sort: &ReviewSortBy, search: Option<&str>, include_remote: bool, limit: i64, @@ -152,10 +152,10 @@ impl PostgresDiaryRepository { ) -> Result, DomainError> { let has_search = search.map(|s| !s.is_empty()).unwrap_or(false); let order_clause = match sort { - SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", - SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", - SortDirection::Ascending => "r.watched_at ASC", - SortDirection::Descending => "r.watched_at DESC", + ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC", + ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC", + ReviewSortBy::Ascending => "r.watched_at ASC", + ReviewSortBy::Descending => "r.watched_at DESC", }; let remote_clause = if include_remote { "" diff --git a/crates/adapters/postgres/src/import_session.rs b/crates/adapters/postgres/src/import_session.rs index 4cd7c30..6448d8e 100644 --- a/crates/adapters/postgres/src/import_session.rs +++ b/crates/adapters/postgres/src/import_session.rs @@ -5,7 +5,6 @@ use domain::{ models::{ AnnotatedRow, FieldMapping, ImportSession, ParsedFile, import::{DomainField, ImportRow, RowResult, Transform}, - import_session::PersistedImportSession, }, ports::ImportSessionRepository, value_objects::{ImportSessionId, UserId}, @@ -267,7 +266,7 @@ impl PostgresImportSessionRepository { Ok(js.into_iter().map(annotated_from_json).collect()) }) .transpose()?; - Ok(ImportSession::from_persistence(PersistedImportSession { + Ok(ImportSession { id: ImportSessionId::from_uuid( id.parse::() .map_err(|e| DomainError::InfrastructureError(e.to_string()))?, @@ -282,7 +281,7 @@ impl PostgresImportSessionRepository { row_results, created_at, expires_at, - })) + }) } } diff --git a/crates/adapters/sqlite/src/diary.rs b/crates/adapters/sqlite/src/diary.rs index 1f31d70..0dc760d 100644 --- a/crates/adapters/sqlite/src/diary.rs +++ b/crates/adapters/sqlite/src/diary.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use domain::{ errors::DomainError, models::{ - DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, SortDirection, + DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, ReviewSortBy, collections::{PageParams, Paginated}, }, ports::DiaryRepository, @@ -45,15 +45,15 @@ impl SqliteDiaryRepository { async fn fetch_all_diary_rows( &self, - sort: &SortDirection, + sort: &ReviewSortBy, limit: i64, offset: i64, ) -> Result, DomainError> { let order_clause = match sort { - SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", - SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", - SortDirection::Ascending => "r.watched_at ASC", - SortDirection::Descending => "r.watched_at DESC", + ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC", + ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC", + ReviewSortBy::Ascending => "r.watched_at ASC", + ReviewSortBy::Descending => "r.watched_at DESC", }; let sql = format!( "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, @@ -75,15 +75,15 @@ impl SqliteDiaryRepository { async fn fetch_movie_diary_rows( &self, movie_id: &str, - sort: &SortDirection, + sort: &ReviewSortBy, limit: i64, offset: i64, ) -> Result, DomainError> { let order_clause = match sort { - SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", - SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", - SortDirection::Ascending => "r.watched_at ASC", - SortDirection::Descending => "r.watched_at DESC", + ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC", + ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC", + ReviewSortBy::Ascending => "r.watched_at ASC", + ReviewSortBy::Descending => "r.watched_at DESC", }; let sql = format!( "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, @@ -136,7 +136,7 @@ impl SqliteDiaryRepository { async fn fetch_user_diary_rows( &self, user_id: &str, - sort: &SortDirection, + sort: &ReviewSortBy, search: Option<&str>, include_remote: bool, limit: i64, @@ -154,10 +154,10 @@ impl SqliteDiaryRepository { "" }; let order_clause = match sort { - SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", - SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", - SortDirection::Ascending => "r.watched_at ASC", - SortDirection::Descending => "r.watched_at DESC", + ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC", + ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC", + ReviewSortBy::Ascending => "r.watched_at ASC", + ReviewSortBy::Descending => "r.watched_at DESC", }; let sql = format!( "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, diff --git a/crates/adapters/sqlite/src/import_session.rs b/crates/adapters/sqlite/src/import_session.rs index 08a1624..7e491c2 100644 --- a/crates/adapters/sqlite/src/import_session.rs +++ b/crates/adapters/sqlite/src/import_session.rs @@ -5,7 +5,6 @@ use domain::{ models::{ AnnotatedRow, FieldMapping, ImportSession, ParsedFile, import::{DomainField, ImportRow, RowResult, Transform}, - import_session::PersistedImportSession, }, ports::ImportSessionRepository, value_objects::{ImportSessionId, UserId}, @@ -276,7 +275,7 @@ impl SqliteImportSessionRepository { }) .transpose()?; - Ok(ImportSession::from_persistence(PersistedImportSession { + Ok(ImportSession { id: ImportSessionId::from_uuid( id.parse::() .map_err(|e| DomainError::InfrastructureError(e.to_string()))?, @@ -291,7 +290,7 @@ impl SqliteImportSessionRepository { row_results, created_at: Self::parse_dt(created_at)?, expires_at: Self::parse_dt(expires_at)?, - })) + }) } } diff --git a/crates/api-types/src/common.rs b/crates/api-types/src/common.rs index cae9bbb..e2e7a42 100644 --- a/crates/api-types/src/common.rs +++ b/crates/api-types/src/common.rs @@ -1,7 +1,23 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, Default)] pub struct PaginationQueryParams { pub limit: Option, pub offset: Option, } + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct PaginatedResponse { + pub items: Vec, + pub total_count: u64, + pub limit: u32, + pub offset: u32, +} + +pub type MoviesResponse = PaginatedResponse; +pub type SocialFeedResponse = PaginatedResponse; +pub type DiaryResponse = PaginatedResponse; +pub type ActivityFeedResponse = PaginatedResponse; +pub type WatchlistResponse = PaginatedResponse; +pub type PaginatedMovieHits = PaginatedResponse; +pub type PaginatedPersonHits = PaginatedResponse; diff --git a/crates/api-types/src/diary.rs b/crates/api-types/src/diary.rs index 108e6b4..e897a9b 100644 --- a/crates/api-types/src/diary.rs +++ b/crates/api-types/src/diary.rs @@ -28,14 +28,6 @@ pub struct DiaryEntryDto { pub review: ReviewDto, } -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct DiaryResponse { - pub items: Vec, - pub total_count: u64, - pub limit: u32, - pub offset: u32, -} - #[derive(Debug, Clone, Deserialize, utoipa::IntoParams)] #[into_params(parameter_in = Query)] pub struct DiaryQueryParams { @@ -65,14 +57,6 @@ pub struct FeedEntryDto { pub actor_url: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct ActivityFeedResponse { - pub items: Vec, - pub total_count: u64, - pub limit: u32, - pub offset: u32, -} - #[derive(Debug, Clone, Deserialize, utoipa::IntoParams)] #[into_params(parameter_in = Query)] pub struct ExportQueryParams { diff --git a/crates/api-types/src/movies.rs b/crates/api-types/src/movies.rs index 16df66a..99d3eb8 100644 --- a/crates/api-types/src/movies.rs +++ b/crates/api-types/src/movies.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; +use crate::common::SocialFeedResponse; + // ── Movie list ──────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Deserialize, utoipa::IntoParams)] @@ -16,14 +18,6 @@ pub struct MoviesQueryParams { pub language: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct MoviesResponse { - pub items: Vec, - pub total_count: u64, - pub limit: u32, - pub offset: u32, -} - // ── Movie profile (enrichment) ──────────────────────────────────────────────── #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] @@ -130,14 +124,6 @@ pub struct SocialReviewDto { pub watch_medium: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct SocialFeedResponse { - pub items: Vec, - pub total_count: u64, - pub limit: u32, - pub offset: u32, -} - #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct MovieDetailResponse { pub movie: MovieDto, diff --git a/crates/api-types/src/search.rs b/crates/api-types/src/search.rs index 37da6ad..31f2e83 100644 --- a/crates/api-types/src/search.rs +++ b/crates/api-types/src/search.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; +pub use crate::common::{PaginatedMovieHits, PaginatedPersonHits}; + #[derive(Debug, Deserialize, IntoParams)] pub struct SearchQueryParams { /// Free-text query matched across title, cast, crew, genres and keywords. @@ -28,23 +30,7 @@ pub struct SearchResponse { pub people: PaginatedPersonHits, } -#[derive(Debug, Serialize, ToSchema)] -pub struct PaginatedMovieHits { - pub items: Vec, - pub total_count: u64, - pub limit: u32, - pub offset: u32, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct PaginatedPersonHits { - pub items: Vec, - pub total_count: u64, - pub limit: u32, - pub offset: u32, -} - -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Clone, Serialize, ToSchema)] pub struct MovieSearchHitDto { pub movie_id: Uuid, pub title: String, @@ -54,7 +40,7 @@ pub struct MovieSearchHitDto { pub genres: Vec, } -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Clone, Serialize, ToSchema)] pub struct PersonSearchHitDto { pub person_id: Uuid, pub name: String, diff --git a/crates/api-types/src/users.rs b/crates/api-types/src/users.rs index 9855cc2..c01a40d 100644 --- a/crates/api-types/src/users.rs +++ b/crates/api-types/src/users.rs @@ -1,7 +1,8 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::diary::{DiaryEntryDto, DiaryResponse}; +use crate::common::DiaryResponse; +use crate::diary::DiaryEntryDto; use crate::goals::GoalDto; #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] diff --git a/crates/api-types/src/watchlist.rs b/crates/api-types/src/watchlist.rs index 9f85bfb..6fe48b9 100644 --- a/crates/api-types/src/watchlist.rs +++ b/crates/api-types/src/watchlist.rs @@ -10,14 +10,6 @@ pub struct WatchlistEntryDto { pub added_at: String, } -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct WatchlistResponse { - pub items: Vec, - pub total_count: u64, - pub limit: u32, - pub offset: u32, -} - #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct AddToWatchlistRequest { #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/application/src/auth/login.rs b/crates/application/src/auth/login.rs index d02ca83..3c25cfc 100644 --- a/crates/application/src/auth/login.rs +++ b/crates/application/src/auth/login.rs @@ -3,7 +3,7 @@ use uuid::Uuid; use domain::{errors::DomainError, models::RefreshSession, value_objects::Email}; -use crate::auth::{deps::LoginDeps, queries::LoginQuery}; +use crate::auth::{deps::LoginDeps, queries::LoginCommand}; pub struct LoginResult { pub token: String, @@ -14,7 +14,7 @@ pub struct LoginResult { pub role: domain::models::UserRole, } -pub async fn execute(deps: &LoginDeps, query: LoginQuery) -> Result { +pub async fn execute(deps: &LoginDeps, query: LoginCommand) -> Result { let email = Email::new(query.email)?; let user = deps .user diff --git a/crates/application/src/auth/queries.rs b/crates/application/src/auth/queries.rs index a363863..3043187 100644 --- a/crates/application/src/auth/queries.rs +++ b/crates/application/src/auth/queries.rs @@ -1,4 +1,4 @@ -pub struct LoginQuery { +pub struct LoginCommand { pub email: String, pub password: String, } diff --git a/crates/application/src/auth/register_and_login.rs b/crates/application/src/auth/register_and_login.rs index 1168478..5a1c90a 100644 --- a/crates/application/src/auth/register_and_login.rs +++ b/crates/application/src/auth/register_and_login.rs @@ -4,7 +4,7 @@ use crate::auth::{ commands::{RegisterAndLoginCommand, RegisterCommand}, deps::{LoginDeps, RegisterAndLoginDeps, RegisterDeps}, login::{self, LoginResult}, - queries::LoginQuery, + queries::LoginCommand, register, }; @@ -37,7 +37,7 @@ pub async fn execute( }; login::execute( &log_deps, - LoginQuery { + LoginCommand { email: cmd.email, password: cmd.password, }, diff --git a/crates/application/src/auth/tests/login.rs b/crates/application/src/auth/tests/login.rs index 4dbb465..c60fd40 100644 --- a/crates/application/src/auth/tests/login.rs +++ b/crates/application/src/auth/tests/login.rs @@ -8,7 +8,7 @@ use crate::{ commands::RegisterCommand, deps::{LoginDeps, RegisterDeps}, login, - queries::LoginQuery, + queries::LoginCommand, register, }, test_helpers::TestContextBuilder, @@ -48,7 +48,7 @@ async fn test_login_valid_credentials_returns_token() { }; let result = login::execute( &deps, - LoginQuery { + LoginCommand { email: "carol@example.com".into(), password: "secret123".into(), }, @@ -76,7 +76,7 @@ async fn test_login_wrong_password_fails() { }; let result = login::execute( &deps, - LoginQuery { + LoginCommand { email: "dave@example.com".into(), password: "wrong_password".into(), }, @@ -98,7 +98,7 @@ async fn test_login_unknown_email_fails() { }; let result = login::execute( &deps, - LoginQuery { + LoginCommand { email: "nobody@example.com".into(), password: "anything".into(), }, diff --git a/crates/application/src/auth/tests/logout.rs b/crates/application/src/auth/tests/logout.rs index 1fdaaa0..4e97242 100644 --- a/crates/application/src/auth/tests/logout.rs +++ b/crates/application/src/auth/tests/logout.rs @@ -8,7 +8,7 @@ use crate::{ commands::RegisterCommand, deps::{LoginDeps, RefreshDeps, RegisterDeps}, login, logout, - queries::LoginQuery, + queries::LoginCommand, refresh, register, }, test_helpers::TestContextBuilder, @@ -45,7 +45,7 @@ async fn logout_revokes_refresh_token() { }; let login_result = login::execute( &login_deps, - LoginQuery { + LoginCommand { email: "bob@example.com".into(), password: "password123".into(), }, diff --git a/crates/application/src/auth/tests/refresh.rs b/crates/application/src/auth/tests/refresh.rs index 34f0796..0919195 100644 --- a/crates/application/src/auth/tests/refresh.rs +++ b/crates/application/src/auth/tests/refresh.rs @@ -8,7 +8,7 @@ use crate::{ commands::RegisterCommand, deps::{LoginDeps, RefreshDeps, RegisterDeps}, login, - queries::LoginQuery, + queries::LoginCommand, refresh, register, }, test_helpers::TestContextBuilder, @@ -41,7 +41,7 @@ async fn login_user(b: &TestContextBuilder) -> login::LoginResult { }; login::execute( &login_deps, - LoginQuery { + LoginCommand { email: "alice@example.com".into(), password: "password123".into(), }, diff --git a/crates/application/src/diary/get_diary.rs b/crates/application/src/diary/get_diary.rs index 1692a1e..68693ef 100644 --- a/crates/application/src/diary/get_diary.rs +++ b/crates/application/src/diary/get_diary.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use domain::{ errors::DomainError, models::{ - DiaryEntry, DiaryFilter, SortDirection, + DiaryEntry, DiaryFilter, ReviewSortBy, collections::{PageParams, Paginated}, }, ports::DiaryRepository, @@ -21,7 +21,7 @@ pub async fn execute( let user_id = query.user_id.map(UserId::from_uuid); let filter = DiaryFilter { - sort_by: query.sort_by.unwrap_or(SortDirection::Descending), + sort_by: query.sort_by.unwrap_or(ReviewSortBy::Descending), page, movie_id, user_id: user_id.clone(), diff --git a/crates/application/src/diary/queries.rs b/crates/application/src/diary/queries.rs index dd6b9fb..447193a 100644 --- a/crates/application/src/diary/queries.rs +++ b/crates/application/src/diary/queries.rs @@ -1,10 +1,10 @@ -use domain::models::SortDirection; +use domain::models::ReviewSortBy; use uuid::Uuid; pub struct GetDiaryQuery { pub limit: Option, pub offset: Option, - pub sort_by: Option, + pub sort_by: Option, pub movie_id: Option, pub user_id: Option, } diff --git a/crates/application/src/users/delete_account.rs b/crates/application/src/users/delete_account.rs index 2454387..51b1e13 100644 --- a/crates/application/src/users/delete_account.rs +++ b/crates/application/src/users/delete_account.rs @@ -1,8 +1,8 @@ use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId}; -use crate::users::deps::UpdateProfileDeps; +use crate::users::deps::DeleteAccountDeps; -pub async fn execute(deps: &UpdateProfileDeps, user_id: uuid::Uuid) -> Result<(), DomainError> { +pub async fn execute(deps: &DeleteAccountDeps, user_id: uuid::Uuid) -> Result<(), DomainError> { let uid = UserId::from_uuid(user_id); deps.user diff --git a/crates/application/src/users/deps.rs b/crates/application/src/users/deps.rs index ade9cd0..c57b864 100644 --- a/crates/application/src/users/deps.rs +++ b/crates/application/src/users/deps.rs @@ -16,3 +16,8 @@ pub struct UpdateProfileDeps { pub object_storage: Arc, pub event_publisher: Arc, } + +pub struct DeleteAccountDeps { + pub user: Arc, + pub event_publisher: Arc, +} diff --git a/crates/application/src/users/get_profile.rs b/crates/application/src/users/get_profile.rs index e98a9c0..4b6ec56 100644 --- a/crates/application/src/users/get_profile.rs +++ b/crates/application/src/users/get_profile.rs @@ -6,7 +6,7 @@ use domain::{ errors::DomainError, models::FeedSortBy, models::{ - DiaryEntry, DiaryFilter, SortDirection, UserStats, UserTrends, + DiaryEntry, DiaryFilter, ReviewSortBy, UserStats, UserTrends, collections::{PageParams, Paginated}, }, value_objects::UserId, @@ -108,18 +108,18 @@ async fn load_social_counts( (following, followers, pending) } -fn feed_sort_to_direction(sort_by: FeedSortBy) -> SortDirection { +fn feed_sort_to_direction(sort_by: FeedSortBy) -> ReviewSortBy { match sort_by { - FeedSortBy::Date => SortDirection::Descending, - FeedSortBy::DateAsc => SortDirection::Ascending, - FeedSortBy::Rating => SortDirection::ByRatingDesc, - FeedSortBy::RatingAsc => SortDirection::ByRatingAsc, + FeedSortBy::Date => ReviewSortBy::Descending, + FeedSortBy::DateAsc => ReviewSortBy::Ascending, + FeedSortBy::Rating => ReviewSortBy::ByRatingDesc, + FeedSortBy::RatingAsc => ReviewSortBy::ByRatingAsc, } } fn paged_user_filter( user_id: UserId, - sort_by: SortDirection, + sort_by: ReviewSortBy, limit: Option, offset: Option, search: Option, @@ -149,19 +149,19 @@ mod helper_tests { use domain::models::FeedSortBy; assert!(matches!( feed_sort_to_direction(FeedSortBy::Date), - SortDirection::Descending + ReviewSortBy::Descending )); assert!(matches!( feed_sort_to_direction(FeedSortBy::DateAsc), - SortDirection::Ascending + ReviewSortBy::Ascending )); assert!(matches!( feed_sort_to_direction(FeedSortBy::Rating), - SortDirection::ByRatingDesc + ReviewSortBy::ByRatingDesc )); assert!(matches!( feed_sort_to_direction(FeedSortBy::RatingAsc), - SortDirection::ByRatingAsc + ReviewSortBy::ByRatingAsc )); } @@ -170,7 +170,7 @@ mod helper_tests { let uid = UserId::from_uuid(uuid::Uuid::new_v4()); let filter = paged_user_filter( uid.clone(), - SortDirection::Descending, + ReviewSortBy::Descending, Some(20), Some(5), Some("blade".into()), diff --git a/crates/domain/src/models/import_session.rs b/crates/domain/src/models/import_session.rs index 3cb97c7..f7a75b1 100644 --- a/crates/domain/src/models/import_session.rs +++ b/crates/domain/src/models/import_session.rs @@ -15,16 +15,6 @@ pub struct ImportSession { pub expires_at: NaiveDateTime, } -pub struct PersistedImportSession { - pub id: ImportSessionId, - pub user_id: UserId, - pub parsed_file: Option, - pub field_mappings: Option>, - pub row_results: Option>, - pub created_at: NaiveDateTime, - pub expires_at: NaiveDateTime, -} - impl ImportSession { pub fn new(user_id: UserId) -> Self { let created_at = chrono::Utc::now().naive_utc(); @@ -39,16 +29,4 @@ impl ImportSession { expires_at, } } - - pub fn from_persistence(p: PersistedImportSession) -> Self { - Self { - id: p.id, - user_id: p.user_id, - parsed_file: p.parsed_file, - field_mappings: p.field_mappings, - row_results: p.row_results, - created_at: p.created_at, - expires_at: p.expires_at, - } - } } diff --git a/crates/domain/src/models/mod.rs b/crates/domain/src/models/mod.rs index 5d194ae..16cda0b 100644 --- a/crates/domain/src/models/mod.rs +++ b/crates/domain/src/models/mod.rs @@ -47,8 +47,9 @@ pub use import::{ pub use import_profile::ImportProfile; pub use import_session::ImportSession; pub use person::{ - CastCredit, CrewCredit, ExternalPersonId, Person, PersonCredits, PersonEnrichmentData, PersonId, + CastCredit, CrewCredit, ExternalPersonId, Person, PersonCredits, PersonEnrichmentData, }; +pub use crate::value_objects::PersonId; pub use refresh_session::{GeneratedToken, RefreshSession}; pub use search::{ EntityType, IndexableDocument, MovieSearchHit, PersonSearchHit, SearchFilters, SearchQuery, @@ -85,7 +86,7 @@ impl std::str::FromStr for GoalType { } #[derive(Clone, Debug, Default)] -pub enum SortDirection { +pub enum ReviewSortBy { #[default] Descending, Ascending, diff --git a/crates/domain/src/models/person.rs b/crates/domain/src/models/person.rs index 48ee2ca..36f0314 100644 --- a/crates/domain/src/models/person.rs +++ b/crates/domain/src/models/person.rs @@ -1,25 +1,4 @@ -use uuid::Uuid; - -use crate::value_objects::MovieId; - -#[derive(Clone, Debug, PartialEq)] -pub struct PersonId(Uuid); - -impl PersonId { - pub fn from_uuid(uuid: Uuid) -> Self { - Self(uuid) - } - - /// Deterministic UUIDv5 from an external person ID string. - /// "tmdb:12345" always maps to the same PersonId. - pub fn from_external(external_id: &ExternalPersonId) -> Self { - Self(Uuid::new_v5(&Uuid::NAMESPACE_URL, external_id.0.as_bytes())) - } - - pub fn value(&self) -> Uuid { - self.0 - } -} +use crate::value_objects::{MovieId, PersonId}; #[derive(Clone, Debug, PartialEq)] pub struct ExternalPersonId(String); diff --git a/crates/domain/src/models/review.rs b/crates/domain/src/models/review.rs index bd408cf..493dd4d 100644 --- a/crates/domain/src/models/review.rs +++ b/crates/domain/src/models/review.rs @@ -165,7 +165,7 @@ impl DiaryEntry { #[derive(Clone, Debug, Default)] pub struct DiaryFilter { - pub sort_by: super::SortDirection, + pub sort_by: super::ReviewSortBy, pub page: crate::models::collections::PageParams, pub movie_id: Option, pub user_id: Option, diff --git a/crates/domain/src/value_objects/ids.rs b/crates/domain/src/value_objects/ids.rs index 678cf61..172ecd6 100644 --- a/crates/domain/src/value_objects/ids.rs +++ b/crates/domain/src/value_objects/ids.rs @@ -42,3 +42,12 @@ uuid_id!(WatchEventId); uuid_id!(WebhookTokenId); uuid_id!(WrapUpId); uuid_id!(GoalId); +uuid_id!(PersonId); + +impl PersonId { + /// Deterministic UUIDv5 from an external person ID string. + /// "tmdb:12345" always maps to the same PersonId. + pub fn from_external(external_id: &crate::models::person::ExternalPersonId) -> Self { + Self(Uuid::new_v5(&Uuid::NAMESPACE_URL, external_id.value().as_bytes())) + } +} diff --git a/crates/presentation/src/csrf.rs b/crates/presentation/src/csrf.rs index c233a06..08b3c40 100644 --- a/crates/presentation/src/csrf.rs +++ b/crates/presentation/src/csrf.rs @@ -19,7 +19,7 @@ pub fn extract_from_cookie(headers: &axum::http::HeaderMap) -> Option { }) } -fn secure_flag() -> &'static str { +pub(crate) fn secure_flag() -> &'static str { if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") { "; Secure" } else { diff --git a/crates/presentation/src/forms.rs b/crates/presentation/src/forms.rs index ee37522..edcf6b9 100644 --- a/crates/presentation/src/forms.rs +++ b/crates/presentation/src/forms.rs @@ -6,7 +6,7 @@ use application::diary::{ commands::{LogReviewCommand, MovieInput}, queries::GetDiaryQuery, }; -use domain::{errors::DomainError, models::SortDirection}; +use domain::{errors::DomainError, models::ReviewSortBy}; use api_types::{DiaryQueryParams, LogReviewRequest}; @@ -258,10 +258,10 @@ pub fn to_diary_query(p: DiaryQueryParams) -> GetDiaryQuery { limit: p.limit, offset: p.offset, sort_by: p.sort_by.as_deref().map(|s| match s { - "date_asc" | "asc" => SortDirection::Ascending, - "rating_desc" => SortDirection::ByRatingDesc, - "rating_asc" => SortDirection::ByRatingAsc, - _ => SortDirection::Descending, + "date_asc" | "asc" => ReviewSortBy::Ascending, + "rating_desc" => ReviewSortBy::ByRatingDesc, + "rating_asc" => ReviewSortBy::ByRatingAsc, + _ => ReviewSortBy::Descending, }), movie_id: p.movie_id, user_id: p.user_id, diff --git a/crates/presentation/src/handlers/auth.rs b/crates/presentation/src/handlers/auth.rs index d7e4165..6cb1ee0 100644 --- a/crates/presentation/src/handlers/auth.rs +++ b/crates/presentation/src/handlers/auth.rs @@ -10,12 +10,12 @@ use application::auth::{ commands::RegisterCommand, deps::{LoginDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps}, login as login_uc, - queries::LoginQuery, + queries::LoginCommand, register as register_uc, }; use crate::{ - csrf::CsrfToken, + csrf::{CsrfToken, secure_flag}, errors::ApiError, forms::{ErrorQuery, LoginForm, RegisterForm}, render::render_page, @@ -29,14 +29,6 @@ use template_askama::{LoginTemplate, RegisterTemplate}; // ── HTML helpers ───────────────────────────────────────────────────────────── -fn secure_flag() -> &'static str { - if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") { - "; Secure" - } else { - "" - } -} - fn set_cookie_header(token: &str, max_age: i64) -> (axum::http::HeaderName, HeaderValue) { let val = format!( "token={}; HttpOnly; Path=/; SameSite=Strict; Max-Age={}{}", @@ -73,7 +65,7 @@ pub async fn login( }; let result = login_uc::execute( &deps, - LoginQuery { + LoginCommand { email: req.email, password: req.password, }, @@ -204,7 +196,7 @@ pub async fn post_login( }; match login_uc::execute( &deps, - LoginQuery { + LoginCommand { email: form.email, password: form.password, }, diff --git a/crates/presentation/src/handlers/rss.rs b/crates/presentation/src/handlers/rss.rs index d05cedd..17e6f3b 100644 --- a/crates/presentation/src/handlers/rss.rs +++ b/crates/presentation/src/handlers/rss.rs @@ -6,7 +6,7 @@ use axum::{ use uuid::Uuid; use application::{diary::get_diary, diary::queries::GetDiaryQuery}; -use domain::{errors::DomainError, models::SortDirection, value_objects::UserId}; +use domain::{errors::DomainError, models::ReviewSortBy, value_objects::UserId}; use crate::{errors::ApiError, state::AppState}; @@ -14,7 +14,7 @@ pub async fn get_feed(State(state): State) -> Result anyhow::Result<(AppState, axum::Router)> { )), #[cfg(feature = "federation")] ap_service, - #[cfg(feature = "federation")] - social_query, }; Ok((state, ap_router)) } diff --git a/crates/presentation/src/state.rs b/crates/presentation/src/state.rs index ddf58dd..9c470e8 100644 --- a/crates/presentation/src/state.rs +++ b/crates/presentation/src/state.rs @@ -10,6 +10,4 @@ pub struct AppState { pub rss_renderer: Arc, #[cfg(feature = "federation")] pub ap_service: Arc, - #[cfg(feature = "federation")] - pub social_query: Arc, } diff --git a/crates/presentation/src/tests/extractors.rs b/crates/presentation/src/tests/extractors.rs index 5225f60..948018f 100644 --- a/crates/presentation/src/tests/extractors.rs +++ b/crates/presentation/src/tests/extractors.rs @@ -843,8 +843,6 @@ pub fn make_test_state(auth_service: Arc) -> crate::state::AppS rss_renderer: Arc::new(Panic), #[cfg(feature = "federation")] ap_service: Arc::new(activitypub::NoopActivityPubService), - #[cfg(feature = "federation")] - social_query: Arc::new(Panic), } } diff --git a/crates/presentation/src/tests/forms.rs b/crates/presentation/src/tests/forms.rs index 048c872..0ac54b8 100644 --- a/crates/presentation/src/tests/forms.rs +++ b/crates/presentation/src/tests/forms.rs @@ -89,7 +89,7 @@ fn sort_by_asc_string_becomes_ascending() { let query = to_diary_query(params); assert!(matches!( query.sort_by, - Some(domain::models::SortDirection::Ascending) + Some(domain::models::ReviewSortBy::Ascending) )); } @@ -105,7 +105,7 @@ fn sort_by_other_string_becomes_descending() { let query = to_diary_query(params); assert!(matches!( query.sort_by, - Some(domain::models::SortDirection::Descending) + Some(domain::models::ReviewSortBy::Descending) )); } diff --git a/crates/presentation/tests/api_test.rs b/crates/presentation/tests/api_test.rs index a6fc8ee..a602b82 100644 --- a/crates/presentation/tests/api_test.rs +++ b/crates/presentation/tests/api_test.rs @@ -500,8 +500,6 @@ async fn test_app() -> Router { rss_renderer: Arc::new(RssAdapter::new("http://localhost:3000".into())), #[cfg(feature = "federation")] ap_service: Arc::new(activitypub::NoopActivityPubService), - #[cfg(feature = "federation")] - social_query: Arc::new(PanicSocialQuery), }; routes::build_router(state, axum::Router::new()) diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 7586527..e0d6b25 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -248,6 +248,8 @@ pub enum Action { ReviewCreateFailed(String), ReviewDeleted(Uuid), ReviewDeleteFailed(String), + FileRead(String), + FileReadFailed(String), BulkItemDone { index: usize, error: Option, @@ -265,6 +267,7 @@ pub enum Command { SaveConfig(String), SaveToken(String), ClearToken, + ReadFile { path: String }, } // Matches the export CSV column order: @@ -863,22 +866,34 @@ pub fn update(app: &mut App, action: Action) -> Vec { && m.bulk_import.stage == BulkImportStage::EnterPath { let path = m.bulk_import.file_path.trim().to_string(); - match std::fs::read_to_string(&path) { - Ok(content) => { - m.bulk_import.parsed = parse_csv(&content); - m.bulk_import.stage = BulkImportStage::Preview; - } - Err(e) => { - app.status = Some(StatusMsg { - text: format!("Cannot read file: {e}"), - is_error: true, - }); - } + if path.is_empty() { + app.status = Some(StatusMsg { + text: "File path required".into(), + is_error: true, + }); + return vec![]; } + return vec![Command::ReadFile { path }]; } vec![] } + Action::FileRead(content) => { + if let Screen::Main(m) = &mut app.screen { + m.bulk_import.parsed = parse_csv(&content); + m.bulk_import.stage = BulkImportStage::Preview; + } + vec![] + } + + Action::FileReadFailed(msg) => { + app.status = Some(StatusMsg { + text: format!("Cannot read file: {msg}"), + is_error: true, + }); + vec![] + } + Action::BulkImportAll => { if let Screen::Main(m) = &mut app.screen && m.tab == Tab::BulkImport diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index f4c8b3a..eef8cbc 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -220,6 +220,21 @@ fn handle_command(cmd: Command, app: &App, client: &Arc, tx: &mpsc::S }); } + Command::ReadFile { path } => { + let tx = tx.clone(); + tokio::spawn(async move { + let action = + match tokio::task::spawn_blocking(move || std::fs::read_to_string(&path)) + .await + .unwrap_or_else(|e| Err(std::io::Error::other(e))) + { + Ok(content) => Action::FileRead(content), + Err(e) => Action::FileReadFailed(e.to_string()), + }; + let _ = tx.send(action).await; + }); + } + Command::ImportNext(index) => { let Some(token) = app.token.clone() else { return;