From c224cc6bd2903b359ff3dfb46f0b1c562752b690 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 10 Jul 2026 05:44:42 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20extract=20adapter-common=20crate=20?= =?UTF-8?q?=E2=80=94=20shared=20sqlx=20error=20mapping,=20row-to-domain=20?= =?UTF-8?q?conversions,=20date/uuid=20utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminates 39 map_err copies, consolidates parse_uuid/parse_datetime/ datetime_to_str/format_year_month, extracts 7 shared row-to-domain conversion functions (movie, review, watchlist, stats, user_summary). Row structs stay per-adapter (FromRow is db-specific), only conversion logic is shared. Net -281 lines. --- Cargo.lock | 17 ++ Cargo.toml | 2 + Dockerfile | 1 + crates/adapters/adapter-common/Cargo.toml | 11 + crates/adapters/adapter-common/src/lib.rs | 223 +++++++++++++++ .../adapters/postgres-federation/Cargo.toml | 1 + .../postgres-federation/src/activity.rs | 3 +- .../adapters/postgres-federation/src/actor.rs | 3 +- .../postgres-federation/src/ap_content.rs | 24 +- .../postgres-federation/src/blocklist.rs | 3 +- .../postgres-federation/src/follow.rs | 3 +- .../adapters/postgres-federation/src/lib.rs | 5 - .../postgres-federation/src/remote_goals.rs | 14 +- .../postgres-federation/src/review.rs | 3 +- crates/adapters/postgres-search/Cargo.toml | 9 +- crates/adapters/postgres-search/src/lib.rs | 24 +- crates/adapters/postgres/Cargo.toml | 1 + crates/adapters/postgres/src/diary.rs | 36 ++- crates/adapters/postgres/src/goals.rs | 16 +- .../adapters/postgres/src/import_profile.rs | 12 +- .../adapters/postgres/src/import_session.rs | 16 +- crates/adapters/postgres/src/lib.rs | 24 -- crates/adapters/postgres/src/models.rs | 228 +++++++-------- crates/adapters/postgres/src/movie.rs | 24 +- crates/adapters/postgres/src/movie_dedup.rs | 36 ++- crates/adapters/postgres/src/persons.rs | 25 +- crates/adapters/postgres/src/profile.rs | 38 ++- .../adapters/postgres/src/refresh_sessions.rs | 13 +- crates/adapters/postgres/src/review.rs | 17 +- crates/adapters/postgres/src/stats.rs | 14 +- crates/adapters/postgres/src/user_settings.rs | 10 +- crates/adapters/postgres/src/users.rs | 14 +- crates/adapters/postgres/src/watch_event.rs | 66 +++-- crates/adapters/postgres/src/watchlist.rs | 19 +- crates/adapters/postgres/src/wrapup.rs | 92 +++---- crates/adapters/sqlite-federation/Cargo.toml | 1 + .../sqlite-federation/src/activity.rs | 3 +- .../adapters/sqlite-federation/src/actor.rs | 3 +- .../sqlite-federation/src/ap_content.rs | 23 +- .../sqlite-federation/src/blocklist.rs | 3 +- .../adapters/sqlite-federation/src/follow.rs | 3 +- crates/adapters/sqlite-federation/src/lib.rs | 5 - .../sqlite-federation/src/remote_goals.rs | 14 +- .../adapters/sqlite-federation/src/review.rs | 3 +- crates/adapters/sqlite-search/Cargo.toml | 9 +- crates/adapters/sqlite-search/src/lib.rs | 30 +- crates/adapters/sqlite/Cargo.toml | 1 + crates/adapters/sqlite/src/diary.rs | 37 ++- crates/adapters/sqlite/src/goals.rs | 14 +- crates/adapters/sqlite/src/import_profile.rs | 13 +- crates/adapters/sqlite/src/import_session.rs | 17 +- crates/adapters/sqlite/src/lib.rs | 24 -- crates/adapters/sqlite/src/models.rs | 259 ++++++++---------- crates/adapters/sqlite/src/movie.rs | 24 +- crates/adapters/sqlite/src/movie_dedup.rs | 36 ++- crates/adapters/sqlite/src/persons.rs | 26 +- crates/adapters/sqlite/src/profile.rs | 38 ++- crates/adapters/sqlite/src/profile_fields.rs | 10 +- .../adapters/sqlite/src/refresh_sessions.rs | 14 +- crates/adapters/sqlite/src/review.rs | 17 +- crates/adapters/sqlite/src/stats.rs | 17 +- crates/adapters/sqlite/src/user_settings.rs | 10 +- crates/adapters/sqlite/src/users.rs | 15 +- crates/adapters/sqlite/src/watch_event.rs | 67 +++-- crates/adapters/sqlite/src/watchlist.rs | 19 +- crates/adapters/sqlite/src/wrapup.rs | 93 +++---- 66 files changed, 924 insertions(+), 971 deletions(-) create mode 100644 crates/adapters/adapter-common/Cargo.toml create mode 100644 crates/adapters/adapter-common/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index bfc1b2a..3fc1f4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,6 +220,17 @@ dependencies = [ "url", ] +[[package]] +name = "adapter-common" +version = "0.1.0" +dependencies = [ + "chrono", + "domain", + "sqlx", + "tracing", + "uuid", +] + [[package]] name = "adler2" version = "2.0.1" @@ -3884,6 +3895,7 @@ dependencies = [ name = "postgres" version = "0.1.0" dependencies = [ + "adapter-common", "anyhow", "async-stream", "async-trait", @@ -3920,6 +3932,7 @@ name = "postgres-federation" version = "0.1.0" dependencies = [ "activitypub", + "adapter-common", "anyhow", "async-trait", "chrono", @@ -3935,6 +3948,7 @@ dependencies = [ name = "postgres-search" version = "0.1.0" dependencies = [ + "adapter-common", "async-trait", "domain", "sqlx", @@ -5164,6 +5178,7 @@ dependencies = [ name = "sqlite" version = "0.1.0" dependencies = [ + "adapter-common", "anyhow", "async-stream", "async-trait", @@ -5200,6 +5215,7 @@ name = "sqlite-federation" version = "0.1.0" dependencies = [ "activitypub", + "adapter-common", "anyhow", "async-trait", "chrono", @@ -5216,6 +5232,7 @@ dependencies = [ name = "sqlite-search" version = "0.1.0" dependencies = [ + "adapter-common", "async-trait", "domain", "sqlx", diff --git a/Cargo.toml b/Cargo.toml index 19a0479..b3e4515 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "crates/adapters/plex", "crates/adapters/sqlite-search", "crates/adapters/postgres-search", + "crates/adapters/adapter-common", "crates/infra-wiring", ] resolver = "2" @@ -92,6 +93,7 @@ plex = { path = "crates/adapters/plex" } image-converter = { path = "crates/adapters/image-converter" } sqlite-search = { path = "crates/adapters/sqlite-search" } postgres-search = { path = "crates/adapters/postgres-search" } +adapter-common = { path = "crates/adapters/adapter-common" } infra-wiring = { path = "crates/infra-wiring" } [profile.dev] diff --git a/Dockerfile b/Dockerfile index 54fb9d8..4ba1adf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,7 @@ COPY crates/tui/Cargo.toml crates/tui/Cargo.toml COPY crates/adapters/image-converter/Cargo.toml crates/adapters/image-converter/Cargo.toml COPY crates/adapters/sqlite-search/Cargo.toml crates/adapters/sqlite-search/Cargo.toml COPY crates/adapters/postgres-search/Cargo.toml crates/adapters/postgres-search/Cargo.toml +COPY crates/adapters/adapter-common/Cargo.toml crates/adapters/adapter-common/Cargo.toml COPY crates/worker/Cargo.toml crates/worker/Cargo.toml COPY crates/infra-wiring/Cargo.toml crates/infra-wiring/Cargo.toml diff --git a/crates/adapters/adapter-common/Cargo.toml b/crates/adapters/adapter-common/Cargo.toml new file mode 100644 index 0000000..0951f37 --- /dev/null +++ b/crates/adapters/adapter-common/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "adapter-common" +version = "0.1.0" +edition = "2024" + +[dependencies] +domain = { workspace = true } +sqlx = { workspace = true } +tracing = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } diff --git a/crates/adapters/adapter-common/src/lib.rs b/crates/adapters/adapter-common/src/lib.rs new file mode 100644 index 0000000..ff17ab8 --- /dev/null +++ b/crates/adapters/adapter-common/src/lib.rs @@ -0,0 +1,223 @@ +use domain::{ + errors::DomainError, + models::{ + Movie, MovieStats, MovieSummary, PersistedReview, Review, ReviewSource, UserSummary, + WatchlistEntry, WatchlistWithMovie, + }, + value_objects::{ + Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear, + ReviewId, UserId, Username, WatchlistEntryId, + }, +}; + +/// Map a [`sqlx::Error`] to a [`DomainError::InfrastructureError`], logging the +/// underlying database error at `error` level. +pub fn map_sqlx_error(e: sqlx::Error) -> DomainError { + tracing::error!("Database error: {:?}", e); + DomainError::InfrastructureError("Database operation failed".into()) +} + +/// Parse a string as a UUID, returning a [`DomainError`] on failure. +pub fn parse_uuid(s: &str) -> Result { + uuid::Uuid::parse_str(s) + .map_err(|e| DomainError::InfrastructureError(format!("Invalid UUID '{}': {}", s, e))) +} + +/// Parse a `%Y-%m-%d %H:%M:%S` string into a [`chrono::NaiveDateTime`]. +pub fn parse_datetime(s: &str) -> Result { + chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") + .map_err(|e| DomainError::InfrastructureError(format!("Invalid datetime '{}': {}", s, e))) +} + +/// Format a [`chrono::NaiveDateTime`] as `%Y-%m-%d %H:%M:%S`. +pub fn datetime_to_str(dt: &chrono::NaiveDateTime) -> String { + dt.format("%Y-%m-%d %H:%M:%S").to_string() +} + +/// Convert a `YYYY-MM` string into a human-readable label like `Jan '24`. +pub fn format_year_month(ym: &str) -> String { + let parts: Vec<&str> = ym.splitn(2, '-').collect(); + if parts.len() != 2 { + return ym.to_string(); + } + let year = parts[0].get(2..).unwrap_or(parts[0]); + let month = match parts[1] { + "01" => "Jan", + "02" => "Feb", + "03" => "Mar", + "04" => "Apr", + "05" => "May", + "06" => "Jun", + "07" => "Jul", + "08" => "Aug", + "09" => "Sep", + "10" => "Oct", + "11" => "Nov", + "12" => "Dec", + _ => parts[1], + }; + format!("{} '{}", month, year) +} + +// --------------------------------------------------------------------------- +// Shared row-to-domain conversion functions +// +// Each database adapter keeps its own `FromRow` structs (sqlite vs postgres +// derive different impls) but the conversion from parsed row fields into +// domain types is identical. These functions capture that shared logic so +// each adapter's `into_domain()` becomes a one-liner delegation. +// --------------------------------------------------------------------------- + +/// Convert raw movie row fields into a [`Movie`] domain object. +pub fn movie_row_to_domain( + id: String, + external_metadata_id: Option, + title: String, + release_year: i64, + director: Option, + poster_path: Option, +) -> Result { + let id = MovieId::from_uuid(parse_uuid(&id)?); + let external_metadata_id = external_metadata_id + .map(ExternalMetadataId::new) + .transpose()?; + let title = MovieTitle::new(title)?; + let release_year = ReleaseYear::new(release_year as u16)?; + let poster_path = poster_path.map(PosterPath::new).transpose()?; + Ok(Movie::from_persistence( + id, + external_metadata_id, + title, + release_year, + director, + poster_path, + )) +} + +/// Convert raw review row fields into a [`Review`] domain object. +#[allow(clippy::too_many_arguments)] +pub fn review_row_to_domain( + id: String, + movie_id: String, + user_id: String, + rating: i64, + comment: Option, + watched_at: String, + created_at: String, + remote_actor_url: Option, + watch_medium: Option, +) -> Result { + let id = ReviewId::from_uuid(parse_uuid(&id)?); + let movie_id = MovieId::from_uuid(parse_uuid(&movie_id)?); + let user_id = UserId::from_uuid(parse_uuid(&user_id)?); + let rating = Rating::new(rating as u8)?; + let comment = comment.map(Comment::new).transpose()?; + let watched_at = parse_datetime(&watched_at)?; + let created_at = parse_datetime(&created_at)?; + let source = match remote_actor_url { + None => ReviewSource::Local, + Some(url) => ReviewSource::Remote { actor_url: url }, + }; + let watch_medium = watch_medium.map(|s| s.parse()).transpose()?; + Ok(Review::from_persistence(PersistedReview { + id, + movie_id, + user_id, + rating, + comment, + watched_at, + created_at, + source, + watch_medium, + })) +} + +/// Assemble a [`MovieSummary`] from an already-converted [`Movie`] and extra +/// metadata fields. The caller is responsible for converting genres into a +/// `Vec` (sqlite splits a comma-separated string, postgres receives a +/// `Vec` directly). +pub fn movie_summary_to_domain( + movie: Movie, + genres: Vec, + runtime_minutes: Option, + original_language: Option, + overview: Option, + collection_name: Option, +) -> MovieSummary { + MovieSummary { + movie, + genres, + runtime_minutes: runtime_minutes.map(|v| v as u32), + original_language, + overview, + collection_name, + } +} + +/// Convert raw aggregate stats into a [`MovieStats`] domain object. +pub fn movie_stats_to_domain( + total_count: i64, + avg_rating: Option, + federated_count: i64, + rating_histogram: [i64; 5], +) -> MovieStats { + MovieStats { + total_count: total_count as u64, + avg_rating, + federated_count: federated_count as u64, + rating_histogram: [ + rating_histogram[0] as u64, + rating_histogram[1] as u64, + rating_histogram[2] as u64, + rating_histogram[3] as u64, + rating_histogram[4] as u64, + ], + } +} + +/// Convert raw user summary row fields into a [`UserSummary`] domain object. +#[allow(clippy::too_many_arguments)] +pub fn user_summary_to_domain( + id: String, + email: String, + username: String, + display_name: Option, + total_movies: i64, + avg_rating: Option, + avatar_path: Option, +) -> Result { + Ok(UserSummary::new( + UserId::from_uuid(parse_uuid(&id)?), + Email::new(email)?, + Username::new(username)?, + display_name, + total_movies, + avg_rating, + avatar_path, + )) +} + +/// Convert raw watchlist entry fields into a [`WatchlistEntry`] domain object. +pub fn watchlist_entry_to_domain( + id: String, + user_id: String, + movie_id: String, + added_at: String, +) -> Result { + Ok(WatchlistEntry { + id: WatchlistEntryId::from_uuid(parse_uuid(&id)?), + user_id: UserId::from_uuid(parse_uuid(&user_id)?), + movie_id: MovieId::from_uuid(parse_uuid(&movie_id)?), + added_at: parse_datetime(&added_at)?, + }) +} + +/// Convert raw watchlist+movie row fields into a [`WatchlistWithMovie`]. +/// +/// Takes the watchlist entry fields and a pre-converted [`Movie`]. +pub fn watchlist_with_movie_to_domain( + entry: WatchlistEntry, + movie: Movie, +) -> WatchlistWithMovie { + WatchlistWithMovie { entry, movie } +} diff --git a/crates/adapters/postgres-federation/Cargo.toml b/crates/adapters/postgres-federation/Cargo.toml index 7a43a71..ac24ca8 100644 --- a/crates/adapters/postgres-federation/Cargo.toml +++ b/crates/adapters/postgres-federation/Cargo.toml @@ -12,6 +12,7 @@ sqlx = { version = "0.8.6", features = [ "chrono", ] } activitypub = { workspace = true } +adapter-common = { workspace = true } k-ap = { version = "0.4.1", registry = "gitea" } domain = { workspace = true } uuid = { workspace = true } diff --git a/crates/adapters/postgres-federation/src/activity.rs b/crates/adapters/postgres-federation/src/activity.rs index 1b7df66..78db23a 100644 --- a/crates/adapters/postgres-federation/src/activity.rs +++ b/crates/adapters/postgres-federation/src/activity.rs @@ -3,7 +3,8 @@ use async_trait::async_trait; use chrono::Utc; use k_ap::ActivityRepository; -use super::{PostgresFederationRepository, datetime_to_str}; +use adapter_common::datetime_to_str; +use super::PostgresFederationRepository; #[async_trait] impl ActivityRepository for PostgresFederationRepository { diff --git a/crates/adapters/postgres-federation/src/actor.rs b/crates/adapters/postgres-federation/src/actor.rs index fb50e05..d370a5c 100644 --- a/crates/adapters/postgres-federation/src/actor.rs +++ b/crates/adapters/postgres-federation/src/actor.rs @@ -4,7 +4,8 @@ use chrono::Utc; use k_ap::{ActorRepository, RemoteActor}; use sqlx::Row; -use super::{PG_ACTOR_COLS, PostgresFederationRepository, datetime_to_str, pg_remote_actor}; +use adapter_common::datetime_to_str; +use super::{PG_ACTOR_COLS, PostgresFederationRepository, pg_remote_actor}; #[async_trait] impl ActorRepository for PostgresFederationRepository { diff --git a/crates/adapters/postgres-federation/src/ap_content.rs b/crates/adapters/postgres-federation/src/ap_content.rs index 8e35e18..be04344 100644 --- a/crates/adapters/postgres-federation/src/ap_content.rs +++ b/crates/adapters/postgres-federation/src/ap_content.rs @@ -12,7 +12,6 @@ use domain::{ }, }; use sqlx::{PgPool, Row}; -use uuid::Uuid; pub struct PostgresApContentQuery { pool: PgPool, @@ -22,24 +21,11 @@ impl PostgresApContentQuery { pub fn new(pool: PgPool) -> Self { Self { pool } } - - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } // ── Local row types ────────────────────────────────────────────────────────── -fn parse_uuid(s: &str) -> Result { - Uuid::parse_str(s) - .map_err(|e| DomainError::InfrastructureError(format!("Invalid UUID '{}': {}", s, e))) -} - -fn parse_datetime(s: &str) -> Result { - chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") - .map_err(|e| DomainError::InfrastructureError(format!("Invalid datetime '{}': {}", s, e))) -} +use adapter_common::{parse_uuid, parse_datetime}; #[derive(sqlx::FromRow)] struct MovieRow { @@ -181,7 +167,7 @@ impl LocalApContentQuery for PostgresApContentQuery { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.into_iter() .map(|row| { @@ -249,7 +235,7 @@ impl LocalApContentQuery for PostgresApContentQuery { .bind(&mid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.into_iter().map(DiaryRow::into_domain).collect() } @@ -282,7 +268,7 @@ impl LocalApContentQuery for PostgresApContentQuery { .bind(limit_i64) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? } else { sqlx::query_as::<_, DiaryRow>( "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, @@ -301,7 +287,7 @@ impl LocalApContentQuery for PostgresApContentQuery { .bind(limit_i64) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? }; rows.into_iter().map(DiaryRow::into_domain).collect() } diff --git a/crates/adapters/postgres-federation/src/blocklist.rs b/crates/adapters/postgres-federation/src/blocklist.rs index 0aaad19..4fbb1e9 100644 --- a/crates/adapters/postgres-federation/src/blocklist.rs +++ b/crates/adapters/postgres-federation/src/blocklist.rs @@ -4,7 +4,8 @@ use chrono::Utc; use k_ap::{BlockedDomain, BlocklistRepository}; use sqlx::Row; -use super::{PostgresFederationRepository, datetime_to_str}; +use adapter_common::datetime_to_str; +use super::PostgresFederationRepository; #[async_trait] impl BlocklistRepository for PostgresFederationRepository { diff --git a/crates/adapters/postgres-federation/src/follow.rs b/crates/adapters/postgres-federation/src/follow.rs index 910f8b3..c54d581 100644 --- a/crates/adapters/postgres-federation/src/follow.rs +++ b/crates/adapters/postgres-federation/src/follow.rs @@ -6,8 +6,9 @@ use k_ap::{ }; use sqlx::Row; +use adapter_common::datetime_to_str; use super::{ - PG_ACTOR_COLS, PostgresFederationRepository, datetime_to_str, pg_remote_actor, status_to_str, + PG_ACTOR_COLS, PostgresFederationRepository, pg_remote_actor, status_to_str, str_to_status, }; diff --git a/crates/adapters/postgres-federation/src/lib.rs b/crates/adapters/postgres-federation/src/lib.rs index 4c49a28..df2bcad 100644 --- a/crates/adapters/postgres-federation/src/lib.rs +++ b/crates/adapters/postgres-federation/src/lib.rs @@ -12,14 +12,9 @@ mod watchlist; pub use ap_content::PostgresApContentQuery; pub use remote_goals::PostgresRemoteGoalRepository; -use chrono::NaiveDateTime; use k_ap::{FollowerStatus, RemoteActor}; use sqlx::{PgPool, Row}; -pub(crate) fn datetime_to_str(dt: &NaiveDateTime) -> String { - dt.format("%Y-%m-%d %H:%M:%S").to_string() -} - pub(crate) fn status_to_str(status: &FollowerStatus) -> &'static str { match status { FollowerStatus::Pending => "pending", diff --git a/crates/adapters/postgres-federation/src/remote_goals.rs b/crates/adapters/postgres-federation/src/remote_goals.rs index 322d1ca..2403633 100644 --- a/crates/adapters/postgres-federation/src/remote_goals.rs +++ b/crates/adapters/postgres-federation/src/remote_goals.rs @@ -12,10 +12,6 @@ impl PostgresRemoteGoalRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -38,7 +34,7 @@ impl RemoteGoalRepository for PostgresRemoteGoalRepository { .bind(&received) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -57,7 +53,7 @@ impl RemoteGoalRepository for PostgresRemoteGoalRepository { .bind(ap_id) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -68,7 +64,7 @@ impl RemoteGoalRepository for PostgresRemoteGoalRepository { .bind(actor_url) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -78,7 +74,7 @@ impl RemoteGoalRepository for PostgresRemoteGoalRepository { .bind(actor_url) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -92,7 +88,7 @@ impl RemoteGoalRepository for PostgresRemoteGoalRepository { .bind(actor_url) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter() .map(|r| { diff --git a/crates/adapters/postgres-federation/src/review.rs b/crates/adapters/postgres-federation/src/review.rs index 0db58e2..433e6eb 100644 --- a/crates/adapters/postgres-federation/src/review.rs +++ b/crates/adapters/postgres-federation/src/review.rs @@ -3,7 +3,8 @@ use anyhow::{Result, anyhow}; use async_trait::async_trait; use domain::models::{Review, ReviewSource}; -use super::{PostgresFederationRepository, datetime_to_str}; +use adapter_common::datetime_to_str; +use super::PostgresFederationRepository; #[async_trait] impl RemoteReviewRepository for PostgresFederationRepository { diff --git a/crates/adapters/postgres-search/Cargo.toml b/crates/adapters/postgres-search/Cargo.toml index 8f22f2f..d3c7ca6 100644 --- a/crates/adapters/postgres-search/Cargo.toml +++ b/crates/adapters/postgres-search/Cargo.toml @@ -4,7 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -domain = { workspace = true } -async-trait = { workspace = true } -sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "macros"] } -uuid = { workspace = true } +domain = { workspace = true } +adapter-common = { workspace = true } +async-trait = { workspace = true } +sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "macros"] } +uuid = { workspace = true } diff --git a/crates/adapters/postgres-search/src/lib.rs b/crates/adapters/postgres-search/src/lib.rs index 14de58a..06c0043 100644 --- a/crates/adapters/postgres-search/src/lib.rs +++ b/crates/adapters/postgres-search/src/lib.rs @@ -31,10 +31,6 @@ pub fn create_search_adapter(pool: PgPool) -> (Arc, Arc DomainError { - DomainError::InfrastructureError(e.to_string()) -} - #[async_trait] impl SearchCommand for PostgresSearchAdapter { async fn index(&self, doc: IndexableDocument) -> Result<(), DomainError> { @@ -91,7 +87,7 @@ impl SearchCommand for PostgresSearchAdapter { .bind(&fts_input) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -113,7 +109,7 @@ impl SearchCommand for PostgresSearchAdapter { .bind(&fts_input) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -127,14 +123,14 @@ impl SearchCommand for PostgresSearchAdapter { .bind(id) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } EntityType::Person => { sqlx::query("DELETE FROM people_search WHERE person_id = $1") .bind(id) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } } Ok(()) @@ -183,7 +179,7 @@ impl PostgresSearchAdapter { .bind(query.filters.year.map(|y| y as i32)) .fetch_one(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; count as u64 } else { let count: i64 = sqlx::query_scalar( @@ -196,7 +192,7 @@ impl PostgresSearchAdapter { .bind(query.filters.year.map(|y| y as i32)) .fetch_one(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; count as u64 }; @@ -221,7 +217,7 @@ impl PostgresSearchAdapter { .bind(offset) .fetch_all(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? } else { sqlx::query_as::<_, Row>( "SELECT m.id, m.title, m.release_year, m.director, m.poster_path, @@ -238,7 +234,7 @@ impl PostgresSearchAdapter { .bind(offset) .fetch_all(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? }; let items = rows @@ -290,7 +286,7 @@ impl PostgresSearchAdapter { .bind(text) .fetch_one(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; count as u64 }; @@ -316,7 +312,7 @@ impl PostgresSearchAdapter { .bind(offset) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let mut items = Vec::with_capacity(rows.len()); for row in rows { diff --git a/crates/adapters/postgres/Cargo.toml b/crates/adapters/postgres/Cargo.toml index 26661ba..c031c05 100644 --- a/crates/adapters/postgres/Cargo.toml +++ b/crates/adapters/postgres/Cargo.toml @@ -11,6 +11,7 @@ sqlx = { version = "0.8.6", features = [ "macros", "chrono", ] } +adapter-common = { workspace = true } domain = { workspace = true } postgres-federation = { workspace = true } anyhow = { workspace = true } diff --git a/crates/adapters/postgres/src/diary.rs b/crates/adapters/postgres/src/diary.rs index 010a601..156611f 100644 --- a/crates/adapters/postgres/src/diary.rs +++ b/crates/adapters/postgres/src/diary.rs @@ -22,23 +22,19 @@ impl PostgresDiaryRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } async fn count_diary_entries(&self, movie_id: Option<&str>) -> Result { match movie_id { None => sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews") .fetch_one(&self.pool) .await - .map_err(Self::map_err), + .map_err(adapter_common::map_sqlx_error), Some(id) => { sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews WHERE movie_id = $1") .bind(id) .fetch_one(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } } } @@ -73,7 +69,7 @@ impl PostgresDiaryRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn fetch_movie_diary_rows( @@ -109,7 +105,7 @@ impl PostgresDiaryRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn count_user_diary_entries( @@ -138,7 +134,7 @@ impl PostgresDiaryRepository { if has_search { q = q.bind(search.unwrap()); } - q.fetch_one(&self.pool).await.map_err(Self::map_err) + q.fetch_one(&self.pool).await.map_err(adapter_common::map_sqlx_error) } async fn fetch_user_diary_rows( @@ -197,7 +193,7 @@ impl PostgresDiaryRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } } @@ -375,7 +371,7 @@ impl DiaryQuery for PostgresDiaryRepository { } let count_q = bind_filter_params!(sqlx::query_scalar::<_, i64>(&count_sql)); - let total = count_q.fetch_one(&self.pool).await.map_err(Self::map_err)?; + let total = count_q.fetch_one(&self.pool).await.map_err(adapter_common::map_sqlx_error)?; let rows_q = bind_filter_params!(sqlx::query_as::<_, FeedRow>(&select_sql)); let rows = rows_q @@ -383,7 +379,7 @@ impl DiaryQuery for PostgresDiaryRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let items = rows .into_iter() @@ -408,7 +404,7 @@ impl DiaryQuery for PostgresDiaryRepository { .bind(&id_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .ok_or_else(|| DomainError::NotFound(format!("Movie {}", id_str)))? .into_domain()?; @@ -423,7 +419,7 @@ impl DiaryQuery for PostgresDiaryRepository { .bind(&id_str) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(ReviewRow::into_domain) .collect::, _>>()?; @@ -448,7 +444,7 @@ impl DiaryQuery for PostgresDiaryRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.into_iter().map(DiaryRow::into_domain).collect() } @@ -477,7 +473,7 @@ impl DiaryQuery for PostgresDiaryRepository { while let Some(row) = futures::StreamExt::next(&mut rows).await { yield match row { Ok(r) => r.into_domain(), - Err(e) => Err(Self::map_err(e)), + Err(e) => Err(adapter_common::map_sqlx_error(e)), }; } }) @@ -500,7 +496,7 @@ impl DiaryQuery for PostgresDiaryRepository { .bind(id_str) .fetch_one(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) .map(MovieStatsRow::into_domain) } @@ -517,7 +513,7 @@ impl DiaryQuery for PostgresDiaryRepository { .bind(&id_str) .fetch_one(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let rows = sqlx::query_as::<_, FeedRow>( "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, @@ -541,7 +537,7 @@ impl DiaryQuery for PostgresDiaryRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let items = rows .into_iter() @@ -561,7 +557,7 @@ impl DiaryQuery for PostgresDiaryRepository { sqlx::query_scalar("SELECT COUNT(*) FROM reviews WHERE remote_actor_url IS NULL") .fetch_one(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(count as u64) } } diff --git a/crates/adapters/postgres/src/goals.rs b/crates/adapters/postgres/src/goals.rs index fd45ee7..f5fac2f 100644 --- a/crates/adapters/postgres/src/goals.rs +++ b/crates/adapters/postgres/src/goals.rs @@ -7,7 +7,7 @@ use domain::{ }; use sqlx::{PgPool, Row}; -use crate::models::{datetime_to_str, parse_datetime, parse_uuid}; +use adapter_common::{datetime_to_str, parse_datetime, parse_uuid}; pub struct PostgresGoalRepository { pool: PgPool, @@ -18,10 +18,6 @@ impl PostgresGoalRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -46,7 +42,7 @@ impl GoalCommand for PostgresGoalRepository { .bind(&created_at) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -60,7 +56,7 @@ impl GoalCommand for PostgresGoalRepository { .bind(&id) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; if result.rows_affected() == 0 { return Err(DomainError::NotFound("Goal not found".into())); @@ -77,7 +73,7 @@ impl GoalCommand for PostgresGoalRepository { .bind(&uid) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; if result.rows_affected() == 0 { return Err(DomainError::NotFound("Goal not found".into())); @@ -105,7 +101,7 @@ impl GoalQuery for PostgresGoalRepository { .bind(y) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.map(|r| row_to_goal(&r)).transpose() } @@ -121,7 +117,7 @@ impl GoalQuery for PostgresGoalRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_goal).collect() } diff --git a/crates/adapters/postgres/src/import_profile.rs b/crates/adapters/postgres/src/import_profile.rs index d66affb..a25b71d 100644 --- a/crates/adapters/postgres/src/import_profile.rs +++ b/crates/adapters/postgres/src/import_profile.rs @@ -97,10 +97,6 @@ impl PostgresImportProfileRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("DB error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -118,7 +114,7 @@ impl ImportProfileRepository for PostgresImportProfileRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn list_for_user(&self, user_id: &UserId) -> Result, DomainError> { @@ -139,7 +135,7 @@ impl ImportProfileRepository for PostgresImportProfileRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.into_iter() .map(|r| { @@ -184,7 +180,7 @@ impl ImportProfileRepository for PostgresImportProfileRepository { .bind(&id_str).bind(&uid_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.map(|r| { Ok(ImportProfile { @@ -212,6 +208,6 @@ impl ImportProfileRepository for PostgresImportProfileRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } } diff --git a/crates/adapters/postgres/src/import_session.rs b/crates/adapters/postgres/src/import_session.rs index 6448d8e..36dedc1 100644 --- a/crates/adapters/postgres/src/import_session.rs +++ b/crates/adapters/postgres/src/import_session.rs @@ -202,10 +202,6 @@ impl PostgresImportSessionRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("DB error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } fn serialize_session( s: &ImportSession, @@ -301,7 +297,7 @@ impl ImportSessionRepository for PostgresImportSessionRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn get( @@ -331,7 +327,7 @@ impl ImportSessionRepository for PostgresImportSessionRepository { .bind(&uid_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.map(|r| { Self::deserialize_session( @@ -359,7 +355,7 @@ impl ImportSessionRepository for PostgresImportSessionRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn delete(&self, id: &ImportSessionId) -> Result<(), DomainError> { @@ -369,14 +365,14 @@ impl ImportSessionRepository for PostgresImportSessionRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn delete_expired(&self) -> Result { let result = sqlx::query("DELETE FROM import_sessions WHERE expires_at < NOW()") .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected()) } @@ -387,6 +383,6 @@ impl ImportSessionRepository for PostgresImportSessionRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } } diff --git a/crates/adapters/postgres/src/lib.rs b/crates/adapters/postgres/src/lib.rs index d11e177..5498bb0 100644 --- a/crates/adapters/postgres/src/lib.rs +++ b/crates/adapters/postgres/src/lib.rs @@ -39,30 +39,6 @@ pub use watch_event::{PostgresWatchEventRepository, PostgresWebhookTokenReposito pub use watchlist::PostgresWatchlistRepository; pub use wrapup::{PostgresWrapUpRepository, PostgresWrapUpStatsQuery}; -pub(crate) fn format_year_month(ym: &str) -> String { - let parts: Vec<&str> = ym.splitn(2, '-').collect(); - if parts.len() != 2 { - return ym.to_string(); - } - let year = parts[0].get(2..).unwrap_or(parts[0]); - let month = match parts[1] { - "01" => "Jan", - "02" => "Feb", - "03" => "Mar", - "04" => "Apr", - "05" => "May", - "06" => "Jun", - "07" => "Jul", - "08" => "Aug", - "09" => "Sep", - "10" => "Oct", - "11" => "Nov", - "12" => "Dec", - _ => parts[1], - }; - format!("{} '{}", month, year) -} - pub async fn migrate(pool: &PgPool) -> Result<(), DomainError> { sqlx::migrate!("./migrations") .set_ignore_missing(true) diff --git a/crates/adapters/postgres/src/models.rs b/crates/adapters/postgres/src/models.rs index c8944d8..65ade01 100644 --- a/crates/adapters/postgres/src/models.rs +++ b/crates/adapters/postgres/src/models.rs @@ -1,16 +1,11 @@ -use chrono::NaiveDateTime; use domain::{ errors::DomainError, - models::{ - DiaryEntry, FeedEntry, Movie, MovieSummary, PersistedReview, Review, ReviewSource, - UserSummary, - }, - value_objects::{ - Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear, - ReviewId, UserId, Username, - }, + models::{DiaryEntry, FeedEntry, Movie, MovieSummary, Review}, +}; +use adapter_common::{ + movie_row_to_domain, movie_stats_to_domain, movie_summary_to_domain, review_row_to_domain, + user_summary_to_domain, }; -use uuid::Uuid; #[derive(sqlx::FromRow)] pub(crate) struct MovieRow { @@ -24,22 +19,14 @@ pub(crate) struct MovieRow { impl MovieRow { pub fn into_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, + movie_row_to_domain( + self.id, + self.external_metadata_id, + self.title, + self.release_year, self.director, - poster_path, - )) + self.poster_path, + ) } } @@ -60,23 +47,22 @@ pub(crate) struct MovieSummaryRow { impl MovieSummaryRow { pub fn into_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, - } - .into_domain()?; - Ok(MovieSummary { + let movie = movie_row_to_domain( + self.id, + self.external_metadata_id, + self.title, + self.release_year, + self.director, + self.poster_path, + )?; + Ok(movie_summary_to_domain( movie, - genres: self.genres.unwrap_or_default(), - runtime_minutes: self.runtime_minutes.map(|v| v as u32), - original_language: self.original_language, - overview: self.overview, - collection_name: self.collection_name, - }) + self.genres.unwrap_or_default(), + self.runtime_minutes, + self.original_language, + self.overview, + self.collection_name, + )) } } @@ -95,29 +81,17 @@ pub(crate) struct ReviewRow { impl ReviewRow { pub fn into_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)?; - let source = match self.remote_actor_url { - None => ReviewSource::Local, - Some(url) => ReviewSource::Remote { actor_url: url }, - }; - let watch_medium = self.watch_medium.map(|s| s.parse()).transpose()?; - Ok(Review::from_persistence(PersistedReview { - id, - movie_id, - user_id, - rating, - comment, - watched_at, - created_at, - source, - watch_medium, - })) + review_row_to_domain( + self.id, + self.movie_id, + self.user_id, + self.rating, + self.comment, + self.watched_at, + self.created_at, + self.remote_actor_url, + self.watch_medium, + ) } } @@ -142,27 +116,25 @@ pub(crate) struct DiaryRow { impl DiaryRow { pub fn into_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, - } - .into_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, - remote_actor_url: self.remote_actor_url, - watch_medium: self.watch_medium, - } - .into_domain()?; + let movie = movie_row_to_domain( + self.id, + self.external_metadata_id, + self.title, + self.release_year, + self.director, + self.poster_path, + )?; + let review = review_row_to_domain( + self.review_id, + self.movie_id, + self.user_id, + self.rating, + self.comment, + self.watched_at, + self.created_at, + self.remote_actor_url, + self.watch_medium, + )?; Ok(DiaryEntry::new(movie, review)) } } @@ -189,24 +161,26 @@ pub(crate) struct FeedRow { impl FeedRow { pub fn into_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, - remote_actor_url: self.remote_actor_url, - watch_medium: self.watch_medium, - } - .into_domain()?; + let movie = movie_row_to_domain( + self.id, + self.external_metadata_id, + self.title, + self.release_year, + self.director, + self.poster_path, + )?; + let review = review_row_to_domain( + self.review_id, + self.movie_id, + self.user_id, + self.rating, + self.comment, + self.watched_at, + self.created_at, + self.remote_actor_url, + self.watch_medium, + )?; + let diary = DiaryEntry::new(movie, review); Ok(FeedEntry::new(diary, self.user_email)) } } @@ -225,18 +199,18 @@ pub(crate) struct MovieStatsRow { impl MovieStatsRow { pub fn into_domain(self) -> domain::models::MovieStats { - domain::models::MovieStats { - total_count: self.total_count as u64, - avg_rating: self.avg_rating, - federated_count: self.federated_count as u64, - rating_histogram: [ - self.rating_1 as u64, - self.rating_2 as u64, - self.rating_3 as u64, - self.rating_4 as u64, - self.rating_5 as u64, + movie_stats_to_domain( + self.total_count, + self.avg_rating, + self.federated_count, + [ + self.rating_1, + self.rating_2, + self.rating_3, + self.rating_4, + self.rating_5, ], - } + ) } } @@ -252,16 +226,16 @@ pub(crate) struct UserSummaryRow { } impl UserSummaryRow { - pub fn into_domain(self) -> Result { - Ok(UserSummary::new( - UserId::from_uuid(parse_uuid(&self.id)?), - Email::new(self.email)?, - Username::new(self.username)?, + pub fn into_domain(self) -> Result { + user_summary_to_domain( + self.id, + self.email, + self.username, self.display_name, self.total_movies, self.avg_rating, self.avatar_path, - )) + ) } } @@ -283,17 +257,3 @@ pub(crate) struct MonthlyRatingRow { 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))) -} diff --git a/crates/adapters/postgres/src/movie.rs b/crates/adapters/postgres/src/movie.rs index 793075e..52d350f 100644 --- a/crates/adapters/postgres/src/movie.rs +++ b/crates/adapters/postgres/src/movie.rs @@ -21,10 +21,6 @@ impl PostgresMovieRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -55,7 +51,7 @@ impl MovieCommand for PostgresMovieRepository { .bind(&poster_path) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -66,7 +62,7 @@ impl MovieCommand for PostgresMovieRepository { .bind(&id) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } } @@ -85,7 +81,7 @@ impl MovieQuery for PostgresMovieRepository { .bind(id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .map(MovieRow::into_domain) .transpose() } @@ -99,7 +95,7 @@ impl MovieQuery for PostgresMovieRepository { .bind(&id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .map(MovieRow::into_domain) .transpose() } @@ -119,7 +115,7 @@ impl MovieQuery for PostgresMovieRepository { .bind(year) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(MovieRow::into_domain) .collect() @@ -139,7 +135,7 @@ impl MovieQuery for PostgresMovieRepository { .bind(&vals) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(rows.into_iter().map(|(id,)| id).collect()) } @@ -162,7 +158,7 @@ impl MovieQuery for PostgresMovieRepository { .bind(&years) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(rows .into_iter() .map(|r| { @@ -211,7 +207,7 @@ impl MovieQuery for PostgresMovieRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let total: i64 = sqlx::query( "SELECT COUNT(DISTINCT m.id) \ @@ -226,7 +222,7 @@ impl MovieQuery for PostgresMovieRepository { .bind(genre) .fetch_one(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .try_get(0) .unwrap_or(0); @@ -250,7 +246,7 @@ impl MovieQuery for PostgresMovieRepository { ) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| r.into_domain()) .collect() diff --git a/crates/adapters/postgres/src/movie_dedup.rs b/crates/adapters/postgres/src/movie_dedup.rs index 4507e32..6a97b47 100644 --- a/crates/adapters/postgres/src/movie_dedup.rs +++ b/crates/adapters/postgres/src/movie_dedup.rs @@ -13,10 +13,6 @@ impl PostgresMovieDeduplicator { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -36,7 +32,7 @@ impl MovieDeduplicator for PostgresMovieDeduplicator { let director = canonical.director().map(str::to_string); let poster = canonical.poster_path().map(|p| p.value().to_string()); - let mut tx = self.pool.begin().await.map_err(Self::map_err)?; + let mut tx = self.pool.begin().await.map_err(adapter_common::map_sqlx_error)?; // 1. Upsert canonical movie record sqlx::query( @@ -47,7 +43,7 @@ impl MovieDeduplicator for PostgresMovieDeduplicator { poster_path = COALESCE(EXCLUDED.poster_path, movies.poster_path)", ) .bind(&new).bind(&ext_id).bind(&title).bind(year).bind(&director).bind(&poster) - .execute(&mut *tx).await.map_err(Self::map_err)?; + .execute(&mut *tx).await.map_err(adapter_common::map_sqlx_error)?; // 2. Re-point simple FK tables let reviews = sqlx::query("UPDATE reviews SET movie_id = $1 WHERE movie_id = $2") @@ -55,7 +51,7 @@ impl MovieDeduplicator for PostgresMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .rows_affected(); let watchlist = @@ -64,7 +60,7 @@ impl MovieDeduplicator for PostgresMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .rows_affected(); let watch_events = sqlx::query("UPDATE watch_events SET movie_id = $1 WHERE movie_id = $2") @@ -72,7 +68,7 @@ impl MovieDeduplicator for PostgresMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .rows_affected(); // 3. Re-point movie_profiles (PK — move only if canonical has none) @@ -81,7 +77,7 @@ impl MovieDeduplicator for PostgresMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .rows_affected(); // 4. Re-point enrichment tables with composite PKs (INSERT … ON CONFLICT DO NOTHING + DELETE) @@ -95,12 +91,12 @@ impl MovieDeduplicator for PostgresMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query("DELETE FROM movie_genres WHERE movie_id = $1") .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query( "INSERT INTO movie_keywords (movie_id, tmdb_id, name) @@ -111,43 +107,43 @@ impl MovieDeduplicator for PostgresMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query("DELETE FROM movie_keywords WHERE movie_id = $1") .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query( "INSERT INTO movie_cast (movie_id, tmdb_person_id, name, character, billing_order, profile_path) SELECT $1, tmdb_person_id, name, character, billing_order, profile_path FROM movie_cast WHERE movie_id = $2 ON CONFLICT DO NOTHING", - ).bind(&new).bind(&old).execute(&mut *tx).await.map_err(Self::map_err)?; + ).bind(&new).bind(&old).execute(&mut *tx).await.map_err(adapter_common::map_sqlx_error)?; sqlx::query("DELETE FROM movie_cast WHERE movie_id = $1") .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query( "INSERT INTO movie_crew (movie_id, tmdb_person_id, name, job, department, profile_path) SELECT $1, tmdb_person_id, name, job, department, profile_path FROM movie_crew WHERE movie_id = $2 ON CONFLICT DO NOTHING", - ).bind(&new).bind(&old).execute(&mut *tx).await.map_err(Self::map_err)?; + ).bind(&new).bind(&old).execute(&mut *tx).await.map_err(adapter_common::map_sqlx_error)?; sqlx::query("DELETE FROM movie_crew WHERE movie_id = $1") .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; // 5. Delete the now-empty old movie record (remaining cascades are safe: all FKs cleared above) sqlx::query("DELETE FROM movies WHERE id = $1") .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; - tx.commit().await.map_err(Self::map_err)?; + tx.commit().await.map_err(adapter_common::map_sqlx_error)?; Ok(reviews + watchlist + watch_events + profiles) } diff --git a/crates/adapters/postgres/src/persons.rs b/crates/adapters/postgres/src/persons.rs index 431e90d..743d48b 100644 --- a/crates/adapters/postgres/src/persons.rs +++ b/crates/adapters/postgres/src/persons.rs @@ -29,9 +29,6 @@ pub fn create_person_adapter(pool: PgPool) -> (Arc, Arc DomainError { - DomainError::InfrastructureError(e.to_string()) -} #[async_trait] impl PersonCommand for PostgresPersonAdapter { @@ -56,7 +53,7 @@ impl PersonCommand for PostgresPersonAdapter { .bind(person.profile_path()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } Ok(()) } @@ -89,7 +86,7 @@ impl PersonCommand for PostgresPersonAdapter { .bind(batch_size as i64) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let has_more = rows.len() as u32 >= batch_size; let mut count = 0u64; @@ -109,7 +106,7 @@ impl PersonCommand for PostgresPersonAdapter { .bind(&row.profile_path) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; count += 1; } Ok((count, has_more)) @@ -137,7 +134,7 @@ impl PersonCommand for PostgresPersonAdapter { .bind(id.value().to_string()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } } @@ -151,7 +148,7 @@ impl PersonQuery for PostgresPersonAdapter { .bind(id.value().to_string()) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(row.map(PersonRow::into_person)) } @@ -166,7 +163,7 @@ impl PersonQuery for PostgresPersonAdapter { .bind(id.value()) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(row.map(PersonRow::into_person)) } @@ -182,7 +179,7 @@ impl PersonQuery for PostgresPersonAdapter { .bind(id.value().to_string()) .fetch_optional(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? .flatten(); let Some(tmdb_id) = tmdb_id else { @@ -219,7 +216,7 @@ impl PersonQuery for PostgresPersonAdapter { .bind(tmdb_id) .fetch_all(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| CastCredit { movie_id: MovieId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()), @@ -238,7 +235,7 @@ impl PersonQuery for PostgresPersonAdapter { .bind(tmdb_id) .fetch_all(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| CrewCredit { movie_id: MovieId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()), @@ -261,7 +258,7 @@ impl PersonQuery for PostgresPersonAdapter { .bind(offset as i64) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(rows.into_iter().map(PersonRow::into_person).collect()) } @@ -279,7 +276,7 @@ impl PersonQuery for PostgresPersonAdapter { ) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(rows .into_iter() diff --git a/crates/adapters/postgres/src/profile.rs b/crates/adapters/postgres/src/profile.rs index 3bcaf9e..1ea0218 100644 --- a/crates/adapters/postgres/src/profile.rs +++ b/crates/adapters/postgres/src/profile.rs @@ -17,10 +17,6 @@ impl PostgresMovieProfileRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -28,7 +24,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository { async fn upsert(&self, p: &MovieProfile) -> Result<(), DomainError> { let movie_id = p.movie_id.value().to_string(); - let mut tx = self.pool.begin().await.map_err(Self::map_err)?; + let mut tx = self.pool.begin().await.map_err(adapter_common::map_sqlx_error)?; sqlx::query( r#"INSERT INTO movie_profiles @@ -61,35 +57,35 @@ impl MovieProfileRepository for PostgresMovieProfileRepository { .bind(p.enriched_at) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query("DELETE FROM movie_genres WHERE movie_id = $1") .bind(&movie_id) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; for g in &p.genres { sqlx::query("INSERT INTO movie_genres (movie_id, tmdb_id, name) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING") .bind(&movie_id).bind(g.tmdb_id as i32).bind(&g.name) - .execute(&mut *tx).await.map_err(Self::map_err)?; + .execute(&mut *tx).await.map_err(adapter_common::map_sqlx_error)?; } sqlx::query("DELETE FROM movie_keywords WHERE movie_id = $1") .bind(&movie_id) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; for k in &p.keywords { sqlx::query("INSERT INTO movie_keywords (movie_id, tmdb_id, name) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING") .bind(&movie_id).bind(k.tmdb_id as i32).bind(&k.name) - .execute(&mut *tx).await.map_err(Self::map_err)?; + .execute(&mut *tx).await.map_err(adapter_common::map_sqlx_error)?; } sqlx::query("DELETE FROM movie_cast WHERE movie_id = $1") .bind(&movie_id) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; for c in &p.cast { sqlx::query( "INSERT INTO movie_cast \ @@ -104,14 +100,14 @@ impl MovieProfileRepository for PostgresMovieProfileRepository { .bind(&c.profile_path) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } sqlx::query("DELETE FROM movie_crew WHERE movie_id = $1") .bind(&movie_id) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; for cr in &p.crew { sqlx::query( "INSERT INTO movie_crew \ @@ -126,10 +122,10 @@ impl MovieProfileRepository for PostgresMovieProfileRepository { .bind(&cr.profile_path) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } - tx.commit().await.map_err(Self::map_err) + tx.commit().await.map_err(adapter_common::map_sqlx_error) } async fn get_by_movie_id(&self, id: &MovieId) -> Result, DomainError> { @@ -144,7 +140,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository { .bind(&movie_id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let row = match row { Some(r) => r, @@ -159,7 +155,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository { .bind(&movie_id) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| Genre { tmdb_id: r.try_get::("tmdb_id").unwrap_or(0) as u32, @@ -171,7 +167,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository { .bind(&movie_id) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| Keyword { tmdb_id: r.try_get::("tmdb_id").unwrap_or(0) as u32, @@ -186,7 +182,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository { .bind(&movie_id) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| CastMember { tmdb_person_id: r.try_get::("tmdb_person_id").unwrap_or(0) as u64, @@ -204,7 +200,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository { .bind(&movie_id) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| CrewMember { tmdb_person_id: r.try_get::("tmdb_person_id").unwrap_or(0) as u64, @@ -257,7 +253,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository { .bind(threshold) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(rows .into_iter() diff --git a/crates/adapters/postgres/src/refresh_sessions.rs b/crates/adapters/postgres/src/refresh_sessions.rs index 48e97f1..1de3517 100644 --- a/crates/adapters/postgres/src/refresh_sessions.rs +++ b/crates/adapters/postgres/src/refresh_sessions.rs @@ -16,9 +16,6 @@ impl PostgresRefreshSessionAdapter { } } -fn map_err(e: sqlx::Error) -> DomainError { - DomainError::InfrastructureError(e.to_string()) -} #[async_trait] impl RefreshSessionRepository for PostgresRefreshSessionAdapter { @@ -34,7 +31,7 @@ impl RefreshSessionRepository for PostgresRefreshSessionAdapter { .bind(session.created_at) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -48,7 +45,7 @@ impl RefreshSessionRepository for PostgresRefreshSessionAdapter { .bind(token) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.map(RefreshSessionRow::into_domain).transpose() } @@ -58,7 +55,7 @@ impl RefreshSessionRepository for PostgresRefreshSessionAdapter { .bind(token) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -67,7 +64,7 @@ impl RefreshSessionRepository for PostgresRefreshSessionAdapter { .bind(user_id.value().to_string()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -75,7 +72,7 @@ impl RefreshSessionRepository for PostgresRefreshSessionAdapter { let result = sqlx::query("DELETE FROM refresh_sessions WHERE expires_at < NOW()") .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected()) } } diff --git a/crates/adapters/postgres/src/review.rs b/crates/adapters/postgres/src/review.rs index a644b73..6a4925b 100644 --- a/crates/adapters/postgres/src/review.rs +++ b/crates/adapters/postgres/src/review.rs @@ -7,7 +7,8 @@ use domain::{ }; use sqlx::PgPool; -use crate::models::{ReviewRow, datetime_to_str}; +use adapter_common::datetime_to_str; +use crate::models::ReviewRow; pub struct PostgresReviewRepository { pool: PgPool, @@ -18,10 +19,6 @@ impl PostgresReviewRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -54,7 +51,7 @@ impl ReviewRepository for PostgresReviewRepository { .bind(review.watch_medium().map(|wm| wm.to_string())) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -72,7 +69,7 @@ impl ReviewRepository for PostgresReviewRepository { .bind(&id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .map(ReviewRow::into_domain) .transpose() } @@ -94,7 +91,7 @@ impl ReviewRepository for PostgresReviewRepository { .bind(&id) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -105,7 +102,7 @@ impl ReviewRepository for PostgresReviewRepository { .bind(&id) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -122,7 +119,7 @@ impl ReviewRepository for PostgresReviewRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(ReviewRow::into_domain) .collect() diff --git a/crates/adapters/postgres/src/stats.rs b/crates/adapters/postgres/src/stats.rs index 0444c4c..0cc3224 100644 --- a/crates/adapters/postgres/src/stats.rs +++ b/crates/adapters/postgres/src/stats.rs @@ -7,7 +7,7 @@ use domain::{ }; use sqlx::PgPool; -use crate::format_year_month; +use adapter_common::format_year_month; use crate::models::{DirectorCountRow, MonthlyRatingRow, UserTotalsRow}; pub struct PostgresStatsRepository { @@ -19,10 +19,6 @@ impl PostgresStatsRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } async fn fetch_user_totals(&self, user_id: &str) -> Result { sqlx::query_as::<_, UserTotalsRow>( @@ -33,7 +29,7 @@ impl PostgresStatsRepository { .bind(user_id) .fetch_one(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn fetch_user_favorite_director( @@ -52,7 +48,7 @@ impl PostgresStatsRepository { .bind(user_id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn fetch_user_most_active_month( @@ -70,7 +66,7 @@ impl PostgresStatsRepository { .bind(user_id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } } @@ -126,7 +122,7 @@ impl StatsRepository for PostgresStatsRepository { .bind(&uid) .fetch_all(&self.pool) ) - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let max_director_count = director_rows.iter().map(|d| d.count).max().unwrap_or(1); diff --git a/crates/adapters/postgres/src/user_settings.rs b/crates/adapters/postgres/src/user_settings.rs index f175972..3d610dd 100644 --- a/crates/adapters/postgres/src/user_settings.rs +++ b/crates/adapters/postgres/src/user_settings.rs @@ -16,10 +16,6 @@ impl PostgresUserSettingsRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -33,7 +29,7 @@ impl UserSettingsRepository for PostgresUserSettingsRepository { .bind(&uid) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; match row { Some(r) => { @@ -65,7 +61,7 @@ impl UserSettingsRepository for PostgresUserSettingsRepository { .bind(settings.federate_watchlist()) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } } @@ -81,7 +77,7 @@ impl UserFederationSettingsQuery for PostgresUserSettingsRepository { .bind(&uid) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; match row { Some(r) => { diff --git a/crates/adapters/postgres/src/users.rs b/crates/adapters/postgres/src/users.rs index e496254..502514d 100644 --- a/crates/adapters/postgres/src/users.rs +++ b/crates/adapters/postgres/src/users.rs @@ -20,10 +20,6 @@ impl PostgresUserRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } fn parse_role(s: &str) -> UserRole { match s { @@ -76,7 +72,7 @@ impl UserRepository for PostgresUserRepository { .bind(email_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref() .map(|r| Self::row_to_user(r, vec![])) .transpose() @@ -90,7 +86,7 @@ impl UserRepository for PostgresUserRepository { .bind(username_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref() .map(|r| Self::row_to_user(r, vec![])) .transpose() @@ -130,7 +126,7 @@ impl UserRepository for PostgresUserRepository { .bind(role) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -140,7 +136,7 @@ impl UserRepository for PostgresUserRepository { .bind(&id_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let Some(r) = row else { return Ok(None) }; @@ -197,7 +193,7 @@ impl UserRepository for PostgresUserRepository { ) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(UserSummaryRow::into_domain) .collect() diff --git a/crates/adapters/postgres/src/watch_event.rs b/crates/adapters/postgres/src/watch_event.rs index f7d6c04..e726941 100644 --- a/crates/adapters/postgres/src/watch_event.rs +++ b/crates/adapters/postgres/src/watch_event.rs @@ -7,12 +7,8 @@ use domain::{ }; use sqlx::{PgPool, Row}; -use crate::models::{parse_datetime, parse_uuid}; +use adapter_common::{parse_datetime, parse_uuid}; -fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) -} // ── WatchEventRepository ────────────────────────────────────────────────────── @@ -52,7 +48,7 @@ impl WatchEventCommand for PostgresWatchEventRepository { .bind(event.created_at()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -70,7 +66,7 @@ impl WatchEventCommand for PostgresWatchEventRepository { .bind(&id_str) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -90,7 +86,7 @@ impl WatchEventCommand for PostgresWatchEventRepository { .bind(&id_strs) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected()) } @@ -103,7 +99,7 @@ impl WatchEventCommand for PostgresWatchEventRepository { .bind(before) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected()) } } @@ -126,7 +122,7 @@ impl WatchEventQuery for PostgresWatchEventRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_watch_event).collect() } @@ -145,7 +141,7 @@ impl WatchEventQuery for PostgresWatchEventRepository { .bind(&id_str) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref().map(row_to_watch_event).transpose() } @@ -166,7 +162,7 @@ impl WatchEventQuery for PostgresWatchEventRepository { .bind(&id_strs) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_watch_event).collect() } @@ -187,23 +183,23 @@ impl WatchEventQuery for PostgresWatchEventRepository { .bind(after) .fetch_one(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(count > 0) } } fn row_to_watch_event(row: &sqlx::postgres::PgRow) -> Result { - let id_str: String = row.try_get("id").map_err(map_err)?; - let user_id_str: String = row.try_get("user_id").map_err(map_err)?; - let movie_id_str: Option = row.try_get("movie_id").map_err(map_err)?; - let title: String = row.try_get("title").map_err(map_err)?; - let year: Option = row.try_get("year").map_err(map_err)?; - let ext_id: Option = row.try_get("external_metadata_id").map_err(map_err)?; - let source_str: String = row.try_get("source").map_err(map_err)?; - let watched_at_str: String = row.try_get("watched_at").map_err(map_err)?; - let status_str: String = row.try_get("status").map_err(map_err)?; - let created_at_str: String = row.try_get("created_at").map_err(map_err)?; + let id_str: String = row.try_get("id").map_err(adapter_common::map_sqlx_error)?; + let user_id_str: String = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?; + let movie_id_str: Option = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; + let title: String = row.try_get("title").map_err(adapter_common::map_sqlx_error)?; + let year: Option = row.try_get("year").map_err(adapter_common::map_sqlx_error)?; + let ext_id: Option = row.try_get("external_metadata_id").map_err(adapter_common::map_sqlx_error)?; + let source_str: String = row.try_get("source").map_err(adapter_common::map_sqlx_error)?; + let watched_at_str: String = row.try_get("watched_at").map_err(adapter_common::map_sqlx_error)?; + let status_str: String = row.try_get("status").map_err(adapter_common::map_sqlx_error)?; + let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?; let source: WatchEventSource = source_str .parse() @@ -265,7 +261,7 @@ impl WebhookTokenRepository for PostgresWebhookTokenRepository { .bind(token.last_used_at()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -280,7 +276,7 @@ impl WebhookTokenRepository for PostgresWebhookTokenRepository { .bind(hash) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref().map(row_to_webhook_token).transpose() } @@ -297,7 +293,7 @@ impl WebhookTokenRepository for PostgresWebhookTokenRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_webhook_token).collect() } @@ -311,7 +307,7 @@ impl WebhookTokenRepository for PostgresWebhookTokenRepository { .bind(&uid) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; if result.rows_affected() == 0 { return Err(DomainError::NotFound(format!("Webhook token {id_str}"))); @@ -326,20 +322,20 @@ impl WebhookTokenRepository for PostgresWebhookTokenRepository { .bind(&id_str) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } } fn row_to_webhook_token(row: &sqlx::postgres::PgRow) -> Result { - let id_str: String = row.try_get("id").map_err(map_err)?; - let user_id_str: String = row.try_get("user_id").map_err(map_err)?; - let token_hash: String = row.try_get("token_hash").map_err(map_err)?; - let provider_str: String = row.try_get("provider").map_err(map_err)?; - let label: Option = row.try_get("label").map_err(map_err)?; - let created_at_str: String = row.try_get("created_at").map_err(map_err)?; - let last_used_str: Option = row.try_get("last_used_at").map_err(map_err)?; + let id_str: String = row.try_get("id").map_err(adapter_common::map_sqlx_error)?; + let user_id_str: String = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?; + let token_hash: String = row.try_get("token_hash").map_err(adapter_common::map_sqlx_error)?; + let provider_str: String = row.try_get("provider").map_err(adapter_common::map_sqlx_error)?; + let label: Option = row.try_get("label").map_err(adapter_common::map_sqlx_error)?; + let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?; + let last_used_str: Option = row.try_get("last_used_at").map_err(adapter_common::map_sqlx_error)?; let provider: WatchEventSource = provider_str .parse() diff --git a/crates/adapters/postgres/src/watchlist.rs b/crates/adapters/postgres/src/watchlist.rs index 4893ba1..6f5cd9c 100644 --- a/crates/adapters/postgres/src/watchlist.rs +++ b/crates/adapters/postgres/src/watchlist.rs @@ -10,7 +10,8 @@ use domain::{ }; use sqlx::{PgPool, Row}; -use crate::models::{MovieRow, parse_datetime, parse_uuid}; +use adapter_common::{parse_datetime, parse_uuid}; +use crate::models::MovieRow; pub struct PostgresWatchlistRepository { pool: PgPool, @@ -21,10 +22,6 @@ impl PostgresWatchlistRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -46,7 +43,7 @@ impl WatchlistRepository for PostgresWatchlistRepository { .bind(added_at) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -61,7 +58,7 @@ impl WatchlistRepository for PostgresWatchlistRepository { .bind(&mid) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; if result.rows_affected() == 0 { return Err(DomainError::NotFound(format!( @@ -85,7 +82,7 @@ impl WatchlistRepository for PostgresWatchlistRepository { .bind(&mid) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected() > 0) } @@ -114,14 +111,14 @@ impl WatchlistRepository for PostgresWatchlistRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM watchlist_entries WHERE user_id = $1") .bind(&uid) .fetch_one(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let items = rows .into_iter() @@ -187,7 +184,7 @@ impl WatchlistRepository for PostgresWatchlistRepository { .bind(&mid) .fetch_one(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(count > 0) } } diff --git a/crates/adapters/postgres/src/wrapup.rs b/crates/adapters/postgres/src/wrapup.rs index 60486ee..906efe6 100644 --- a/crates/adapters/postgres/src/wrapup.rs +++ b/crates/adapters/postgres/src/wrapup.rs @@ -13,12 +13,8 @@ use domain::{ use sqlx::{PgPool, Row}; use uuid::Uuid; -use crate::models::{parse_datetime, parse_uuid}; +use adapter_common::{parse_datetime, parse_uuid}; -fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) -} fn status_to_str(s: &WrapUpStatus) -> &'static str { match s { @@ -76,7 +72,7 @@ impl WrapUpRepository for PostgresWrapUpRepository { .bind(record.completed_at) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -96,7 +92,7 @@ impl WrapUpRepository for PostgresWrapUpRepository { .bind(&id_str) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -115,7 +111,7 @@ impl WrapUpRepository for PostgresWrapUpRepository { .bind(&id_str) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -132,7 +128,7 @@ impl WrapUpRepository for PostgresWrapUpRepository { .bind(&id_str) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref().map(row_to_record).transpose() } @@ -149,7 +145,7 @@ impl WrapUpRepository for PostgresWrapUpRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_record).collect() } @@ -163,7 +159,7 @@ impl WrapUpRepository for PostgresWrapUpRepository { ) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_record).collect() } @@ -190,7 +186,7 @@ impl WrapUpRepository for PostgresWrapUpRepository { .bind(end) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref().map(row_to_record).transpose() } @@ -200,7 +196,7 @@ impl WrapUpRepository for PostgresWrapUpRepository { .bind(id.value().to_string()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -213,21 +209,21 @@ impl WrapUpRepository for PostgresWrapUpRepository { .bind(before) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected()) } } fn row_to_record(row: &sqlx::postgres::PgRow) -> Result { - let id_str: String = row.try_get("id").map_err(map_err)?; - let user_id_str: Option = row.try_get("user_id").map_err(map_err)?; - let start_date: NaiveDate = row.try_get("start_date").map_err(map_err)?; - let end_date: NaiveDate = row.try_get("end_date").map_err(map_err)?; - let status_str: String = row.try_get("status").map_err(map_err)?; - let report_json: Option = row.try_get("report_json").map_err(map_err)?; - let error_message: Option = row.try_get("error_message").map_err(map_err)?; - let created_at_str: String = row.try_get("created_at").map_err(map_err)?; - let completed_at_str: Option = row.try_get("completed_at").map_err(map_err)?; + let id_str: String = row.try_get("id").map_err(adapter_common::map_sqlx_error)?; + let user_id_str: Option = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?; + let start_date: NaiveDate = row.try_get("start_date").map_err(adapter_common::map_sqlx_error)?; + let end_date: NaiveDate = row.try_get("end_date").map_err(adapter_common::map_sqlx_error)?; + let status_str: String = row.try_get("status").map_err(adapter_common::map_sqlx_error)?; + let report_json: Option = row.try_get("report_json").map_err(adapter_common::map_sqlx_error)?; + let error_message: Option = row.try_get("error_message").map_err(adapter_common::map_sqlx_error)?; + let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?; + let completed_at_str: Option = row.try_get("completed_at").map_err(adapter_common::map_sqlx_error)?; let user_id = user_id_str.as_deref().map(parse_uuid).transpose()?; @@ -292,7 +288,7 @@ impl WrapUpStatsQuery for PostgresWrapUpStatsQuery { q = q.bind(uid); } - let rows = q.fetch_all(&self.pool).await.map_err(map_err)?; + let rows = q.fetch_all(&self.pool).await.map_err(adapter_common::map_sqlx_error)?; if rows.is_empty() { return Ok(vec![]); @@ -302,7 +298,7 @@ impl WrapUpStatsQuery for PostgresWrapUpStatsQuery { let mut movie_ids: Vec = Vec::new(); let mut seen = std::collections::HashSet::new(); for row in &rows { - let mid: String = row.try_get("movie_id").map_err(map_err)?; + let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; if seen.insert(mid.clone()) { movie_ids.push(mid); } @@ -318,18 +314,18 @@ impl WrapUpStatsQuery for PostgresWrapUpStatsQuery { // 3) Build result let mut result = Vec::with_capacity(rows.len()); for row in &rows { - let movie_id_str: String = row.try_get("movie_id").map_err(map_err)?; - let title: String = row.try_get("title").map_err(map_err)?; - let release_year: i64 = row.try_get("release_year").map_err(map_err)?; - let director: Option = row.try_get("director").map_err(map_err)?; - let poster_path: Option = row.try_get("poster_path").map_err(map_err)?; - let rating: i64 = row.try_get("rating").map_err(map_err)?; - let watched_at_str: String = row.try_get("watched_at").map_err(map_err)?; - let user_id_str: String = row.try_get("user_id").map_err(map_err)?; - let runtime_minutes: Option = row.try_get("runtime_minutes").map_err(map_err)?; - let budget_usd: Option = row.try_get("budget_usd").map_err(map_err)?; + let movie_id_str: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; + let title: String = row.try_get("title").map_err(adapter_common::map_sqlx_error)?; + let release_year: i64 = row.try_get("release_year").map_err(adapter_common::map_sqlx_error)?; + let director: Option = row.try_get("director").map_err(adapter_common::map_sqlx_error)?; + let poster_path: Option = row.try_get("poster_path").map_err(adapter_common::map_sqlx_error)?; + let rating: i64 = row.try_get("rating").map_err(adapter_common::map_sqlx_error)?; + let watched_at_str: String = row.try_get("watched_at").map_err(adapter_common::map_sqlx_error)?; + let user_id_str: String = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?; + let runtime_minutes: Option = row.try_get("runtime_minutes").map_err(adapter_common::map_sqlx_error)?; + let budget_usd: Option = row.try_get("budget_usd").map_err(adapter_common::map_sqlx_error)?; let original_language: Option = - row.try_get("original_language").map_err(map_err)?; + row.try_get("original_language").map_err(adapter_common::map_sqlx_error)?; let genres = genres_map.get(&movie_id_str).cloned().unwrap_or_default(); let keywords = keywords_map.get(&movie_id_str).cloned().unwrap_or_default(); @@ -383,12 +379,12 @@ async fn fetch_genres_pg( .bind(movie_ids) .fetch_all(pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let mut map: HashMap> = HashMap::new(); for row in rows { - let mid: String = row.try_get("movie_id").map_err(map_err)?; - let name: String = row.try_get("name").map_err(map_err)?; + let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; + let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?; map.entry(mid).or_default().push(name); } Ok(map) @@ -404,12 +400,12 @@ async fn fetch_keywords_pg( .bind(movie_ids) .fetch_all(pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let mut map: HashMap> = HashMap::new(); for row in rows { - let mid: String = row.try_get("movie_id").map_err(map_err)?; - let name: String = row.try_get("name").map_err(map_err)?; + let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; + let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?; map.entry(mid).or_default().push(name); } Ok(map) @@ -428,15 +424,15 @@ async fn fetch_cast_pg( .bind(movie_ids) .fetch_all(pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let mut map: HashMap> = HashMap::new(); for row in rows { - let mid: String = row.try_get("movie_id").map_err(map_err)?; - let name: String = row.try_get("name").map_err(map_err)?; - let billing_order: i32 = row.try_get("billing_order").map_err(map_err)?; - let tmdb_person_id: i64 = row.try_get("tmdb_person_id").map_err(map_err)?; - let profile_path: Option = row.try_get("profile_path").map_err(map_err)?; + let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; + let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?; + let billing_order: i32 = row.try_get("billing_order").map_err(adapter_common::map_sqlx_error)?; + let tmdb_person_id: i64 = row.try_get("tmdb_person_id").map_err(adapter_common::map_sqlx_error)?; + let profile_path: Option = row.try_get("profile_path").map_err(adapter_common::map_sqlx_error)?; map.entry(mid).or_default().push(CastEntry { name, billing_order: billing_order as u32, diff --git a/crates/adapters/sqlite-federation/Cargo.toml b/crates/adapters/sqlite-federation/Cargo.toml index f193276..fbc6847 100644 --- a/crates/adapters/sqlite-federation/Cargo.toml +++ b/crates/adapters/sqlite-federation/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] sqlx = { workspace = true } activitypub = { workspace = true } +adapter-common = { workspace = true } k-ap = { version = "0.4.1", registry = "gitea" } domain = { workspace = true } anyhow = { workspace = true } diff --git a/crates/adapters/sqlite-federation/src/activity.rs b/crates/adapters/sqlite-federation/src/activity.rs index 6953a72..bf239f1 100644 --- a/crates/adapters/sqlite-federation/src/activity.rs +++ b/crates/adapters/sqlite-federation/src/activity.rs @@ -3,7 +3,8 @@ use async_trait::async_trait; use chrono::Utc; use k_ap::ActivityRepository; -use super::{SqliteFederationRepository, datetime_to_str}; +use adapter_common::datetime_to_str; +use super::SqliteFederationRepository; #[async_trait] impl ActivityRepository for SqliteFederationRepository { diff --git a/crates/adapters/sqlite-federation/src/actor.rs b/crates/adapters/sqlite-federation/src/actor.rs index 2ea918c..9dfeff5 100644 --- a/crates/adapters/sqlite-federation/src/actor.rs +++ b/crates/adapters/sqlite-federation/src/actor.rs @@ -4,7 +4,8 @@ use chrono::Utc; use k_ap::{ActorRepository, RemoteActor}; use sqlx::Row; -use super::{SqliteFederationRepository, datetime_to_str, remote_actor_from_row}; +use adapter_common::datetime_to_str; +use super::{SqliteFederationRepository, remote_actor_from_row}; #[async_trait] impl ActorRepository for SqliteFederationRepository { diff --git a/crates/adapters/sqlite-federation/src/ap_content.rs b/crates/adapters/sqlite-federation/src/ap_content.rs index aa47863..fd0b5f3 100644 --- a/crates/adapters/sqlite-federation/src/ap_content.rs +++ b/crates/adapters/sqlite-federation/src/ap_content.rs @@ -12,7 +12,6 @@ use domain::{ }, }; use sqlx::SqlitePool; -use uuid::Uuid; pub struct SqliteApContentQuery { pool: SqlitePool, @@ -23,23 +22,11 @@ impl SqliteApContentQuery { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } // ── Local row types ────────────────────────────────────────────────────────── -fn parse_uuid(s: &str) -> Result { - Uuid::parse_str(s) - .map_err(|e| DomainError::InfrastructureError(format!("Invalid UUID '{}': {}", s, e))) -} - -fn parse_datetime(s: &str) -> Result { - chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") - .map_err(|e| DomainError::InfrastructureError(format!("Invalid datetime '{}': {}", s, e))) -} +use adapter_common::{parse_uuid, parse_datetime}; #[derive(sqlx::FromRow)] struct MovieRow { @@ -215,7 +202,7 @@ impl LocalApContentQuery for SqliteApContentQuery { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.into_iter().map(WatchlistRow::into_domain).collect() } @@ -235,7 +222,7 @@ impl LocalApContentQuery for SqliteApContentQuery { .bind(&mid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.into_iter().map(DiaryRow::into_domain).collect() } @@ -264,7 +251,7 @@ impl LocalApContentQuery for SqliteApContentQuery { .bind(limit_i64) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? } else { sqlx::query_as::<_, DiaryRow>( "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, @@ -279,7 +266,7 @@ impl LocalApContentQuery for SqliteApContentQuery { .bind(limit_i64) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? }; rows.into_iter().map(DiaryRow::into_domain).collect() } diff --git a/crates/adapters/sqlite-federation/src/blocklist.rs b/crates/adapters/sqlite-federation/src/blocklist.rs index 57aae94..d8d6743 100644 --- a/crates/adapters/sqlite-federation/src/blocklist.rs +++ b/crates/adapters/sqlite-federation/src/blocklist.rs @@ -4,7 +4,8 @@ use chrono::Utc; use k_ap::{BlockedDomain, BlocklistRepository}; use sqlx::Row; -use super::{SqliteFederationRepository, datetime_to_str}; +use adapter_common::datetime_to_str; +use super::SqliteFederationRepository; #[async_trait] impl BlocklistRepository for SqliteFederationRepository { diff --git a/crates/adapters/sqlite-federation/src/follow.rs b/crates/adapters/sqlite-federation/src/follow.rs index b7a8e8b..659f4e5 100644 --- a/crates/adapters/sqlite-federation/src/follow.rs +++ b/crates/adapters/sqlite-federation/src/follow.rs @@ -6,8 +6,9 @@ use k_ap::{ }; use sqlx::Row; +use adapter_common::datetime_to_str; use super::{ - SqliteFederationRepository, datetime_to_str, remote_actor_from_row, status_to_str, + SqliteFederationRepository, remote_actor_from_row, status_to_str, str_to_status, }; diff --git a/crates/adapters/sqlite-federation/src/lib.rs b/crates/adapters/sqlite-federation/src/lib.rs index 65110f2..5514b3c 100644 --- a/crates/adapters/sqlite-federation/src/lib.rs +++ b/crates/adapters/sqlite-federation/src/lib.rs @@ -13,14 +13,9 @@ pub mod remote_goals; pub use ap_content::SqliteApContentQuery; pub use remote_goals::SqliteRemoteGoalRepository; -use chrono::NaiveDateTime; use k_ap::{FollowerStatus, RemoteActor}; use sqlx::SqlitePool; -pub(crate) fn datetime_to_str(dt: &NaiveDateTime) -> String { - dt.format("%Y-%m-%d %H:%M:%S").to_string() -} - pub(crate) fn status_to_str(status: &FollowerStatus) -> &'static str { match status { FollowerStatus::Pending => "pending", diff --git a/crates/adapters/sqlite-federation/src/remote_goals.rs b/crates/adapters/sqlite-federation/src/remote_goals.rs index d232a6c..e0a13a3 100644 --- a/crates/adapters/sqlite-federation/src/remote_goals.rs +++ b/crates/adapters/sqlite-federation/src/remote_goals.rs @@ -12,10 +12,6 @@ impl SqliteRemoteGoalRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -36,7 +32,7 @@ impl RemoteGoalRepository for SqliteRemoteGoalRepository { .bind(&received) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -53,7 +49,7 @@ impl RemoteGoalRepository for SqliteRemoteGoalRepository { .bind(ap_id) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -64,7 +60,7 @@ impl RemoteGoalRepository for SqliteRemoteGoalRepository { .bind(actor_url) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -74,7 +70,7 @@ impl RemoteGoalRepository for SqliteRemoteGoalRepository { .bind(actor_url) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -87,7 +83,7 @@ impl RemoteGoalRepository for SqliteRemoteGoalRepository { .bind(actor_url) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter() .map(|r| { diff --git a/crates/adapters/sqlite-federation/src/review.rs b/crates/adapters/sqlite-federation/src/review.rs index ae92e3f..e9ae61b 100644 --- a/crates/adapters/sqlite-federation/src/review.rs +++ b/crates/adapters/sqlite-federation/src/review.rs @@ -3,7 +3,8 @@ use anyhow::{Result, anyhow}; use async_trait::async_trait; use domain::models::{Review, ReviewSource}; -use super::{SqliteFederationRepository, datetime_to_str}; +use adapter_common::datetime_to_str; +use super::SqliteFederationRepository; #[async_trait] impl RemoteReviewRepository for SqliteFederationRepository { diff --git a/crates/adapters/sqlite-search/Cargo.toml b/crates/adapters/sqlite-search/Cargo.toml index a313e86..b6da39d 100644 --- a/crates/adapters/sqlite-search/Cargo.toml +++ b/crates/adapters/sqlite-search/Cargo.toml @@ -4,10 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] -domain = { workspace = true } -async-trait = { workspace = true } -sqlx = { workspace = true } -uuid = { workspace = true } +domain = { workspace = true } +adapter-common = { workspace = true } +async-trait = { workspace = true } +sqlx = { workspace = true } +uuid = { workspace = true } [dev-dependencies] tokio = { workspace = true } diff --git a/crates/adapters/sqlite-search/src/lib.rs b/crates/adapters/sqlite-search/src/lib.rs index 8109b58..36e95e5 100644 --- a/crates/adapters/sqlite-search/src/lib.rs +++ b/crates/adapters/sqlite-search/src/lib.rs @@ -31,10 +31,6 @@ pub fn create_search_adapter(pool: SqlitePool) -> (Arc, Arc DomainError { - DomainError::InfrastructureError(e.to_string()) -} - #[async_trait] impl SearchCommand for SqliteSearchAdapter { async fn index(&self, doc: IndexableDocument) -> Result<(), DomainError> { @@ -86,7 +82,7 @@ impl SearchCommand for SqliteSearchAdapter { .bind(&movie_id) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query( "INSERT INTO movies_fts(movie_id, title, director, overview, genres, keywords, cast_names, crew_names, release_year, language) @@ -104,7 +100,7 @@ impl SearchCommand for SqliteSearchAdapter { .bind(&language) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -118,7 +114,7 @@ impl SearchCommand for SqliteSearchAdapter { .bind(&person_id) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query( "INSERT INTO people_fts(person_id, name, known_for_department) VALUES (?, ?, ?)", @@ -128,7 +124,7 @@ impl SearchCommand for SqliteSearchAdapter { .bind(person.known_for_department()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -144,7 +140,7 @@ impl SearchCommand for SqliteSearchAdapter { .bind(id) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } EntityType::Person => { sqlx::query( @@ -153,7 +149,7 @@ impl SearchCommand for SqliteSearchAdapter { .bind(id) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } } Ok(()) @@ -205,7 +201,7 @@ impl SqliteSearchAdapter { .bind(query.filters.year.map(|y| y as i64)) .fetch_one(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; count as u64 } else { let count: i64 = sqlx::query_scalar( @@ -221,7 +217,7 @@ impl SqliteSearchAdapter { .bind(query.filters.year.map(|y| y as i64)) .fetch_one(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; count as u64 }; @@ -249,7 +245,7 @@ impl SqliteSearchAdapter { .bind(offset) .fetch_all(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? } else { sqlx::query_as::<_, Row>( "SELECT m.id, m.title, m.release_year, m.director, m.poster_path, @@ -270,7 +266,7 @@ impl SqliteSearchAdapter { .bind(offset) .fetch_all(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? }; let items = rows .into_iter() @@ -321,7 +317,7 @@ impl SqliteSearchAdapter { .bind(&fts_query) .fetch_one(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; count as u64 }; @@ -346,7 +342,7 @@ impl SqliteSearchAdapter { .bind(offset) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let mut items = Vec::with_capacity(rows.len()); for row in rows { @@ -355,7 +351,7 @@ impl SqliteSearchAdapter { .bind(&row.person_id) .fetch_optional(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? .flatten(); let known_for_titles = if let Some(tid) = tmdb_id { diff --git a/crates/adapters/sqlite/Cargo.toml b/crates/adapters/sqlite/Cargo.toml index f31eccc..a1fd990 100644 --- a/crates/adapters/sqlite/Cargo.toml +++ b/crates/adapters/sqlite/Cargo.toml @@ -11,6 +11,7 @@ sqlx = { version = "0.8.6", features = [ "macros", ] } +adapter-common = { workspace = true } domain = { workspace = true } sqlite-federation = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/adapters/sqlite/src/diary.rs b/crates/adapters/sqlite/src/diary.rs index e0ce51c..c1cfff4 100644 --- a/crates/adapters/sqlite/src/diary.rs +++ b/crates/adapters/sqlite/src/diary.rs @@ -22,23 +22,18 @@ impl SqliteDiaryRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } - async fn count_diary_entries(&self, movie_id: Option<&str>) -> Result { match movie_id { None => sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews") .fetch_one(&self.pool) .await - .map_err(Self::map_err), + .map_err(adapter_common::map_sqlx_error), Some(id) => { sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews WHERE movie_id = ?") .bind(id) .fetch_one(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } } } @@ -69,7 +64,7 @@ impl SqliteDiaryRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn fetch_movie_diary_rows( @@ -101,7 +96,7 @@ impl SqliteDiaryRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn count_user_diary_entries( @@ -130,7 +125,7 @@ impl SqliteDiaryRepository { if has_search { q = q.bind(search.unwrap()); } - q.fetch_one(&self.pool).await.map_err(Self::map_err) + q.fetch_one(&self.pool).await.map_err(adapter_common::map_sqlx_error) } async fn fetch_user_diary_rows( @@ -176,7 +171,7 @@ impl SqliteDiaryRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } } @@ -340,7 +335,7 @@ impl DiaryQuery for SqliteDiaryRepository { } let count_q = bind_filter_params!(sqlx::query_scalar::<_, i64>(&count_sql)); - let total = count_q.fetch_one(&self.pool).await.map_err(Self::map_err)?; + let total = count_q.fetch_one(&self.pool).await.map_err(adapter_common::map_sqlx_error)?; let rows_q = bind_filter_params!(sqlx::query_as::<_, FeedRow>(&select_sql)); let rows = rows_q @@ -348,7 +343,7 @@ impl DiaryQuery for SqliteDiaryRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let items = rows .into_iter() @@ -373,7 +368,7 @@ impl DiaryQuery for SqliteDiaryRepository { .bind(&id_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .ok_or_else(|| DomainError::NotFound(format!("Movie {}", id_str)))? .into_domain()?; @@ -384,7 +379,7 @@ impl DiaryQuery for SqliteDiaryRepository { .bind(&id_str) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(ReviewRow::into_domain) .collect::, _>>()?; @@ -405,7 +400,7 @@ impl DiaryQuery for SqliteDiaryRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.into_iter().map(DiaryRow::into_domain).collect() } @@ -430,7 +425,7 @@ impl DiaryQuery for SqliteDiaryRepository { while let Some(row) = futures::StreamExt::next(&mut rows).await { yield match row { Ok(r) => r.into_domain(), - Err(e) => Err(Self::map_err(e)), + Err(e) => Err(adapter_common::map_sqlx_error(e)), }; } }) @@ -453,7 +448,7 @@ impl DiaryQuery for SqliteDiaryRepository { .bind(id_str) .fetch_one(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) .map(MovieStatsRow::into_domain) } @@ -471,7 +466,7 @@ impl DiaryQuery for SqliteDiaryRepository { .bind(&id_str) .fetch_one(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let rows = sqlx::query_as::<_, FeedRow>( "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path, @@ -492,7 +487,7 @@ impl DiaryQuery for SqliteDiaryRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let items = rows .into_iter() @@ -512,7 +507,7 @@ impl DiaryQuery for SqliteDiaryRepository { sqlx::query_scalar("SELECT COUNT(*) FROM reviews WHERE remote_actor_url IS NULL") .fetch_one(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(count as u64) } } diff --git a/crates/adapters/sqlite/src/goals.rs b/crates/adapters/sqlite/src/goals.rs index 30bd37e..0193d4d 100644 --- a/crates/adapters/sqlite/src/goals.rs +++ b/crates/adapters/sqlite/src/goals.rs @@ -16,10 +16,6 @@ impl SqliteGoalRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -44,7 +40,7 @@ impl GoalCommand for SqliteGoalRepository { .bind(&created_at) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -58,7 +54,7 @@ impl GoalCommand for SqliteGoalRepository { .bind(&id) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; if result.rows_affected() == 0 { return Err(DomainError::NotFound("Goal not found".into())); @@ -75,7 +71,7 @@ impl GoalCommand for SqliteGoalRepository { .bind(&uid) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; if result.rows_affected() == 0 { return Err(DomainError::NotFound("Goal not found".into())); @@ -102,7 +98,7 @@ impl GoalQuery for SqliteGoalRepository { .bind(y) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.map(|r| row_to_goal(&r)).transpose() } @@ -117,7 +113,7 @@ impl GoalQuery for SqliteGoalRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_goal).collect() } diff --git a/crates/adapters/sqlite/src/import_profile.rs b/crates/adapters/sqlite/src/import_profile.rs index 3574ebd..535f01c 100644 --- a/crates/adapters/sqlite/src/import_profile.rs +++ b/crates/adapters/sqlite/src/import_profile.rs @@ -97,11 +97,6 @@ impl SqliteImportProfileRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("DB error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } - fn parse_dt(s: &str) -> Result { NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")) @@ -130,7 +125,7 @@ impl ImportProfileRepository for SqliteImportProfileRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn list_for_user(&self, user_id: &UserId) -> Result, DomainError> { @@ -141,7 +136,7 @@ impl ImportProfileRepository for SqliteImportProfileRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter() .map(|r| { @@ -183,7 +178,7 @@ impl ImportProfileRepository for SqliteImportProfileRepository { .bind(&uid_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.map(|r| { use sqlx::Row; @@ -215,6 +210,6 @@ impl ImportProfileRepository for SqliteImportProfileRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } } diff --git a/crates/adapters/sqlite/src/import_session.rs b/crates/adapters/sqlite/src/import_session.rs index 7e491c2..f978ece 100644 --- a/crates/adapters/sqlite/src/import_session.rs +++ b/crates/adapters/sqlite/src/import_session.rs @@ -202,11 +202,6 @@ impl SqliteImportSessionRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("DB error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } - fn parse_dt(s: &str) -> Result { NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")) @@ -316,7 +311,7 @@ impl ImportSessionRepository for SqliteImportSessionRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn get( @@ -334,7 +329,7 @@ impl ImportSessionRepository for SqliteImportSessionRepository { .bind(&uid_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.map(|r| { use sqlx::Row; @@ -361,7 +356,7 @@ impl ImportSessionRepository for SqliteImportSessionRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn delete(&self, id: &ImportSessionId) -> Result<(), DomainError> { @@ -371,14 +366,14 @@ impl ImportSessionRepository for SqliteImportSessionRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn delete_expired(&self) -> Result { let result = sqlx::query("DELETE FROM import_sessions WHERE expires_at < datetime('now')") .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected()) } @@ -391,6 +386,6 @@ impl ImportSessionRepository for SqliteImportSessionRepository { .execute(&self.pool) .await .map(|_| ()) - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } } diff --git a/crates/adapters/sqlite/src/lib.rs b/crates/adapters/sqlite/src/lib.rs index b29756e..f9d2a9a 100644 --- a/crates/adapters/sqlite/src/lib.rs +++ b/crates/adapters/sqlite/src/lib.rs @@ -45,30 +45,6 @@ pub fn create_profile_fields_repo( std::sync::Arc::new(SqliteProfileFieldsRepository::new(pool)) } -pub(crate) fn format_year_month(ym: &str) -> String { - let parts: Vec<&str> = ym.splitn(2, '-').collect(); - if parts.len() != 2 { - return ym.to_string(); - } - let year = parts[0].get(2..).unwrap_or(parts[0]); - let month = match parts[1] { - "01" => "Jan", - "02" => "Feb", - "03" => "Mar", - "04" => "Apr", - "05" => "May", - "06" => "Jun", - "07" => "Jul", - "08" => "Aug", - "09" => "Sep", - "10" => "Oct", - "11" => "Nov", - "12" => "Dec", - _ => parts[1], - }; - format!("{} '{}", month, year) -} - pub async fn migrate(pool: &SqlitePool) -> Result<(), domain::errors::DomainError> { migrations::run(pool).await } diff --git a/crates/adapters/sqlite/src/models.rs b/crates/adapters/sqlite/src/models.rs index 03c7e75..53e99d5 100644 --- a/crates/adapters/sqlite/src/models.rs +++ b/crates/adapters/sqlite/src/models.rs @@ -1,16 +1,11 @@ -use chrono::NaiveDateTime; use domain::{ errors::DomainError, - models::{ - DiaryEntry, FeedEntry, Movie, MovieSummary, PersistedReview, Review, ReviewSource, - UserSummary, WatchlistEntry, WatchlistWithMovie, - }, - value_objects::{ - Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear, - ReviewId, UserId, Username, WatchlistEntryId, - }, + models::{DiaryEntry, FeedEntry, Movie, MovieSummary, Review, WatchlistWithMovie}, +}; +use adapter_common::{ + movie_row_to_domain, movie_stats_to_domain, movie_summary_to_domain, review_row_to_domain, + user_summary_to_domain, watchlist_entry_to_domain, watchlist_with_movie_to_domain, }; -use uuid::Uuid; #[derive(sqlx::FromRow)] pub(crate) struct MovieRow { @@ -24,22 +19,14 @@ pub(crate) struct MovieRow { impl MovieRow { pub fn into_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, + movie_row_to_domain( + self.id, + self.external_metadata_id, + self.title, + self.release_year, self.director, - poster_path, - )) + self.poster_path, + ) } } @@ -60,27 +47,26 @@ pub(crate) struct MovieSummaryRow { impl MovieSummaryRow { pub fn into_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, - } - .into_domain()?; + let movie = movie_row_to_domain( + self.id, + self.external_metadata_id, + self.title, + self.release_year, + self.director, + self.poster_path, + )?; let genres = self .genres .map(|g| g.split(',').map(str::to_string).collect()) .unwrap_or_default(); - Ok(MovieSummary { + Ok(movie_summary_to_domain( movie, genres, - runtime_minutes: self.runtime_minutes.map(|v| v as u32), - original_language: self.original_language, - overview: self.overview, - collection_name: self.collection_name, - }) + self.runtime_minutes, + self.original_language, + self.overview, + self.collection_name, + )) } } @@ -99,29 +85,17 @@ pub(crate) struct ReviewRow { impl ReviewRow { pub fn into_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)?; - let source = match self.remote_actor_url { - None => ReviewSource::Local, - Some(url) => ReviewSource::Remote { actor_url: url }, - }; - let watch_medium = self.watch_medium.map(|s| s.parse()).transpose()?; - Ok(Review::from_persistence(PersistedReview { - id, - movie_id, - user_id, - rating, - comment, - watched_at, - created_at, - source, - watch_medium, - })) + review_row_to_domain( + self.id, + self.movie_id, + self.user_id, + self.rating, + self.comment, + self.watched_at, + self.created_at, + self.remote_actor_url, + self.watch_medium, + ) } } @@ -147,29 +121,25 @@ pub(crate) struct DiaryRow { impl DiaryRow { pub fn into_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, - } - .into_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, - remote_actor_url: self.remote_actor_url, - watch_medium: self.watch_medium, - } - .into_domain()?; - + let movie = movie_row_to_domain( + self.id, + self.external_metadata_id, + self.title, + self.release_year, + self.director, + self.poster_path, + )?; + let review = review_row_to_domain( + self.review_id, + self.movie_id, + self.user_id, + self.rating, + self.comment, + self.watched_at, + self.created_at, + self.remote_actor_url, + self.watch_medium, + )?; Ok(DiaryEntry::new(movie, review)) } } @@ -188,18 +158,18 @@ pub(crate) struct MovieStatsRow { impl MovieStatsRow { pub fn into_domain(self) -> domain::models::MovieStats { - domain::models::MovieStats { - total_count: self.total_count as u64, - avg_rating: self.avg_rating, - federated_count: self.federated_count as u64, - rating_histogram: [ - self.rating_1 as u64, - self.rating_2 as u64, - self.rating_3 as u64, - self.rating_4 as u64, - self.rating_5 as u64, + movie_stats_to_domain( + self.total_count, + self.avg_rating, + self.federated_count, + [ + self.rating_1, + self.rating_2, + self.rating_3, + self.rating_4, + self.rating_5, ], - } + ) } } @@ -226,24 +196,26 @@ pub(crate) struct FeedRow { impl FeedRow { pub fn into_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, - remote_actor_url: self.remote_actor_url, - watch_medium: self.watch_medium, - } - .into_domain()?; + let movie = movie_row_to_domain( + self.id, + self.external_metadata_id, + self.title, + self.release_year, + self.director, + self.poster_path, + )?; + let review = review_row_to_domain( + self.review_id, + self.movie_id, + self.user_id, + self.rating, + self.comment, + self.watched_at, + self.created_at, + self.remote_actor_url, + self.watch_medium, + )?; + let diary = DiaryEntry::new(movie, review); Ok(FeedEntry::new(diary, self.user_email)) } } @@ -260,16 +232,16 @@ pub(crate) struct UserSummaryRow { } impl UserSummaryRow { - pub fn into_domain(self) -> Result { - Ok(UserSummary::new( - UserId::from_uuid(parse_uuid(&self.id)?), - Email::new(self.email)?, - Username::new(self.username)?, + pub fn into_domain(self) -> Result { + user_summary_to_domain( + self.id, + self.email, + self.username, self.display_name, self.total_movies, self.avg_rating, self.avatar_path, - )) + ) } } @@ -308,35 +280,20 @@ pub(crate) struct WatchlistRow { impl WatchlistRow { pub fn into_domain(self) -> Result { - let entry = WatchlistEntry { - id: WatchlistEntryId::from_uuid(parse_uuid(&self.id)?), - user_id: UserId::from_uuid(parse_uuid(&self.user_id)?), - movie_id: MovieId::from_uuid(parse_uuid(&self.movie_id)?), - added_at: parse_datetime(&self.added_at)?, - }; - let movie = MovieRow { - id: self.m_id, - external_metadata_id: self.external_metadata_id, - title: self.title, - release_year: self.release_year, - director: self.director, - poster_path: self.poster_path, - } - .into_domain()?; - Ok(WatchlistWithMovie { entry, movie }) + let entry = watchlist_entry_to_domain( + self.id, + self.user_id, + self.movie_id, + self.added_at, + )?; + let movie = movie_row_to_domain( + self.m_id, + self.external_metadata_id, + self.title, + self.release_year, + self.director, + self.poster_path, + )?; + Ok(watchlist_with_movie_to_domain(entry, movie)) } } - -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))) -} diff --git a/crates/adapters/sqlite/src/movie.rs b/crates/adapters/sqlite/src/movie.rs index c34adab..163db0e 100644 --- a/crates/adapters/sqlite/src/movie.rs +++ b/crates/adapters/sqlite/src/movie.rs @@ -21,10 +21,6 @@ impl SqliteMovieRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -55,7 +51,7 @@ impl MovieCommand for SqliteMovieRepository { .bind(&poster_path) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -66,7 +62,7 @@ impl MovieCommand for SqliteMovieRepository { .bind(&id) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } } @@ -85,7 +81,7 @@ impl MovieQuery for SqliteMovieRepository { .bind(id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .map(MovieRow::into_domain) .transpose() } @@ -99,7 +95,7 @@ impl MovieQuery for SqliteMovieRepository { .bind(&id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .map(MovieRow::into_domain) .transpose() } @@ -119,7 +115,7 @@ impl MovieQuery for SqliteMovieRepository { .bind(y) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(MovieRow::into_domain) .collect() @@ -141,7 +137,7 @@ impl MovieQuery for SqliteMovieRepository { for id in ids { q = q.bind(id.value().to_string()); } - let rows = q.fetch_all(&self.pool).await.map_err(Self::map_err)?; + let rows = q.fetch_all(&self.pool).await.map_err(adapter_common::map_sqlx_error)?; Ok(rows.into_iter().collect()) } @@ -165,7 +161,7 @@ impl MovieQuery for SqliteMovieRepository { for (t, y) in pairs { q = q.bind(t.value().to_string()).bind(y.value() as i64); } - let rows = q.fetch_all(&self.pool).await.map_err(Self::map_err)?; + let rows = q.fetch_all(&self.pool).await.map_err(adapter_common::map_sqlx_error)?; Ok(rows .into_iter() .map(|r| { @@ -217,7 +213,7 @@ impl MovieQuery for SqliteMovieRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let total: i64 = sqlx::query( "SELECT COUNT(DISTINCT m.id) \ @@ -235,7 +231,7 @@ impl MovieQuery for SqliteMovieRepository { .bind(genre) .fetch_one(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .try_get(0) .unwrap_or(0); @@ -259,7 +255,7 @@ impl MovieQuery for SqliteMovieRepository { ) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(MovieRow::into_domain) .collect() diff --git a/crates/adapters/sqlite/src/movie_dedup.rs b/crates/adapters/sqlite/src/movie_dedup.rs index f20160d..bc93002 100644 --- a/crates/adapters/sqlite/src/movie_dedup.rs +++ b/crates/adapters/sqlite/src/movie_dedup.rs @@ -13,10 +13,6 @@ impl SqliteMovieDeduplicator { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -36,7 +32,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator { let director = canonical.director().map(str::to_string); let poster = canonical.poster_path().map(|p| p.value().to_string()); - let mut tx = self.pool.begin().await.map_err(Self::map_err)?; + let mut tx = self.pool.begin().await.map_err(adapter_common::map_sqlx_error)?; // 1. Upsert canonical movie record sqlx::query( @@ -47,7 +43,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator { poster_path = COALESCE(excluded.poster_path, movies.poster_path)", ) .bind(&new).bind(&ext_id).bind(&title).bind(year).bind(&director).bind(&poster) - .execute(&mut *tx).await.map_err(Self::map_err)?; + .execute(&mut *tx).await.map_err(adapter_common::map_sqlx_error)?; // 2. Re-point simple FK tables let reviews = sqlx::query("UPDATE reviews SET movie_id = ? WHERE movie_id = ?") @@ -55,7 +51,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .rows_affected(); let watchlist = sqlx::query("UPDATE watchlist_entries SET movie_id = ? WHERE movie_id = ?") @@ -63,7 +59,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .rows_affected(); let watch_events = sqlx::query("UPDATE watch_events SET movie_id = ? WHERE movie_id = ?") @@ -71,7 +67,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .rows_affected(); // 3. Re-point movie_profiles (PK — move only if canonical has none) @@ -80,7 +76,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .rows_affected(); // 4. Re-point enrichment tables with composite PKs (INSERT OR IGNORE + DELETE) @@ -93,12 +89,12 @@ impl MovieDeduplicator for SqliteMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query("DELETE FROM movie_genres WHERE movie_id = ?") .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query( "INSERT OR IGNORE INTO movie_keywords (movie_id, tmdb_id, name) @@ -108,41 +104,41 @@ impl MovieDeduplicator for SqliteMovieDeduplicator { .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query("DELETE FROM movie_keywords WHERE movie_id = ?") .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query( "INSERT OR IGNORE INTO movie_cast (movie_id, tmdb_person_id, name, character, billing_order, profile_path) SELECT ?, tmdb_person_id, name, character, billing_order, profile_path FROM movie_cast WHERE movie_id = ?", - ).bind(&new).bind(&old).execute(&mut *tx).await.map_err(Self::map_err)?; + ).bind(&new).bind(&old).execute(&mut *tx).await.map_err(adapter_common::map_sqlx_error)?; sqlx::query("DELETE FROM movie_cast WHERE movie_id = ?") .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query( "INSERT OR IGNORE INTO movie_crew (movie_id, tmdb_person_id, name, job, department, profile_path) SELECT ?, tmdb_person_id, name, job, department, profile_path FROM movie_crew WHERE movie_id = ?", - ).bind(&new).bind(&old).execute(&mut *tx).await.map_err(Self::map_err)?; + ).bind(&new).bind(&old).execute(&mut *tx).await.map_err(adapter_common::map_sqlx_error)?; sqlx::query("DELETE FROM movie_crew WHERE movie_id = ?") .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; // 5. Delete the now-empty old movie record (remaining cascades are safe: all FKs cleared above) sqlx::query("DELETE FROM movies WHERE id = ?") .bind(&old) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; - tx.commit().await.map_err(Self::map_err)?; + tx.commit().await.map_err(adapter_common::map_sqlx_error)?; Ok(reviews + watchlist + watch_events + profiles) } diff --git a/crates/adapters/sqlite/src/persons.rs b/crates/adapters/sqlite/src/persons.rs index 2e71c1c..e4cf30c 100644 --- a/crates/adapters/sqlite/src/persons.rs +++ b/crates/adapters/sqlite/src/persons.rs @@ -29,10 +29,6 @@ pub fn create_person_adapter(pool: SqlitePool) -> (Arc, Arc DomainError { - DomainError::InfrastructureError(e.to_string()) -} - #[async_trait] impl PersonCommand for SqlitePersonAdapter { async fn upsert_batch(&self, persons: &[Person]) -> Result<(), DomainError> { @@ -56,7 +52,7 @@ impl PersonCommand for SqlitePersonAdapter { .bind(person.profile_path()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } Ok(()) } @@ -89,7 +85,7 @@ impl PersonCommand for SqlitePersonAdapter { .bind(batch_size) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let has_more = rows.len() as u32 >= batch_size; let mut count = 0u64; @@ -109,7 +105,7 @@ impl PersonCommand for SqlitePersonAdapter { .bind(&row.profile_path) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; count += 1; } Ok((count, has_more)) @@ -137,7 +133,7 @@ impl PersonCommand for SqlitePersonAdapter { .bind(id.value().to_string()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } } @@ -151,7 +147,7 @@ impl PersonQuery for SqlitePersonAdapter { .bind(id.value().to_string()) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(row.map(PersonRow::into_person)) } @@ -166,7 +162,7 @@ impl PersonQuery for SqlitePersonAdapter { .bind(id.value()) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(row.map(PersonRow::into_person)) } @@ -182,7 +178,7 @@ impl PersonQuery for SqlitePersonAdapter { .bind(id.value().to_string()) .fetch_optional(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? .flatten(); let Some(tmdb_id) = tmdb_id else { @@ -203,7 +199,7 @@ impl PersonQuery for SqlitePersonAdapter { .bind(tmdb_id) .fetch_all(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| CastCredit { movie_id: MovieId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()), @@ -224,7 +220,7 @@ impl PersonQuery for SqlitePersonAdapter { .bind(tmdb_id) .fetch_all(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| CrewCredit { movie_id: MovieId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()), @@ -247,7 +243,7 @@ impl PersonQuery for SqlitePersonAdapter { .bind(offset) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(rows.into_iter().map(PersonRow::into_person).collect()) } @@ -265,7 +261,7 @@ impl PersonQuery for SqlitePersonAdapter { ) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(rows .into_iter() diff --git a/crates/adapters/sqlite/src/profile.rs b/crates/adapters/sqlite/src/profile.rs index 80bcec5..aa84699 100644 --- a/crates/adapters/sqlite/src/profile.rs +++ b/crates/adapters/sqlite/src/profile.rs @@ -17,10 +17,6 @@ impl SqliteMovieProfileRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -29,7 +25,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { let movie_id = p.movie_id.value().to_string(); let enriched_at = p.enriched_at.to_rfc3339(); - let mut tx = self.pool.begin().await.map_err(Self::map_err)?; + let mut tx = self.pool.begin().await.map_err(adapter_common::map_sqlx_error)?; sqlx::query( r#"INSERT INTO movie_profiles @@ -62,13 +58,13 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { .bind(&enriched_at) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; sqlx::query("DELETE FROM movie_genres WHERE movie_id = ?") .bind(&movie_id) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; for g in &p.genres { sqlx::query( "INSERT OR IGNORE INTO movie_genres (movie_id, tmdb_id, name) VALUES (?,?,?)", @@ -78,14 +74,14 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { .bind(&g.name) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } sqlx::query("DELETE FROM movie_keywords WHERE movie_id = ?") .bind(&movie_id) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; for k in &p.keywords { sqlx::query( "INSERT OR IGNORE INTO movie_keywords (movie_id, tmdb_id, name) VALUES (?,?,?)", @@ -95,14 +91,14 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { .bind(&k.name) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } sqlx::query("DELETE FROM movie_cast WHERE movie_id = ?") .bind(&movie_id) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; for c in &p.cast { sqlx::query( "INSERT OR IGNORE INTO movie_cast \ @@ -117,14 +113,14 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { .bind(&c.profile_path) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } sqlx::query("DELETE FROM movie_crew WHERE movie_id = ?") .bind(&movie_id) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; for cr in &p.crew { sqlx::query( "INSERT OR IGNORE INTO movie_crew \ @@ -139,10 +135,10 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { .bind(&cr.profile_path) .execute(&mut *tx) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } - tx.commit().await.map_err(Self::map_err) + tx.commit().await.map_err(adapter_common::map_sqlx_error) } async fn get_by_movie_id(&self, id: &MovieId) -> Result, DomainError> { @@ -157,7 +153,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { .bind(&movie_id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let row = match row { Some(r) => r, @@ -175,7 +171,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { .bind(&movie_id) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| Genre { tmdb_id: r.try_get::("tmdb_id").unwrap_or(0) as u32, @@ -187,7 +183,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { .bind(&movie_id) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| Keyword { tmdb_id: r.try_get::("tmdb_id").unwrap_or(0) as u32, @@ -202,7 +198,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { .bind(&movie_id) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| CastMember { tmdb_person_id: r.try_get::("tmdb_person_id").unwrap_or(0) as u64, @@ -220,7 +216,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { .bind(&movie_id) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(|r| CrewMember { tmdb_person_id: r.try_get::("tmdb_person_id").unwrap_or(0) as u64, @@ -273,7 +269,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository { .bind(&threshold) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(rows .into_iter() diff --git a/crates/adapters/sqlite/src/profile_fields.rs b/crates/adapters/sqlite/src/profile_fields.rs index 5e13feb..329ddd1 100644 --- a/crates/adapters/sqlite/src/profile_fields.rs +++ b/crates/adapters/sqlite/src/profile_fields.rs @@ -15,10 +15,6 @@ impl SqliteProfileFieldsRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -31,7 +27,7 @@ impl UserProfileFieldsRepository for SqliteProfileFieldsRepository { .bind(&id_str) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(rows .iter() @@ -53,7 +49,7 @@ impl UserProfileFieldsRepository for SqliteProfileFieldsRepository { .bind(&id_str) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; for (i, field) in fields.into_iter().enumerate() { let id = uuid::Uuid::new_v4().to_string(); @@ -68,7 +64,7 @@ impl UserProfileFieldsRepository for SqliteProfileFieldsRepository { .bind(position) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; } Ok(()) diff --git a/crates/adapters/sqlite/src/refresh_sessions.rs b/crates/adapters/sqlite/src/refresh_sessions.rs index e17c0ee..c2124eb 100644 --- a/crates/adapters/sqlite/src/refresh_sessions.rs +++ b/crates/adapters/sqlite/src/refresh_sessions.rs @@ -16,10 +16,6 @@ impl SqliteRefreshSessionAdapter { } } -fn map_err(e: sqlx::Error) -> DomainError { - DomainError::InfrastructureError(e.to_string()) -} - #[async_trait] impl RefreshSessionRepository for SqliteRefreshSessionAdapter { async fn create(&self, session: &RefreshSession) -> Result<(), DomainError> { @@ -34,7 +30,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter { .bind(session.created_at.to_rfc3339()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -45,7 +41,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter { .bind(token) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.map(RefreshSessionRow::into_domain).transpose() } @@ -55,7 +51,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter { .bind(token) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -64,7 +60,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter { .bind(user_id.value().to_string()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -74,7 +70,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter { .bind(&now) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected()) } } diff --git a/crates/adapters/sqlite/src/review.rs b/crates/adapters/sqlite/src/review.rs index 80b1933..42b2e85 100644 --- a/crates/adapters/sqlite/src/review.rs +++ b/crates/adapters/sqlite/src/review.rs @@ -7,7 +7,8 @@ use domain::{ }; use sqlx::SqlitePool; -use crate::models::{ReviewRow, datetime_to_str}; +use adapter_common::datetime_to_str; +use crate::models::ReviewRow; pub struct SqliteReviewRepository { pool: SqlitePool, @@ -18,10 +19,6 @@ impl SqliteReviewRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -54,7 +51,7 @@ impl ReviewRepository for SqliteReviewRepository { .bind(review.watch_medium().map(|wm| wm.to_string())) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -68,7 +65,7 @@ impl ReviewRepository for SqliteReviewRepository { .bind(&id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .map(ReviewRow::into_domain) .transpose() } @@ -90,7 +87,7 @@ impl ReviewRepository for SqliteReviewRepository { .bind(&id) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -101,7 +98,7 @@ impl ReviewRepository for SqliteReviewRepository { .bind(&id) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -114,7 +111,7 @@ impl ReviewRepository for SqliteReviewRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(ReviewRow::into_domain) .collect() diff --git a/crates/adapters/sqlite/src/stats.rs b/crates/adapters/sqlite/src/stats.rs index 5a2bffd..93dd663 100644 --- a/crates/adapters/sqlite/src/stats.rs +++ b/crates/adapters/sqlite/src/stats.rs @@ -18,11 +18,6 @@ impl SqliteStatsRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } - async fn fetch_user_totals(&self, user_id: &str) -> Result { sqlx::query_as::<_, UserTotalsRow>( "SELECT COUNT(DISTINCT movie_id) AS total, @@ -32,7 +27,7 @@ impl SqliteStatsRepository { .bind(user_id) .fetch_one(&self.pool) .await - .map_err(Self::map_err) + .map_err(adapter_common::map_sqlx_error) } async fn fetch_user_favorite_director( @@ -51,7 +46,7 @@ impl SqliteStatsRepository { .bind(user_id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(row) } @@ -70,7 +65,7 @@ impl SqliteStatsRepository { .bind(user_id) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(row) } } @@ -86,7 +81,7 @@ impl StatsRepository for SqliteStatsRepository { self.fetch_user_most_active_month(&uid) )?; - let most_active_month = most_active.map(|ym| crate::format_year_month(&ym)); + let most_active_month = most_active.map(|ym| adapter_common::format_year_month(&ym)); Ok(UserStats { total_movies: totals.total, @@ -128,14 +123,14 @@ impl StatsRepository for SqliteStatsRepository { .bind(&uid) .fetch_all(&self.pool) ) - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let max_director_count = director_rows.iter().map(|d| d.count).max().unwrap_or(1); let monthly_ratings = rating_rows .into_iter() .map(|r| MonthlyRating { - month_label: crate::format_year_month(&r.month), + month_label: adapter_common::format_year_month(&r.month), year_month: r.month, avg_rating: r.avg_rating, count: r.count, diff --git a/crates/adapters/sqlite/src/user_settings.rs b/crates/adapters/sqlite/src/user_settings.rs index 990f5a7..e3a7b87 100644 --- a/crates/adapters/sqlite/src/user_settings.rs +++ b/crates/adapters/sqlite/src/user_settings.rs @@ -16,10 +16,6 @@ impl SqliteUserSettingsRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -33,7 +29,7 @@ impl UserSettingsRepository for SqliteUserSettingsRepository { .bind(&uid) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; match row { Some(r) => { @@ -68,7 +64,7 @@ impl UserSettingsRepository for SqliteUserSettingsRepository { }) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } } @@ -84,7 +80,7 @@ impl UserFederationSettingsQuery for SqliteUserSettingsRepository { .bind(&uid) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; match row { Some(r) => { diff --git a/crates/adapters/sqlite/src/users.rs b/crates/adapters/sqlite/src/users.rs index 74f48ea..9c6abd6 100644 --- a/crates/adapters/sqlite/src/users.rs +++ b/crates/adapters/sqlite/src/users.rs @@ -19,11 +19,6 @@ impl SqliteUserRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } - fn parse_role(s: &str) -> UserRole { match s { "admin" => UserRole::Admin, @@ -73,7 +68,7 @@ impl UserRepository for SqliteUserRepository { .bind(email_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref() .map(|r| Self::row_to_user(r, vec![])) @@ -86,7 +81,7 @@ impl UserRepository for SqliteUserRepository { .bind(username_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref() .map(|r| Self::row_to_user(r, vec![])) @@ -126,7 +121,7 @@ impl UserRepository for SqliteUserRepository { .bind(role) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -137,7 +132,7 @@ impl UserRepository for SqliteUserRepository { .bind(&id_str) .fetch_optional(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let Some(r) = row else { return Ok(None) }; @@ -194,7 +189,7 @@ impl UserRepository for SqliteUserRepository { ) .fetch_all(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .into_iter() .map(UserSummaryRow::into_domain) .collect() diff --git a/crates/adapters/sqlite/src/watch_event.rs b/crates/adapters/sqlite/src/watch_event.rs index 190ffff..9016ccc 100644 --- a/crates/adapters/sqlite/src/watch_event.rs +++ b/crates/adapters/sqlite/src/watch_event.rs @@ -7,12 +7,7 @@ use domain::{ }; use sqlx::{Row, SqlitePool}; -use crate::models::datetime_to_str; - -fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) -} +use adapter_common::datetime_to_str; fn parse_uuid(s: &str) -> Result { s.parse() @@ -65,7 +60,7 @@ impl WatchEventCommand for SqliteWatchEventRepository { .bind(&created_at) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -83,7 +78,7 @@ impl WatchEventCommand for SqliteWatchEventRepository { .bind(&id_str) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -105,7 +100,7 @@ impl WatchEventCommand for SqliteWatchEventRepository { for id in ids { q = q.bind(id.value().to_string()); } - let result = q.execute(&self.pool).await.map_err(map_err)?; + let result = q.execute(&self.pool).await.map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected()) } @@ -119,7 +114,7 @@ impl WatchEventCommand for SqliteWatchEventRepository { .bind(&before_str) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected()) } } @@ -139,7 +134,7 @@ impl WatchEventQuery for SqliteWatchEventRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_watch_event).collect() } @@ -155,7 +150,7 @@ impl WatchEventQuery for SqliteWatchEventRepository { .bind(&id_str) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref().map(row_to_watch_event).transpose() } @@ -175,7 +170,7 @@ impl WatchEventQuery for SqliteWatchEventRepository { for id in ids { q = q.bind(id.value().to_string()); } - let rows = q.fetch_all(&self.pool).await.map_err(map_err)?; + let rows = q.fetch_all(&self.pool).await.map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_watch_event).collect() } @@ -197,7 +192,7 @@ impl WatchEventQuery for SqliteWatchEventRepository { .bind(&after_str) .fetch_one(&self.pool) .await - .map_err(map_err)? + .map_err(adapter_common::map_sqlx_error)? .try_get(0) .unwrap_or(0); @@ -206,16 +201,16 @@ impl WatchEventQuery for SqliteWatchEventRepository { } fn row_to_watch_event(row: &sqlx::sqlite::SqliteRow) -> Result { - let id_str: &str = row.try_get("id").map_err(map_err)?; - let user_id_str: &str = row.try_get("user_id").map_err(map_err)?; - let movie_id_str: Option<&str> = row.try_get("movie_id").map_err(map_err)?; - let title: String = row.try_get("title").map_err(map_err)?; - let year: Option = row.try_get("year").map_err(map_err)?; - let ext_id: Option = row.try_get("external_metadata_id").map_err(map_err)?; - let source_str: String = row.try_get("source").map_err(map_err)?; - let watched_at_str: String = row.try_get("watched_at").map_err(map_err)?; - let status_str: String = row.try_get("status").map_err(map_err)?; - let created_at_str: String = row.try_get("created_at").map_err(map_err)?; + let id_str: &str = row.try_get("id").map_err(adapter_common::map_sqlx_error)?; + let user_id_str: &str = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?; + let movie_id_str: Option<&str> = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; + let title: String = row.try_get("title").map_err(adapter_common::map_sqlx_error)?; + let year: Option = row.try_get("year").map_err(adapter_common::map_sqlx_error)?; + let ext_id: Option = row.try_get("external_metadata_id").map_err(adapter_common::map_sqlx_error)?; + let source_str: String = row.try_get("source").map_err(adapter_common::map_sqlx_error)?; + let watched_at_str: String = row.try_get("watched_at").map_err(adapter_common::map_sqlx_error)?; + let status_str: String = row.try_get("status").map_err(adapter_common::map_sqlx_error)?; + let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?; let source: WatchEventSource = source_str .parse() @@ -278,7 +273,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository { .bind(&last_used) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -291,7 +286,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository { .bind(hash) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref().map(row_to_webhook_token).transpose() } @@ -306,7 +301,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_webhook_token).collect() } @@ -320,7 +315,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository { .bind(&uid) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; if result.rows_affected() == 0 { return Err(DomainError::NotFound(format!("Webhook token {id_str}"))); @@ -337,20 +332,20 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository { .bind(&id_str) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } } fn row_to_webhook_token(row: &sqlx::sqlite::SqliteRow) -> Result { - let id_str: &str = row.try_get("id").map_err(map_err)?; - let user_id_str: &str = row.try_get("user_id").map_err(map_err)?; - let token_hash: String = row.try_get("token_hash").map_err(map_err)?; - let provider_str: String = row.try_get("provider").map_err(map_err)?; - let label: Option = row.try_get("label").map_err(map_err)?; - let created_at_str: String = row.try_get("created_at").map_err(map_err)?; - let last_used_str: Option = row.try_get("last_used_at").map_err(map_err)?; + let id_str: &str = row.try_get("id").map_err(adapter_common::map_sqlx_error)?; + let user_id_str: &str = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?; + let token_hash: String = row.try_get("token_hash").map_err(adapter_common::map_sqlx_error)?; + let provider_str: String = row.try_get("provider").map_err(adapter_common::map_sqlx_error)?; + let label: Option = row.try_get("label").map_err(adapter_common::map_sqlx_error)?; + let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?; + let last_used_str: Option = row.try_get("last_used_at").map_err(adapter_common::map_sqlx_error)?; let provider: WatchEventSource = provider_str .parse() diff --git a/crates/adapters/sqlite/src/watchlist.rs b/crates/adapters/sqlite/src/watchlist.rs index e63f558..cf1c444 100644 --- a/crates/adapters/sqlite/src/watchlist.rs +++ b/crates/adapters/sqlite/src/watchlist.rs @@ -10,7 +10,8 @@ use domain::{ }; use sqlx::{Row, SqlitePool}; -use crate::models::{WatchlistRow, datetime_to_str}; +use adapter_common::datetime_to_str; +use crate::models::WatchlistRow; pub struct SqliteWatchlistRepository { pool: SqlitePool, @@ -21,10 +22,6 @@ impl SqliteWatchlistRepository { Self { pool } } - fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) - } } #[async_trait] @@ -45,7 +42,7 @@ impl WatchlistRepository for SqliteWatchlistRepository { .bind(&added_at) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -60,7 +57,7 @@ impl WatchlistRepository for SqliteWatchlistRepository { .bind(&mid) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; if result.rows_affected() == 0 { return Err(DomainError::NotFound(format!( @@ -84,7 +81,7 @@ impl WatchlistRepository for SqliteWatchlistRepository { .bind(&mid) .execute(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected() > 0) } @@ -112,13 +109,13 @@ impl WatchlistRepository for SqliteWatchlistRepository { .bind(offset) .fetch_all(&self.pool) .await - .map_err(Self::map_err)?; + .map_err(adapter_common::map_sqlx_error)?; let total: i64 = sqlx::query("SELECT COUNT(*) FROM watchlist_entries WHERE user_id = ?") .bind(&uid) .fetch_one(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .try_get(0) .unwrap_or(0); @@ -145,7 +142,7 @@ impl WatchlistRepository for SqliteWatchlistRepository { .bind(&mid) .fetch_one(&self.pool) .await - .map_err(Self::map_err)? + .map_err(adapter_common::map_sqlx_error)? .try_get(0) .unwrap_or(0); Ok(count > 0) diff --git a/crates/adapters/sqlite/src/wrapup.rs b/crates/adapters/sqlite/src/wrapup.rs index aa11c7d..1086c35 100644 --- a/crates/adapters/sqlite/src/wrapup.rs +++ b/crates/adapters/sqlite/src/wrapup.rs @@ -13,12 +13,7 @@ use domain::{ use sqlx::{Row, SqlitePool}; use uuid::Uuid; -use crate::models::{parse_datetime, parse_uuid}; - -fn map_err(e: sqlx::Error) -> DomainError { - tracing::error!("Database error: {:?}", e); - DomainError::InfrastructureError("Database operation failed".into()) -} +use adapter_common::{parse_datetime, parse_uuid}; fn status_to_str(s: &WrapUpStatus) -> &'static str { match s { @@ -87,7 +82,7 @@ impl WrapUpRepository for SqliteWrapUpRepository { .bind(&completed) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -107,7 +102,7 @@ impl WrapUpRepository for SqliteWrapUpRepository { .bind(&id_str) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -126,7 +121,7 @@ impl WrapUpRepository for SqliteWrapUpRepository { .bind(&id_str) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -142,7 +137,7 @@ impl WrapUpRepository for SqliteWrapUpRepository { .bind(&id_str) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref().map(row_to_record).transpose() } @@ -158,7 +153,7 @@ impl WrapUpRepository for SqliteWrapUpRepository { .bind(&uid) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_record).collect() } @@ -171,7 +166,7 @@ impl WrapUpRepository for SqliteWrapUpRepository { ) .fetch_all(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; rows.iter().map(row_to_record).collect() } @@ -200,7 +195,7 @@ impl WrapUpRepository for SqliteWrapUpRepository { .bind(&end_str) .fetch_optional(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; row.as_ref().map(row_to_record).transpose() } @@ -210,7 +205,7 @@ impl WrapUpRepository for SqliteWrapUpRepository { .bind(id.value().to_string()) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(()) } @@ -224,21 +219,21 @@ impl WrapUpRepository for SqliteWrapUpRepository { .bind(&before_str) .execute(&self.pool) .await - .map_err(map_err)?; + .map_err(adapter_common::map_sqlx_error)?; Ok(result.rows_affected()) } } fn row_to_record(row: &sqlx::sqlite::SqliteRow) -> Result { - let id_str: String = row.try_get("id").map_err(map_err)?; - let user_id_str: Option = row.try_get("user_id").map_err(map_err)?; - let start_date_str: String = row.try_get("start_date").map_err(map_err)?; - let end_date_str: String = row.try_get("end_date").map_err(map_err)?; - let status_str: String = row.try_get("status").map_err(map_err)?; - let report_json: Option = row.try_get("report_json").map_err(map_err)?; - let error_message: Option = row.try_get("error_message").map_err(map_err)?; - let created_at_str: String = row.try_get("created_at").map_err(map_err)?; - let completed_at_str: Option = row.try_get("completed_at").map_err(map_err)?; + let id_str: String = row.try_get("id").map_err(adapter_common::map_sqlx_error)?; + let user_id_str: Option = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?; + let start_date_str: String = row.try_get("start_date").map_err(adapter_common::map_sqlx_error)?; + let end_date_str: String = row.try_get("end_date").map_err(adapter_common::map_sqlx_error)?; + let status_str: String = row.try_get("status").map_err(adapter_common::map_sqlx_error)?; + let report_json: Option = row.try_get("report_json").map_err(adapter_common::map_sqlx_error)?; + let error_message: Option = row.try_get("error_message").map_err(adapter_common::map_sqlx_error)?; + let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?; + let completed_at_str: Option = row.try_get("completed_at").map_err(adapter_common::map_sqlx_error)?; let user_id = user_id_str.as_deref().map(parse_uuid).transpose()?; @@ -304,7 +299,7 @@ impl WrapUpStatsQuery for SqliteWrapUpStatsQuery { q = q.bind(uid); } - let rows = q.fetch_all(&self.pool).await.map_err(map_err)?; + let rows = q.fetch_all(&self.pool).await.map_err(adapter_common::map_sqlx_error)?; if rows.is_empty() { return Ok(vec![]); @@ -314,7 +309,7 @@ impl WrapUpStatsQuery for SqliteWrapUpStatsQuery { let mut movie_ids: Vec = Vec::new(); let mut seen = std::collections::HashSet::new(); for row in &rows { - let mid: String = row.try_get("movie_id").map_err(map_err)?; + let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; if seen.insert(mid.clone()) { movie_ids.push(mid); } @@ -330,18 +325,18 @@ impl WrapUpStatsQuery for SqliteWrapUpStatsQuery { // 3) Build result let mut result = Vec::with_capacity(rows.len()); for row in &rows { - let movie_id_str: String = row.try_get("movie_id").map_err(map_err)?; - let title: String = row.try_get("title").map_err(map_err)?; - let release_year: i64 = row.try_get("release_year").map_err(map_err)?; - let director: Option = row.try_get("director").map_err(map_err)?; - let poster_path: Option = row.try_get("poster_path").map_err(map_err)?; - let rating: i64 = row.try_get("rating").map_err(map_err)?; - let watched_at_str: String = row.try_get("watched_at").map_err(map_err)?; - let user_id_str: String = row.try_get("user_id").map_err(map_err)?; - let runtime_minutes: Option = row.try_get("runtime_minutes").map_err(map_err)?; - let budget_usd: Option = row.try_get("budget_usd").map_err(map_err)?; + let movie_id_str: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; + let title: String = row.try_get("title").map_err(adapter_common::map_sqlx_error)?; + let release_year: i64 = row.try_get("release_year").map_err(adapter_common::map_sqlx_error)?; + let director: Option = row.try_get("director").map_err(adapter_common::map_sqlx_error)?; + let poster_path: Option = row.try_get("poster_path").map_err(adapter_common::map_sqlx_error)?; + let rating: i64 = row.try_get("rating").map_err(adapter_common::map_sqlx_error)?; + let watched_at_str: String = row.try_get("watched_at").map_err(adapter_common::map_sqlx_error)?; + let user_id_str: String = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?; + let runtime_minutes: Option = row.try_get("runtime_minutes").map_err(adapter_common::map_sqlx_error)?; + let budget_usd: Option = row.try_get("budget_usd").map_err(adapter_common::map_sqlx_error)?; let original_language: Option = - row.try_get("original_language").map_err(map_err)?; + row.try_get("original_language").map_err(adapter_common::map_sqlx_error)?; let genres = genres_map.get(&movie_id_str).cloned().unwrap_or_default(); let keywords = keywords_map.get(&movie_id_str).cloned().unwrap_or_default(); @@ -411,12 +406,12 @@ async fn fetch_genres_sqlite( for id in movie_ids { q = q.bind(id); } - let rows = q.fetch_all(pool).await.map_err(map_err)?; + let rows = q.fetch_all(pool).await.map_err(adapter_common::map_sqlx_error)?; let mut map: HashMap> = HashMap::new(); for row in rows { - let mid: String = row.try_get("movie_id").map_err(map_err)?; - let name: String = row.try_get("name").map_err(map_err)?; + let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; + let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?; map.entry(mid).or_default().push(name); } Ok(map) @@ -437,12 +432,12 @@ async fn fetch_keywords_sqlite( for id in movie_ids { q = q.bind(id); } - let rows = q.fetch_all(pool).await.map_err(map_err)?; + let rows = q.fetch_all(pool).await.map_err(adapter_common::map_sqlx_error)?; let mut map: HashMap> = HashMap::new(); for row in rows { - let mid: String = row.try_get("movie_id").map_err(map_err)?; - let name: String = row.try_get("name").map_err(map_err)?; + let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; + let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?; map.entry(mid).or_default().push(name); } Ok(map) @@ -466,15 +461,15 @@ async fn fetch_cast_sqlite( for id in movie_ids { q = q.bind(id); } - let rows = q.fetch_all(pool).await.map_err(map_err)?; + let rows = q.fetch_all(pool).await.map_err(adapter_common::map_sqlx_error)?; let mut map: HashMap> = HashMap::new(); for row in rows { - let mid: String = row.try_get("movie_id").map_err(map_err)?; - let name: String = row.try_get("name").map_err(map_err)?; - let billing_order: i32 = row.try_get("billing_order").map_err(map_err)?; - let tmdb_person_id: i64 = row.try_get("tmdb_person_id").map_err(map_err)?; - let profile_path: Option = row.try_get("profile_path").map_err(map_err)?; + let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?; + let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?; + let billing_order: i32 = row.try_get("billing_order").map_err(adapter_common::map_sqlx_error)?; + let tmdb_person_id: i64 = row.try_get("tmdb_person_id").map_err(adapter_common::map_sqlx_error)?; + let profile_path: Option = row.try_get("profile_path").map_err(adapter_common::map_sqlx_error)?; map.entry(mid).or_default().push(CastEntry { name, billing_order: billing_order as u32,