refactor: extract adapter-common crate — shared sqlx error mapping,
row-to-domain conversions, date/uuid utils 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.
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -220,6 +220,17 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adapter-common"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"domain",
|
||||||
|
"sqlx",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler2"
|
name = "adler2"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -3884,6 +3895,7 @@ dependencies = [
|
|||||||
name = "postgres"
|
name = "postgres"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"adapter-common",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3920,6 +3932,7 @@ name = "postgres-federation"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activitypub",
|
"activitypub",
|
||||||
|
"adapter-common",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3935,6 +3948,7 @@ dependencies = [
|
|||||||
name = "postgres-search"
|
name = "postgres-search"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"adapter-common",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"domain",
|
"domain",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
@@ -5164,6 +5178,7 @@ dependencies = [
|
|||||||
name = "sqlite"
|
name = "sqlite"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"adapter-common",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -5200,6 +5215,7 @@ name = "sqlite-federation"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activitypub",
|
"activitypub",
|
||||||
|
"adapter-common",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -5216,6 +5232,7 @@ dependencies = [
|
|||||||
name = "sqlite-search"
|
name = "sqlite-search"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"adapter-common",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"domain",
|
"domain",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ members = [
|
|||||||
"crates/adapters/plex",
|
"crates/adapters/plex",
|
||||||
"crates/adapters/sqlite-search",
|
"crates/adapters/sqlite-search",
|
||||||
"crates/adapters/postgres-search",
|
"crates/adapters/postgres-search",
|
||||||
|
"crates/adapters/adapter-common",
|
||||||
"crates/infra-wiring",
|
"crates/infra-wiring",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
@@ -92,6 +93,7 @@ plex = { path = "crates/adapters/plex" }
|
|||||||
image-converter = { path = "crates/adapters/image-converter" }
|
image-converter = { path = "crates/adapters/image-converter" }
|
||||||
sqlite-search = { path = "crates/adapters/sqlite-search" }
|
sqlite-search = { path = "crates/adapters/sqlite-search" }
|
||||||
postgres-search = { path = "crates/adapters/postgres-search" }
|
postgres-search = { path = "crates/adapters/postgres-search" }
|
||||||
|
adapter-common = { path = "crates/adapters/adapter-common" }
|
||||||
infra-wiring = { path = "crates/infra-wiring" }
|
infra-wiring = { path = "crates/infra-wiring" }
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
|
|||||||
@@ -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/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/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/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/worker/Cargo.toml crates/worker/Cargo.toml
|
||||||
COPY crates/infra-wiring/Cargo.toml crates/infra-wiring/Cargo.toml
|
COPY crates/infra-wiring/Cargo.toml crates/infra-wiring/Cargo.toml
|
||||||
|
|
||||||
|
|||||||
11
crates/adapters/adapter-common/Cargo.toml
Normal file
11
crates/adapters/adapter-common/Cargo.toml
Normal file
@@ -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 }
|
||||||
223
crates/adapters/adapter-common/src/lib.rs
Normal file
223
crates/adapters/adapter-common/src/lib.rs
Normal file
@@ -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, DomainError> {
|
||||||
|
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, DomainError> {
|
||||||
|
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<String>,
|
||||||
|
title: String,
|
||||||
|
release_year: i64,
|
||||||
|
director: Option<String>,
|
||||||
|
poster_path: Option<String>,
|
||||||
|
) -> Result<Movie, DomainError> {
|
||||||
|
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<String>,
|
||||||
|
watched_at: String,
|
||||||
|
created_at: String,
|
||||||
|
remote_actor_url: Option<String>,
|
||||||
|
watch_medium: Option<String>,
|
||||||
|
) -> Result<Review, DomainError> {
|
||||||
|
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<String>` (sqlite splits a comma-separated string, postgres receives a
|
||||||
|
/// `Vec` directly).
|
||||||
|
pub fn movie_summary_to_domain(
|
||||||
|
movie: Movie,
|
||||||
|
genres: Vec<String>,
|
||||||
|
runtime_minutes: Option<i64>,
|
||||||
|
original_language: Option<String>,
|
||||||
|
overview: Option<String>,
|
||||||
|
collection_name: Option<String>,
|
||||||
|
) -> 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<f64>,
|
||||||
|
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<String>,
|
||||||
|
total_movies: i64,
|
||||||
|
avg_rating: Option<f64>,
|
||||||
|
avatar_path: Option<String>,
|
||||||
|
) -> Result<UserSummary, DomainError> {
|
||||||
|
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<WatchlistEntry, DomainError> {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ sqlx = { version = "0.8.6", features = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
] }
|
] }
|
||||||
activitypub = { workspace = true }
|
activitypub = { workspace = true }
|
||||||
|
adapter-common = { workspace = true }
|
||||||
k-ap = { version = "0.4.1", registry = "gitea" }
|
k-ap = { version = "0.4.1", registry = "gitea" }
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use async_trait::async_trait;
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use k_ap::ActivityRepository;
|
use k_ap::ActivityRepository;
|
||||||
|
|
||||||
use super::{PostgresFederationRepository, datetime_to_str};
|
use adapter_common::datetime_to_str;
|
||||||
|
use super::PostgresFederationRepository;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ActivityRepository for PostgresFederationRepository {
|
impl ActivityRepository for PostgresFederationRepository {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ use chrono::Utc;
|
|||||||
use k_ap::{ActorRepository, RemoteActor};
|
use k_ap::{ActorRepository, RemoteActor};
|
||||||
use sqlx::Row;
|
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]
|
#[async_trait]
|
||||||
impl ActorRepository for PostgresFederationRepository {
|
impl ActorRepository for PostgresFederationRepository {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use domain::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row};
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub struct PostgresApContentQuery {
|
pub struct PostgresApContentQuery {
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
@@ -22,24 +21,11 @@ impl PostgresApContentQuery {
|
|||||||
pub fn new(pool: PgPool) -> Self {
|
pub fn new(pool: PgPool) -> Self {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Local row types ──────────────────────────────────────────────────────────
|
// ── Local row types ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse_uuid(s: &str) -> Result<Uuid, DomainError> {
|
use adapter_common::{parse_uuid, parse_datetime};
|
||||||
Uuid::parse_str(s)
|
|
||||||
.map_err(|e| DomainError::InfrastructureError(format!("Invalid UUID '{}': {}", s, e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_datetime(s: &str) -> Result<chrono::NaiveDateTime, DomainError> {
|
|
||||||
chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
|
|
||||||
.map_err(|e| DomainError::InfrastructureError(format!("Invalid datetime '{}': {}", s, e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
struct MovieRow {
|
struct MovieRow {
|
||||||
@@ -181,7 +167,7 @@ impl LocalApContentQuery for PostgresApContentQuery {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
@@ -249,7 +235,7 @@ impl LocalApContentQuery for PostgresApContentQuery {
|
|||||||
.bind(&mid)
|
.bind(&mid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
rows.into_iter().map(DiaryRow::into_domain).collect()
|
rows.into_iter().map(DiaryRow::into_domain).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +268,7 @@ impl LocalApContentQuery for PostgresApContentQuery {
|
|||||||
.bind(limit_i64)
|
.bind(limit_i64)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
} else {
|
} else {
|
||||||
sqlx::query_as::<_, DiaryRow>(
|
sqlx::query_as::<_, DiaryRow>(
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
@@ -301,7 +287,7 @@ impl LocalApContentQuery for PostgresApContentQuery {
|
|||||||
.bind(limit_i64)
|
.bind(limit_i64)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
};
|
};
|
||||||
rows.into_iter().map(DiaryRow::into_domain).collect()
|
rows.into_iter().map(DiaryRow::into_domain).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ use chrono::Utc;
|
|||||||
use k_ap::{BlockedDomain, BlocklistRepository};
|
use k_ap::{BlockedDomain, BlocklistRepository};
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
|
|
||||||
use super::{PostgresFederationRepository, datetime_to_str};
|
use adapter_common::datetime_to_str;
|
||||||
|
use super::PostgresFederationRepository;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl BlocklistRepository for PostgresFederationRepository {
|
impl BlocklistRepository for PostgresFederationRepository {
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ use k_ap::{
|
|||||||
};
|
};
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
|
|
||||||
|
use adapter_common::datetime_to_str;
|
||||||
use super::{
|
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,
|
str_to_status,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,9 @@ mod watchlist;
|
|||||||
pub use ap_content::PostgresApContentQuery;
|
pub use ap_content::PostgresApContentQuery;
|
||||||
pub use remote_goals::PostgresRemoteGoalRepository;
|
pub use remote_goals::PostgresRemoteGoalRepository;
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use k_ap::{FollowerStatus, RemoteActor};
|
use k_ap::{FollowerStatus, RemoteActor};
|
||||||
use sqlx::{PgPool, Row};
|
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 {
|
pub(crate) fn status_to_str(status: &FollowerStatus) -> &'static str {
|
||||||
match status {
|
match status {
|
||||||
FollowerStatus::Pending => "pending",
|
FollowerStatus::Pending => "pending",
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ impl PostgresRemoteGoalRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -38,7 +34,7 @@ impl RemoteGoalRepository for PostgresRemoteGoalRepository {
|
|||||||
.bind(&received)
|
.bind(&received)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -57,7 +53,7 @@ impl RemoteGoalRepository for PostgresRemoteGoalRepository {
|
|||||||
.bind(ap_id)
|
.bind(ap_id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -68,7 +64,7 @@ impl RemoteGoalRepository for PostgresRemoteGoalRepository {
|
|||||||
.bind(actor_url)
|
.bind(actor_url)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -78,7 +74,7 @@ impl RemoteGoalRepository for PostgresRemoteGoalRepository {
|
|||||||
.bind(actor_url)
|
.bind(actor_url)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -92,7 +88,7 @@ impl RemoteGoalRepository for PostgresRemoteGoalRepository {
|
|||||||
.bind(actor_url)
|
.bind(actor_url)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter()
|
rows.iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use anyhow::{Result, anyhow};
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::models::{Review, ReviewSource};
|
use domain::models::{Review, ReviewSource};
|
||||||
|
|
||||||
use super::{PostgresFederationRepository, datetime_to_str};
|
use adapter_common::datetime_to_str;
|
||||||
|
use super::PostgresFederationRepository;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl RemoteReviewRepository for PostgresFederationRepository {
|
impl RemoteReviewRepository for PostgresFederationRepository {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
|
adapter-common = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "macros"] }
|
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "macros"] }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ pub fn create_search_adapter(pool: PgPool) -> (Arc<dyn SearchCommand>, Arc<dyn S
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
DomainError::InfrastructureError(e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl SearchCommand for PostgresSearchAdapter {
|
impl SearchCommand for PostgresSearchAdapter {
|
||||||
async fn index(&self, doc: IndexableDocument) -> Result<(), DomainError> {
|
async fn index(&self, doc: IndexableDocument) -> Result<(), DomainError> {
|
||||||
@@ -91,7 +87,7 @@ impl SearchCommand for PostgresSearchAdapter {
|
|||||||
.bind(&fts_input)
|
.bind(&fts_input)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -113,7 +109,7 @@ impl SearchCommand for PostgresSearchAdapter {
|
|||||||
.bind(&fts_input)
|
.bind(&fts_input)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -127,14 +123,14 @@ impl SearchCommand for PostgresSearchAdapter {
|
|||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
}
|
}
|
||||||
EntityType::Person => {
|
EntityType::Person => {
|
||||||
sqlx::query("DELETE FROM people_search WHERE person_id = $1")
|
sqlx::query("DELETE FROM people_search WHERE person_id = $1")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -183,7 +179,7 @@ impl PostgresSearchAdapter {
|
|||||||
.bind(query.filters.year.map(|y| y as i32))
|
.bind(query.filters.year.map(|y| y as i32))
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
count as u64
|
count as u64
|
||||||
} else {
|
} else {
|
||||||
let count: i64 = sqlx::query_scalar(
|
let count: i64 = sqlx::query_scalar(
|
||||||
@@ -196,7 +192,7 @@ impl PostgresSearchAdapter {
|
|||||||
.bind(query.filters.year.map(|y| y as i32))
|
.bind(query.filters.year.map(|y| y as i32))
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
count as u64
|
count as u64
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -221,7 +217,7 @@ impl PostgresSearchAdapter {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
} else {
|
} else {
|
||||||
sqlx::query_as::<_, Row>(
|
sqlx::query_as::<_, Row>(
|
||||||
"SELECT m.id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
@@ -238,7 +234,7 @@ impl PostgresSearchAdapter {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let items = rows
|
let items = rows
|
||||||
@@ -290,7 +286,7 @@ impl PostgresSearchAdapter {
|
|||||||
.bind(text)
|
.bind(text)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
count as u64
|
count as u64
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -316,7 +312,7 @@ impl PostgresSearchAdapter {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let mut items = Vec::with_capacity(rows.len());
|
let mut items = Vec::with_capacity(rows.len());
|
||||||
for row in rows {
|
for row in rows {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ sqlx = { version = "0.8.6", features = [
|
|||||||
"macros",
|
"macros",
|
||||||
"chrono",
|
"chrono",
|
||||||
] }
|
] }
|
||||||
|
adapter-common = { workspace = true }
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
postgres-federation = { workspace = true }
|
postgres-federation = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|||||||
@@ -22,23 +22,19 @@ impl PostgresDiaryRepository {
|
|||||||
Self { pool }
|
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<i64, DomainError> {
|
async fn count_diary_entries(&self, movie_id: Option<&str>) -> Result<i64, DomainError> {
|
||||||
match movie_id {
|
match movie_id {
|
||||||
None => sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews")
|
None => sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews")
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err),
|
.map_err(adapter_common::map_sqlx_error),
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews WHERE movie_id = $1")
|
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews WHERE movie_id = $1")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +69,7 @@ impl PostgresDiaryRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_movie_diary_rows(
|
async fn fetch_movie_diary_rows(
|
||||||
@@ -109,7 +105,7 @@ impl PostgresDiaryRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn count_user_diary_entries(
|
async fn count_user_diary_entries(
|
||||||
@@ -138,7 +134,7 @@ impl PostgresDiaryRepository {
|
|||||||
if has_search {
|
if has_search {
|
||||||
q = q.bind(search.unwrap());
|
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(
|
async fn fetch_user_diary_rows(
|
||||||
@@ -197,7 +193,7 @@ impl PostgresDiaryRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.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 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_q = bind_filter_params!(sqlx::query_as::<_, FeedRow>(&select_sql));
|
||||||
let rows = rows_q
|
let rows = rows_q
|
||||||
@@ -383,7 +379,7 @@ impl DiaryQuery for PostgresDiaryRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let items = rows
|
let items = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -408,7 +404,7 @@ impl DiaryQuery for PostgresDiaryRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.ok_or_else(|| DomainError::NotFound(format!("Movie {}", id_str)))?
|
.ok_or_else(|| DomainError::NotFound(format!("Movie {}", id_str)))?
|
||||||
.into_domain()?;
|
.into_domain()?;
|
||||||
|
|
||||||
@@ -423,7 +419,7 @@ impl DiaryQuery for PostgresDiaryRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ReviewRow::into_domain)
|
.map(ReviewRow::into_domain)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
@@ -448,7 +444,7 @@ impl DiaryQuery for PostgresDiaryRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.into_iter().map(DiaryRow::into_domain).collect()
|
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 {
|
while let Some(row) = futures::StreamExt::next(&mut rows).await {
|
||||||
yield match row {
|
yield match row {
|
||||||
Ok(r) => r.into_domain(),
|
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)
|
.bind(id_str)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
.map(MovieStatsRow::into_domain)
|
.map(MovieStatsRow::into_domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,7 +513,7 @@ impl DiaryQuery for PostgresDiaryRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let rows = sqlx::query_as::<_, FeedRow>(
|
let rows = sqlx::query_as::<_, FeedRow>(
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
@@ -541,7 +537,7 @@ impl DiaryQuery for PostgresDiaryRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let items = rows
|
let items = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -561,7 +557,7 @@ impl DiaryQuery for PostgresDiaryRepository {
|
|||||||
sqlx::query_scalar("SELECT COUNT(*) FROM reviews WHERE remote_actor_url IS NULL")
|
sqlx::query_scalar("SELECT COUNT(*) FROM reviews WHERE remote_actor_url IS NULL")
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(count as u64)
|
Ok(count as u64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use domain::{
|
|||||||
};
|
};
|
||||||
use sqlx::{PgPool, Row};
|
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 {
|
pub struct PostgresGoalRepository {
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
@@ -18,10 +18,6 @@ impl PostgresGoalRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -46,7 +42,7 @@ impl GoalCommand for PostgresGoalRepository {
|
|||||||
.bind(&created_at)
|
.bind(&created_at)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -60,7 +56,7 @@ impl GoalCommand for PostgresGoalRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
return Err(DomainError::NotFound("Goal not found".into()));
|
return Err(DomainError::NotFound("Goal not found".into()));
|
||||||
@@ -77,7 +73,7 @@ impl GoalCommand for PostgresGoalRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
return Err(DomainError::NotFound("Goal not found".into()));
|
return Err(DomainError::NotFound("Goal not found".into()));
|
||||||
@@ -105,7 +101,7 @@ impl GoalQuery for PostgresGoalRepository {
|
|||||||
.bind(y)
|
.bind(y)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.map(|r| row_to_goal(&r)).transpose()
|
row.map(|r| row_to_goal(&r)).transpose()
|
||||||
}
|
}
|
||||||
@@ -121,7 +117,7 @@ impl GoalQuery for PostgresGoalRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter().map(row_to_goal).collect()
|
rows.iter().map(row_to_goal).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,10 +97,6 @@ impl PostgresImportProfileRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("DB error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -118,7 +114,7 @@ impl ImportProfileRepository for PostgresImportProfileRepository {
|
|||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_for_user(&self, user_id: &UserId) -> Result<Vec<ImportProfile>, DomainError> {
|
async fn list_for_user(&self, user_id: &UserId) -> Result<Vec<ImportProfile>, DomainError> {
|
||||||
@@ -139,7 +135,7 @@ impl ImportProfileRepository for PostgresImportProfileRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
@@ -184,7 +180,7 @@ impl ImportProfileRepository for PostgresImportProfileRepository {
|
|||||||
.bind(&id_str).bind(&uid_str)
|
.bind(&id_str).bind(&uid_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.map(|r| {
|
row.map(|r| {
|
||||||
Ok(ImportProfile {
|
Ok(ImportProfile {
|
||||||
@@ -212,6 +208,6 @@ impl ImportProfileRepository for PostgresImportProfileRepository {
|
|||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,10 +202,6 @@ impl PostgresImportSessionRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("DB error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_session(
|
fn serialize_session(
|
||||||
s: &ImportSession,
|
s: &ImportSession,
|
||||||
@@ -301,7 +297,7 @@ impl ImportSessionRepository for PostgresImportSessionRepository {
|
|||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(
|
async fn get(
|
||||||
@@ -331,7 +327,7 @@ impl ImportSessionRepository for PostgresImportSessionRepository {
|
|||||||
.bind(&uid_str)
|
.bind(&uid_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.map(|r| {
|
row.map(|r| {
|
||||||
Self::deserialize_session(
|
Self::deserialize_session(
|
||||||
@@ -359,7 +355,7 @@ impl ImportSessionRepository for PostgresImportSessionRepository {
|
|||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(&self, id: &ImportSessionId) -> Result<(), DomainError> {
|
async fn delete(&self, id: &ImportSessionId) -> Result<(), DomainError> {
|
||||||
@@ -369,14 +365,14 @@ impl ImportSessionRepository for PostgresImportSessionRepository {
|
|||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_expired(&self) -> Result<u64, DomainError> {
|
async fn delete_expired(&self) -> Result<u64, DomainError> {
|
||||||
let result = sqlx::query("DELETE FROM import_sessions WHERE expires_at < NOW()")
|
let result = sqlx::query("DELETE FROM import_sessions WHERE expires_at < NOW()")
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(result.rows_affected())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,6 +383,6 @@ impl ImportSessionRepository for PostgresImportSessionRepository {
|
|||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,30 +39,6 @@ pub use watch_event::{PostgresWatchEventRepository, PostgresWebhookTokenReposito
|
|||||||
pub use watchlist::PostgresWatchlistRepository;
|
pub use watchlist::PostgresWatchlistRepository;
|
||||||
pub use wrapup::{PostgresWrapUpRepository, PostgresWrapUpStatsQuery};
|
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> {
|
pub async fn migrate(pool: &PgPool) -> Result<(), DomainError> {
|
||||||
sqlx::migrate!("./migrations")
|
sqlx::migrate!("./migrations")
|
||||||
.set_ignore_missing(true)
|
.set_ignore_missing(true)
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
use chrono::NaiveDateTime;
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{
|
models::{DiaryEntry, FeedEntry, Movie, MovieSummary, Review},
|
||||||
DiaryEntry, FeedEntry, Movie, MovieSummary, PersistedReview, Review, ReviewSource,
|
};
|
||||||
UserSummary,
|
use adapter_common::{
|
||||||
},
|
movie_row_to_domain, movie_stats_to_domain, movie_summary_to_domain, review_row_to_domain,
|
||||||
value_objects::{
|
user_summary_to_domain,
|
||||||
Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear,
|
|
||||||
ReviewId, UserId, Username,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
pub(crate) struct MovieRow {
|
pub(crate) struct MovieRow {
|
||||||
@@ -24,22 +19,14 @@ pub(crate) struct MovieRow {
|
|||||||
|
|
||||||
impl MovieRow {
|
impl MovieRow {
|
||||||
pub fn into_domain(self) -> Result<Movie, DomainError> {
|
pub fn into_domain(self) -> Result<Movie, DomainError> {
|
||||||
let id = MovieId::from_uuid(parse_uuid(&self.id)?);
|
movie_row_to_domain(
|
||||||
let external_metadata_id = self
|
self.id,
|
||||||
.external_metadata_id
|
self.external_metadata_id,
|
||||||
.map(ExternalMetadataId::new)
|
self.title,
|
||||||
.transpose()?;
|
self.release_year,
|
||||||
let title = MovieTitle::new(self.title)?;
|
|
||||||
let release_year = ReleaseYear::new(self.release_year as u16)?;
|
|
||||||
let poster_path = self.poster_path.map(PosterPath::new).transpose()?;
|
|
||||||
Ok(Movie::from_persistence(
|
|
||||||
id,
|
|
||||||
external_metadata_id,
|
|
||||||
title,
|
|
||||||
release_year,
|
|
||||||
self.director,
|
self.director,
|
||||||
poster_path,
|
self.poster_path,
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,23 +47,22 @@ pub(crate) struct MovieSummaryRow {
|
|||||||
|
|
||||||
impl MovieSummaryRow {
|
impl MovieSummaryRow {
|
||||||
pub fn into_domain(self) -> Result<MovieSummary, DomainError> {
|
pub fn into_domain(self) -> Result<MovieSummary, DomainError> {
|
||||||
let movie = MovieRow {
|
let movie = movie_row_to_domain(
|
||||||
id: self.id,
|
self.id,
|
||||||
external_metadata_id: self.external_metadata_id,
|
self.external_metadata_id,
|
||||||
title: self.title,
|
self.title,
|
||||||
release_year: self.release_year,
|
self.release_year,
|
||||||
director: self.director,
|
self.director,
|
||||||
poster_path: self.poster_path,
|
self.poster_path,
|
||||||
}
|
)?;
|
||||||
.into_domain()?;
|
Ok(movie_summary_to_domain(
|
||||||
Ok(MovieSummary {
|
|
||||||
movie,
|
movie,
|
||||||
genres: self.genres.unwrap_or_default(),
|
self.genres.unwrap_or_default(),
|
||||||
runtime_minutes: self.runtime_minutes.map(|v| v as u32),
|
self.runtime_minutes,
|
||||||
original_language: self.original_language,
|
self.original_language,
|
||||||
overview: self.overview,
|
self.overview,
|
||||||
collection_name: self.collection_name,
|
self.collection_name,
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,29 +81,17 @@ pub(crate) struct ReviewRow {
|
|||||||
|
|
||||||
impl ReviewRow {
|
impl ReviewRow {
|
||||||
pub fn into_domain(self) -> Result<Review, DomainError> {
|
pub fn into_domain(self) -> Result<Review, DomainError> {
|
||||||
let id = ReviewId::from_uuid(parse_uuid(&self.id)?);
|
review_row_to_domain(
|
||||||
let movie_id = MovieId::from_uuid(parse_uuid(&self.movie_id)?);
|
self.id,
|
||||||
let user_id = UserId::from_uuid(parse_uuid(&self.user_id)?);
|
self.movie_id,
|
||||||
let rating = Rating::new(self.rating as u8)?;
|
self.user_id,
|
||||||
let comment = self.comment.map(Comment::new).transpose()?;
|
self.rating,
|
||||||
let watched_at = parse_datetime(&self.watched_at)?;
|
self.comment,
|
||||||
let created_at = parse_datetime(&self.created_at)?;
|
self.watched_at,
|
||||||
let source = match self.remote_actor_url {
|
self.created_at,
|
||||||
None => ReviewSource::Local,
|
self.remote_actor_url,
|
||||||
Some(url) => ReviewSource::Remote { actor_url: url },
|
self.watch_medium,
|
||||||
};
|
)
|
||||||
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,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,27 +116,25 @@ pub(crate) struct DiaryRow {
|
|||||||
|
|
||||||
impl DiaryRow {
|
impl DiaryRow {
|
||||||
pub fn into_domain(self) -> Result<DiaryEntry, DomainError> {
|
pub fn into_domain(self) -> Result<DiaryEntry, DomainError> {
|
||||||
let movie = MovieRow {
|
let movie = movie_row_to_domain(
|
||||||
id: self.id,
|
self.id,
|
||||||
external_metadata_id: self.external_metadata_id,
|
self.external_metadata_id,
|
||||||
title: self.title,
|
self.title,
|
||||||
release_year: self.release_year,
|
self.release_year,
|
||||||
director: self.director,
|
self.director,
|
||||||
poster_path: self.poster_path,
|
self.poster_path,
|
||||||
}
|
)?;
|
||||||
.into_domain()?;
|
let review = review_row_to_domain(
|
||||||
let review = ReviewRow {
|
self.review_id,
|
||||||
id: self.review_id,
|
self.movie_id,
|
||||||
movie_id: self.movie_id,
|
self.user_id,
|
||||||
user_id: self.user_id,
|
self.rating,
|
||||||
rating: self.rating,
|
self.comment,
|
||||||
comment: self.comment,
|
self.watched_at,
|
||||||
watched_at: self.watched_at,
|
self.created_at,
|
||||||
created_at: self.created_at,
|
self.remote_actor_url,
|
||||||
remote_actor_url: self.remote_actor_url,
|
self.watch_medium,
|
||||||
watch_medium: self.watch_medium,
|
)?;
|
||||||
}
|
|
||||||
.into_domain()?;
|
|
||||||
Ok(DiaryEntry::new(movie, review))
|
Ok(DiaryEntry::new(movie, review))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,24 +161,26 @@ pub(crate) struct FeedRow {
|
|||||||
|
|
||||||
impl FeedRow {
|
impl FeedRow {
|
||||||
pub fn into_domain(self) -> Result<FeedEntry, DomainError> {
|
pub fn into_domain(self) -> Result<FeedEntry, DomainError> {
|
||||||
let diary = DiaryRow {
|
let movie = movie_row_to_domain(
|
||||||
id: self.id,
|
self.id,
|
||||||
external_metadata_id: self.external_metadata_id,
|
self.external_metadata_id,
|
||||||
title: self.title,
|
self.title,
|
||||||
release_year: self.release_year,
|
self.release_year,
|
||||||
director: self.director,
|
self.director,
|
||||||
poster_path: self.poster_path,
|
self.poster_path,
|
||||||
review_id: self.review_id,
|
)?;
|
||||||
movie_id: self.movie_id,
|
let review = review_row_to_domain(
|
||||||
user_id: self.user_id,
|
self.review_id,
|
||||||
rating: self.rating,
|
self.movie_id,
|
||||||
comment: self.comment,
|
self.user_id,
|
||||||
watched_at: self.watched_at,
|
self.rating,
|
||||||
created_at: self.created_at,
|
self.comment,
|
||||||
remote_actor_url: self.remote_actor_url,
|
self.watched_at,
|
||||||
watch_medium: self.watch_medium,
|
self.created_at,
|
||||||
}
|
self.remote_actor_url,
|
||||||
.into_domain()?;
|
self.watch_medium,
|
||||||
|
)?;
|
||||||
|
let diary = DiaryEntry::new(movie, review);
|
||||||
Ok(FeedEntry::new(diary, self.user_email))
|
Ok(FeedEntry::new(diary, self.user_email))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,18 +199,18 @@ pub(crate) struct MovieStatsRow {
|
|||||||
|
|
||||||
impl MovieStatsRow {
|
impl MovieStatsRow {
|
||||||
pub fn into_domain(self) -> domain::models::MovieStats {
|
pub fn into_domain(self) -> domain::models::MovieStats {
|
||||||
domain::models::MovieStats {
|
movie_stats_to_domain(
|
||||||
total_count: self.total_count as u64,
|
self.total_count,
|
||||||
avg_rating: self.avg_rating,
|
self.avg_rating,
|
||||||
federated_count: self.federated_count as u64,
|
self.federated_count,
|
||||||
rating_histogram: [
|
[
|
||||||
self.rating_1 as u64,
|
self.rating_1,
|
||||||
self.rating_2 as u64,
|
self.rating_2,
|
||||||
self.rating_3 as u64,
|
self.rating_3,
|
||||||
self.rating_4 as u64,
|
self.rating_4,
|
||||||
self.rating_5 as u64,
|
self.rating_5,
|
||||||
],
|
],
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,16 +226,16 @@ pub(crate) struct UserSummaryRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UserSummaryRow {
|
impl UserSummaryRow {
|
||||||
pub fn into_domain(self) -> Result<UserSummary, DomainError> {
|
pub fn into_domain(self) -> Result<domain::models::UserSummary, DomainError> {
|
||||||
Ok(UserSummary::new(
|
user_summary_to_domain(
|
||||||
UserId::from_uuid(parse_uuid(&self.id)?),
|
self.id,
|
||||||
Email::new(self.email)?,
|
self.email,
|
||||||
Username::new(self.username)?,
|
self.username,
|
||||||
self.display_name,
|
self.display_name,
|
||||||
self.total_movies,
|
self.total_movies,
|
||||||
self.avg_rating,
|
self.avg_rating,
|
||||||
self.avatar_path,
|
self.avatar_path,
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,17 +257,3 @@ pub(crate) struct MonthlyRatingRow {
|
|||||||
pub avg_rating: f64,
|
pub avg_rating: f64,
|
||||||
pub count: i64,
|
pub count: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_uuid(s: &str) -> Result<Uuid, DomainError> {
|
|
||||||
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, DomainError> {
|
|
||||||
NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
|
|
||||||
.map_err(|e| DomainError::InfrastructureError(format!("Invalid datetime '{}': {}", s, e)))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ impl PostgresMovieRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -55,7 +51,7 @@ impl MovieCommand for PostgresMovieRepository {
|
|||||||
.bind(&poster_path)
|
.bind(&poster_path)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -66,7 +62,7 @@ impl MovieCommand for PostgresMovieRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +81,7 @@ impl MovieQuery for PostgresMovieRepository {
|
|||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.map(MovieRow::into_domain)
|
.map(MovieRow::into_domain)
|
||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
@@ -99,7 +95,7 @@ impl MovieQuery for PostgresMovieRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.map(MovieRow::into_domain)
|
.map(MovieRow::into_domain)
|
||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
@@ -119,7 +115,7 @@ impl MovieQuery for PostgresMovieRepository {
|
|||||||
.bind(year)
|
.bind(year)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(MovieRow::into_domain)
|
.map(MovieRow::into_domain)
|
||||||
.collect()
|
.collect()
|
||||||
@@ -139,7 +135,7 @@ impl MovieQuery for PostgresMovieRepository {
|
|||||||
.bind(&vals)
|
.bind(&vals)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(rows.into_iter().map(|(id,)| id).collect())
|
Ok(rows.into_iter().map(|(id,)| id).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +158,7 @@ impl MovieQuery for PostgresMovieRepository {
|
|||||||
.bind(&years)
|
.bind(&years)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
@@ -211,7 +207,7 @@ impl MovieQuery for PostgresMovieRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let total: i64 = sqlx::query(
|
let total: i64 = sqlx::query(
|
||||||
"SELECT COUNT(DISTINCT m.id) \
|
"SELECT COUNT(DISTINCT m.id) \
|
||||||
@@ -226,7 +222,7 @@ impl MovieQuery for PostgresMovieRepository {
|
|||||||
.bind(genre)
|
.bind(genre)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.try_get(0)
|
.try_get(0)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
@@ -250,7 +246,7 @@ impl MovieQuery for PostgresMovieRepository {
|
|||||||
)
|
)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| r.into_domain())
|
.map(|r| r.into_domain())
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ impl PostgresMovieDeduplicator {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -36,7 +32,7 @@ impl MovieDeduplicator for PostgresMovieDeduplicator {
|
|||||||
let director = canonical.director().map(str::to_string);
|
let director = canonical.director().map(str::to_string);
|
||||||
let poster = canonical.poster_path().map(|p| p.value().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
|
// 1. Upsert canonical movie record
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -47,7 +43,7 @@ impl MovieDeduplicator for PostgresMovieDeduplicator {
|
|||||||
poster_path = COALESCE(EXCLUDED.poster_path, movies.poster_path)",
|
poster_path = COALESCE(EXCLUDED.poster_path, movies.poster_path)",
|
||||||
)
|
)
|
||||||
.bind(&new).bind(&ext_id).bind(&title).bind(year).bind(&director).bind(&poster)
|
.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
|
// 2. Re-point simple FK tables
|
||||||
let reviews = sqlx::query("UPDATE reviews SET movie_id = $1 WHERE movie_id = $2")
|
let reviews = sqlx::query("UPDATE reviews SET movie_id = $1 WHERE movie_id = $2")
|
||||||
@@ -55,7 +51,7 @@ impl MovieDeduplicator for PostgresMovieDeduplicator {
|
|||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.rows_affected();
|
.rows_affected();
|
||||||
|
|
||||||
let watchlist =
|
let watchlist =
|
||||||
@@ -64,7 +60,7 @@ impl MovieDeduplicator for PostgresMovieDeduplicator {
|
|||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.rows_affected();
|
.rows_affected();
|
||||||
|
|
||||||
let watch_events = sqlx::query("UPDATE watch_events SET movie_id = $1 WHERE movie_id = $2")
|
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)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.rows_affected();
|
.rows_affected();
|
||||||
|
|
||||||
// 3. Re-point movie_profiles (PK — move only if canonical has none)
|
// 3. Re-point movie_profiles (PK — move only if canonical has none)
|
||||||
@@ -81,7 +77,7 @@ impl MovieDeduplicator for PostgresMovieDeduplicator {
|
|||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.rows_affected();
|
.rows_affected();
|
||||||
|
|
||||||
// 4. Re-point enrichment tables with composite PKs (INSERT … ON CONFLICT DO NOTHING + DELETE)
|
// 4. Re-point enrichment tables with composite PKs (INSERT … ON CONFLICT DO NOTHING + DELETE)
|
||||||
@@ -95,12 +91,12 @@ impl MovieDeduplicator for PostgresMovieDeduplicator {
|
|||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
sqlx::query("DELETE FROM movie_genres WHERE movie_id = $1")
|
sqlx::query("DELETE FROM movie_genres WHERE movie_id = $1")
|
||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO movie_keywords (movie_id, tmdb_id, name)
|
"INSERT INTO movie_keywords (movie_id, tmdb_id, name)
|
||||||
@@ -111,43 +107,43 @@ impl MovieDeduplicator for PostgresMovieDeduplicator {
|
|||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
sqlx::query("DELETE FROM movie_keywords WHERE movie_id = $1")
|
sqlx::query("DELETE FROM movie_keywords WHERE movie_id = $1")
|
||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO movie_cast (movie_id, tmdb_person_id, name, character, billing_order, profile_path)
|
"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
|
SELECT $1, tmdb_person_id, name, character, billing_order, profile_path FROM movie_cast WHERE movie_id = $2
|
||||||
ON CONFLICT DO NOTHING",
|
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")
|
sqlx::query("DELETE FROM movie_cast WHERE movie_id = $1")
|
||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO movie_crew (movie_id, tmdb_person_id, name, job, department, profile_path)
|
"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
|
SELECT $1, tmdb_person_id, name, job, department, profile_path FROM movie_crew WHERE movie_id = $2
|
||||||
ON CONFLICT DO NOTHING",
|
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")
|
sqlx::query("DELETE FROM movie_crew WHERE movie_id = $1")
|
||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.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)
|
// 5. Delete the now-empty old movie record (remaining cascades are safe: all FKs cleared above)
|
||||||
sqlx::query("DELETE FROM movies WHERE id = $1")
|
sqlx::query("DELETE FROM movies WHERE id = $1")
|
||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.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)
|
Ok(reviews + watchlist + watch_events + profiles)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ pub fn create_person_adapter(pool: PgPool) -> (Arc<dyn PersonCommand>, Arc<dyn P
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
DomainError::InfrastructureError(e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl PersonCommand for PostgresPersonAdapter {
|
impl PersonCommand for PostgresPersonAdapter {
|
||||||
@@ -56,7 +53,7 @@ impl PersonCommand for PostgresPersonAdapter {
|
|||||||
.bind(person.profile_path())
|
.bind(person.profile_path())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -89,7 +86,7 @@ impl PersonCommand for PostgresPersonAdapter {
|
|||||||
.bind(batch_size as i64)
|
.bind(batch_size as i64)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let has_more = rows.len() as u32 >= batch_size;
|
let has_more = rows.len() as u32 >= batch_size;
|
||||||
let mut count = 0u64;
|
let mut count = 0u64;
|
||||||
@@ -109,7 +106,7 @@ impl PersonCommand for PostgresPersonAdapter {
|
|||||||
.bind(&row.profile_path)
|
.bind(&row.profile_path)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
Ok((count, has_more))
|
Ok((count, has_more))
|
||||||
@@ -137,7 +134,7 @@ impl PersonCommand for PostgresPersonAdapter {
|
|||||||
.bind(id.value().to_string())
|
.bind(id.value().to_string())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +148,7 @@ impl PersonQuery for PostgresPersonAdapter {
|
|||||||
.bind(id.value().to_string())
|
.bind(id.value().to_string())
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(row.map(PersonRow::into_person))
|
Ok(row.map(PersonRow::into_person))
|
||||||
}
|
}
|
||||||
@@ -166,7 +163,7 @@ impl PersonQuery for PostgresPersonAdapter {
|
|||||||
.bind(id.value())
|
.bind(id.value())
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(row.map(PersonRow::into_person))
|
Ok(row.map(PersonRow::into_person))
|
||||||
}
|
}
|
||||||
@@ -182,7 +179,7 @@ impl PersonQuery for PostgresPersonAdapter {
|
|||||||
.bind(id.value().to_string())
|
.bind(id.value().to_string())
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
let Some(tmdb_id) = tmdb_id else {
|
let Some(tmdb_id) = tmdb_id else {
|
||||||
@@ -219,7 +216,7 @@ impl PersonQuery for PostgresPersonAdapter {
|
|||||||
.bind(tmdb_id)
|
.bind(tmdb_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| CastCredit {
|
.map(|r| CastCredit {
|
||||||
movie_id: MovieId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()),
|
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)
|
.bind(tmdb_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| CrewCredit {
|
.map(|r| CrewCredit {
|
||||||
movie_id: MovieId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()),
|
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)
|
.bind(offset as i64)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(rows.into_iter().map(PersonRow::into_person).collect())
|
Ok(rows.into_iter().map(PersonRow::into_person).collect())
|
||||||
}
|
}
|
||||||
@@ -279,7 +276,7 @@ impl PersonQuery for PostgresPersonAdapter {
|
|||||||
)
|
)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ impl PostgresMovieProfileRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -28,7 +24,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
|||||||
async fn upsert(&self, p: &MovieProfile) -> Result<(), DomainError> {
|
async fn upsert(&self, p: &MovieProfile) -> Result<(), DomainError> {
|
||||||
let movie_id = p.movie_id.value().to_string();
|
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(
|
sqlx::query(
|
||||||
r#"INSERT INTO movie_profiles
|
r#"INSERT INTO movie_profiles
|
||||||
@@ -61,35 +57,35 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
|||||||
.bind(p.enriched_at)
|
.bind(p.enriched_at)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
sqlx::query("DELETE FROM movie_genres WHERE movie_id = $1")
|
sqlx::query("DELETE FROM movie_genres WHERE movie_id = $1")
|
||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
for g in &p.genres {
|
for g in &p.genres {
|
||||||
sqlx::query("INSERT INTO movie_genres (movie_id, tmdb_id, name) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING")
|
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)
|
.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")
|
sqlx::query("DELETE FROM movie_keywords WHERE movie_id = $1")
|
||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
for k in &p.keywords {
|
for k in &p.keywords {
|
||||||
sqlx::query("INSERT INTO movie_keywords (movie_id, tmdb_id, name) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING")
|
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)
|
.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")
|
sqlx::query("DELETE FROM movie_cast WHERE movie_id = $1")
|
||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
for c in &p.cast {
|
for c in &p.cast {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO movie_cast \
|
"INSERT INTO movie_cast \
|
||||||
@@ -104,14 +100,14 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
|||||||
.bind(&c.profile_path)
|
.bind(&c.profile_path)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlx::query("DELETE FROM movie_crew WHERE movie_id = $1")
|
sqlx::query("DELETE FROM movie_crew WHERE movie_id = $1")
|
||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
for cr in &p.crew {
|
for cr in &p.crew {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO movie_crew \
|
"INSERT INTO movie_crew \
|
||||||
@@ -126,10 +122,10 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
|||||||
.bind(&cr.profile_path)
|
.bind(&cr.profile_path)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.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<Option<MovieProfile>, DomainError> {
|
async fn get_by_movie_id(&self, id: &MovieId) -> Result<Option<MovieProfile>, DomainError> {
|
||||||
@@ -144,7 +140,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
|||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let row = match row {
|
let row = match row {
|
||||||
Some(r) => r,
|
Some(r) => r,
|
||||||
@@ -159,7 +155,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
|||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| Genre {
|
.map(|r| Genre {
|
||||||
tmdb_id: r.try_get::<i32, _>("tmdb_id").unwrap_or(0) as u32,
|
tmdb_id: r.try_get::<i32, _>("tmdb_id").unwrap_or(0) as u32,
|
||||||
@@ -171,7 +167,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
|||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| Keyword {
|
.map(|r| Keyword {
|
||||||
tmdb_id: r.try_get::<i32, _>("tmdb_id").unwrap_or(0) as u32,
|
tmdb_id: r.try_get::<i32, _>("tmdb_id").unwrap_or(0) as u32,
|
||||||
@@ -186,7 +182,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
|||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| CastMember {
|
.map(|r| CastMember {
|
||||||
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
||||||
@@ -204,7 +200,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
|||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| CrewMember {
|
.map(|r| CrewMember {
|
||||||
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
||||||
@@ -257,7 +253,7 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
|||||||
.bind(threshold)
|
.bind(threshold)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ impl PostgresRefreshSessionAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
DomainError::InfrastructureError(e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl RefreshSessionRepository for PostgresRefreshSessionAdapter {
|
impl RefreshSessionRepository for PostgresRefreshSessionAdapter {
|
||||||
@@ -34,7 +31,7 @@ impl RefreshSessionRepository for PostgresRefreshSessionAdapter {
|
|||||||
.bind(session.created_at)
|
.bind(session.created_at)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +45,7 @@ impl RefreshSessionRepository for PostgresRefreshSessionAdapter {
|
|||||||
.bind(token)
|
.bind(token)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.map(RefreshSessionRow::into_domain).transpose()
|
row.map(RefreshSessionRow::into_domain).transpose()
|
||||||
}
|
}
|
||||||
@@ -58,7 +55,7 @@ impl RefreshSessionRepository for PostgresRefreshSessionAdapter {
|
|||||||
.bind(token)
|
.bind(token)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +64,7 @@ impl RefreshSessionRepository for PostgresRefreshSessionAdapter {
|
|||||||
.bind(user_id.value().to_string())
|
.bind(user_id.value().to_string())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +72,7 @@ impl RefreshSessionRepository for PostgresRefreshSessionAdapter {
|
|||||||
let result = sqlx::query("DELETE FROM refresh_sessions WHERE expires_at < NOW()")
|
let result = sqlx::query("DELETE FROM refresh_sessions WHERE expires_at < NOW()")
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(result.rows_affected())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use domain::{
|
|||||||
};
|
};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::models::{ReviewRow, datetime_to_str};
|
use adapter_common::datetime_to_str;
|
||||||
|
use crate::models::ReviewRow;
|
||||||
|
|
||||||
pub struct PostgresReviewRepository {
|
pub struct PostgresReviewRepository {
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
@@ -18,10 +19,6 @@ impl PostgresReviewRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -54,7 +51,7 @@ impl ReviewRepository for PostgresReviewRepository {
|
|||||||
.bind(review.watch_medium().map(|wm| wm.to_string()))
|
.bind(review.watch_medium().map(|wm| wm.to_string()))
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -72,7 +69,7 @@ impl ReviewRepository for PostgresReviewRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.map(ReviewRow::into_domain)
|
.map(ReviewRow::into_domain)
|
||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
@@ -94,7 +91,7 @@ impl ReviewRepository for PostgresReviewRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -105,7 +102,7 @@ impl ReviewRepository for PostgresReviewRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +119,7 @@ impl ReviewRepository for PostgresReviewRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ReviewRow::into_domain)
|
.map(ReviewRow::into_domain)
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use domain::{
|
|||||||
};
|
};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::format_year_month;
|
use adapter_common::format_year_month;
|
||||||
use crate::models::{DirectorCountRow, MonthlyRatingRow, UserTotalsRow};
|
use crate::models::{DirectorCountRow, MonthlyRatingRow, UserTotalsRow};
|
||||||
|
|
||||||
pub struct PostgresStatsRepository {
|
pub struct PostgresStatsRepository {
|
||||||
@@ -19,10 +19,6 @@ impl PostgresStatsRepository {
|
|||||||
Self { pool }
|
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<UserTotalsRow, DomainError> {
|
async fn fetch_user_totals(&self, user_id: &str) -> Result<UserTotalsRow, DomainError> {
|
||||||
sqlx::query_as::<_, UserTotalsRow>(
|
sqlx::query_as::<_, UserTotalsRow>(
|
||||||
@@ -33,7 +29,7 @@ impl PostgresStatsRepository {
|
|||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_user_favorite_director(
|
async fn fetch_user_favorite_director(
|
||||||
@@ -52,7 +48,7 @@ impl PostgresStatsRepository {
|
|||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_user_most_active_month(
|
async fn fetch_user_most_active_month(
|
||||||
@@ -70,7 +66,7 @@ impl PostgresStatsRepository {
|
|||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +122,7 @@ impl StatsRepository for PostgresStatsRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.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 max_director_count = director_rows.iter().map(|d| d.count).max().unwrap_or(1);
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ impl PostgresUserSettingsRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -33,7 +29,7 @@ impl UserSettingsRepository for PostgresUserSettingsRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
Some(r) => {
|
Some(r) => {
|
||||||
@@ -65,7 +61,7 @@ impl UserSettingsRepository for PostgresUserSettingsRepository {
|
|||||||
.bind(settings.federate_watchlist())
|
.bind(settings.federate_watchlist())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +77,7 @@ impl UserFederationSettingsQuery for PostgresUserSettingsRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
Some(r) => {
|
Some(r) => {
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ impl PostgresUserRepository {
|
|||||||
Self { pool }
|
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 {
|
fn parse_role(s: &str) -> UserRole {
|
||||||
match s {
|
match s {
|
||||||
@@ -76,7 +72,7 @@ impl UserRepository for PostgresUserRepository {
|
|||||||
.bind(email_str)
|
.bind(email_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
row.as_ref()
|
row.as_ref()
|
||||||
.map(|r| Self::row_to_user(r, vec![]))
|
.map(|r| Self::row_to_user(r, vec![]))
|
||||||
.transpose()
|
.transpose()
|
||||||
@@ -90,7 +86,7 @@ impl UserRepository for PostgresUserRepository {
|
|||||||
.bind(username_str)
|
.bind(username_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
row.as_ref()
|
row.as_ref()
|
||||||
.map(|r| Self::row_to_user(r, vec![]))
|
.map(|r| Self::row_to_user(r, vec![]))
|
||||||
.transpose()
|
.transpose()
|
||||||
@@ -130,7 +126,7 @@ impl UserRepository for PostgresUserRepository {
|
|||||||
.bind(role)
|
.bind(role)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +136,7 @@ impl UserRepository for PostgresUserRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let Some(r) = row else { return Ok(None) };
|
let Some(r) = row else { return Ok(None) };
|
||||||
|
|
||||||
@@ -197,7 +193,7 @@ impl UserRepository for PostgresUserRepository {
|
|||||||
)
|
)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(UserSummaryRow::into_domain)
|
.map(UserSummaryRow::into_domain)
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -7,12 +7,8 @@ use domain::{
|
|||||||
};
|
};
|
||||||
use sqlx::{PgPool, Row};
|
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 ──────────────────────────────────────────────────────
|
// ── WatchEventRepository ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -52,7 +48,7 @@ impl WatchEventCommand for PostgresWatchEventRepository {
|
|||||||
.bind(event.created_at())
|
.bind(event.created_at())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -70,7 +66,7 @@ impl WatchEventCommand for PostgresWatchEventRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -90,7 +86,7 @@ impl WatchEventCommand for PostgresWatchEventRepository {
|
|||||||
.bind(&id_strs)
|
.bind(&id_strs)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(result.rows_affected())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +99,7 @@ impl WatchEventCommand for PostgresWatchEventRepository {
|
|||||||
.bind(before)
|
.bind(before)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(result.rows_affected())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +122,7 @@ impl WatchEventQuery for PostgresWatchEventRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter().map(row_to_watch_event).collect()
|
rows.iter().map(row_to_watch_event).collect()
|
||||||
}
|
}
|
||||||
@@ -145,7 +141,7 @@ impl WatchEventQuery for PostgresWatchEventRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.as_ref().map(row_to_watch_event).transpose()
|
row.as_ref().map(row_to_watch_event).transpose()
|
||||||
}
|
}
|
||||||
@@ -166,7 +162,7 @@ impl WatchEventQuery for PostgresWatchEventRepository {
|
|||||||
.bind(&id_strs)
|
.bind(&id_strs)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
rows.iter().map(row_to_watch_event).collect()
|
rows.iter().map(row_to_watch_event).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,23 +183,23 @@ impl WatchEventQuery for PostgresWatchEventRepository {
|
|||||||
.bind(after)
|
.bind(after)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row_to_watch_event(row: &sqlx::postgres::PgRow) -> Result<WatchEvent, DomainError> {
|
fn row_to_watch_event(row: &sqlx::postgres::PgRow) -> Result<WatchEvent, DomainError> {
|
||||||
let id_str: String = row.try_get("id").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(map_err)?;
|
let user_id_str: String = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let movie_id_str: Option<String> = row.try_get("movie_id").map_err(map_err)?;
|
let movie_id_str: Option<String> = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let title: String = row.try_get("title").map_err(map_err)?;
|
let title: String = row.try_get("title").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let year: Option<i32> = row.try_get("year").map_err(map_err)?;
|
let year: Option<i32> = row.try_get("year").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let ext_id: Option<String> = row.try_get("external_metadata_id").map_err(map_err)?;
|
let ext_id: Option<String> = row.try_get("external_metadata_id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let source_str: String = row.try_get("source").map_err(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let source: WatchEventSource = source_str
|
let source: WatchEventSource = source_str
|
||||||
.parse()
|
.parse()
|
||||||
@@ -265,7 +261,7 @@ impl WebhookTokenRepository for PostgresWebhookTokenRepository {
|
|||||||
.bind(token.last_used_at())
|
.bind(token.last_used_at())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -280,7 +276,7 @@ impl WebhookTokenRepository for PostgresWebhookTokenRepository {
|
|||||||
.bind(hash)
|
.bind(hash)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.as_ref().map(row_to_webhook_token).transpose()
|
row.as_ref().map(row_to_webhook_token).transpose()
|
||||||
}
|
}
|
||||||
@@ -297,7 +293,7 @@ impl WebhookTokenRepository for PostgresWebhookTokenRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter().map(row_to_webhook_token).collect()
|
rows.iter().map(row_to_webhook_token).collect()
|
||||||
}
|
}
|
||||||
@@ -311,7 +307,7 @@ impl WebhookTokenRepository for PostgresWebhookTokenRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
return Err(DomainError::NotFound(format!("Webhook token {id_str}")));
|
return Err(DomainError::NotFound(format!("Webhook token {id_str}")));
|
||||||
@@ -326,20 +322,20 @@ impl WebhookTokenRepository for PostgresWebhookTokenRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row_to_webhook_token(row: &sqlx::postgres::PgRow) -> Result<WebhookToken, DomainError> {
|
fn row_to_webhook_token(row: &sqlx::postgres::PgRow) -> Result<WebhookToken, DomainError> {
|
||||||
let id_str: String = row.try_get("id").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(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
let provider_str: String = row.try_get("provider").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let label: Option<String> = row.try_get("label").map_err(map_err)?;
|
let label: Option<String> = row.try_get("label").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let created_at_str: String = row.try_get("created_at").map_err(map_err)?;
|
let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let last_used_str: Option<String> = row.try_get("last_used_at").map_err(map_err)?;
|
let last_used_str: Option<String> = row.try_get("last_used_at").map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let provider: WatchEventSource = provider_str
|
let provider: WatchEventSource = provider_str
|
||||||
.parse()
|
.parse()
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ use domain::{
|
|||||||
};
|
};
|
||||||
use sqlx::{PgPool, Row};
|
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 {
|
pub struct PostgresWatchlistRepository {
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
@@ -21,10 +22,6 @@ impl PostgresWatchlistRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -46,7 +43,7 @@ impl WatchlistRepository for PostgresWatchlistRepository {
|
|||||||
.bind(added_at)
|
.bind(added_at)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -61,7 +58,7 @@ impl WatchlistRepository for PostgresWatchlistRepository {
|
|||||||
.bind(&mid)
|
.bind(&mid)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
return Err(DomainError::NotFound(format!(
|
return Err(DomainError::NotFound(format!(
|
||||||
@@ -85,7 +82,7 @@ impl WatchlistRepository for PostgresWatchlistRepository {
|
|||||||
.bind(&mid)
|
.bind(&mid)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(result.rows_affected() > 0)
|
Ok(result.rows_affected() > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,14 +111,14 @@ impl WatchlistRepository for PostgresWatchlistRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let total: i64 =
|
let total: i64 =
|
||||||
sqlx::query_scalar("SELECT COUNT(*) FROM watchlist_entries WHERE user_id = $1")
|
sqlx::query_scalar("SELECT COUNT(*) FROM watchlist_entries WHERE user_id = $1")
|
||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let items = rows
|
let items = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -187,7 +184,7 @@ impl WatchlistRepository for PostgresWatchlistRepository {
|
|||||||
.bind(&mid)
|
.bind(&mid)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,8 @@ use domain::{
|
|||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row};
|
||||||
use uuid::Uuid;
|
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 {
|
fn status_to_str(s: &WrapUpStatus) -> &'static str {
|
||||||
match s {
|
match s {
|
||||||
@@ -76,7 +72,7 @@ impl WrapUpRepository for PostgresWrapUpRepository {
|
|||||||
.bind(record.completed_at)
|
.bind(record.completed_at)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -96,7 +92,7 @@ impl WrapUpRepository for PostgresWrapUpRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -115,7 +111,7 @@ impl WrapUpRepository for PostgresWrapUpRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -132,7 +128,7 @@ impl WrapUpRepository for PostgresWrapUpRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.as_ref().map(row_to_record).transpose()
|
row.as_ref().map(row_to_record).transpose()
|
||||||
}
|
}
|
||||||
@@ -149,7 +145,7 @@ impl WrapUpRepository for PostgresWrapUpRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter().map(row_to_record).collect()
|
rows.iter().map(row_to_record).collect()
|
||||||
}
|
}
|
||||||
@@ -163,7 +159,7 @@ impl WrapUpRepository for PostgresWrapUpRepository {
|
|||||||
)
|
)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter().map(row_to_record).collect()
|
rows.iter().map(row_to_record).collect()
|
||||||
}
|
}
|
||||||
@@ -190,7 +186,7 @@ impl WrapUpRepository for PostgresWrapUpRepository {
|
|||||||
.bind(end)
|
.bind(end)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.as_ref().map(row_to_record).transpose()
|
row.as_ref().map(row_to_record).transpose()
|
||||||
}
|
}
|
||||||
@@ -200,7 +196,7 @@ impl WrapUpRepository for PostgresWrapUpRepository {
|
|||||||
.bind(id.value().to_string())
|
.bind(id.value().to_string())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,21 +209,21 @@ impl WrapUpRepository for PostgresWrapUpRepository {
|
|||||||
.bind(before)
|
.bind(before)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(result.rows_affected())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row_to_record(row: &sqlx::postgres::PgRow) -> Result<WrapUpRecord, DomainError> {
|
fn row_to_record(row: &sqlx::postgres::PgRow) -> Result<WrapUpRecord, DomainError> {
|
||||||
let id_str: String = row.try_get("id").map_err(map_err)?;
|
let id_str: String = row.try_get("id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let user_id_str: Option<String> = row.try_get("user_id").map_err(map_err)?;
|
let user_id_str: Option<String> = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let start_date: NaiveDate = row.try_get("start_date").map_err(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
let status_str: String = row.try_get("status").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let report_json: Option<String> = row.try_get("report_json").map_err(map_err)?;
|
let report_json: Option<String> = row.try_get("report_json").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let error_message: Option<String> = row.try_get("error_message").map_err(map_err)?;
|
let error_message: Option<String> = row.try_get("error_message").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let created_at_str: String = row.try_get("created_at").map_err(map_err)?;
|
let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let completed_at_str: Option<String> = row.try_get("completed_at").map_err(map_err)?;
|
let completed_at_str: Option<String> = row.try_get("completed_at").map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let user_id = user_id_str.as_deref().map(parse_uuid).transpose()?;
|
let user_id = user_id_str.as_deref().map(parse_uuid).transpose()?;
|
||||||
|
|
||||||
@@ -292,7 +288,7 @@ impl WrapUpStatsQuery for PostgresWrapUpStatsQuery {
|
|||||||
q = q.bind(uid);
|
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() {
|
if rows.is_empty() {
|
||||||
return Ok(vec![]);
|
return Ok(vec![]);
|
||||||
@@ -302,7 +298,7 @@ impl WrapUpStatsQuery for PostgresWrapUpStatsQuery {
|
|||||||
let mut movie_ids: Vec<String> = Vec::new();
|
let mut movie_ids: Vec<String> = Vec::new();
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
for row in &rows {
|
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()) {
|
if seen.insert(mid.clone()) {
|
||||||
movie_ids.push(mid);
|
movie_ids.push(mid);
|
||||||
}
|
}
|
||||||
@@ -318,18 +314,18 @@ impl WrapUpStatsQuery for PostgresWrapUpStatsQuery {
|
|||||||
// 3) Build result
|
// 3) Build result
|
||||||
let mut result = Vec::with_capacity(rows.len());
|
let mut result = Vec::with_capacity(rows.len());
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
let movie_id_str: String = row.try_get("movie_id").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(map_err)?;
|
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(map_err)?;
|
let release_year: i64 = row.try_get("release_year").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let director: Option<String> = row.try_get("director").map_err(map_err)?;
|
let director: Option<String> = row.try_get("director").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let poster_path: Option<String> = row.try_get("poster_path").map_err(map_err)?;
|
let poster_path: Option<String> = row.try_get("poster_path").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let rating: i64 = row.try_get("rating").map_err(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
let user_id_str: String = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let runtime_minutes: Option<i32> = row.try_get("runtime_minutes").map_err(map_err)?;
|
let runtime_minutes: Option<i32> = row.try_get("runtime_minutes").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let budget_usd: Option<i64> = row.try_get("budget_usd").map_err(map_err)?;
|
let budget_usd: Option<i64> = row.try_get("budget_usd").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let original_language: Option<String> =
|
let original_language: Option<String> =
|
||||||
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 genres = genres_map.get(&movie_id_str).cloned().unwrap_or_default();
|
||||||
let keywords = keywords_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)
|
.bind(movie_ids)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
for row in rows {
|
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)?;
|
||||||
let name: String = row.try_get("name").map_err(map_err)?;
|
let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?;
|
||||||
map.entry(mid).or_default().push(name);
|
map.entry(mid).or_default().push(name);
|
||||||
}
|
}
|
||||||
Ok(map)
|
Ok(map)
|
||||||
@@ -404,12 +400,12 @@ async fn fetch_keywords_pg(
|
|||||||
.bind(movie_ids)
|
.bind(movie_ids)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
for row in rows {
|
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)?;
|
||||||
let name: String = row.try_get("name").map_err(map_err)?;
|
let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?;
|
||||||
map.entry(mid).or_default().push(name);
|
map.entry(mid).or_default().push(name);
|
||||||
}
|
}
|
||||||
Ok(map)
|
Ok(map)
|
||||||
@@ -428,15 +424,15 @@ async fn fetch_cast_pg(
|
|||||||
.bind(movie_ids)
|
.bind(movie_ids)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let mut map: HashMap<String, Vec<CastEntry>> = HashMap::new();
|
let mut map: HashMap<String, Vec<CastEntry>> = HashMap::new();
|
||||||
for row in rows {
|
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)?;
|
||||||
let name: String = row.try_get("name").map_err(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
let tmdb_person_id: i64 = row.try_get("tmdb_person_id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let profile_path: Option<String> = row.try_get("profile_path").map_err(map_err)?;
|
let profile_path: Option<String> = row.try_get("profile_path").map_err(adapter_common::map_sqlx_error)?;
|
||||||
map.entry(mid).or_default().push(CastEntry {
|
map.entry(mid).or_default().push(CastEntry {
|
||||||
name,
|
name,
|
||||||
billing_order: billing_order as u32,
|
billing_order: billing_order as u32,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
activitypub = { workspace = true }
|
activitypub = { workspace = true }
|
||||||
|
adapter-common = { workspace = true }
|
||||||
k-ap = { version = "0.4.1", registry = "gitea" }
|
k-ap = { version = "0.4.1", registry = "gitea" }
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use async_trait::async_trait;
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use k_ap::ActivityRepository;
|
use k_ap::ActivityRepository;
|
||||||
|
|
||||||
use super::{SqliteFederationRepository, datetime_to_str};
|
use adapter_common::datetime_to_str;
|
||||||
|
use super::SqliteFederationRepository;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ActivityRepository for SqliteFederationRepository {
|
impl ActivityRepository for SqliteFederationRepository {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ use chrono::Utc;
|
|||||||
use k_ap::{ActorRepository, RemoteActor};
|
use k_ap::{ActorRepository, RemoteActor};
|
||||||
use sqlx::Row;
|
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]
|
#[async_trait]
|
||||||
impl ActorRepository for SqliteFederationRepository {
|
impl ActorRepository for SqliteFederationRepository {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use domain::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub struct SqliteApContentQuery {
|
pub struct SqliteApContentQuery {
|
||||||
pool: SqlitePool,
|
pool: SqlitePool,
|
||||||
@@ -23,23 +22,11 @@ impl SqliteApContentQuery {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Local row types ──────────────────────────────────────────────────────────
|
// ── Local row types ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse_uuid(s: &str) -> Result<Uuid, DomainError> {
|
use adapter_common::{parse_uuid, parse_datetime};
|
||||||
Uuid::parse_str(s)
|
|
||||||
.map_err(|e| DomainError::InfrastructureError(format!("Invalid UUID '{}': {}", s, e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_datetime(s: &str) -> Result<chrono::NaiveDateTime, DomainError> {
|
|
||||||
chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
|
|
||||||
.map_err(|e| DomainError::InfrastructureError(format!("Invalid datetime '{}': {}", s, e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
struct MovieRow {
|
struct MovieRow {
|
||||||
@@ -215,7 +202,7 @@ impl LocalApContentQuery for SqliteApContentQuery {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
rows.into_iter().map(WatchlistRow::into_domain).collect()
|
rows.into_iter().map(WatchlistRow::into_domain).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +222,7 @@ impl LocalApContentQuery for SqliteApContentQuery {
|
|||||||
.bind(&mid)
|
.bind(&mid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
rows.into_iter().map(DiaryRow::into_domain).collect()
|
rows.into_iter().map(DiaryRow::into_domain).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +251,7 @@ impl LocalApContentQuery for SqliteApContentQuery {
|
|||||||
.bind(limit_i64)
|
.bind(limit_i64)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
} else {
|
} else {
|
||||||
sqlx::query_as::<_, DiaryRow>(
|
sqlx::query_as::<_, DiaryRow>(
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
@@ -279,7 +266,7 @@ impl LocalApContentQuery for SqliteApContentQuery {
|
|||||||
.bind(limit_i64)
|
.bind(limit_i64)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
};
|
};
|
||||||
rows.into_iter().map(DiaryRow::into_domain).collect()
|
rows.into_iter().map(DiaryRow::into_domain).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ use chrono::Utc;
|
|||||||
use k_ap::{BlockedDomain, BlocklistRepository};
|
use k_ap::{BlockedDomain, BlocklistRepository};
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
|
|
||||||
use super::{SqliteFederationRepository, datetime_to_str};
|
use adapter_common::datetime_to_str;
|
||||||
|
use super::SqliteFederationRepository;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl BlocklistRepository for SqliteFederationRepository {
|
impl BlocklistRepository for SqliteFederationRepository {
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ use k_ap::{
|
|||||||
};
|
};
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
|
|
||||||
|
use adapter_common::datetime_to_str;
|
||||||
use super::{
|
use super::{
|
||||||
SqliteFederationRepository, datetime_to_str, remote_actor_from_row, status_to_str,
|
SqliteFederationRepository, remote_actor_from_row, status_to_str,
|
||||||
str_to_status,
|
str_to_status,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,9 @@ pub mod remote_goals;
|
|||||||
pub use ap_content::SqliteApContentQuery;
|
pub use ap_content::SqliteApContentQuery;
|
||||||
pub use remote_goals::SqliteRemoteGoalRepository;
|
pub use remote_goals::SqliteRemoteGoalRepository;
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use k_ap::{FollowerStatus, RemoteActor};
|
use k_ap::{FollowerStatus, RemoteActor};
|
||||||
use sqlx::SqlitePool;
|
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 {
|
pub(crate) fn status_to_str(status: &FollowerStatus) -> &'static str {
|
||||||
match status {
|
match status {
|
||||||
FollowerStatus::Pending => "pending",
|
FollowerStatus::Pending => "pending",
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ impl SqliteRemoteGoalRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -36,7 +32,7 @@ impl RemoteGoalRepository for SqliteRemoteGoalRepository {
|
|||||||
.bind(&received)
|
.bind(&received)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -53,7 +49,7 @@ impl RemoteGoalRepository for SqliteRemoteGoalRepository {
|
|||||||
.bind(ap_id)
|
.bind(ap_id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -64,7 +60,7 @@ impl RemoteGoalRepository for SqliteRemoteGoalRepository {
|
|||||||
.bind(actor_url)
|
.bind(actor_url)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -74,7 +70,7 @@ impl RemoteGoalRepository for SqliteRemoteGoalRepository {
|
|||||||
.bind(actor_url)
|
.bind(actor_url)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -87,7 +83,7 @@ impl RemoteGoalRepository for SqliteRemoteGoalRepository {
|
|||||||
.bind(actor_url)
|
.bind(actor_url)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter()
|
rows.iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use anyhow::{Result, anyhow};
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::models::{Review, ReviewSource};
|
use domain::models::{Review, ReviewSource};
|
||||||
|
|
||||||
use super::{SqliteFederationRepository, datetime_to_str};
|
use adapter_common::datetime_to_str;
|
||||||
|
use super::SqliteFederationRepository;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl RemoteReviewRepository for SqliteFederationRepository {
|
impl RemoteReviewRepository for SqliteFederationRepository {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
|
adapter-common = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ pub fn create_search_adapter(pool: SqlitePool) -> (Arc<dyn SearchCommand>, Arc<d
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
DomainError::InfrastructureError(e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl SearchCommand for SqliteSearchAdapter {
|
impl SearchCommand for SqliteSearchAdapter {
|
||||||
async fn index(&self, doc: IndexableDocument) -> Result<(), DomainError> {
|
async fn index(&self, doc: IndexableDocument) -> Result<(), DomainError> {
|
||||||
@@ -86,7 +82,7 @@ impl SearchCommand for SqliteSearchAdapter {
|
|||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO movies_fts(movie_id, title, director, overview, genres, keywords, cast_names, crew_names, release_year, language)
|
"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)
|
.bind(&language)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -118,7 +114,7 @@ impl SearchCommand for SqliteSearchAdapter {
|
|||||||
.bind(&person_id)
|
.bind(&person_id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO people_fts(person_id, name, known_for_department) VALUES (?, ?, ?)",
|
"INSERT INTO people_fts(person_id, name, known_for_department) VALUES (?, ?, ?)",
|
||||||
@@ -128,7 +124,7 @@ impl SearchCommand for SqliteSearchAdapter {
|
|||||||
.bind(person.known_for_department())
|
.bind(person.known_for_department())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -144,7 +140,7 @@ impl SearchCommand for SqliteSearchAdapter {
|
|||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
}
|
}
|
||||||
EntityType::Person => {
|
EntityType::Person => {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -153,7 +149,7 @@ impl SearchCommand for SqliteSearchAdapter {
|
|||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -205,7 +201,7 @@ impl SqliteSearchAdapter {
|
|||||||
.bind(query.filters.year.map(|y| y as i64))
|
.bind(query.filters.year.map(|y| y as i64))
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
count as u64
|
count as u64
|
||||||
} else {
|
} else {
|
||||||
let count: i64 = sqlx::query_scalar(
|
let count: i64 = sqlx::query_scalar(
|
||||||
@@ -221,7 +217,7 @@ impl SqliteSearchAdapter {
|
|||||||
.bind(query.filters.year.map(|y| y as i64))
|
.bind(query.filters.year.map(|y| y as i64))
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
count as u64
|
count as u64
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -249,7 +245,7 @@ impl SqliteSearchAdapter {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
} else {
|
} else {
|
||||||
sqlx::query_as::<_, Row>(
|
sqlx::query_as::<_, Row>(
|
||||||
"SELECT m.id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
@@ -270,7 +266,7 @@ impl SqliteSearchAdapter {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
};
|
};
|
||||||
let items = rows
|
let items = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -321,7 +317,7 @@ impl SqliteSearchAdapter {
|
|||||||
.bind(&fts_query)
|
.bind(&fts_query)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
count as u64
|
count as u64
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -346,7 +342,7 @@ impl SqliteSearchAdapter {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let mut items = Vec::with_capacity(rows.len());
|
let mut items = Vec::with_capacity(rows.len());
|
||||||
for row in rows {
|
for row in rows {
|
||||||
@@ -355,7 +351,7 @@ impl SqliteSearchAdapter {
|
|||||||
.bind(&row.person_id)
|
.bind(&row.person_id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
let known_for_titles = if let Some(tid) = tmdb_id {
|
let known_for_titles = if let Some(tid) = tmdb_id {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ sqlx = { version = "0.8.6", features = [
|
|||||||
"macros",
|
"macros",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
adapter-common = { workspace = true }
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
sqlite-federation = { workspace = true }
|
sqlite-federation = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|||||||
@@ -22,23 +22,18 @@ impl SqliteDiaryRepository {
|
|||||||
Self { pool }
|
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<i64, DomainError> {
|
async fn count_diary_entries(&self, movie_id: Option<&str>) -> Result<i64, DomainError> {
|
||||||
match movie_id {
|
match movie_id {
|
||||||
None => sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews")
|
None => sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews")
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err),
|
.map_err(adapter_common::map_sqlx_error),
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews WHERE movie_id = ?")
|
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews WHERE movie_id = ?")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +64,7 @@ impl SqliteDiaryRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_movie_diary_rows(
|
async fn fetch_movie_diary_rows(
|
||||||
@@ -101,7 +96,7 @@ impl SqliteDiaryRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn count_user_diary_entries(
|
async fn count_user_diary_entries(
|
||||||
@@ -130,7 +125,7 @@ impl SqliteDiaryRepository {
|
|||||||
if has_search {
|
if has_search {
|
||||||
q = q.bind(search.unwrap());
|
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(
|
async fn fetch_user_diary_rows(
|
||||||
@@ -176,7 +171,7 @@ impl SqliteDiaryRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.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 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_q = bind_filter_params!(sqlx::query_as::<_, FeedRow>(&select_sql));
|
||||||
let rows = rows_q
|
let rows = rows_q
|
||||||
@@ -348,7 +343,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let items = rows
|
let items = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -373,7 +368,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.ok_or_else(|| DomainError::NotFound(format!("Movie {}", id_str)))?
|
.ok_or_else(|| DomainError::NotFound(format!("Movie {}", id_str)))?
|
||||||
.into_domain()?;
|
.into_domain()?;
|
||||||
|
|
||||||
@@ -384,7 +379,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ReviewRow::into_domain)
|
.map(ReviewRow::into_domain)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
@@ -405,7 +400,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.into_iter().map(DiaryRow::into_domain).collect()
|
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 {
|
while let Some(row) = futures::StreamExt::next(&mut rows).await {
|
||||||
yield match row {
|
yield match row {
|
||||||
Ok(r) => r.into_domain(),
|
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)
|
.bind(id_str)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
.map(MovieStatsRow::into_domain)
|
.map(MovieStatsRow::into_domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +466,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let rows = sqlx::query_as::<_, FeedRow>(
|
let rows = sqlx::query_as::<_, FeedRow>(
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
@@ -492,7 +487,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let items = rows
|
let items = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -512,7 +507,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
|||||||
sqlx::query_scalar("SELECT COUNT(*) FROM reviews WHERE remote_actor_url IS NULL")
|
sqlx::query_scalar("SELECT COUNT(*) FROM reviews WHERE remote_actor_url IS NULL")
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(count as u64)
|
Ok(count as u64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ impl SqliteGoalRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -44,7 +40,7 @@ impl GoalCommand for SqliteGoalRepository {
|
|||||||
.bind(&created_at)
|
.bind(&created_at)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -58,7 +54,7 @@ impl GoalCommand for SqliteGoalRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
return Err(DomainError::NotFound("Goal not found".into()));
|
return Err(DomainError::NotFound("Goal not found".into()));
|
||||||
@@ -75,7 +71,7 @@ impl GoalCommand for SqliteGoalRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
return Err(DomainError::NotFound("Goal not found".into()));
|
return Err(DomainError::NotFound("Goal not found".into()));
|
||||||
@@ -102,7 +98,7 @@ impl GoalQuery for SqliteGoalRepository {
|
|||||||
.bind(y)
|
.bind(y)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.map(|r| row_to_goal(&r)).transpose()
|
row.map(|r| row_to_goal(&r)).transpose()
|
||||||
}
|
}
|
||||||
@@ -117,7 +113,7 @@ impl GoalQuery for SqliteGoalRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter().map(row_to_goal).collect()
|
rows.iter().map(row_to_goal).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,11 +97,6 @@ impl SqliteImportProfileRepository {
|
|||||||
Self { pool }
|
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, DomainError> {
|
fn parse_dt(s: &str) -> Result<NaiveDateTime, DomainError> {
|
||||||
NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
|
NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
|
||||||
.or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%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)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_for_user(&self, user_id: &UserId) -> Result<Vec<ImportProfile>, DomainError> {
|
async fn list_for_user(&self, user_id: &UserId) -> Result<Vec<ImportProfile>, DomainError> {
|
||||||
@@ -141,7 +136,7 @@ impl ImportProfileRepository for SqliteImportProfileRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter()
|
rows.iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
@@ -183,7 +178,7 @@ impl ImportProfileRepository for SqliteImportProfileRepository {
|
|||||||
.bind(&uid_str)
|
.bind(&uid_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.map(|r| {
|
row.map(|r| {
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
@@ -215,6 +210,6 @@ impl ImportProfileRepository for SqliteImportProfileRepository {
|
|||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,11 +202,6 @@ impl SqliteImportSessionRepository {
|
|||||||
Self { pool }
|
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, DomainError> {
|
fn parse_dt(s: &str) -> Result<NaiveDateTime, DomainError> {
|
||||||
NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
|
NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
|
||||||
.or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%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)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(
|
async fn get(
|
||||||
@@ -334,7 +329,7 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
|||||||
.bind(&uid_str)
|
.bind(&uid_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.map(|r| {
|
row.map(|r| {
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
@@ -361,7 +356,7 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
|||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(&self, id: &ImportSessionId) -> Result<(), DomainError> {
|
async fn delete(&self, id: &ImportSessionId) -> Result<(), DomainError> {
|
||||||
@@ -371,14 +366,14 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
|||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_expired(&self) -> Result<u64, DomainError> {
|
async fn delete_expired(&self) -> Result<u64, DomainError> {
|
||||||
let result = sqlx::query("DELETE FROM import_sessions WHERE expires_at < datetime('now')")
|
let result = sqlx::query("DELETE FROM import_sessions WHERE expires_at < datetime('now')")
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(result.rows_affected())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +386,6 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
|||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,30 +45,6 @@ pub fn create_profile_fields_repo(
|
|||||||
std::sync::Arc::new(SqliteProfileFieldsRepository::new(pool))
|
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> {
|
pub async fn migrate(pool: &SqlitePool) -> Result<(), domain::errors::DomainError> {
|
||||||
migrations::run(pool).await
|
migrations::run(pool).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
use chrono::NaiveDateTime;
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{
|
models::{DiaryEntry, FeedEntry, Movie, MovieSummary, Review, WatchlistWithMovie},
|
||||||
DiaryEntry, FeedEntry, Movie, MovieSummary, PersistedReview, Review, ReviewSource,
|
};
|
||||||
UserSummary, WatchlistEntry, WatchlistWithMovie,
|
use adapter_common::{
|
||||||
},
|
movie_row_to_domain, movie_stats_to_domain, movie_summary_to_domain, review_row_to_domain,
|
||||||
value_objects::{
|
user_summary_to_domain, watchlist_entry_to_domain, watchlist_with_movie_to_domain,
|
||||||
Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear,
|
|
||||||
ReviewId, UserId, Username, WatchlistEntryId,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
pub(crate) struct MovieRow {
|
pub(crate) struct MovieRow {
|
||||||
@@ -24,22 +19,14 @@ pub(crate) struct MovieRow {
|
|||||||
|
|
||||||
impl MovieRow {
|
impl MovieRow {
|
||||||
pub fn into_domain(self) -> Result<Movie, DomainError> {
|
pub fn into_domain(self) -> Result<Movie, DomainError> {
|
||||||
let id = MovieId::from_uuid(parse_uuid(&self.id)?);
|
movie_row_to_domain(
|
||||||
let external_metadata_id = self
|
self.id,
|
||||||
.external_metadata_id
|
self.external_metadata_id,
|
||||||
.map(ExternalMetadataId::new)
|
self.title,
|
||||||
.transpose()?;
|
self.release_year,
|
||||||
let title = MovieTitle::new(self.title)?;
|
|
||||||
let release_year = ReleaseYear::new(self.release_year as u16)?;
|
|
||||||
let poster_path = self.poster_path.map(PosterPath::new).transpose()?;
|
|
||||||
Ok(Movie::from_persistence(
|
|
||||||
id,
|
|
||||||
external_metadata_id,
|
|
||||||
title,
|
|
||||||
release_year,
|
|
||||||
self.director,
|
self.director,
|
||||||
poster_path,
|
self.poster_path,
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,27 +47,26 @@ pub(crate) struct MovieSummaryRow {
|
|||||||
|
|
||||||
impl MovieSummaryRow {
|
impl MovieSummaryRow {
|
||||||
pub fn into_domain(self) -> Result<MovieSummary, DomainError> {
|
pub fn into_domain(self) -> Result<MovieSummary, DomainError> {
|
||||||
let movie = MovieRow {
|
let movie = movie_row_to_domain(
|
||||||
id: self.id,
|
self.id,
|
||||||
external_metadata_id: self.external_metadata_id,
|
self.external_metadata_id,
|
||||||
title: self.title,
|
self.title,
|
||||||
release_year: self.release_year,
|
self.release_year,
|
||||||
director: self.director,
|
self.director,
|
||||||
poster_path: self.poster_path,
|
self.poster_path,
|
||||||
}
|
)?;
|
||||||
.into_domain()?;
|
|
||||||
let genres = self
|
let genres = self
|
||||||
.genres
|
.genres
|
||||||
.map(|g| g.split(',').map(str::to_string).collect())
|
.map(|g| g.split(',').map(str::to_string).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
Ok(MovieSummary {
|
Ok(movie_summary_to_domain(
|
||||||
movie,
|
movie,
|
||||||
genres,
|
genres,
|
||||||
runtime_minutes: self.runtime_minutes.map(|v| v as u32),
|
self.runtime_minutes,
|
||||||
original_language: self.original_language,
|
self.original_language,
|
||||||
overview: self.overview,
|
self.overview,
|
||||||
collection_name: self.collection_name,
|
self.collection_name,
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,29 +85,17 @@ pub(crate) struct ReviewRow {
|
|||||||
|
|
||||||
impl ReviewRow {
|
impl ReviewRow {
|
||||||
pub fn into_domain(self) -> Result<Review, DomainError> {
|
pub fn into_domain(self) -> Result<Review, DomainError> {
|
||||||
let id = ReviewId::from_uuid(parse_uuid(&self.id)?);
|
review_row_to_domain(
|
||||||
let movie_id = MovieId::from_uuid(parse_uuid(&self.movie_id)?);
|
self.id,
|
||||||
let user_id = UserId::from_uuid(parse_uuid(&self.user_id)?);
|
self.movie_id,
|
||||||
let rating = Rating::new(self.rating as u8)?;
|
self.user_id,
|
||||||
let comment = self.comment.map(Comment::new).transpose()?;
|
self.rating,
|
||||||
let watched_at = parse_datetime(&self.watched_at)?;
|
self.comment,
|
||||||
let created_at = parse_datetime(&self.created_at)?;
|
self.watched_at,
|
||||||
let source = match self.remote_actor_url {
|
self.created_at,
|
||||||
None => ReviewSource::Local,
|
self.remote_actor_url,
|
||||||
Some(url) => ReviewSource::Remote { actor_url: url },
|
self.watch_medium,
|
||||||
};
|
)
|
||||||
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,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,29 +121,25 @@ pub(crate) struct DiaryRow {
|
|||||||
|
|
||||||
impl DiaryRow {
|
impl DiaryRow {
|
||||||
pub fn into_domain(self) -> Result<DiaryEntry, DomainError> {
|
pub fn into_domain(self) -> Result<DiaryEntry, DomainError> {
|
||||||
let movie = MovieRow {
|
let movie = movie_row_to_domain(
|
||||||
id: self.id,
|
self.id,
|
||||||
external_metadata_id: self.external_metadata_id,
|
self.external_metadata_id,
|
||||||
title: self.title,
|
self.title,
|
||||||
release_year: self.release_year,
|
self.release_year,
|
||||||
director: self.director,
|
self.director,
|
||||||
poster_path: self.poster_path,
|
self.poster_path,
|
||||||
}
|
)?;
|
||||||
.into_domain()?;
|
let review = review_row_to_domain(
|
||||||
|
self.review_id,
|
||||||
let review = ReviewRow {
|
self.movie_id,
|
||||||
id: self.review_id,
|
self.user_id,
|
||||||
movie_id: self.movie_id,
|
self.rating,
|
||||||
user_id: self.user_id,
|
self.comment,
|
||||||
rating: self.rating,
|
self.watched_at,
|
||||||
comment: self.comment,
|
self.created_at,
|
||||||
watched_at: self.watched_at,
|
self.remote_actor_url,
|
||||||
created_at: self.created_at,
|
self.watch_medium,
|
||||||
remote_actor_url: self.remote_actor_url,
|
)?;
|
||||||
watch_medium: self.watch_medium,
|
|
||||||
}
|
|
||||||
.into_domain()?;
|
|
||||||
|
|
||||||
Ok(DiaryEntry::new(movie, review))
|
Ok(DiaryEntry::new(movie, review))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,18 +158,18 @@ pub(crate) struct MovieStatsRow {
|
|||||||
|
|
||||||
impl MovieStatsRow {
|
impl MovieStatsRow {
|
||||||
pub fn into_domain(self) -> domain::models::MovieStats {
|
pub fn into_domain(self) -> domain::models::MovieStats {
|
||||||
domain::models::MovieStats {
|
movie_stats_to_domain(
|
||||||
total_count: self.total_count as u64,
|
self.total_count,
|
||||||
avg_rating: self.avg_rating,
|
self.avg_rating,
|
||||||
federated_count: self.federated_count as u64,
|
self.federated_count,
|
||||||
rating_histogram: [
|
[
|
||||||
self.rating_1 as u64,
|
self.rating_1,
|
||||||
self.rating_2 as u64,
|
self.rating_2,
|
||||||
self.rating_3 as u64,
|
self.rating_3,
|
||||||
self.rating_4 as u64,
|
self.rating_4,
|
||||||
self.rating_5 as u64,
|
self.rating_5,
|
||||||
],
|
],
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,24 +196,26 @@ pub(crate) struct FeedRow {
|
|||||||
|
|
||||||
impl FeedRow {
|
impl FeedRow {
|
||||||
pub fn into_domain(self) -> Result<FeedEntry, DomainError> {
|
pub fn into_domain(self) -> Result<FeedEntry, DomainError> {
|
||||||
let diary = DiaryRow {
|
let movie = movie_row_to_domain(
|
||||||
id: self.id,
|
self.id,
|
||||||
external_metadata_id: self.external_metadata_id,
|
self.external_metadata_id,
|
||||||
title: self.title,
|
self.title,
|
||||||
release_year: self.release_year,
|
self.release_year,
|
||||||
director: self.director,
|
self.director,
|
||||||
poster_path: self.poster_path,
|
self.poster_path,
|
||||||
review_id: self.review_id,
|
)?;
|
||||||
movie_id: self.movie_id,
|
let review = review_row_to_domain(
|
||||||
user_id: self.user_id,
|
self.review_id,
|
||||||
rating: self.rating,
|
self.movie_id,
|
||||||
comment: self.comment,
|
self.user_id,
|
||||||
watched_at: self.watched_at,
|
self.rating,
|
||||||
created_at: self.created_at,
|
self.comment,
|
||||||
remote_actor_url: self.remote_actor_url,
|
self.watched_at,
|
||||||
watch_medium: self.watch_medium,
|
self.created_at,
|
||||||
}
|
self.remote_actor_url,
|
||||||
.into_domain()?;
|
self.watch_medium,
|
||||||
|
)?;
|
||||||
|
let diary = DiaryEntry::new(movie, review);
|
||||||
Ok(FeedEntry::new(diary, self.user_email))
|
Ok(FeedEntry::new(diary, self.user_email))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,16 +232,16 @@ pub(crate) struct UserSummaryRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UserSummaryRow {
|
impl UserSummaryRow {
|
||||||
pub fn into_domain(self) -> Result<UserSummary, DomainError> {
|
pub fn into_domain(self) -> Result<domain::models::UserSummary, DomainError> {
|
||||||
Ok(UserSummary::new(
|
user_summary_to_domain(
|
||||||
UserId::from_uuid(parse_uuid(&self.id)?),
|
self.id,
|
||||||
Email::new(self.email)?,
|
self.email,
|
||||||
Username::new(self.username)?,
|
self.username,
|
||||||
self.display_name,
|
self.display_name,
|
||||||
self.total_movies,
|
self.total_movies,
|
||||||
self.avg_rating,
|
self.avg_rating,
|
||||||
self.avatar_path,
|
self.avatar_path,
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,35 +280,20 @@ pub(crate) struct WatchlistRow {
|
|||||||
|
|
||||||
impl WatchlistRow {
|
impl WatchlistRow {
|
||||||
pub fn into_domain(self) -> Result<WatchlistWithMovie, DomainError> {
|
pub fn into_domain(self) -> Result<WatchlistWithMovie, DomainError> {
|
||||||
let entry = WatchlistEntry {
|
let entry = watchlist_entry_to_domain(
|
||||||
id: WatchlistEntryId::from_uuid(parse_uuid(&self.id)?),
|
self.id,
|
||||||
user_id: UserId::from_uuid(parse_uuid(&self.user_id)?),
|
self.user_id,
|
||||||
movie_id: MovieId::from_uuid(parse_uuid(&self.movie_id)?),
|
self.movie_id,
|
||||||
added_at: parse_datetime(&self.added_at)?,
|
self.added_at,
|
||||||
};
|
)?;
|
||||||
let movie = MovieRow {
|
let movie = movie_row_to_domain(
|
||||||
id: self.m_id,
|
self.m_id,
|
||||||
external_metadata_id: self.external_metadata_id,
|
self.external_metadata_id,
|
||||||
title: self.title,
|
self.title,
|
||||||
release_year: self.release_year,
|
self.release_year,
|
||||||
director: self.director,
|
self.director,
|
||||||
poster_path: self.poster_path,
|
self.poster_path,
|
||||||
}
|
)?;
|
||||||
.into_domain()?;
|
Ok(watchlist_with_movie_to_domain(entry, movie))
|
||||||
Ok(WatchlistWithMovie { entry, movie })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_uuid(s: &str) -> Result<Uuid, DomainError> {
|
|
||||||
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, DomainError> {
|
|
||||||
NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
|
|
||||||
.map_err(|e| DomainError::InfrastructureError(format!("Invalid datetime '{}': {}", s, e)))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ impl SqliteMovieRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -55,7 +51,7 @@ impl MovieCommand for SqliteMovieRepository {
|
|||||||
.bind(&poster_path)
|
.bind(&poster_path)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -66,7 +62,7 @@ impl MovieCommand for SqliteMovieRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +81,7 @@ impl MovieQuery for SqliteMovieRepository {
|
|||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.map(MovieRow::into_domain)
|
.map(MovieRow::into_domain)
|
||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
@@ -99,7 +95,7 @@ impl MovieQuery for SqliteMovieRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.map(MovieRow::into_domain)
|
.map(MovieRow::into_domain)
|
||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
@@ -119,7 +115,7 @@ impl MovieQuery for SqliteMovieRepository {
|
|||||||
.bind(y)
|
.bind(y)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(MovieRow::into_domain)
|
.map(MovieRow::into_domain)
|
||||||
.collect()
|
.collect()
|
||||||
@@ -141,7 +137,7 @@ impl MovieQuery for SqliteMovieRepository {
|
|||||||
for id in ids {
|
for id in ids {
|
||||||
q = q.bind(id.value().to_string());
|
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())
|
Ok(rows.into_iter().collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +161,7 @@ impl MovieQuery for SqliteMovieRepository {
|
|||||||
for (t, y) in pairs {
|
for (t, y) in pairs {
|
||||||
q = q.bind(t.value().to_string()).bind(y.value() as i64);
|
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
|
Ok(rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
@@ -217,7 +213,7 @@ impl MovieQuery for SqliteMovieRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let total: i64 = sqlx::query(
|
let total: i64 = sqlx::query(
|
||||||
"SELECT COUNT(DISTINCT m.id) \
|
"SELECT COUNT(DISTINCT m.id) \
|
||||||
@@ -235,7 +231,7 @@ impl MovieQuery for SqliteMovieRepository {
|
|||||||
.bind(genre)
|
.bind(genre)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.try_get(0)
|
.try_get(0)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
@@ -259,7 +255,7 @@ impl MovieQuery for SqliteMovieRepository {
|
|||||||
)
|
)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(MovieRow::into_domain)
|
.map(MovieRow::into_domain)
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ impl SqliteMovieDeduplicator {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -36,7 +32,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
|||||||
let director = canonical.director().map(str::to_string);
|
let director = canonical.director().map(str::to_string);
|
||||||
let poster = canonical.poster_path().map(|p| p.value().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
|
// 1. Upsert canonical movie record
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -47,7 +43,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
|||||||
poster_path = COALESCE(excluded.poster_path, movies.poster_path)",
|
poster_path = COALESCE(excluded.poster_path, movies.poster_path)",
|
||||||
)
|
)
|
||||||
.bind(&new).bind(&ext_id).bind(&title).bind(year).bind(&director).bind(&poster)
|
.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
|
// 2. Re-point simple FK tables
|
||||||
let reviews = sqlx::query("UPDATE reviews SET movie_id = ? WHERE movie_id = ?")
|
let reviews = sqlx::query("UPDATE reviews SET movie_id = ? WHERE movie_id = ?")
|
||||||
@@ -55,7 +51,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
|||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.rows_affected();
|
.rows_affected();
|
||||||
|
|
||||||
let watchlist = sqlx::query("UPDATE watchlist_entries SET movie_id = ? WHERE movie_id = ?")
|
let watchlist = sqlx::query("UPDATE watchlist_entries SET movie_id = ? WHERE movie_id = ?")
|
||||||
@@ -63,7 +59,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
|||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.rows_affected();
|
.rows_affected();
|
||||||
|
|
||||||
let watch_events = sqlx::query("UPDATE watch_events SET movie_id = ? WHERE movie_id = ?")
|
let watch_events = sqlx::query("UPDATE watch_events SET movie_id = ? WHERE movie_id = ?")
|
||||||
@@ -71,7 +67,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
|||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.rows_affected();
|
.rows_affected();
|
||||||
|
|
||||||
// 3. Re-point movie_profiles (PK — move only if canonical has none)
|
// 3. Re-point movie_profiles (PK — move only if canonical has none)
|
||||||
@@ -80,7 +76,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
|||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.rows_affected();
|
.rows_affected();
|
||||||
|
|
||||||
// 4. Re-point enrichment tables with composite PKs (INSERT OR IGNORE + DELETE)
|
// 4. Re-point enrichment tables with composite PKs (INSERT OR IGNORE + DELETE)
|
||||||
@@ -93,12 +89,12 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
|||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
sqlx::query("DELETE FROM movie_genres WHERE movie_id = ?")
|
sqlx::query("DELETE FROM movie_genres WHERE movie_id = ?")
|
||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT OR IGNORE INTO movie_keywords (movie_id, tmdb_id, name)
|
"INSERT OR IGNORE INTO movie_keywords (movie_id, tmdb_id, name)
|
||||||
@@ -108,41 +104,41 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
|||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
sqlx::query("DELETE FROM movie_keywords WHERE movie_id = ?")
|
sqlx::query("DELETE FROM movie_keywords WHERE movie_id = ?")
|
||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT OR IGNORE INTO movie_cast (movie_id, tmdb_person_id, name, character, billing_order, profile_path)
|
"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 = ?",
|
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 = ?")
|
sqlx::query("DELETE FROM movie_cast WHERE movie_id = ?")
|
||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT OR IGNORE INTO movie_crew (movie_id, tmdb_person_id, name, job, department, profile_path)
|
"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 = ?",
|
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 = ?")
|
sqlx::query("DELETE FROM movie_crew WHERE movie_id = ?")
|
||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.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)
|
// 5. Delete the now-empty old movie record (remaining cascades are safe: all FKs cleared above)
|
||||||
sqlx::query("DELETE FROM movies WHERE id = ?")
|
sqlx::query("DELETE FROM movies WHERE id = ?")
|
||||||
.bind(&old)
|
.bind(&old)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.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)
|
Ok(reviews + watchlist + watch_events + profiles)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,6 @@ pub fn create_person_adapter(pool: SqlitePool) -> (Arc<dyn PersonCommand>, Arc<d
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
DomainError::InfrastructureError(e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl PersonCommand for SqlitePersonAdapter {
|
impl PersonCommand for SqlitePersonAdapter {
|
||||||
async fn upsert_batch(&self, persons: &[Person]) -> Result<(), DomainError> {
|
async fn upsert_batch(&self, persons: &[Person]) -> Result<(), DomainError> {
|
||||||
@@ -56,7 +52,7 @@ impl PersonCommand for SqlitePersonAdapter {
|
|||||||
.bind(person.profile_path())
|
.bind(person.profile_path())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -89,7 +85,7 @@ impl PersonCommand for SqlitePersonAdapter {
|
|||||||
.bind(batch_size)
|
.bind(batch_size)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let has_more = rows.len() as u32 >= batch_size;
|
let has_more = rows.len() as u32 >= batch_size;
|
||||||
let mut count = 0u64;
|
let mut count = 0u64;
|
||||||
@@ -109,7 +105,7 @@ impl PersonCommand for SqlitePersonAdapter {
|
|||||||
.bind(&row.profile_path)
|
.bind(&row.profile_path)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
Ok((count, has_more))
|
Ok((count, has_more))
|
||||||
@@ -137,7 +133,7 @@ impl PersonCommand for SqlitePersonAdapter {
|
|||||||
.bind(id.value().to_string())
|
.bind(id.value().to_string())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +147,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
|||||||
.bind(id.value().to_string())
|
.bind(id.value().to_string())
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(row.map(PersonRow::into_person))
|
Ok(row.map(PersonRow::into_person))
|
||||||
}
|
}
|
||||||
@@ -166,7 +162,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
|||||||
.bind(id.value())
|
.bind(id.value())
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(row.map(PersonRow::into_person))
|
Ok(row.map(PersonRow::into_person))
|
||||||
}
|
}
|
||||||
@@ -182,7 +178,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
|||||||
.bind(id.value().to_string())
|
.bind(id.value().to_string())
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
let Some(tmdb_id) = tmdb_id else {
|
let Some(tmdb_id) = tmdb_id else {
|
||||||
@@ -203,7 +199,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
|||||||
.bind(tmdb_id)
|
.bind(tmdb_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| CastCredit {
|
.map(|r| CastCredit {
|
||||||
movie_id: MovieId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()),
|
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)
|
.bind(tmdb_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| CrewCredit {
|
.map(|r| CrewCredit {
|
||||||
movie_id: MovieId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()),
|
movie_id: MovieId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()),
|
||||||
@@ -247,7 +243,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(rows.into_iter().map(PersonRow::into_person).collect())
|
Ok(rows.into_iter().map(PersonRow::into_person).collect())
|
||||||
}
|
}
|
||||||
@@ -265,7 +261,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
|||||||
)
|
)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ impl SqliteMovieProfileRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -29,7 +25,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
let movie_id = p.movie_id.value().to_string();
|
let movie_id = p.movie_id.value().to_string();
|
||||||
let enriched_at = p.enriched_at.to_rfc3339();
|
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(
|
sqlx::query(
|
||||||
r#"INSERT INTO movie_profiles
|
r#"INSERT INTO movie_profiles
|
||||||
@@ -62,13 +58,13 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
.bind(&enriched_at)
|
.bind(&enriched_at)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
sqlx::query("DELETE FROM movie_genres WHERE movie_id = ?")
|
sqlx::query("DELETE FROM movie_genres WHERE movie_id = ?")
|
||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
for g in &p.genres {
|
for g in &p.genres {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT OR IGNORE INTO movie_genres (movie_id, tmdb_id, name) VALUES (?,?,?)",
|
"INSERT OR IGNORE INTO movie_genres (movie_id, tmdb_id, name) VALUES (?,?,?)",
|
||||||
@@ -78,14 +74,14 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
.bind(&g.name)
|
.bind(&g.name)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlx::query("DELETE FROM movie_keywords WHERE movie_id = ?")
|
sqlx::query("DELETE FROM movie_keywords WHERE movie_id = ?")
|
||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
for k in &p.keywords {
|
for k in &p.keywords {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT OR IGNORE INTO movie_keywords (movie_id, tmdb_id, name) VALUES (?,?,?)",
|
"INSERT OR IGNORE INTO movie_keywords (movie_id, tmdb_id, name) VALUES (?,?,?)",
|
||||||
@@ -95,14 +91,14 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
.bind(&k.name)
|
.bind(&k.name)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlx::query("DELETE FROM movie_cast WHERE movie_id = ?")
|
sqlx::query("DELETE FROM movie_cast WHERE movie_id = ?")
|
||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
for c in &p.cast {
|
for c in &p.cast {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT OR IGNORE INTO movie_cast \
|
"INSERT OR IGNORE INTO movie_cast \
|
||||||
@@ -117,14 +113,14 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
.bind(&c.profile_path)
|
.bind(&c.profile_path)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlx::query("DELETE FROM movie_crew WHERE movie_id = ?")
|
sqlx::query("DELETE FROM movie_crew WHERE movie_id = ?")
|
||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
for cr in &p.crew {
|
for cr in &p.crew {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT OR IGNORE INTO movie_crew \
|
"INSERT OR IGNORE INTO movie_crew \
|
||||||
@@ -139,10 +135,10 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
.bind(&cr.profile_path)
|
.bind(&cr.profile_path)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.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<Option<MovieProfile>, DomainError> {
|
async fn get_by_movie_id(&self, id: &MovieId) -> Result<Option<MovieProfile>, DomainError> {
|
||||||
@@ -157,7 +153,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let row = match row {
|
let row = match row {
|
||||||
Some(r) => r,
|
Some(r) => r,
|
||||||
@@ -175,7 +171,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| Genre {
|
.map(|r| Genre {
|
||||||
tmdb_id: r.try_get::<i64, _>("tmdb_id").unwrap_or(0) as u32,
|
tmdb_id: r.try_get::<i64, _>("tmdb_id").unwrap_or(0) as u32,
|
||||||
@@ -187,7 +183,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| Keyword {
|
.map(|r| Keyword {
|
||||||
tmdb_id: r.try_get::<i64, _>("tmdb_id").unwrap_or(0) as u32,
|
tmdb_id: r.try_get::<i64, _>("tmdb_id").unwrap_or(0) as u32,
|
||||||
@@ -202,7 +198,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| CastMember {
|
.map(|r| CastMember {
|
||||||
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
||||||
@@ -220,7 +216,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
.bind(&movie_id)
|
.bind(&movie_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| CrewMember {
|
.map(|r| CrewMember {
|
||||||
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
||||||
@@ -273,7 +269,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
|||||||
.bind(&threshold)
|
.bind(&threshold)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ impl SqliteProfileFieldsRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -31,7 +27,7 @@ impl UserProfileFieldsRepository for SqliteProfileFieldsRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.iter()
|
.iter()
|
||||||
@@ -53,7 +49,7 @@ impl UserProfileFieldsRepository for SqliteProfileFieldsRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
for (i, field) in fields.into_iter().enumerate() {
|
for (i, field) in fields.into_iter().enumerate() {
|
||||||
let id = uuid::Uuid::new_v4().to_string();
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
@@ -68,7 +64,7 @@ impl UserProfileFieldsRepository for SqliteProfileFieldsRepository {
|
|||||||
.bind(position)
|
.bind(position)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ impl SqliteRefreshSessionAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
DomainError::InfrastructureError(e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
||||||
async fn create(&self, session: &RefreshSession) -> Result<(), DomainError> {
|
async fn create(&self, session: &RefreshSession) -> Result<(), DomainError> {
|
||||||
@@ -34,7 +30,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
|||||||
.bind(session.created_at.to_rfc3339())
|
.bind(session.created_at.to_rfc3339())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +41,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
|||||||
.bind(token)
|
.bind(token)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.map(RefreshSessionRow::into_domain).transpose()
|
row.map(RefreshSessionRow::into_domain).transpose()
|
||||||
}
|
}
|
||||||
@@ -55,7 +51,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
|||||||
.bind(token)
|
.bind(token)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +60,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
|||||||
.bind(user_id.value().to_string())
|
.bind(user_id.value().to_string())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +70,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
|||||||
.bind(&now)
|
.bind(&now)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(result.rows_affected())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use domain::{
|
|||||||
};
|
};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::models::{ReviewRow, datetime_to_str};
|
use adapter_common::datetime_to_str;
|
||||||
|
use crate::models::ReviewRow;
|
||||||
|
|
||||||
pub struct SqliteReviewRepository {
|
pub struct SqliteReviewRepository {
|
||||||
pool: SqlitePool,
|
pool: SqlitePool,
|
||||||
@@ -18,10 +19,6 @@ impl SqliteReviewRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -54,7 +51,7 @@ impl ReviewRepository for SqliteReviewRepository {
|
|||||||
.bind(review.watch_medium().map(|wm| wm.to_string()))
|
.bind(review.watch_medium().map(|wm| wm.to_string()))
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -68,7 +65,7 @@ impl ReviewRepository for SqliteReviewRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.map(ReviewRow::into_domain)
|
.map(ReviewRow::into_domain)
|
||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
@@ -90,7 +87,7 @@ impl ReviewRepository for SqliteReviewRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -101,7 +98,7 @@ impl ReviewRepository for SqliteReviewRepository {
|
|||||||
.bind(&id)
|
.bind(&id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +111,7 @@ impl ReviewRepository for SqliteReviewRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ReviewRow::into_domain)
|
.map(ReviewRow::into_domain)
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -18,11 +18,6 @@ impl SqliteStatsRepository {
|
|||||||
Self { pool }
|
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<UserTotalsRow, DomainError> {
|
async fn fetch_user_totals(&self, user_id: &str) -> Result<UserTotalsRow, DomainError> {
|
||||||
sqlx::query_as::<_, UserTotalsRow>(
|
sqlx::query_as::<_, UserTotalsRow>(
|
||||||
"SELECT COUNT(DISTINCT movie_id) AS total,
|
"SELECT COUNT(DISTINCT movie_id) AS total,
|
||||||
@@ -32,7 +27,7 @@ impl SqliteStatsRepository {
|
|||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(adapter_common::map_sqlx_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_user_favorite_director(
|
async fn fetch_user_favorite_director(
|
||||||
@@ -51,7 +46,7 @@ impl SqliteStatsRepository {
|
|||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(row)
|
Ok(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +65,7 @@ impl SqliteStatsRepository {
|
|||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(row)
|
Ok(row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +81,7 @@ impl StatsRepository for SqliteStatsRepository {
|
|||||||
self.fetch_user_most_active_month(&uid)
|
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 {
|
Ok(UserStats {
|
||||||
total_movies: totals.total,
|
total_movies: totals.total,
|
||||||
@@ -128,14 +123,14 @@ impl StatsRepository for SqliteStatsRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.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 max_director_count = director_rows.iter().map(|d| d.count).max().unwrap_or(1);
|
||||||
|
|
||||||
let monthly_ratings = rating_rows
|
let monthly_ratings = rating_rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| MonthlyRating {
|
.map(|r| MonthlyRating {
|
||||||
month_label: crate::format_year_month(&r.month),
|
month_label: adapter_common::format_year_month(&r.month),
|
||||||
year_month: r.month,
|
year_month: r.month,
|
||||||
avg_rating: r.avg_rating,
|
avg_rating: r.avg_rating,
|
||||||
count: r.count,
|
count: r.count,
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ impl SqliteUserSettingsRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -33,7 +29,7 @@ impl UserSettingsRepository for SqliteUserSettingsRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
Some(r) => {
|
Some(r) => {
|
||||||
@@ -68,7 +64,7 @@ impl UserSettingsRepository for SqliteUserSettingsRepository {
|
|||||||
})
|
})
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +80,7 @@ impl UserFederationSettingsQuery for SqliteUserSettingsRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
Some(r) => {
|
Some(r) => {
|
||||||
|
|||||||
@@ -19,11 +19,6 @@ impl SqliteUserRepository {
|
|||||||
Self { pool }
|
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 {
|
fn parse_role(s: &str) -> UserRole {
|
||||||
match s {
|
match s {
|
||||||
"admin" => UserRole::Admin,
|
"admin" => UserRole::Admin,
|
||||||
@@ -73,7 +68,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
.bind(email_str)
|
.bind(email_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.as_ref()
|
row.as_ref()
|
||||||
.map(|r| Self::row_to_user(r, vec![]))
|
.map(|r| Self::row_to_user(r, vec![]))
|
||||||
@@ -86,7 +81,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
.bind(username_str)
|
.bind(username_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.as_ref()
|
row.as_ref()
|
||||||
.map(|r| Self::row_to_user(r, vec![]))
|
.map(|r| Self::row_to_user(r, vec![]))
|
||||||
@@ -126,7 +121,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
.bind(role)
|
.bind(role)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -137,7 +132,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let Some(r) = row else { return Ok(None) };
|
let Some(r) = row else { return Ok(None) };
|
||||||
|
|
||||||
@@ -194,7 +189,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
)
|
)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(UserSummaryRow::into_domain)
|
.map(UserSummaryRow::into_domain)
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -7,12 +7,7 @@ use domain::{
|
|||||||
};
|
};
|
||||||
use sqlx::{Row, SqlitePool};
|
use sqlx::{Row, SqlitePool};
|
||||||
|
|
||||||
use crate::models::datetime_to_str;
|
use adapter_common::datetime_to_str;
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_uuid(s: &str) -> Result<uuid::Uuid, DomainError> {
|
fn parse_uuid(s: &str) -> Result<uuid::Uuid, DomainError> {
|
||||||
s.parse()
|
s.parse()
|
||||||
@@ -65,7 +60,7 @@ impl WatchEventCommand for SqliteWatchEventRepository {
|
|||||||
.bind(&created_at)
|
.bind(&created_at)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -83,7 +78,7 @@ impl WatchEventCommand for SqliteWatchEventRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -105,7 +100,7 @@ impl WatchEventCommand for SqliteWatchEventRepository {
|
|||||||
for id in ids {
|
for id in ids {
|
||||||
q = q.bind(id.value().to_string());
|
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())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +114,7 @@ impl WatchEventCommand for SqliteWatchEventRepository {
|
|||||||
.bind(&before_str)
|
.bind(&before_str)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(result.rows_affected())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,7 +134,7 @@ impl WatchEventQuery for SqliteWatchEventRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter().map(row_to_watch_event).collect()
|
rows.iter().map(row_to_watch_event).collect()
|
||||||
}
|
}
|
||||||
@@ -155,7 +150,7 @@ impl WatchEventQuery for SqliteWatchEventRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.as_ref().map(row_to_watch_event).transpose()
|
row.as_ref().map(row_to_watch_event).transpose()
|
||||||
}
|
}
|
||||||
@@ -175,7 +170,7 @@ impl WatchEventQuery for SqliteWatchEventRepository {
|
|||||||
for id in ids {
|
for id in ids {
|
||||||
q = q.bind(id.value().to_string());
|
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()
|
rows.iter().map(row_to_watch_event).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +192,7 @@ impl WatchEventQuery for SqliteWatchEventRepository {
|
|||||||
.bind(&after_str)
|
.bind(&after_str)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.try_get(0)
|
.try_get(0)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
@@ -206,16 +201,16 @@ impl WatchEventQuery for SqliteWatchEventRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn row_to_watch_event(row: &sqlx::sqlite::SqliteRow) -> Result<WatchEvent, DomainError> {
|
fn row_to_watch_event(row: &sqlx::sqlite::SqliteRow) -> Result<WatchEvent, DomainError> {
|
||||||
let id_str: &str = row.try_get("id").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(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
let title: String = row.try_get("title").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let year: Option<i64> = row.try_get("year").map_err(map_err)?;
|
let year: Option<i64> = row.try_get("year").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let ext_id: Option<String> = row.try_get("external_metadata_id").map_err(map_err)?;
|
let ext_id: Option<String> = row.try_get("external_metadata_id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let source_str: String = row.try_get("source").map_err(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let source: WatchEventSource = source_str
|
let source: WatchEventSource = source_str
|
||||||
.parse()
|
.parse()
|
||||||
@@ -278,7 +273,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
|||||||
.bind(&last_used)
|
.bind(&last_used)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -291,7 +286,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
|||||||
.bind(hash)
|
.bind(hash)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.as_ref().map(row_to_webhook_token).transpose()
|
row.as_ref().map(row_to_webhook_token).transpose()
|
||||||
}
|
}
|
||||||
@@ -306,7 +301,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter().map(row_to_webhook_token).collect()
|
rows.iter().map(row_to_webhook_token).collect()
|
||||||
}
|
}
|
||||||
@@ -320,7 +315,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
return Err(DomainError::NotFound(format!("Webhook token {id_str}")));
|
return Err(DomainError::NotFound(format!("Webhook token {id_str}")));
|
||||||
@@ -337,20 +332,20 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row_to_webhook_token(row: &sqlx::sqlite::SqliteRow) -> Result<WebhookToken, DomainError> {
|
fn row_to_webhook_token(row: &sqlx::sqlite::SqliteRow) -> Result<WebhookToken, DomainError> {
|
||||||
let id_str: &str = row.try_get("id").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(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
let provider_str: String = row.try_get("provider").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let label: Option<String> = row.try_get("label").map_err(map_err)?;
|
let label: Option<String> = row.try_get("label").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let created_at_str: String = row.try_get("created_at").map_err(map_err)?;
|
let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let last_used_str: Option<String> = row.try_get("last_used_at").map_err(map_err)?;
|
let last_used_str: Option<String> = row.try_get("last_used_at").map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let provider: WatchEventSource = provider_str
|
let provider: WatchEventSource = provider_str
|
||||||
.parse()
|
.parse()
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ use domain::{
|
|||||||
};
|
};
|
||||||
use sqlx::{Row, SqlitePool};
|
use sqlx::{Row, SqlitePool};
|
||||||
|
|
||||||
use crate::models::{WatchlistRow, datetime_to_str};
|
use adapter_common::datetime_to_str;
|
||||||
|
use crate::models::WatchlistRow;
|
||||||
|
|
||||||
pub struct SqliteWatchlistRepository {
|
pub struct SqliteWatchlistRepository {
|
||||||
pool: SqlitePool,
|
pool: SqlitePool,
|
||||||
@@ -21,10 +22,6 @@ impl SqliteWatchlistRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_err(e: sqlx::Error) -> DomainError {
|
|
||||||
tracing::error!("Database error: {:?}", e);
|
|
||||||
DomainError::InfrastructureError("Database operation failed".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -45,7 +42,7 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
|||||||
.bind(&added_at)
|
.bind(&added_at)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -60,7 +57,7 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
|||||||
.bind(&mid)
|
.bind(&mid)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
return Err(DomainError::NotFound(format!(
|
return Err(DomainError::NotFound(format!(
|
||||||
@@ -84,7 +81,7 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
|||||||
.bind(&mid)
|
.bind(&mid)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(result.rows_affected() > 0)
|
Ok(result.rows_affected() > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,13 +109,13 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
|||||||
.bind(offset)
|
.bind(offset)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.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 = ?")
|
let total: i64 = sqlx::query("SELECT COUNT(*) FROM watchlist_entries WHERE user_id = ?")
|
||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.try_get(0)
|
.try_get(0)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
@@ -145,7 +142,7 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
|||||||
.bind(&mid)
|
.bind(&mid)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?
|
.map_err(adapter_common::map_sqlx_error)?
|
||||||
.try_get(0)
|
.try_get(0)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ use domain::{
|
|||||||
use sqlx::{Row, SqlitePool};
|
use sqlx::{Row, SqlitePool};
|
||||||
use uuid::Uuid;
|
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 {
|
fn status_to_str(s: &WrapUpStatus) -> &'static str {
|
||||||
match s {
|
match s {
|
||||||
@@ -87,7 +82,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
|||||||
.bind(&completed)
|
.bind(&completed)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -107,7 +102,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -126,7 +121,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -142,7 +137,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
|||||||
.bind(&id_str)
|
.bind(&id_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.as_ref().map(row_to_record).transpose()
|
row.as_ref().map(row_to_record).transpose()
|
||||||
}
|
}
|
||||||
@@ -158,7 +153,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
|||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter().map(row_to_record).collect()
|
rows.iter().map(row_to_record).collect()
|
||||||
}
|
}
|
||||||
@@ -171,7 +166,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
|||||||
)
|
)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
rows.iter().map(row_to_record).collect()
|
rows.iter().map(row_to_record).collect()
|
||||||
}
|
}
|
||||||
@@ -200,7 +195,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
|||||||
.bind(&end_str)
|
.bind(&end_str)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
row.as_ref().map(row_to_record).transpose()
|
row.as_ref().map(row_to_record).transpose()
|
||||||
}
|
}
|
||||||
@@ -210,7 +205,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
|||||||
.bind(id.value().to_string())
|
.bind(id.value().to_string())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,21 +219,21 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
|||||||
.bind(&before_str)
|
.bind(&before_str)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(map_err)?;
|
.map_err(adapter_common::map_sqlx_error)?;
|
||||||
Ok(result.rows_affected())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row_to_record(row: &sqlx::sqlite::SqliteRow) -> Result<WrapUpRecord, DomainError> {
|
fn row_to_record(row: &sqlx::sqlite::SqliteRow) -> Result<WrapUpRecord, DomainError> {
|
||||||
let id_str: String = row.try_get("id").map_err(map_err)?;
|
let id_str: String = row.try_get("id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let user_id_str: Option<String> = row.try_get("user_id").map_err(map_err)?;
|
let user_id_str: Option<String> = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let start_date_str: String = row.try_get("start_date").map_err(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
let status_str: String = row.try_get("status").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let report_json: Option<String> = row.try_get("report_json").map_err(map_err)?;
|
let report_json: Option<String> = row.try_get("report_json").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let error_message: Option<String> = row.try_get("error_message").map_err(map_err)?;
|
let error_message: Option<String> = row.try_get("error_message").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let created_at_str: String = row.try_get("created_at").map_err(map_err)?;
|
let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let completed_at_str: Option<String> = row.try_get("completed_at").map_err(map_err)?;
|
let completed_at_str: Option<String> = row.try_get("completed_at").map_err(adapter_common::map_sqlx_error)?;
|
||||||
|
|
||||||
let user_id = user_id_str.as_deref().map(parse_uuid).transpose()?;
|
let user_id = user_id_str.as_deref().map(parse_uuid).transpose()?;
|
||||||
|
|
||||||
@@ -304,7 +299,7 @@ impl WrapUpStatsQuery for SqliteWrapUpStatsQuery {
|
|||||||
q = q.bind(uid);
|
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() {
|
if rows.is_empty() {
|
||||||
return Ok(vec![]);
|
return Ok(vec![]);
|
||||||
@@ -314,7 +309,7 @@ impl WrapUpStatsQuery for SqliteWrapUpStatsQuery {
|
|||||||
let mut movie_ids: Vec<String> = Vec::new();
|
let mut movie_ids: Vec<String> = Vec::new();
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
for row in &rows {
|
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()) {
|
if seen.insert(mid.clone()) {
|
||||||
movie_ids.push(mid);
|
movie_ids.push(mid);
|
||||||
}
|
}
|
||||||
@@ -330,18 +325,18 @@ impl WrapUpStatsQuery for SqliteWrapUpStatsQuery {
|
|||||||
// 3) Build result
|
// 3) Build result
|
||||||
let mut result = Vec::with_capacity(rows.len());
|
let mut result = Vec::with_capacity(rows.len());
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
let movie_id_str: String = row.try_get("movie_id").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(map_err)?;
|
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(map_err)?;
|
let release_year: i64 = row.try_get("release_year").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let director: Option<String> = row.try_get("director").map_err(map_err)?;
|
let director: Option<String> = row.try_get("director").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let poster_path: Option<String> = row.try_get("poster_path").map_err(map_err)?;
|
let poster_path: Option<String> = row.try_get("poster_path").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let rating: i64 = row.try_get("rating").map_err(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
let user_id_str: String = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let runtime_minutes: Option<i32> = row.try_get("runtime_minutes").map_err(map_err)?;
|
let runtime_minutes: Option<i32> = row.try_get("runtime_minutes").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let budget_usd: Option<i64> = row.try_get("budget_usd").map_err(map_err)?;
|
let budget_usd: Option<i64> = row.try_get("budget_usd").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let original_language: Option<String> =
|
let original_language: Option<String> =
|
||||||
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 genres = genres_map.get(&movie_id_str).cloned().unwrap_or_default();
|
||||||
let keywords = keywords_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 {
|
for id in movie_ids {
|
||||||
q = q.bind(id);
|
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<String, Vec<String>> = HashMap::new();
|
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
for row in rows {
|
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)?;
|
||||||
let name: String = row.try_get("name").map_err(map_err)?;
|
let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?;
|
||||||
map.entry(mid).or_default().push(name);
|
map.entry(mid).or_default().push(name);
|
||||||
}
|
}
|
||||||
Ok(map)
|
Ok(map)
|
||||||
@@ -437,12 +432,12 @@ async fn fetch_keywords_sqlite(
|
|||||||
for id in movie_ids {
|
for id in movie_ids {
|
||||||
q = q.bind(id);
|
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<String, Vec<String>> = HashMap::new();
|
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
for row in rows {
|
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)?;
|
||||||
let name: String = row.try_get("name").map_err(map_err)?;
|
let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?;
|
||||||
map.entry(mid).or_default().push(name);
|
map.entry(mid).or_default().push(name);
|
||||||
}
|
}
|
||||||
Ok(map)
|
Ok(map)
|
||||||
@@ -466,15 +461,15 @@ async fn fetch_cast_sqlite(
|
|||||||
for id in movie_ids {
|
for id in movie_ids {
|
||||||
q = q.bind(id);
|
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<String, Vec<CastEntry>> = HashMap::new();
|
let mut map: HashMap<String, Vec<CastEntry>> = HashMap::new();
|
||||||
for row in rows {
|
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)?;
|
||||||
let name: String = row.try_get("name").map_err(map_err)?;
|
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(map_err)?;
|
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(map_err)?;
|
let tmdb_person_id: i64 = row.try_get("tmdb_person_id").map_err(adapter_common::map_sqlx_error)?;
|
||||||
let profile_path: Option<String> = row.try_get("profile_path").map_err(map_err)?;
|
let profile_path: Option<String> = row.try_get("profile_path").map_err(adapter_common::map_sqlx_error)?;
|
||||||
map.entry(mid).or_default().push(CastEntry {
|
map.entry(mid).or_default().push(CastEntry {
|
||||||
name,
|
name,
|
||||||
billing_order: billing_order as u32,
|
billing_order: billing_order as u32,
|
||||||
|
|||||||
Reference in New Issue
Block a user