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:
@@ -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<Vec<DiaryRow>, 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<Vec<DiaryRow>, 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<Vec<DiaryRow>, 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 {
|
||||
""
|
||||
|
||||
@@ -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::<uuid::Uuid>()
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
@@ -282,7 +281,7 @@ impl PostgresImportSessionRepository {
|
||||
row_results,
|
||||
created_at,
|
||||
expires_at,
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Vec<DiaryRow>, 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<Vec<DiaryRow>, 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,
|
||||
|
||||
@@ -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::<uuid::Uuid>()
|
||||
.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)?,
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
pub struct PaginationQueryParams {
|
||||
pub limit: 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>;
|
||||
|
||||
@@ -28,14 +28,6 @@ pub struct DiaryEntryDto {
|
||||
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)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub struct DiaryQueryParams {
|
||||
@@ -65,14 +57,6 @@ pub struct FeedEntryDto {
|
||||
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)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub struct ExportQueryParams {
|
||||
|
||||
@@ -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<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) ────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
@@ -130,14 +124,6 @@ pub struct SocialReviewDto {
|
||||
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)]
|
||||
pub struct MovieDetailResponse {
|
||||
pub movie: MovieDto,
|
||||
|
||||
@@ -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<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)]
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct PersonSearchHitDto {
|
||||
pub person_id: Uuid,
|
||||
pub name: String,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -10,14 +10,6 @@ pub struct WatchlistEntryDto {
|
||||
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)]
|
||||
pub struct AddToWatchlistRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -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<LoginResult, DomainError> {
|
||||
pub async fn execute(deps: &LoginDeps, query: LoginCommand) -> Result<LoginResult, DomainError> {
|
||||
let email = Email::new(query.email)?;
|
||||
let user = deps
|
||||
.user
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub struct LoginQuery {
|
||||
pub struct LoginCommand {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use domain::models::SortDirection;
|
||||
use domain::models::ReviewSortBy;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct GetDiaryQuery {
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
pub sort_by: Option<SortDirection>,
|
||||
pub sort_by: Option<ReviewSortBy>,
|
||||
pub movie_id: Option<Uuid>,
|
||||
pub user_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,3 +16,8 @@ pub struct UpdateProfileDeps {
|
||||
pub object_storage: Arc<dyn ObjectStorage>,
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
pub struct DeleteAccountDeps {
|
||||
pub user: Arc<dyn UserRepository>,
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
@@ -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<u32>,
|
||||
offset: Option<u32>,
|
||||
search: Option<String>,
|
||||
@@ -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()),
|
||||
|
||||
@@ -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<ParsedFile>,
|
||||
pub field_mappings: Option<Vec<FieldMapping>>,
|
||||
pub row_results: Option<Vec<AnnotatedRow>>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<MovieId>,
|
||||
pub user_id: Option<UserId>,
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
"; Secure"
|
||||
} else {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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<AppState>) -> Result<impl IntoResponse
|
||||
let query = GetDiaryQuery {
|
||||
limit: Some(super::RSS_FEED_LIMIT),
|
||||
offset: Some(0),
|
||||
sort_by: Some(SortDirection::Descending),
|
||||
sort_by: Some(ReviewSortBy::Descending),
|
||||
movie_id: None,
|
||||
user_id: None,
|
||||
};
|
||||
@@ -45,7 +45,7 @@ pub async fn get_user_feed(
|
||||
let query = GetDiaryQuery {
|
||||
limit: Some(super::RSS_FEED_LIMIT),
|
||||
offset: Some(0),
|
||||
sort_by: Some(SortDirection::Descending),
|
||||
sort_by: Some(ReviewSortBy::Descending),
|
||||
movie_id: None,
|
||||
user_id: Some(user_id),
|
||||
};
|
||||
|
||||
@@ -209,8 +209,6 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
||||
)),
|
||||
#[cfg(feature = "federation")]
|
||||
ap_service,
|
||||
#[cfg(feature = "federation")]
|
||||
social_query,
|
||||
};
|
||||
Ok((state, ap_router))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,4 @@ pub struct AppState {
|
||||
pub rss_renderer: Arc<dyn RssFeedRenderer>,
|
||||
#[cfg(feature = "federation")]
|
||||
pub ap_service: Arc<dyn activitypub::ActivityPubPort>,
|
||||
#[cfg(feature = "federation")]
|
||||
pub social_query: Arc<dyn domain::ports::SocialQueryPort>,
|
||||
}
|
||||
|
||||
@@ -843,8 +843,6 @@ pub fn make_test_state(auth_service: Arc<dyn AuthService>) -> 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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -248,6 +248,8 @@ pub enum Action {
|
||||
ReviewCreateFailed(String),
|
||||
ReviewDeleted(Uuid),
|
||||
ReviewDeleteFailed(String),
|
||||
FileRead(String),
|
||||
FileReadFailed(String),
|
||||
BulkItemDone {
|
||||
index: usize,
|
||||
error: Option<String>,
|
||||
@@ -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<Command> {
|
||||
&& 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
|
||||
|
||||
@@ -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) => {
|
||||
let Some(token) = app.token.clone() else {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user