refactor: LOW cleanups — dedup secure_flag, remove vestigial social_query,

drop PersistedImportSession, rename LoginQuery→LoginCommand, own
DeleteAccountDeps, PersonId via uuid_id! macro, rename SortDirection→
ReviewSortBy, TUI fs::read→Command, generic PaginatedResponse<T>
This commit is contained in:
2026-07-10 05:04:35 +02:00
parent 5266646b0b
commit 5e0dde656c
37 changed files with 167 additions and 218 deletions

View File

@@ -2,7 +2,7 @@ use async_trait::async_trait;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
models::{ models::{
DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, SortDirection, DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, ReviewSortBy,
collections::{PageParams, Paginated}, collections::{PageParams, Paginated},
}, },
ports::DiaryRepository, ports::DiaryRepository,
@@ -45,15 +45,15 @@ impl PostgresDiaryRepository {
async fn fetch_all_diary_rows( async fn fetch_all_diary_rows(
&self, &self,
sort: &SortDirection, sort: &ReviewSortBy,
limit: i64, limit: i64,
offset: i64, offset: i64,
) -> Result<Vec<DiaryRow>, DomainError> { ) -> Result<Vec<DiaryRow>, DomainError> {
let order = match sort { let order = match sort {
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
SortDirection::Ascending => "r.watched_at ASC", ReviewSortBy::Ascending => "r.watched_at ASC",
SortDirection::Descending => "r.watched_at DESC", ReviewSortBy::Descending => "r.watched_at DESC",
}; };
let sql = format!( let sql = format!(
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, "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( async fn fetch_movie_diary_rows(
&self, &self,
movie_id: &str, movie_id: &str,
sort: &SortDirection, sort: &ReviewSortBy,
limit: i64, limit: i64,
offset: i64, offset: i64,
) -> Result<Vec<DiaryRow>, DomainError> { ) -> Result<Vec<DiaryRow>, DomainError> {
let order = match sort { let order = match sort {
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
SortDirection::Ascending => "r.watched_at ASC", ReviewSortBy::Ascending => "r.watched_at ASC",
SortDirection::Descending => "r.watched_at DESC", ReviewSortBy::Descending => "r.watched_at DESC",
}; };
let sql = format!( let sql = format!(
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, "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( async fn fetch_user_diary_rows(
&self, &self,
user_id: &str, user_id: &str,
sort: &SortDirection, sort: &ReviewSortBy,
search: Option<&str>, search: Option<&str>,
include_remote: bool, include_remote: bool,
limit: i64, limit: i64,
@@ -152,10 +152,10 @@ impl PostgresDiaryRepository {
) -> Result<Vec<DiaryRow>, DomainError> { ) -> Result<Vec<DiaryRow>, DomainError> {
let has_search = search.map(|s| !s.is_empty()).unwrap_or(false); let has_search = search.map(|s| !s.is_empty()).unwrap_or(false);
let order_clause = match sort { let order_clause = match sort {
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
SortDirection::Ascending => "r.watched_at ASC", ReviewSortBy::Ascending => "r.watched_at ASC",
SortDirection::Descending => "r.watched_at DESC", ReviewSortBy::Descending => "r.watched_at DESC",
}; };
let remote_clause = if include_remote { let remote_clause = if include_remote {
"" ""

View File

@@ -5,7 +5,6 @@ use domain::{
models::{ models::{
AnnotatedRow, FieldMapping, ImportSession, ParsedFile, AnnotatedRow, FieldMapping, ImportSession, ParsedFile,
import::{DomainField, ImportRow, RowResult, Transform}, import::{DomainField, ImportRow, RowResult, Transform},
import_session::PersistedImportSession,
}, },
ports::ImportSessionRepository, ports::ImportSessionRepository,
value_objects::{ImportSessionId, UserId}, value_objects::{ImportSessionId, UserId},
@@ -267,7 +266,7 @@ impl PostgresImportSessionRepository {
Ok(js.into_iter().map(annotated_from_json).collect()) Ok(js.into_iter().map(annotated_from_json).collect())
}) })
.transpose()?; .transpose()?;
Ok(ImportSession::from_persistence(PersistedImportSession { Ok(ImportSession {
id: ImportSessionId::from_uuid( id: ImportSessionId::from_uuid(
id.parse::<uuid::Uuid>() id.parse::<uuid::Uuid>()
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?, .map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
@@ -282,7 +281,7 @@ impl PostgresImportSessionRepository {
row_results, row_results,
created_at, created_at,
expires_at, expires_at,
})) })
} }
} }

View File

@@ -2,7 +2,7 @@ use async_trait::async_trait;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
models::{ models::{
DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, SortDirection, DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, ReviewSortBy,
collections::{PageParams, Paginated}, collections::{PageParams, Paginated},
}, },
ports::DiaryRepository, ports::DiaryRepository,
@@ -45,15 +45,15 @@ impl SqliteDiaryRepository {
async fn fetch_all_diary_rows( async fn fetch_all_diary_rows(
&self, &self,
sort: &SortDirection, sort: &ReviewSortBy,
limit: i64, limit: i64,
offset: i64, offset: i64,
) -> Result<Vec<DiaryRow>, DomainError> { ) -> Result<Vec<DiaryRow>, DomainError> {
let order_clause = match sort { let order_clause = match sort {
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
SortDirection::Ascending => "r.watched_at ASC", ReviewSortBy::Ascending => "r.watched_at ASC",
SortDirection::Descending => "r.watched_at DESC", ReviewSortBy::Descending => "r.watched_at DESC",
}; };
let sql = format!( let sql = format!(
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, "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( async fn fetch_movie_diary_rows(
&self, &self,
movie_id: &str, movie_id: &str,
sort: &SortDirection, sort: &ReviewSortBy,
limit: i64, limit: i64,
offset: i64, offset: i64,
) -> Result<Vec<DiaryRow>, DomainError> { ) -> Result<Vec<DiaryRow>, DomainError> {
let order_clause = match sort { let order_clause = match sort {
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
SortDirection::Ascending => "r.watched_at ASC", ReviewSortBy::Ascending => "r.watched_at ASC",
SortDirection::Descending => "r.watched_at DESC", ReviewSortBy::Descending => "r.watched_at DESC",
}; };
let sql = format!( let sql = format!(
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, "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( async fn fetch_user_diary_rows(
&self, &self,
user_id: &str, user_id: &str,
sort: &SortDirection, sort: &ReviewSortBy,
search: Option<&str>, search: Option<&str>,
include_remote: bool, include_remote: bool,
limit: i64, limit: i64,
@@ -154,10 +154,10 @@ impl SqliteDiaryRepository {
"" ""
}; };
let order_clause = match sort { let order_clause = match sort {
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC", ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC", ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
SortDirection::Ascending => "r.watched_at ASC", ReviewSortBy::Ascending => "r.watched_at ASC",
SortDirection::Descending => "r.watched_at DESC", ReviewSortBy::Descending => "r.watched_at DESC",
}; };
let sql = format!( let sql = format!(
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,

View File

@@ -5,7 +5,6 @@ use domain::{
models::{ models::{
AnnotatedRow, FieldMapping, ImportSession, ParsedFile, AnnotatedRow, FieldMapping, ImportSession, ParsedFile,
import::{DomainField, ImportRow, RowResult, Transform}, import::{DomainField, ImportRow, RowResult, Transform},
import_session::PersistedImportSession,
}, },
ports::ImportSessionRepository, ports::ImportSessionRepository,
value_objects::{ImportSessionId, UserId}, value_objects::{ImportSessionId, UserId},
@@ -276,7 +275,7 @@ impl SqliteImportSessionRepository {
}) })
.transpose()?; .transpose()?;
Ok(ImportSession::from_persistence(PersistedImportSession { Ok(ImportSession {
id: ImportSessionId::from_uuid( id: ImportSessionId::from_uuid(
id.parse::<uuid::Uuid>() id.parse::<uuid::Uuid>()
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?, .map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
@@ -291,7 +290,7 @@ impl SqliteImportSessionRepository {
row_results, row_results,
created_at: Self::parse_dt(created_at)?, created_at: Self::parse_dt(created_at)?,
expires_at: Self::parse_dt(expires_at)?, expires_at: Self::parse_dt(expires_at)?,
})) })
} }
} }

View File

@@ -1,7 +1,23 @@
use serde::Deserialize; use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Default)] #[derive(Debug, Clone, Deserialize, Default)]
pub struct PaginationQueryParams { pub struct PaginationQueryParams {
pub limit: Option<u32>, pub limit: Option<u32>,
pub offset: Option<u32>, pub offset: Option<u32>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct PaginatedResponse<T: std::fmt::Debug + Clone> {
pub items: Vec<T>,
pub total_count: u64,
pub limit: u32,
pub offset: u32,
}
pub type MoviesResponse = PaginatedResponse<crate::movies::MovieDto>;
pub type SocialFeedResponse = PaginatedResponse<crate::movies::SocialReviewDto>;
pub type DiaryResponse = PaginatedResponse<crate::diary::DiaryEntryDto>;
pub type ActivityFeedResponse = PaginatedResponse<crate::diary::FeedEntryDto>;
pub type WatchlistResponse = PaginatedResponse<crate::watchlist::WatchlistEntryDto>;
pub type PaginatedMovieHits = PaginatedResponse<crate::search::MovieSearchHitDto>;
pub type PaginatedPersonHits = PaginatedResponse<crate::search::PersonSearchHitDto>;

View File

@@ -28,14 +28,6 @@ pub struct DiaryEntryDto {
pub review: ReviewDto, pub review: ReviewDto,
} }
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DiaryResponse {
pub items: Vec<DiaryEntryDto>,
pub total_count: u64,
pub limit: u32,
pub offset: u32,
}
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)] #[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
#[into_params(parameter_in = Query)] #[into_params(parameter_in = Query)]
pub struct DiaryQueryParams { pub struct DiaryQueryParams {
@@ -65,14 +57,6 @@ pub struct FeedEntryDto {
pub actor_url: Option<String>, pub actor_url: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ActivityFeedResponse {
pub items: Vec<FeedEntryDto>,
pub total_count: u64,
pub limit: u32,
pub offset: u32,
}
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)] #[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
#[into_params(parameter_in = Query)] #[into_params(parameter_in = Query)]
pub struct ExportQueryParams { pub struct ExportQueryParams {

View File

@@ -1,6 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::common::SocialFeedResponse;
// ── Movie list ──────────────────────────────────────────────────────────────── // ── Movie list ────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)] #[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
@@ -16,14 +18,6 @@ pub struct MoviesQueryParams {
pub language: Option<String>, pub language: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct MoviesResponse {
pub items: Vec<MovieDto>,
pub total_count: u64,
pub limit: u32,
pub offset: u32,
}
// ── Movie profile (enrichment) ──────────────────────────────────────────────── // ── Movie profile (enrichment) ────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
@@ -130,14 +124,6 @@ pub struct SocialReviewDto {
pub watch_medium: Option<domain::value_objects::WatchMedium>, pub watch_medium: Option<domain::value_objects::WatchMedium>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SocialFeedResponse {
pub items: Vec<SocialReviewDto>,
pub total_count: u64,
pub limit: u32,
pub offset: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct MovieDetailResponse { pub struct MovieDetailResponse {
pub movie: MovieDto, pub movie: MovieDto,

View File

@@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema}; use utoipa::{IntoParams, ToSchema};
use uuid::Uuid; use uuid::Uuid;
pub use crate::common::{PaginatedMovieHits, PaginatedPersonHits};
#[derive(Debug, Deserialize, IntoParams)] #[derive(Debug, Deserialize, IntoParams)]
pub struct SearchQueryParams { pub struct SearchQueryParams {
/// Free-text query matched across title, cast, crew, genres and keywords. /// Free-text query matched across title, cast, crew, genres and keywords.
@@ -28,23 +30,7 @@ pub struct SearchResponse {
pub people: PaginatedPersonHits, pub people: PaginatedPersonHits,
} }
#[derive(Debug, Serialize, ToSchema)] #[derive(Debug, Clone, Serialize, ToSchema)]
pub struct PaginatedMovieHits {
pub items: Vec<MovieSearchHitDto>,
pub total_count: u64,
pub limit: u32,
pub offset: u32,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct PaginatedPersonHits {
pub items: Vec<PersonSearchHitDto>,
pub total_count: u64,
pub limit: u32,
pub offset: u32,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct MovieSearchHitDto { pub struct MovieSearchHitDto {
pub movie_id: Uuid, pub movie_id: Uuid,
pub title: String, pub title: String,
@@ -54,7 +40,7 @@ pub struct MovieSearchHitDto {
pub genres: Vec<String>, pub genres: Vec<String>,
} }
#[derive(Debug, Serialize, ToSchema)] #[derive(Debug, Clone, Serialize, ToSchema)]
pub struct PersonSearchHitDto { pub struct PersonSearchHitDto {
pub person_id: Uuid, pub person_id: Uuid,
pub name: String, pub name: String,

View File

@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::diary::{DiaryEntryDto, DiaryResponse}; use crate::common::DiaryResponse;
use crate::diary::DiaryEntryDto;
use crate::goals::GoalDto; use crate::goals::GoalDto;
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]

View File

@@ -10,14 +10,6 @@ pub struct WatchlistEntryDto {
pub added_at: String, pub added_at: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct WatchlistResponse {
pub items: Vec<WatchlistEntryDto>,
pub total_count: u64,
pub limit: u32,
pub offset: u32,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct AddToWatchlistRequest { pub struct AddToWatchlistRequest {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]

View File

@@ -3,7 +3,7 @@ use uuid::Uuid;
use domain::{errors::DomainError, models::RefreshSession, value_objects::Email}; 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 struct LoginResult {
pub token: String, pub token: String,
@@ -14,7 +14,7 @@ pub struct LoginResult {
pub role: domain::models::UserRole, pub role: domain::models::UserRole,
} }
pub async fn execute(deps: &LoginDeps, query: LoginQuery) -> Result<LoginResult, DomainError> { pub async fn execute(deps: &LoginDeps, query: LoginCommand) -> Result<LoginResult, DomainError> {
let email = Email::new(query.email)?; let email = Email::new(query.email)?;
let user = deps let user = deps
.user .user

View File

@@ -1,4 +1,4 @@
pub struct LoginQuery { pub struct LoginCommand {
pub email: String, pub email: String,
pub password: String, pub password: String,
} }

View File

@@ -4,7 +4,7 @@ use crate::auth::{
commands::{RegisterAndLoginCommand, RegisterCommand}, commands::{RegisterAndLoginCommand, RegisterCommand},
deps::{LoginDeps, RegisterAndLoginDeps, RegisterDeps}, deps::{LoginDeps, RegisterAndLoginDeps, RegisterDeps},
login::{self, LoginResult}, login::{self, LoginResult},
queries::LoginQuery, queries::LoginCommand,
register, register,
}; };
@@ -37,7 +37,7 @@ pub async fn execute(
}; };
login::execute( login::execute(
&log_deps, &log_deps,
LoginQuery { LoginCommand {
email: cmd.email, email: cmd.email,
password: cmd.password, password: cmd.password,
}, },

View File

@@ -8,7 +8,7 @@ use crate::{
commands::RegisterCommand, commands::RegisterCommand,
deps::{LoginDeps, RegisterDeps}, deps::{LoginDeps, RegisterDeps},
login, login,
queries::LoginQuery, queries::LoginCommand,
register, register,
}, },
test_helpers::TestContextBuilder, test_helpers::TestContextBuilder,
@@ -48,7 +48,7 @@ async fn test_login_valid_credentials_returns_token() {
}; };
let result = login::execute( let result = login::execute(
&deps, &deps,
LoginQuery { LoginCommand {
email: "carol@example.com".into(), email: "carol@example.com".into(),
password: "secret123".into(), password: "secret123".into(),
}, },
@@ -76,7 +76,7 @@ async fn test_login_wrong_password_fails() {
}; };
let result = login::execute( let result = login::execute(
&deps, &deps,
LoginQuery { LoginCommand {
email: "dave@example.com".into(), email: "dave@example.com".into(),
password: "wrong_password".into(), password: "wrong_password".into(),
}, },
@@ -98,7 +98,7 @@ async fn test_login_unknown_email_fails() {
}; };
let result = login::execute( let result = login::execute(
&deps, &deps,
LoginQuery { LoginCommand {
email: "nobody@example.com".into(), email: "nobody@example.com".into(),
password: "anything".into(), password: "anything".into(),
}, },

View File

@@ -8,7 +8,7 @@ use crate::{
commands::RegisterCommand, commands::RegisterCommand,
deps::{LoginDeps, RefreshDeps, RegisterDeps}, deps::{LoginDeps, RefreshDeps, RegisterDeps},
login, logout, login, logout,
queries::LoginQuery, queries::LoginCommand,
refresh, register, refresh, register,
}, },
test_helpers::TestContextBuilder, test_helpers::TestContextBuilder,
@@ -45,7 +45,7 @@ async fn logout_revokes_refresh_token() {
}; };
let login_result = login::execute( let login_result = login::execute(
&login_deps, &login_deps,
LoginQuery { LoginCommand {
email: "bob@example.com".into(), email: "bob@example.com".into(),
password: "password123".into(), password: "password123".into(),
}, },

View File

@@ -8,7 +8,7 @@ use crate::{
commands::RegisterCommand, commands::RegisterCommand,
deps::{LoginDeps, RefreshDeps, RegisterDeps}, deps::{LoginDeps, RefreshDeps, RegisterDeps},
login, login,
queries::LoginQuery, queries::LoginCommand,
refresh, register, refresh, register,
}, },
test_helpers::TestContextBuilder, test_helpers::TestContextBuilder,
@@ -41,7 +41,7 @@ async fn login_user(b: &TestContextBuilder) -> login::LoginResult {
}; };
login::execute( login::execute(
&login_deps, &login_deps,
LoginQuery { LoginCommand {
email: "alice@example.com".into(), email: "alice@example.com".into(),
password: "password123".into(), password: "password123".into(),
}, },

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
models::{ models::{
DiaryEntry, DiaryFilter, SortDirection, DiaryEntry, DiaryFilter, ReviewSortBy,
collections::{PageParams, Paginated}, collections::{PageParams, Paginated},
}, },
ports::DiaryRepository, ports::DiaryRepository,
@@ -21,7 +21,7 @@ pub async fn execute(
let user_id = query.user_id.map(UserId::from_uuid); let user_id = query.user_id.map(UserId::from_uuid);
let filter = DiaryFilter { let filter = DiaryFilter {
sort_by: query.sort_by.unwrap_or(SortDirection::Descending), sort_by: query.sort_by.unwrap_or(ReviewSortBy::Descending),
page, page,
movie_id, movie_id,
user_id: user_id.clone(), user_id: user_id.clone(),

View File

@@ -1,10 +1,10 @@
use domain::models::SortDirection; use domain::models::ReviewSortBy;
use uuid::Uuid; use uuid::Uuid;
pub struct GetDiaryQuery { pub struct GetDiaryQuery {
pub limit: Option<u32>, pub limit: Option<u32>,
pub offset: Option<u32>, pub offset: Option<u32>,
pub sort_by: Option<SortDirection>, pub sort_by: Option<ReviewSortBy>,
pub movie_id: Option<Uuid>, pub movie_id: Option<Uuid>,
pub user_id: Option<Uuid>, pub user_id: Option<Uuid>,
} }

View File

@@ -1,8 +1,8 @@
use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId}; 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); let uid = UserId::from_uuid(user_id);
deps.user deps.user

View File

@@ -16,3 +16,8 @@ pub struct UpdateProfileDeps {
pub object_storage: Arc<dyn ObjectStorage>, pub object_storage: Arc<dyn ObjectStorage>,
pub event_publisher: Arc<dyn EventPublisher>, pub event_publisher: Arc<dyn EventPublisher>,
} }
pub struct DeleteAccountDeps {
pub user: Arc<dyn UserRepository>,
pub event_publisher: Arc<dyn EventPublisher>,
}

View File

@@ -6,7 +6,7 @@ use domain::{
errors::DomainError, errors::DomainError,
models::FeedSortBy, models::FeedSortBy,
models::{ models::{
DiaryEntry, DiaryFilter, SortDirection, UserStats, UserTrends, DiaryEntry, DiaryFilter, ReviewSortBy, UserStats, UserTrends,
collections::{PageParams, Paginated}, collections::{PageParams, Paginated},
}, },
value_objects::UserId, value_objects::UserId,
@@ -108,18 +108,18 @@ async fn load_social_counts(
(following, followers, pending) (following, followers, pending)
} }
fn feed_sort_to_direction(sort_by: FeedSortBy) -> SortDirection { fn feed_sort_to_direction(sort_by: FeedSortBy) -> ReviewSortBy {
match sort_by { match sort_by {
FeedSortBy::Date => SortDirection::Descending, FeedSortBy::Date => ReviewSortBy::Descending,
FeedSortBy::DateAsc => SortDirection::Ascending, FeedSortBy::DateAsc => ReviewSortBy::Ascending,
FeedSortBy::Rating => SortDirection::ByRatingDesc, FeedSortBy::Rating => ReviewSortBy::ByRatingDesc,
FeedSortBy::RatingAsc => SortDirection::ByRatingAsc, FeedSortBy::RatingAsc => ReviewSortBy::ByRatingAsc,
} }
} }
fn paged_user_filter( fn paged_user_filter(
user_id: UserId, user_id: UserId,
sort_by: SortDirection, sort_by: ReviewSortBy,
limit: Option<u32>, limit: Option<u32>,
offset: Option<u32>, offset: Option<u32>,
search: Option<String>, search: Option<String>,
@@ -149,19 +149,19 @@ mod helper_tests {
use domain::models::FeedSortBy; use domain::models::FeedSortBy;
assert!(matches!( assert!(matches!(
feed_sort_to_direction(FeedSortBy::Date), feed_sort_to_direction(FeedSortBy::Date),
SortDirection::Descending ReviewSortBy::Descending
)); ));
assert!(matches!( assert!(matches!(
feed_sort_to_direction(FeedSortBy::DateAsc), feed_sort_to_direction(FeedSortBy::DateAsc),
SortDirection::Ascending ReviewSortBy::Ascending
)); ));
assert!(matches!( assert!(matches!(
feed_sort_to_direction(FeedSortBy::Rating), feed_sort_to_direction(FeedSortBy::Rating),
SortDirection::ByRatingDesc ReviewSortBy::ByRatingDesc
)); ));
assert!(matches!( assert!(matches!(
feed_sort_to_direction(FeedSortBy::RatingAsc), 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 uid = UserId::from_uuid(uuid::Uuid::new_v4());
let filter = paged_user_filter( let filter = paged_user_filter(
uid.clone(), uid.clone(),
SortDirection::Descending, ReviewSortBy::Descending,
Some(20), Some(20),
Some(5), Some(5),
Some("blade".into()), Some("blade".into()),

View File

@@ -15,16 +15,6 @@ pub struct ImportSession {
pub expires_at: NaiveDateTime, pub expires_at: NaiveDateTime,
} }
pub struct PersistedImportSession {
pub id: ImportSessionId,
pub user_id: UserId,
pub parsed_file: Option<ParsedFile>,
pub field_mappings: Option<Vec<FieldMapping>>,
pub row_results: Option<Vec<AnnotatedRow>>,
pub created_at: NaiveDateTime,
pub expires_at: NaiveDateTime,
}
impl ImportSession { impl ImportSession {
pub fn new(user_id: UserId) -> Self { pub fn new(user_id: UserId) -> Self {
let created_at = chrono::Utc::now().naive_utc(); let created_at = chrono::Utc::now().naive_utc();
@@ -39,16 +29,4 @@ impl ImportSession {
expires_at, 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,
}
}
} }

View File

@@ -47,8 +47,9 @@ pub use import::{
pub use import_profile::ImportProfile; pub use import_profile::ImportProfile;
pub use import_session::ImportSession; pub use import_session::ImportSession;
pub use person::{ 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 refresh_session::{GeneratedToken, RefreshSession};
pub use search::{ pub use search::{
EntityType, IndexableDocument, MovieSearchHit, PersonSearchHit, SearchFilters, SearchQuery, EntityType, IndexableDocument, MovieSearchHit, PersonSearchHit, SearchFilters, SearchQuery,
@@ -85,7 +86,7 @@ impl std::str::FromStr for GoalType {
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub enum SortDirection { pub enum ReviewSortBy {
#[default] #[default]
Descending, Descending,
Ascending, Ascending,

View File

@@ -1,25 +1,4 @@
use uuid::Uuid; use crate::value_objects::{MovieId, PersonId};
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
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct ExternalPersonId(String); pub struct ExternalPersonId(String);

View File

@@ -165,7 +165,7 @@ impl DiaryEntry {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct DiaryFilter { pub struct DiaryFilter {
pub sort_by: super::SortDirection, pub sort_by: super::ReviewSortBy,
pub page: crate::models::collections::PageParams, pub page: crate::models::collections::PageParams,
pub movie_id: Option<MovieId>, pub movie_id: Option<MovieId>,
pub user_id: Option<UserId>, pub user_id: Option<UserId>,

View File

@@ -42,3 +42,12 @@ uuid_id!(WatchEventId);
uuid_id!(WebhookTokenId); uuid_id!(WebhookTokenId);
uuid_id!(WrapUpId); uuid_id!(WrapUpId);
uuid_id!(GoalId); 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()))
}
}

View File

@@ -19,7 +19,7 @@ pub fn extract_from_cookie(headers: &axum::http::HeaderMap) -> Option<String> {
}) })
} }
fn secure_flag() -> &'static str { pub(crate) fn secure_flag() -> &'static str {
if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") { if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") {
"; Secure" "; Secure"
} else { } else {

View File

@@ -6,7 +6,7 @@ use application::diary::{
commands::{LogReviewCommand, MovieInput}, commands::{LogReviewCommand, MovieInput},
queries::GetDiaryQuery, queries::GetDiaryQuery,
}; };
use domain::{errors::DomainError, models::SortDirection}; use domain::{errors::DomainError, models::ReviewSortBy};
use api_types::{DiaryQueryParams, LogReviewRequest}; use api_types::{DiaryQueryParams, LogReviewRequest};
@@ -258,10 +258,10 @@ pub fn to_diary_query(p: DiaryQueryParams) -> GetDiaryQuery {
limit: p.limit, limit: p.limit,
offset: p.offset, offset: p.offset,
sort_by: p.sort_by.as_deref().map(|s| match s { sort_by: p.sort_by.as_deref().map(|s| match s {
"date_asc" | "asc" => SortDirection::Ascending, "date_asc" | "asc" => ReviewSortBy::Ascending,
"rating_desc" => SortDirection::ByRatingDesc, "rating_desc" => ReviewSortBy::ByRatingDesc,
"rating_asc" => SortDirection::ByRatingAsc, "rating_asc" => ReviewSortBy::ByRatingAsc,
_ => SortDirection::Descending, _ => ReviewSortBy::Descending,
}), }),
movie_id: p.movie_id, movie_id: p.movie_id,
user_id: p.user_id, user_id: p.user_id,

View File

@@ -10,12 +10,12 @@ use application::auth::{
commands::RegisterCommand, commands::RegisterCommand,
deps::{LoginDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps}, deps::{LoginDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps},
login as login_uc, login as login_uc,
queries::LoginQuery, queries::LoginCommand,
register as register_uc, register as register_uc,
}; };
use crate::{ use crate::{
csrf::CsrfToken, csrf::{CsrfToken, secure_flag},
errors::ApiError, errors::ApiError,
forms::{ErrorQuery, LoginForm, RegisterForm}, forms::{ErrorQuery, LoginForm, RegisterForm},
render::render_page, render::render_page,
@@ -29,14 +29,6 @@ use template_askama::{LoginTemplate, RegisterTemplate};
// ── HTML helpers ───────────────────────────────────────────────────────────── // ── 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) { fn set_cookie_header(token: &str, max_age: i64) -> (axum::http::HeaderName, HeaderValue) {
let val = format!( let val = format!(
"token={}; HttpOnly; Path=/; SameSite=Strict; Max-Age={}{}", "token={}; HttpOnly; Path=/; SameSite=Strict; Max-Age={}{}",
@@ -73,7 +65,7 @@ pub async fn login(
}; };
let result = login_uc::execute( let result = login_uc::execute(
&deps, &deps,
LoginQuery { LoginCommand {
email: req.email, email: req.email,
password: req.password, password: req.password,
}, },
@@ -204,7 +196,7 @@ pub async fn post_login(
}; };
match login_uc::execute( match login_uc::execute(
&deps, &deps,
LoginQuery { LoginCommand {
email: form.email, email: form.email,
password: form.password, password: form.password,
}, },

View File

@@ -6,7 +6,7 @@ use axum::{
use uuid::Uuid; use uuid::Uuid;
use application::{diary::get_diary, diary::queries::GetDiaryQuery}; 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}; use crate::{errors::ApiError, state::AppState};
@@ -14,7 +14,7 @@ pub async fn get_feed(State(state): State<AppState>) -> Result<impl IntoResponse
let query = GetDiaryQuery { let query = GetDiaryQuery {
limit: Some(super::RSS_FEED_LIMIT), limit: Some(super::RSS_FEED_LIMIT),
offset: Some(0), offset: Some(0),
sort_by: Some(SortDirection::Descending), sort_by: Some(ReviewSortBy::Descending),
movie_id: None, movie_id: None,
user_id: None, user_id: None,
}; };
@@ -45,7 +45,7 @@ pub async fn get_user_feed(
let query = GetDiaryQuery { let query = GetDiaryQuery {
limit: Some(super::RSS_FEED_LIMIT), limit: Some(super::RSS_FEED_LIMIT),
offset: Some(0), offset: Some(0),
sort_by: Some(SortDirection::Descending), sort_by: Some(ReviewSortBy::Descending),
movie_id: None, movie_id: None,
user_id: Some(user_id), user_id: Some(user_id),
}; };

View File

@@ -209,8 +209,6 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
)), )),
#[cfg(feature = "federation")] #[cfg(feature = "federation")]
ap_service, ap_service,
#[cfg(feature = "federation")]
social_query,
}; };
Ok((state, ap_router)) Ok((state, ap_router))
} }

View File

@@ -10,6 +10,4 @@ pub struct AppState {
pub rss_renderer: Arc<dyn RssFeedRenderer>, pub rss_renderer: Arc<dyn RssFeedRenderer>,
#[cfg(feature = "federation")] #[cfg(feature = "federation")]
pub ap_service: Arc<dyn activitypub::ActivityPubPort>, pub ap_service: Arc<dyn activitypub::ActivityPubPort>,
#[cfg(feature = "federation")]
pub social_query: Arc<dyn domain::ports::SocialQueryPort>,
} }

View File

@@ -843,8 +843,6 @@ pub fn make_test_state(auth_service: Arc<dyn AuthService>) -> crate::state::AppS
rss_renderer: Arc::new(Panic), rss_renderer: Arc::new(Panic),
#[cfg(feature = "federation")] #[cfg(feature = "federation")]
ap_service: Arc::new(activitypub::NoopActivityPubService), ap_service: Arc::new(activitypub::NoopActivityPubService),
#[cfg(feature = "federation")]
social_query: Arc::new(Panic),
} }
} }

View File

@@ -89,7 +89,7 @@ fn sort_by_asc_string_becomes_ascending() {
let query = to_diary_query(params); let query = to_diary_query(params);
assert!(matches!( assert!(matches!(
query.sort_by, 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); let query = to_diary_query(params);
assert!(matches!( assert!(matches!(
query.sort_by, query.sort_by,
Some(domain::models::SortDirection::Descending) Some(domain::models::ReviewSortBy::Descending)
)); ));
} }

View File

@@ -500,8 +500,6 @@ async fn test_app() -> Router {
rss_renderer: Arc::new(RssAdapter::new("http://localhost:3000".into())), rss_renderer: Arc::new(RssAdapter::new("http://localhost:3000".into())),
#[cfg(feature = "federation")] #[cfg(feature = "federation")]
ap_service: Arc::new(activitypub::NoopActivityPubService), ap_service: Arc::new(activitypub::NoopActivityPubService),
#[cfg(feature = "federation")]
social_query: Arc::new(PanicSocialQuery),
}; };
routes::build_router(state, axum::Router::new()) routes::build_router(state, axum::Router::new())

View File

@@ -248,6 +248,8 @@ pub enum Action {
ReviewCreateFailed(String), ReviewCreateFailed(String),
ReviewDeleted(Uuid), ReviewDeleted(Uuid),
ReviewDeleteFailed(String), ReviewDeleteFailed(String),
FileRead(String),
FileReadFailed(String),
BulkItemDone { BulkItemDone {
index: usize, index: usize,
error: Option<String>, error: Option<String>,
@@ -265,6 +267,7 @@ pub enum Command {
SaveConfig(String), SaveConfig(String),
SaveToken(String), SaveToken(String),
ClearToken, ClearToken,
ReadFile { path: String },
} }
// Matches the export CSV column order: // Matches the export CSV column order:
@@ -863,19 +866,31 @@ pub fn update(app: &mut App, action: Action) -> Vec<Command> {
&& m.bulk_import.stage == BulkImportStage::EnterPath && m.bulk_import.stage == BulkImportStage::EnterPath
{ {
let path = m.bulk_import.file_path.trim().to_string(); let path = m.bulk_import.file_path.trim().to_string();
match std::fs::read_to_string(&path) { if path.is_empty() {
Ok(content) => { 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.parsed = parse_csv(&content);
m.bulk_import.stage = BulkImportStage::Preview; m.bulk_import.stage = BulkImportStage::Preview;
} }
Err(e) => { vec![]
}
Action::FileReadFailed(msg) => {
app.status = Some(StatusMsg { app.status = Some(StatusMsg {
text: format!("Cannot read file: {e}"), text: format!("Cannot read file: {msg}"),
is_error: true, is_error: true,
}); });
}
}
}
vec![] vec![]
} }

View File

@@ -220,6 +220,21 @@ fn handle_command(cmd: Command, app: &App, client: &Arc<ApiClient>, 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) => { Command::ImportNext(index) => {
let Some(token) = app.token.clone() else { let Some(token) = app.token.clone() else {
return; return;