use chrono::NaiveDateTime; use domain::{ errors::DomainError, models::{DiaryEntry, FeedEntry, Movie, Review, UserSummary}, value_objects::{ Comment, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear, ReviewId, UserId, }, }; use uuid::Uuid; #[derive(sqlx::FromRow)] pub(crate) struct MovieRow { pub id: String, pub external_metadata_id: Option, pub title: String, pub release_year: i64, pub director: Option, pub poster_path: Option, } impl MovieRow { pub fn to_domain(self) -> Result { let id = MovieId::from_uuid(parse_uuid(&self.id)?); let external_metadata_id = self .external_metadata_id .map(ExternalMetadataId::new) .transpose()?; let title = MovieTitle::new(self.title)?; let release_year = ReleaseYear::new(self.release_year as u16)?; let poster_path = self.poster_path.map(PosterPath::new).transpose()?; Ok(Movie::from_persistence( id, external_metadata_id, title, release_year, self.director, poster_path, )) } } #[derive(sqlx::FromRow)] pub(crate) struct ReviewRow { pub id: String, pub movie_id: String, pub user_id: String, pub rating: i64, pub comment: Option, pub watched_at: String, pub created_at: String, } impl ReviewRow { pub fn to_domain(self) -> Result { let id = ReviewId::from_uuid(parse_uuid(&self.id)?); let movie_id = MovieId::from_uuid(parse_uuid(&self.movie_id)?); let user_id = UserId::from_uuid(parse_uuid(&self.user_id)?); let rating = Rating::new(self.rating as u8)?; let comment = self.comment.map(Comment::new).transpose()?; let watched_at = parse_datetime(&self.watched_at)?; let created_at = parse_datetime(&self.created_at)?; Ok(Review::from_persistence( id, movie_id, user_id, rating, comment, watched_at, created_at, )) } } // Used by query_diary JOIN — r.id aliased to review_id to avoid ambiguity with m.id #[derive(sqlx::FromRow)] pub(crate) struct DiaryRow { pub id: String, pub external_metadata_id: Option, pub title: String, pub release_year: i64, pub director: Option, pub poster_path: Option, pub review_id: String, pub movie_id: String, pub user_id: String, pub rating: i64, pub comment: Option, pub watched_at: String, pub created_at: String, } impl DiaryRow { pub fn to_domain(self) -> Result { let movie = MovieRow { id: self.id, external_metadata_id: self.external_metadata_id, title: self.title, release_year: self.release_year, director: self.director, poster_path: self.poster_path, } .to_domain()?; let review = ReviewRow { id: self.review_id, movie_id: self.movie_id, user_id: self.user_id, rating: self.rating, comment: self.comment, watched_at: self.watched_at, created_at: self.created_at, } .to_domain()?; Ok(DiaryEntry::new(movie, review)) } } // Like DiaryRow but includes user_email from JOIN with users table #[derive(sqlx::FromRow)] pub(crate) struct FeedRow { pub id: String, pub external_metadata_id: Option, pub title: String, pub release_year: i64, pub director: Option, pub poster_path: Option, pub review_id: String, pub movie_id: String, pub user_id: String, pub rating: i64, pub comment: Option, pub watched_at: String, pub created_at: String, pub user_email: String, } impl FeedRow { pub fn to_domain(self) -> Result { let diary = DiaryRow { id: self.id, external_metadata_id: self.external_metadata_id, title: self.title, release_year: self.release_year, director: self.director, poster_path: self.poster_path, review_id: self.review_id, movie_id: self.movie_id, user_id: self.user_id, rating: self.rating, comment: self.comment, watched_at: self.watched_at, created_at: self.created_at, } .to_domain()?; Ok(FeedEntry::new(diary, self.user_email)) } } #[derive(sqlx::FromRow)] pub(crate) struct UserSummaryRow { pub id: String, pub email: String, pub total_movies: i64, pub avg_rating: Option, } impl UserSummaryRow { pub fn to_domain(self) -> Result { Ok(UserSummary { user_id: UserId::from_uuid(parse_uuid(&self.id)?), email: self.email, total_movies: self.total_movies, avg_rating: self.avg_rating, }) } } #[derive(sqlx::FromRow)] pub(crate) struct UserTotalsRow { pub total: i64, pub avg_rating: Option, } #[derive(sqlx::FromRow)] pub(crate) struct DirectorCountRow { pub director: String, pub count: i64, } #[derive(sqlx::FromRow)] pub(crate) struct MonthlyRatingRow { pub month: String, pub avg_rating: f64, pub count: i64, } pub(crate) fn parse_uuid(s: &str) -> Result { Uuid::parse_str(s) .map_err(|e| DomainError::InfrastructureError(format!("Invalid UUID '{}': {}", s, e))) } pub(crate) fn datetime_to_str(dt: &NaiveDateTime) -> String { dt.format("%Y-%m-%d %H:%M:%S").to_string() } pub(crate) fn parse_datetime(s: &str) -> Result { NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") .map_err(|e| DomainError::InfrastructureError(format!("Invalid datetime '{}': {}", s, e))) }