feat: implement movie listing functionality with pagination and search
This commit is contained in:
@@ -375,6 +375,52 @@ impl MovieRepository for PostgresRepository {
|
|||||||
.map_err(Self::map_err)?;
|
.map_err(Self::map_err)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_movies(
|
||||||
|
&self,
|
||||||
|
page: &domain::models::collections::PageParams,
|
||||||
|
search: Option<&str>,
|
||||||
|
) -> Result<domain::models::collections::Paginated<domain::models::Movie>, DomainError> {
|
||||||
|
use sqlx::Row;
|
||||||
|
let limit = page.limit as i64;
|
||||||
|
let offset = page.offset as i64;
|
||||||
|
let pattern = search.map(|s| format!("%{}%", s.to_lowercase()));
|
||||||
|
|
||||||
|
let rows: Vec<models::MovieRow> = sqlx::query_as(
|
||||||
|
"SELECT id, external_metadata_id, title, release_year, director, poster_path \
|
||||||
|
FROM movies \
|
||||||
|
WHERE ($1::text IS NULL OR LOWER(title) LIKE $1) \
|
||||||
|
ORDER BY title ASC \
|
||||||
|
LIMIT $2 OFFSET $3",
|
||||||
|
)
|
||||||
|
.bind(&pattern)
|
||||||
|
.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_err)?;
|
||||||
|
|
||||||
|
let total: i64 = sqlx::query(
|
||||||
|
"SELECT COUNT(*) FROM movies WHERE ($1::text IS NULL OR LOWER(title) LIKE $1)",
|
||||||
|
)
|
||||||
|
.bind(&pattern)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_err)?
|
||||||
|
.try_get(0)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let items = rows.into_iter()
|
||||||
|
.map(|r| r.to_domain())
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
Ok(domain::models::collections::Paginated {
|
||||||
|
items,
|
||||||
|
total_count: total as u64,
|
||||||
|
limit: page.limit,
|
||||||
|
offset: page.offset,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -383,6 +383,54 @@ impl MovieRepository for SqliteMovieRepository {
|
|||||||
.map_err(Self::map_err)?;
|
.map_err(Self::map_err)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_movies(
|
||||||
|
&self,
|
||||||
|
page: &domain::models::collections::PageParams,
|
||||||
|
search: Option<&str>,
|
||||||
|
) -> Result<domain::models::collections::Paginated<domain::models::Movie>, DomainError> {
|
||||||
|
use sqlx::Row;
|
||||||
|
let limit = page.limit as i64;
|
||||||
|
let offset = page.offset as i64;
|
||||||
|
let pattern = search.map(|s| format!("%{}%", s.to_lowercase()));
|
||||||
|
|
||||||
|
let rows: Vec<models::MovieRow> = sqlx::query_as(
|
||||||
|
"SELECT id, external_metadata_id, title, release_year, director, poster_path \
|
||||||
|
FROM movies \
|
||||||
|
WHERE (? IS NULL OR LOWER(title) LIKE ?) \
|
||||||
|
ORDER BY title ASC \
|
||||||
|
LIMIT ? OFFSET ?",
|
||||||
|
)
|
||||||
|
.bind(&pattern)
|
||||||
|
.bind(&pattern)
|
||||||
|
.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_err)?;
|
||||||
|
|
||||||
|
let total: i64 = sqlx::query(
|
||||||
|
"SELECT COUNT(*) FROM movies WHERE (? IS NULL OR LOWER(title) LIKE ?)",
|
||||||
|
)
|
||||||
|
.bind(&pattern)
|
||||||
|
.bind(&pattern)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_err)?
|
||||||
|
.try_get(0)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let items = rows.into_iter()
|
||||||
|
.map(|r| r.to_domain())
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
Ok(domain::models::collections::Paginated {
|
||||||
|
items,
|
||||||
|
total_count: total as u64,
|
||||||
|
limit: page.limit,
|
||||||
|
offset: page.offset,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// ── Movie list ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
||||||
|
#[into_params(parameter_in = Query)]
|
||||||
|
pub struct MoviesQueryParams {
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
/// Optional title filter (case-insensitive substring match)
|
||||||
|
pub search: 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)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use domain::models::{ExportFormat, FieldMapping, FileFormat, UserRole};
|
use domain::models::{FieldMapping, FileFormat, UserRole};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct LogReviewCommand {
|
pub struct LogReviewCommand {
|
||||||
@@ -21,11 +21,6 @@ pub struct SyncPosterCommand {
|
|||||||
pub external_metadata_id: String,
|
pub external_metadata_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LoginCommand {
|
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RegisterCommand {
|
pub struct RegisterCommand {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
@@ -38,11 +33,6 @@ pub struct DeleteReviewCommand {
|
|||||||
pub requesting_user_id: Uuid,
|
pub requesting_user_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ExportCommand {
|
|
||||||
pub user_id: Uuid,
|
|
||||||
pub format: ExportFormat,
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileFormat is now in domain::models — no longer defined here
|
// FileFormat is now in domain::models — no longer defined here
|
||||||
|
|
||||||
pub struct CreateImportSessionCommand {
|
pub struct CreateImportSessionCommand {
|
||||||
@@ -79,3 +69,10 @@ pub struct DeleteImportProfileCommand {
|
|||||||
pub user_id: Uuid,
|
pub user_id: Uuid,
|
||||||
pub profile_id: Uuid,
|
pub profile_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct UpdateProfileCommand {
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub bio: Option<String>,
|
||||||
|
pub avatar_bytes: Option<Vec<u8>>,
|
||||||
|
pub avatar_content_type: Option<String>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -226,9 +226,8 @@ mod tests {
|
|||||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
||||||
panic!("unexpected")
|
panic!("unexpected")
|
||||||
}
|
}
|
||||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { panic!("unexpected") }
|
||||||
panic!("unexpected")
|
async fn list_movies(&self, _: &domain::models::collections::PageParams, _: Option<&str>) -> Result<domain::models::collections::Paginated<Movie>, DomainError> { panic!("unexpected") }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -249,12 +248,9 @@ mod tests {
|
|||||||
) -> Result<Vec<Movie>, DomainError> {
|
) -> Result<Vec<Movie>, DomainError> {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> { panic!("unexpected") }
|
||||||
panic!("unexpected")
|
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { panic!("unexpected") }
|
||||||
}
|
async fn list_movies(&self, _: &domain::models::collections::PageParams, _: Option<&str>) -> Result<domain::models::collections::Paginated<Movie>, DomainError> { panic!("unexpected") }
|
||||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
|
||||||
panic!("unexpected")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -275,12 +271,9 @@ mod tests {
|
|||||||
) -> Result<Vec<Movie>, DomainError> {
|
) -> Result<Vec<Movie>, DomainError> {
|
||||||
Ok(vec![self.0.clone()])
|
Ok(vec![self.0.clone()])
|
||||||
}
|
}
|
||||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> { panic!("unexpected") }
|
||||||
panic!("unexpected")
|
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { panic!("unexpected") }
|
||||||
}
|
async fn list_movies(&self, _: &domain::models::collections::PageParams, _: Option<&str>) -> Result<domain::models::collections::Paginated<Movie>, DomainError> { panic!("unexpected") }
|
||||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
|
||||||
panic!("unexpected")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MetaReturnsMovie(Movie);
|
struct MetaReturnsMovie(Movie);
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
use domain::models::SortDirection;
|
use domain::models::{ExportFormat, SortDirection};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct LoginQuery {
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExportQuery {
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub format: ExportFormat,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct GetDiaryQuery {
|
pub struct GetDiaryQuery {
|
||||||
pub limit: Option<u32>,
|
pub limit: Option<u32>,
|
||||||
pub offset: Option<u32>,
|
pub offset: Option<u32>,
|
||||||
@@ -70,3 +80,9 @@ pub struct GetMovieSocialPageQuery {
|
|||||||
pub limit: u32,
|
pub limit: u32,
|
||||||
pub offset: u32,
|
pub offset: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct GetMoviesQuery {
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
pub search: Option<String>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use domain::{errors::DomainError, value_objects::UserId};
|
use domain::{errors::DomainError, value_objects::UserId};
|
||||||
|
|
||||||
use crate::{commands::ExportCommand, context::AppContext};
|
use crate::{context::AppContext, queries::ExportQuery};
|
||||||
|
|
||||||
pub async fn execute(ctx: &AppContext, cmd: ExportCommand) -> Result<Vec<u8>, DomainError> {
|
pub async fn execute(ctx: &AppContext, query: ExportQuery) -> Result<Vec<u8>, DomainError> {
|
||||||
let entries = ctx
|
let entries = ctx
|
||||||
.diary_repository
|
.diary_repository
|
||||||
.get_user_history(&UserId::from_uuid(cmd.user_id))
|
.get_user_history(&UserId::from_uuid(query.user_id))
|
||||||
.await?;
|
.await?;
|
||||||
ctx.diary_exporter
|
ctx.diary_exporter
|
||||||
.serialize_entries(&entries, cmd.format)
|
.serialize_entries(&entries, query.format)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
14
crates/application/src/use_cases/get_movies.rs
Normal file
14
crates/application/src/use_cases/get_movies.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
|
models::collections::{PageParams, Paginated},
|
||||||
|
models::Movie,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{context::AppContext, queries::GetMoviesQuery};
|
||||||
|
|
||||||
|
pub async fn execute(ctx: &AppContext, query: GetMoviesQuery) -> Result<Paginated<Movie>, DomainError> {
|
||||||
|
let page = PageParams::new(query.limit, query.offset)?;
|
||||||
|
ctx.movie_repository
|
||||||
|
.list_movies(&page, query.search.as_deref())
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use domain::{errors::DomainError, value_objects::Email};
|
use domain::{errors::DomainError, value_objects::Email};
|
||||||
|
|
||||||
use crate::{commands::LoginCommand, context::AppContext};
|
use crate::{context::AppContext, queries::LoginQuery};
|
||||||
|
|
||||||
pub struct LoginResult {
|
pub struct LoginResult {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
@@ -12,8 +12,8 @@ pub struct LoginResult {
|
|||||||
pub expires_at: DateTime<Utc>,
|
pub expires_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(ctx: &AppContext, cmd: LoginCommand) -> Result<LoginResult, DomainError> {
|
pub async fn execute(ctx: &AppContext, query: LoginQuery) -> Result<LoginResult, DomainError> {
|
||||||
let email = Email::new(cmd.email)?;
|
let email = Email::new(query.email)?;
|
||||||
let user = ctx
|
let user = ctx
|
||||||
.user_repository
|
.user_repository
|
||||||
.find_by_email(&email)
|
.find_by_email(&email)
|
||||||
@@ -22,7 +22,7 @@ pub async fn execute(ctx: &AppContext, cmd: LoginCommand) -> Result<LoginResult,
|
|||||||
|
|
||||||
let valid = ctx
|
let valid = ctx
|
||||||
.password_hasher
|
.password_hasher
|
||||||
.verify(&cmd.password, user.password_hash())
|
.verify(&query.password, user.password_hash())
|
||||||
.await?;
|
.await?;
|
||||||
if !valid {
|
if !valid {
|
||||||
return Err(DomainError::Unauthorized("Invalid credentials".into()));
|
return Err(DomainError::Unauthorized("Invalid credentials".into()));
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub mod export_diary;
|
|||||||
pub mod get_activity_feed;
|
pub mod get_activity_feed;
|
||||||
pub mod get_diary;
|
pub mod get_diary;
|
||||||
pub mod get_movie_social_page;
|
pub mod get_movie_social_page;
|
||||||
|
pub mod get_movies;
|
||||||
pub mod get_review_history;
|
pub mod get_review_history;
|
||||||
pub mod get_user_profile;
|
pub mod get_user_profile;
|
||||||
pub mod get_users;
|
pub mod get_users;
|
||||||
|
|||||||
@@ -4,14 +4,7 @@ use domain::{
|
|||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::context::AppContext;
|
use crate::{commands::UpdateProfileCommand, context::AppContext};
|
||||||
|
|
||||||
pub struct UpdateProfileCommand {
|
|
||||||
pub user_id: uuid::Uuid,
|
|
||||||
pub bio: Option<String>,
|
|
||||||
pub avatar_bytes: Option<Vec<u8>>,
|
|
||||||
pub avatar_content_type: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(), DomainError> {
|
pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(), DomainError> {
|
||||||
let user_id = UserId::from_uuid(cmd.user_id);
|
let user_id = UserId::from_uuid(cmd.user_id);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
AnnotatedRow, DiaryEntry, DiaryFilter, ExportFormat, FeedEntry, FieldMapping,
|
AnnotatedRow, DiaryEntry, DiaryFilter, ExportFormat, FeedEntry, FieldMapping,
|
||||||
FileFormat, ImportError, ImportProfile, ImportSession, Movie, MovieProfile, MovieStats,
|
FileFormat, ImportError, ImportProfile, ImportSession, Movie, MovieProfile, MovieStats,
|
||||||
ParsedFile, Review, ReviewHistory, User, UserStats, UserSummary, UserTrends,
|
ParsedFile, Review, ReviewHistory, User, UserStats, UserSummary, UserTrends,
|
||||||
collections::{PageParams, Paginated},
|
collections::{self, PageParams, Paginated},
|
||||||
},
|
},
|
||||||
value_objects::{
|
value_objects::{
|
||||||
Email, ExternalMetadataId, ImportProfileId, ImportSessionId, MovieId, MovieTitle,
|
Email, ExternalMetadataId, ImportProfileId, ImportSessionId, MovieId, MovieTitle,
|
||||||
@@ -83,6 +83,11 @@ pub trait MovieRepository: Send + Sync {
|
|||||||
) -> Result<Vec<Movie>, DomainError>;
|
) -> Result<Vec<Movie>, DomainError>;
|
||||||
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError>;
|
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError>;
|
||||||
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError>;
|
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError>;
|
||||||
|
async fn list_movies(
|
||||||
|
&self,
|
||||||
|
page: &collections::PageParams,
|
||||||
|
search: Option<&str>,
|
||||||
|
) -> Result<collections::Paginated<Movie>, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -177,6 +177,9 @@ mod tests {
|
|||||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
||||||
panic!()
|
panic!()
|
||||||
}
|
}
|
||||||
|
async fn list_movies(&self, _: &domain::models::collections::PageParams, _: Option<&str>) -> Result<domain::models::collections::Paginated<Movie>, DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl ReviewRepository for Panic {
|
impl ReviewRepository for Panic {
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use application::{
|
use application::{
|
||||||
commands::{
|
commands::{
|
||||||
DeleteReviewCommand, ExportCommand, LoginCommand, RegisterCommand, SyncPosterCommand,
|
DeleteReviewCommand, RegisterCommand, SyncPosterCommand,
|
||||||
},
|
},
|
||||||
queries::{
|
queries::{
|
||||||
GetActivityFeedQuery, GetMovieSocialPageQuery, GetReviewHistoryQuery, GetUserProfileQuery,
|
ExportQuery, GetActivityFeedQuery, GetMovieSocialPageQuery, GetMoviesQuery,
|
||||||
GetUsersQuery,
|
GetReviewHistoryQuery, GetUserProfileQuery, GetUsersQuery, LoginQuery,
|
||||||
},
|
},
|
||||||
use_cases::{
|
use_cases::{
|
||||||
delete_review, export_diary as export_diary_uc, get_activity_feed as get_feed_uc,
|
delete_review, export_diary as export_diary_uc, get_activity_feed as get_feed_uc,
|
||||||
get_diary, get_movie_social_page, get_review_history,
|
get_diary, get_movie_social_page, get_movies, get_review_history,
|
||||||
get_user_profile as get_user_profile_uc, get_users, log_review, login as login_uc,
|
get_user_profile as get_user_profile_uc, get_users, log_review, login as login_uc,
|
||||||
register as register_uc, sync_poster, update_profile,
|
register as register_uc, sync_poster, update_profile,
|
||||||
},
|
},
|
||||||
@@ -40,9 +40,9 @@ use api_types::{
|
|||||||
DiaryQueryParams, DiaryResponse, DirectorStatDto, ExportQueryParams, FeedEntryDto,
|
DiaryQueryParams, DiaryResponse, DirectorStatDto, ExportQueryParams, FeedEntryDto,
|
||||||
GenreDto, KeywordDto, LogReviewRequest, LoginRequest, LoginResponse, MonthActivityDto,
|
GenreDto, KeywordDto, LogReviewRequest, LoginRequest, LoginResponse, MonthActivityDto,
|
||||||
MonthlyRatingDto, MovieDetailResponse, MovieDto, MovieProfileResponse, MovieStatsDto,
|
MonthlyRatingDto, MovieDetailResponse, MovieDto, MovieProfileResponse, MovieStatsDto,
|
||||||
PaginationQueryParams, ProfileResponse, RegisterRequest, ReviewDto, ReviewHistoryResponse,
|
MoviesQueryParams, MoviesResponse, PaginationQueryParams, ProfileResponse, RegisterRequest,
|
||||||
SocialFeedResponse, SocialReviewDto, UserProfileQueryParams, UserProfileResponse, UserStatsDto,
|
ReviewDto, ReviewHistoryResponse, SocialFeedResponse, SocialReviewDto, UserProfileQueryParams,
|
||||||
UserSummaryDto, UserTrendsDto, UsersResponse,
|
UserProfileResponse, UserStatsDto, UserSummaryDto, UserTrendsDto, UsersResponse,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::ApiError,
|
errors::ApiError,
|
||||||
@@ -74,6 +74,35 @@ pub async fn get_diary(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get, path = "/api/v1/movies",
|
||||||
|
params(MoviesQueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, body = MoviesResponse),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn list_movies(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(params): Query<MoviesQueryParams>,
|
||||||
|
) -> Result<Json<MoviesResponse>, ApiError> {
|
||||||
|
let page = get_movies::execute(
|
||||||
|
&state.app_ctx,
|
||||||
|
GetMoviesQuery {
|
||||||
|
limit: params.limit,
|
||||||
|
offset: params.offset,
|
||||||
|
search: params.search,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(MoviesResponse {
|
||||||
|
items: page.items.iter().map(movie_to_dto).collect(),
|
||||||
|
total_count: page.total_count,
|
||||||
|
limit: page.limit,
|
||||||
|
offset: page.offset,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get, path = "/api/v1/movies/{id}/history",
|
get, path = "/api/v1/movies/{id}/history",
|
||||||
params(("id" = Uuid, Path, description = "Movie ID")),
|
params(("id" = Uuid, Path, description = "Movie ID")),
|
||||||
@@ -179,7 +208,7 @@ pub async fn login(
|
|||||||
) -> Result<Json<LoginResponse>, ApiError> {
|
) -> Result<Json<LoginResponse>, ApiError> {
|
||||||
let result = login_uc::execute(
|
let result = login_uc::execute(
|
||||||
&state.app_ctx,
|
&state.app_ctx,
|
||||||
LoginCommand {
|
LoginQuery {
|
||||||
email: req.email,
|
email: req.email,
|
||||||
password: req.password,
|
password: req.password,
|
||||||
},
|
},
|
||||||
@@ -415,7 +444,7 @@ pub async fn update_profile_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmd = update_profile::UpdateProfileCommand {
|
let cmd = application::commands::UpdateProfileCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
bio,
|
bio,
|
||||||
avatar_bytes,
|
avatar_bytes,
|
||||||
@@ -1036,11 +1065,11 @@ pub async fn export_diary(
|
|||||||
ExportFormat::Csv => ("text/csv; charset=utf-8", "diary.csv"),
|
ExportFormat::Csv => ("text/csv; charset=utf-8", "diary.csv"),
|
||||||
ExportFormat::Json => ("application/json", "diary.json"),
|
ExportFormat::Json => ("application/json", "diary.json"),
|
||||||
};
|
};
|
||||||
let cmd = ExportCommand {
|
let query = ExportQuery {
|
||||||
user_id: user.0.value(),
|
user_id: user.0.value(),
|
||||||
format,
|
format,
|
||||||
};
|
};
|
||||||
match export_diary_uc::execute(&state.app_ctx, cmd).await {
|
match export_diary_uc::execute(&state.app_ctx, query).await {
|
||||||
Ok(bytes) => (
|
Ok(bytes) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ use application::ports::{
|
|||||||
FollowersPageData, FollowingPageData,
|
FollowersPageData, FollowingPageData,
|
||||||
};
|
};
|
||||||
use application::{
|
use application::{
|
||||||
commands::{DeleteReviewCommand, ExportCommand, LoginCommand, RegisterCommand},
|
commands::{DeleteReviewCommand, RegisterCommand},
|
||||||
|
queries::{ExportQuery, GetMovieSocialPageQuery, LoginQuery},
|
||||||
ports::{
|
ports::{
|
||||||
HtmlPageContext, LoginPageData, MovieDetailPageData, NewReviewPageData,
|
HtmlPageContext, LoginPageData, MovieDetailPageData, NewReviewPageData,
|
||||||
ProfileSettingsPageData, RegisterPageData, RemoteActorView,
|
ProfileSettingsPageData, RegisterPageData, RemoteActorView,
|
||||||
},
|
},
|
||||||
queries::GetMovieSocialPageQuery,
|
|
||||||
use_cases::{
|
use_cases::{
|
||||||
delete_review, export_diary as export_diary_uc, get_movie_social_page, log_review,
|
delete_review, export_diary as export_diary_uc, get_movie_social_page, log_review,
|
||||||
login as login_uc, register as register_uc, update_profile,
|
login as login_uc, register as register_uc, update_profile,
|
||||||
@@ -133,7 +133,7 @@ pub async fn post_login(
|
|||||||
}
|
}
|
||||||
match login_uc::execute(
|
match login_uc::execute(
|
||||||
&state.app_ctx,
|
&state.app_ctx,
|
||||||
LoginCommand {
|
LoginQuery {
|
||||||
email: form.email,
|
email: form.email,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
},
|
},
|
||||||
@@ -215,7 +215,7 @@ pub async fn post_register(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
match login_uc::execute(&state.app_ctx, LoginCommand { email, password }).await {
|
match login_uc::execute(&state.app_ctx, LoginQuery { email, password }).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
let max_age = (result.expires_at - Utc::now()).num_seconds().max(0);
|
let max_age = (result.expires_at - Utc::now()).num_seconds().max(0);
|
||||||
let cookie = set_cookie_header(&result.token, max_age);
|
let cookie = set_cookie_header(&result.token, max_age);
|
||||||
@@ -320,11 +320,11 @@ pub async fn get_export(
|
|||||||
ExportFormat::Csv => ("text/csv; charset=utf-8", "diary.csv"),
|
ExportFormat::Csv => ("text/csv; charset=utf-8", "diary.csv"),
|
||||||
ExportFormat::Json => ("application/json", "diary.json"),
|
ExportFormat::Json => ("application/json", "diary.json"),
|
||||||
};
|
};
|
||||||
let cmd = ExportCommand {
|
let query = ExportQuery {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
format,
|
format,
|
||||||
};
|
};
|
||||||
match export_diary_uc::execute(&state.app_ctx, cmd).await {
|
match export_diary_uc::execute(&state.app_ctx, query).await {
|
||||||
Ok(bytes) => (
|
Ok(bytes) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
[
|
[
|
||||||
@@ -1230,7 +1230,7 @@ pub async fn post_profile_settings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmd = update_profile::UpdateProfileCommand {
|
let cmd = application::commands::UpdateProfileCommand {
|
||||||
user_id: user_id.value(),
|
user_id: user_id.value(),
|
||||||
bio,
|
bio,
|
||||||
avatar_bytes,
|
avatar_bytes,
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
use api_types::{
|
use api_types::{
|
||||||
DirectorStatDto, MonthActivityDto, MonthlyRatingDto, MovieDetailResponse, MovieDto,
|
CastMemberDto, CrewMemberDto, DirectorStatDto, GenreDto, KeywordDto, MonthActivityDto,
|
||||||
MovieStatsDto, ReviewHistoryResponse, SocialFeedResponse, SocialReviewDto, UserTrendsDto,
|
MonthlyRatingDto, MovieDetailResponse, MovieDto, MovieProfileResponse, MovieStatsDto,
|
||||||
|
MoviesQueryParams, MoviesResponse, ReviewHistoryResponse, SocialFeedResponse, SocialReviewDto,
|
||||||
|
UserTrendsDto,
|
||||||
};
|
};
|
||||||
use utoipa::OpenApi;
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
paths(
|
paths(
|
||||||
|
crate::handlers::api::list_movies,
|
||||||
crate::handlers::api::get_movie_detail,
|
crate::handlers::api::get_movie_detail,
|
||||||
crate::handlers::api::get_review_history,
|
crate::handlers::api::get_review_history,
|
||||||
|
crate::handlers::api::get_movie_profile,
|
||||||
crate::handlers::api::sync_poster,
|
crate::handlers::api::sync_poster,
|
||||||
),
|
),
|
||||||
components(schemas(
|
components(schemas(
|
||||||
|
MoviesResponse,
|
||||||
MovieDto,
|
MovieDto,
|
||||||
MovieDetailResponse,
|
MovieDetailResponse,
|
||||||
MovieStatsDto,
|
MovieStatsDto,
|
||||||
|
MovieProfileResponse,
|
||||||
|
GenreDto,
|
||||||
|
KeywordDto,
|
||||||
|
CastMemberDto,
|
||||||
|
CrewMemberDto,
|
||||||
ReviewHistoryResponse,
|
ReviewHistoryResponse,
|
||||||
SocialFeedResponse,
|
SocialFeedResponse,
|
||||||
SocialReviewDto,
|
SocialReviewDto,
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ fn api_routes(rate_limit: u64) -> Router<AppState> {
|
|||||||
"/movies/{id}/history",
|
"/movies/{id}/history",
|
||||||
routing::get(handlers::api::get_review_history),
|
routing::get(handlers::api::get_review_history),
|
||||||
)
|
)
|
||||||
|
.route("/movies", routing::get(handlers::api::list_movies))
|
||||||
.route(
|
.route(
|
||||||
"/movies/{id}",
|
"/movies/{id}",
|
||||||
routing::get(handlers::api::get_movie_detail),
|
routing::get(handlers::api::get_movie_detail),
|
||||||
|
|||||||
Reference in New Issue
Block a user