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:
@@ -11,6 +11,7 @@ sqlx = { version = "0.8.6", features = [
|
||||
"macros",
|
||||
] }
|
||||
|
||||
adapter-common = { workspace = true }
|
||||
domain = { workspace = true }
|
||||
sqlite-federation = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -22,23 +22,18 @@ impl SqliteDiaryRepository {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
|
||||
async fn count_diary_entries(&self, movie_id: Option<&str>) -> Result<i64, DomainError> {
|
||||
match movie_id {
|
||||
None => sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews")
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err),
|
||||
.map_err(adapter_common::map_sqlx_error),
|
||||
Some(id) => {
|
||||
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM reviews WHERE movie_id = ?")
|
||||
.bind(id)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +64,7 @@ impl SqliteDiaryRepository {
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
|
||||
async fn fetch_movie_diary_rows(
|
||||
@@ -101,7 +96,7 @@ impl SqliteDiaryRepository {
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
|
||||
async fn count_user_diary_entries(
|
||||
@@ -130,7 +125,7 @@ impl SqliteDiaryRepository {
|
||||
if has_search {
|
||||
q = q.bind(search.unwrap());
|
||||
}
|
||||
q.fetch_one(&self.pool).await.map_err(Self::map_err)
|
||||
q.fetch_one(&self.pool).await.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
|
||||
async fn fetch_user_diary_rows(
|
||||
@@ -176,7 +171,7 @@ impl SqliteDiaryRepository {
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +335,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
||||
}
|
||||
|
||||
let count_q = bind_filter_params!(sqlx::query_scalar::<_, i64>(&count_sql));
|
||||
let total = count_q.fetch_one(&self.pool).await.map_err(Self::map_err)?;
|
||||
let total = count_q.fetch_one(&self.pool).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let rows_q = bind_filter_params!(sqlx::query_as::<_, FeedRow>(&select_sql));
|
||||
let rows = rows_q
|
||||
@@ -348,7 +343,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let items = rows
|
||||
.into_iter()
|
||||
@@ -373,7 +368,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
||||
.bind(&id_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.ok_or_else(|| DomainError::NotFound(format!("Movie {}", id_str)))?
|
||||
.into_domain()?;
|
||||
|
||||
@@ -384,7 +379,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
||||
.bind(&id_str)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.into_iter()
|
||||
.map(ReviewRow::into_domain)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
@@ -405,7 +400,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
||||
.bind(&uid)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
rows.into_iter().map(DiaryRow::into_domain).collect()
|
||||
}
|
||||
@@ -430,7 +425,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
||||
while let Some(row) = futures::StreamExt::next(&mut rows).await {
|
||||
yield match row {
|
||||
Ok(r) => r.into_domain(),
|
||||
Err(e) => Err(Self::map_err(e)),
|
||||
Err(e) => Err(adapter_common::map_sqlx_error(e)),
|
||||
};
|
||||
}
|
||||
})
|
||||
@@ -453,7 +448,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
||||
.bind(id_str)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
.map(MovieStatsRow::into_domain)
|
||||
}
|
||||
|
||||
@@ -471,7 +466,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
||||
.bind(&id_str)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let rows = sqlx::query_as::<_, FeedRow>(
|
||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||
@@ -492,7 +487,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let items = rows
|
||||
.into_iter()
|
||||
@@ -512,7 +507,7 @@ impl DiaryQuery for SqliteDiaryRepository {
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM reviews WHERE remote_actor_url IS NULL")
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(count as u64)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,6 @@ impl SqliteGoalRepository {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -44,7 +40,7 @@ impl GoalCommand for SqliteGoalRepository {
|
||||
.bind(&created_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -58,7 +54,7 @@ impl GoalCommand for SqliteGoalRepository {
|
||||
.bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(DomainError::NotFound("Goal not found".into()));
|
||||
@@ -75,7 +71,7 @@ impl GoalCommand for SqliteGoalRepository {
|
||||
.bind(&uid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(DomainError::NotFound("Goal not found".into()));
|
||||
@@ -102,7 +98,7 @@ impl GoalQuery for SqliteGoalRepository {
|
||||
.bind(y)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
row.map(|r| row_to_goal(&r)).transpose()
|
||||
}
|
||||
@@ -117,7 +113,7 @@ impl GoalQuery for SqliteGoalRepository {
|
||||
.bind(&uid)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
rows.iter().map(row_to_goal).collect()
|
||||
}
|
||||
|
||||
@@ -97,11 +97,6 @@ impl SqliteImportProfileRepository {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("DB error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
|
||||
fn parse_dt(s: &str) -> Result<NaiveDateTime, DomainError> {
|
||||
NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
|
||||
.or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S"))
|
||||
@@ -130,7 +125,7 @@ impl ImportProfileRepository for SqliteImportProfileRepository {
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
|
||||
async fn list_for_user(&self, user_id: &UserId) -> Result<Vec<ImportProfile>, DomainError> {
|
||||
@@ -141,7 +136,7 @@ impl ImportProfileRepository for SqliteImportProfileRepository {
|
||||
.bind(&uid)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
rows.iter()
|
||||
.map(|r| {
|
||||
@@ -183,7 +178,7 @@ impl ImportProfileRepository for SqliteImportProfileRepository {
|
||||
.bind(&uid_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
row.map(|r| {
|
||||
use sqlx::Row;
|
||||
@@ -215,6 +210,6 @@ impl ImportProfileRepository for SqliteImportProfileRepository {
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,11 +202,6 @@ impl SqliteImportSessionRepository {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("DB error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
|
||||
fn parse_dt(s: &str) -> Result<NaiveDateTime, DomainError> {
|
||||
NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
|
||||
.or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S"))
|
||||
@@ -316,7 +311,7 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
|
||||
async fn get(
|
||||
@@ -334,7 +329,7 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
||||
.bind(&uid_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
row.map(|r| {
|
||||
use sqlx::Row;
|
||||
@@ -361,7 +356,7 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &ImportSessionId) -> Result<(), DomainError> {
|
||||
@@ -371,14 +366,14 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
|
||||
async fn delete_expired(&self) -> Result<u64, DomainError> {
|
||||
let result = sqlx::query("DELETE FROM import_sessions WHERE expires_at < datetime('now')")
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
@@ -391,6 +386,6 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,30 +45,6 @@ pub fn create_profile_fields_repo(
|
||||
std::sync::Arc::new(SqliteProfileFieldsRepository::new(pool))
|
||||
}
|
||||
|
||||
pub(crate) fn format_year_month(ym: &str) -> String {
|
||||
let parts: Vec<&str> = ym.splitn(2, '-').collect();
|
||||
if parts.len() != 2 {
|
||||
return ym.to_string();
|
||||
}
|
||||
let year = parts[0].get(2..).unwrap_or(parts[0]);
|
||||
let month = match parts[1] {
|
||||
"01" => "Jan",
|
||||
"02" => "Feb",
|
||||
"03" => "Mar",
|
||||
"04" => "Apr",
|
||||
"05" => "May",
|
||||
"06" => "Jun",
|
||||
"07" => "Jul",
|
||||
"08" => "Aug",
|
||||
"09" => "Sep",
|
||||
"10" => "Oct",
|
||||
"11" => "Nov",
|
||||
"12" => "Dec",
|
||||
_ => parts[1],
|
||||
};
|
||||
format!("{} '{}", month, year)
|
||||
}
|
||||
|
||||
pub async fn migrate(pool: &SqlitePool) -> Result<(), domain::errors::DomainError> {
|
||||
migrations::run(pool).await
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{
|
||||
DiaryEntry, FeedEntry, Movie, MovieSummary, PersistedReview, Review, ReviewSource,
|
||||
UserSummary, WatchlistEntry, WatchlistWithMovie,
|
||||
},
|
||||
value_objects::{
|
||||
Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear,
|
||||
ReviewId, UserId, Username, WatchlistEntryId,
|
||||
},
|
||||
models::{DiaryEntry, FeedEntry, Movie, MovieSummary, Review, WatchlistWithMovie},
|
||||
};
|
||||
use adapter_common::{
|
||||
movie_row_to_domain, movie_stats_to_domain, movie_summary_to_domain, review_row_to_domain,
|
||||
user_summary_to_domain, watchlist_entry_to_domain, watchlist_with_movie_to_domain,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub(crate) struct MovieRow {
|
||||
@@ -24,22 +19,14 @@ pub(crate) struct MovieRow {
|
||||
|
||||
impl MovieRow {
|
||||
pub fn into_domain(self) -> Result<Movie, DomainError> {
|
||||
let id = MovieId::from_uuid(parse_uuid(&self.id)?);
|
||||
let external_metadata_id = self
|
||||
.external_metadata_id
|
||||
.map(ExternalMetadataId::new)
|
||||
.transpose()?;
|
||||
let title = MovieTitle::new(self.title)?;
|
||||
let release_year = ReleaseYear::new(self.release_year as u16)?;
|
||||
let poster_path = self.poster_path.map(PosterPath::new).transpose()?;
|
||||
Ok(Movie::from_persistence(
|
||||
id,
|
||||
external_metadata_id,
|
||||
title,
|
||||
release_year,
|
||||
movie_row_to_domain(
|
||||
self.id,
|
||||
self.external_metadata_id,
|
||||
self.title,
|
||||
self.release_year,
|
||||
self.director,
|
||||
poster_path,
|
||||
))
|
||||
self.poster_path,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,27 +47,26 @@ pub(crate) struct MovieSummaryRow {
|
||||
|
||||
impl MovieSummaryRow {
|
||||
pub fn into_domain(self) -> Result<MovieSummary, DomainError> {
|
||||
let movie = MovieRow {
|
||||
id: self.id,
|
||||
external_metadata_id: self.external_metadata_id,
|
||||
title: self.title,
|
||||
release_year: self.release_year,
|
||||
director: self.director,
|
||||
poster_path: self.poster_path,
|
||||
}
|
||||
.into_domain()?;
|
||||
let movie = movie_row_to_domain(
|
||||
self.id,
|
||||
self.external_metadata_id,
|
||||
self.title,
|
||||
self.release_year,
|
||||
self.director,
|
||||
self.poster_path,
|
||||
)?;
|
||||
let genres = self
|
||||
.genres
|
||||
.map(|g| g.split(',').map(str::to_string).collect())
|
||||
.unwrap_or_default();
|
||||
Ok(MovieSummary {
|
||||
Ok(movie_summary_to_domain(
|
||||
movie,
|
||||
genres,
|
||||
runtime_minutes: self.runtime_minutes.map(|v| v as u32),
|
||||
original_language: self.original_language,
|
||||
overview: self.overview,
|
||||
collection_name: self.collection_name,
|
||||
})
|
||||
self.runtime_minutes,
|
||||
self.original_language,
|
||||
self.overview,
|
||||
self.collection_name,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,29 +85,17 @@ pub(crate) struct ReviewRow {
|
||||
|
||||
impl ReviewRow {
|
||||
pub fn into_domain(self) -> Result<Review, DomainError> {
|
||||
let id = ReviewId::from_uuid(parse_uuid(&self.id)?);
|
||||
let movie_id = MovieId::from_uuid(parse_uuid(&self.movie_id)?);
|
||||
let user_id = UserId::from_uuid(parse_uuid(&self.user_id)?);
|
||||
let rating = Rating::new(self.rating as u8)?;
|
||||
let comment = self.comment.map(Comment::new).transpose()?;
|
||||
let watched_at = parse_datetime(&self.watched_at)?;
|
||||
let created_at = parse_datetime(&self.created_at)?;
|
||||
let source = match self.remote_actor_url {
|
||||
None => ReviewSource::Local,
|
||||
Some(url) => ReviewSource::Remote { actor_url: url },
|
||||
};
|
||||
let watch_medium = self.watch_medium.map(|s| s.parse()).transpose()?;
|
||||
Ok(Review::from_persistence(PersistedReview {
|
||||
id,
|
||||
movie_id,
|
||||
user_id,
|
||||
rating,
|
||||
comment,
|
||||
watched_at,
|
||||
created_at,
|
||||
source,
|
||||
watch_medium,
|
||||
}))
|
||||
review_row_to_domain(
|
||||
self.id,
|
||||
self.movie_id,
|
||||
self.user_id,
|
||||
self.rating,
|
||||
self.comment,
|
||||
self.watched_at,
|
||||
self.created_at,
|
||||
self.remote_actor_url,
|
||||
self.watch_medium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,29 +121,25 @@ pub(crate) struct DiaryRow {
|
||||
|
||||
impl DiaryRow {
|
||||
pub fn into_domain(self) -> Result<DiaryEntry, DomainError> {
|
||||
let movie = MovieRow {
|
||||
id: self.id,
|
||||
external_metadata_id: self.external_metadata_id,
|
||||
title: self.title,
|
||||
release_year: self.release_year,
|
||||
director: self.director,
|
||||
poster_path: self.poster_path,
|
||||
}
|
||||
.into_domain()?;
|
||||
|
||||
let review = ReviewRow {
|
||||
id: self.review_id,
|
||||
movie_id: self.movie_id,
|
||||
user_id: self.user_id,
|
||||
rating: self.rating,
|
||||
comment: self.comment,
|
||||
watched_at: self.watched_at,
|
||||
created_at: self.created_at,
|
||||
remote_actor_url: self.remote_actor_url,
|
||||
watch_medium: self.watch_medium,
|
||||
}
|
||||
.into_domain()?;
|
||||
|
||||
let movie = movie_row_to_domain(
|
||||
self.id,
|
||||
self.external_metadata_id,
|
||||
self.title,
|
||||
self.release_year,
|
||||
self.director,
|
||||
self.poster_path,
|
||||
)?;
|
||||
let review = review_row_to_domain(
|
||||
self.review_id,
|
||||
self.movie_id,
|
||||
self.user_id,
|
||||
self.rating,
|
||||
self.comment,
|
||||
self.watched_at,
|
||||
self.created_at,
|
||||
self.remote_actor_url,
|
||||
self.watch_medium,
|
||||
)?;
|
||||
Ok(DiaryEntry::new(movie, review))
|
||||
}
|
||||
}
|
||||
@@ -188,18 +158,18 @@ pub(crate) struct MovieStatsRow {
|
||||
|
||||
impl MovieStatsRow {
|
||||
pub fn into_domain(self) -> domain::models::MovieStats {
|
||||
domain::models::MovieStats {
|
||||
total_count: self.total_count as u64,
|
||||
avg_rating: self.avg_rating,
|
||||
federated_count: self.federated_count as u64,
|
||||
rating_histogram: [
|
||||
self.rating_1 as u64,
|
||||
self.rating_2 as u64,
|
||||
self.rating_3 as u64,
|
||||
self.rating_4 as u64,
|
||||
self.rating_5 as u64,
|
||||
movie_stats_to_domain(
|
||||
self.total_count,
|
||||
self.avg_rating,
|
||||
self.federated_count,
|
||||
[
|
||||
self.rating_1,
|
||||
self.rating_2,
|
||||
self.rating_3,
|
||||
self.rating_4,
|
||||
self.rating_5,
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,24 +196,26 @@ pub(crate) struct FeedRow {
|
||||
|
||||
impl FeedRow {
|
||||
pub fn into_domain(self) -> Result<FeedEntry, DomainError> {
|
||||
let diary = DiaryRow {
|
||||
id: self.id,
|
||||
external_metadata_id: self.external_metadata_id,
|
||||
title: self.title,
|
||||
release_year: self.release_year,
|
||||
director: self.director,
|
||||
poster_path: self.poster_path,
|
||||
review_id: self.review_id,
|
||||
movie_id: self.movie_id,
|
||||
user_id: self.user_id,
|
||||
rating: self.rating,
|
||||
comment: self.comment,
|
||||
watched_at: self.watched_at,
|
||||
created_at: self.created_at,
|
||||
remote_actor_url: self.remote_actor_url,
|
||||
watch_medium: self.watch_medium,
|
||||
}
|
||||
.into_domain()?;
|
||||
let movie = movie_row_to_domain(
|
||||
self.id,
|
||||
self.external_metadata_id,
|
||||
self.title,
|
||||
self.release_year,
|
||||
self.director,
|
||||
self.poster_path,
|
||||
)?;
|
||||
let review = review_row_to_domain(
|
||||
self.review_id,
|
||||
self.movie_id,
|
||||
self.user_id,
|
||||
self.rating,
|
||||
self.comment,
|
||||
self.watched_at,
|
||||
self.created_at,
|
||||
self.remote_actor_url,
|
||||
self.watch_medium,
|
||||
)?;
|
||||
let diary = DiaryEntry::new(movie, review);
|
||||
Ok(FeedEntry::new(diary, self.user_email))
|
||||
}
|
||||
}
|
||||
@@ -260,16 +232,16 @@ pub(crate) struct UserSummaryRow {
|
||||
}
|
||||
|
||||
impl UserSummaryRow {
|
||||
pub fn into_domain(self) -> Result<UserSummary, DomainError> {
|
||||
Ok(UserSummary::new(
|
||||
UserId::from_uuid(parse_uuid(&self.id)?),
|
||||
Email::new(self.email)?,
|
||||
Username::new(self.username)?,
|
||||
pub fn into_domain(self) -> Result<domain::models::UserSummary, DomainError> {
|
||||
user_summary_to_domain(
|
||||
self.id,
|
||||
self.email,
|
||||
self.username,
|
||||
self.display_name,
|
||||
self.total_movies,
|
||||
self.avg_rating,
|
||||
self.avatar_path,
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,35 +280,20 @@ pub(crate) struct WatchlistRow {
|
||||
|
||||
impl WatchlistRow {
|
||||
pub fn into_domain(self) -> Result<WatchlistWithMovie, DomainError> {
|
||||
let entry = WatchlistEntry {
|
||||
id: WatchlistEntryId::from_uuid(parse_uuid(&self.id)?),
|
||||
user_id: UserId::from_uuid(parse_uuid(&self.user_id)?),
|
||||
movie_id: MovieId::from_uuid(parse_uuid(&self.movie_id)?),
|
||||
added_at: parse_datetime(&self.added_at)?,
|
||||
};
|
||||
let movie = MovieRow {
|
||||
id: self.m_id,
|
||||
external_metadata_id: self.external_metadata_id,
|
||||
title: self.title,
|
||||
release_year: self.release_year,
|
||||
director: self.director,
|
||||
poster_path: self.poster_path,
|
||||
}
|
||||
.into_domain()?;
|
||||
Ok(WatchlistWithMovie { entry, movie })
|
||||
let entry = watchlist_entry_to_domain(
|
||||
self.id,
|
||||
self.user_id,
|
||||
self.movie_id,
|
||||
self.added_at,
|
||||
)?;
|
||||
let movie = movie_row_to_domain(
|
||||
self.m_id,
|
||||
self.external_metadata_id,
|
||||
self.title,
|
||||
self.release_year,
|
||||
self.director,
|
||||
self.poster_path,
|
||||
)?;
|
||||
Ok(watchlist_with_movie_to_domain(entry, movie))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_uuid(s: &str) -> Result<Uuid, 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 }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -55,7 +51,7 @@ impl MovieCommand for SqliteMovieRepository {
|
||||
.bind(&poster_path)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -66,7 +62,7 @@ impl MovieCommand for SqliteMovieRepository {
|
||||
.bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -85,7 +81,7 @@ impl MovieQuery for SqliteMovieRepository {
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.map(MovieRow::into_domain)
|
||||
.transpose()
|
||||
}
|
||||
@@ -99,7 +95,7 @@ impl MovieQuery for SqliteMovieRepository {
|
||||
.bind(&id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.map(MovieRow::into_domain)
|
||||
.transpose()
|
||||
}
|
||||
@@ -119,7 +115,7 @@ impl MovieQuery for SqliteMovieRepository {
|
||||
.bind(y)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.into_iter()
|
||||
.map(MovieRow::into_domain)
|
||||
.collect()
|
||||
@@ -141,7 +137,7 @@ impl MovieQuery for SqliteMovieRepository {
|
||||
for id in ids {
|
||||
q = q.bind(id.value().to_string());
|
||||
}
|
||||
let rows = q.fetch_all(&self.pool).await.map_err(Self::map_err)?;
|
||||
let rows = q.fetch_all(&self.pool).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(rows.into_iter().collect())
|
||||
}
|
||||
|
||||
@@ -165,7 +161,7 @@ impl MovieQuery for SqliteMovieRepository {
|
||||
for (t, y) in pairs {
|
||||
q = q.bind(t.value().to_string()).bind(y.value() as i64);
|
||||
}
|
||||
let rows = q.fetch_all(&self.pool).await.map_err(Self::map_err)?;
|
||||
let rows = q.fetch_all(&self.pool).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
@@ -217,7 +213,7 @@ impl MovieQuery for SqliteMovieRepository {
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let total: i64 = sqlx::query(
|
||||
"SELECT COUNT(DISTINCT m.id) \
|
||||
@@ -235,7 +231,7 @@ impl MovieQuery for SqliteMovieRepository {
|
||||
.bind(genre)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.try_get(0)
|
||||
.unwrap_or(0);
|
||||
|
||||
@@ -259,7 +255,7 @@ impl MovieQuery for SqliteMovieRepository {
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.into_iter()
|
||||
.map(MovieRow::into_domain)
|
||||
.collect()
|
||||
|
||||
@@ -13,10 +13,6 @@ impl SqliteMovieDeduplicator {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -36,7 +32,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
||||
let director = canonical.director().map(str::to_string);
|
||||
let poster = canonical.poster_path().map(|p| p.value().to_string());
|
||||
|
||||
let mut tx = self.pool.begin().await.map_err(Self::map_err)?;
|
||||
let mut tx = self.pool.begin().await.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
// 1. Upsert canonical movie record
|
||||
sqlx::query(
|
||||
@@ -47,7 +43,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
||||
poster_path = COALESCE(excluded.poster_path, movies.poster_path)",
|
||||
)
|
||||
.bind(&new).bind(&ext_id).bind(&title).bind(year).bind(&director).bind(&poster)
|
||||
.execute(&mut *tx).await.map_err(Self::map_err)?;
|
||||
.execute(&mut *tx).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
// 2. Re-point simple FK tables
|
||||
let reviews = sqlx::query("UPDATE reviews SET movie_id = ? WHERE movie_id = ?")
|
||||
@@ -55,7 +51,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
||||
.bind(&old)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.rows_affected();
|
||||
|
||||
let watchlist = sqlx::query("UPDATE watchlist_entries SET movie_id = ? WHERE movie_id = ?")
|
||||
@@ -63,7 +59,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
||||
.bind(&old)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.rows_affected();
|
||||
|
||||
let watch_events = sqlx::query("UPDATE watch_events SET movie_id = ? WHERE movie_id = ?")
|
||||
@@ -71,7 +67,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
||||
.bind(&old)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.rows_affected();
|
||||
|
||||
// 3. Re-point movie_profiles (PK — move only if canonical has none)
|
||||
@@ -80,7 +76,7 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
||||
.bind(&old)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.rows_affected();
|
||||
|
||||
// 4. Re-point enrichment tables with composite PKs (INSERT OR IGNORE + DELETE)
|
||||
@@ -93,12 +89,12 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
||||
.bind(&old)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
sqlx::query("DELETE FROM movie_genres WHERE movie_id = ?")
|
||||
.bind(&old)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO movie_keywords (movie_id, tmdb_id, name)
|
||||
@@ -108,41 +104,41 @@ impl MovieDeduplicator for SqliteMovieDeduplicator {
|
||||
.bind(&old)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
sqlx::query("DELETE FROM movie_keywords WHERE movie_id = ?")
|
||||
.bind(&old)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO movie_cast (movie_id, tmdb_person_id, name, character, billing_order, profile_path)
|
||||
SELECT ?, tmdb_person_id, name, character, billing_order, profile_path FROM movie_cast WHERE movie_id = ?",
|
||||
).bind(&new).bind(&old).execute(&mut *tx).await.map_err(Self::map_err)?;
|
||||
).bind(&new).bind(&old).execute(&mut *tx).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
sqlx::query("DELETE FROM movie_cast WHERE movie_id = ?")
|
||||
.bind(&old)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO movie_crew (movie_id, tmdb_person_id, name, job, department, profile_path)
|
||||
SELECT ?, tmdb_person_id, name, job, department, profile_path FROM movie_crew WHERE movie_id = ?",
|
||||
).bind(&new).bind(&old).execute(&mut *tx).await.map_err(Self::map_err)?;
|
||||
).bind(&new).bind(&old).execute(&mut *tx).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
sqlx::query("DELETE FROM movie_crew WHERE movie_id = ?")
|
||||
.bind(&old)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
// 5. Delete the now-empty old movie record (remaining cascades are safe: all FKs cleared above)
|
||||
sqlx::query("DELETE FROM movies WHERE id = ?")
|
||||
.bind(&old)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
tx.commit().await.map_err(Self::map_err)?;
|
||||
tx.commit().await.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(reviews + watchlist + watch_events + profiles)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
impl PersonCommand for SqlitePersonAdapter {
|
||||
async fn upsert_batch(&self, persons: &[Person]) -> Result<(), DomainError> {
|
||||
@@ -56,7 +52,7 @@ impl PersonCommand for SqlitePersonAdapter {
|
||||
.bind(person.profile_path())
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -89,7 +85,7 @@ impl PersonCommand for SqlitePersonAdapter {
|
||||
.bind(batch_size)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let has_more = rows.len() as u32 >= batch_size;
|
||||
let mut count = 0u64;
|
||||
@@ -109,7 +105,7 @@ impl PersonCommand for SqlitePersonAdapter {
|
||||
.bind(&row.profile_path)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
count += 1;
|
||||
}
|
||||
Ok((count, has_more))
|
||||
@@ -137,7 +133,7 @@ impl PersonCommand for SqlitePersonAdapter {
|
||||
.bind(id.value().to_string())
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -151,7 +147,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
||||
.bind(id.value().to_string())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(row.map(PersonRow::into_person))
|
||||
}
|
||||
@@ -166,7 +162,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
||||
.bind(id.value())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(row.map(PersonRow::into_person))
|
||||
}
|
||||
@@ -182,7 +178,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
||||
.bind(id.value().to_string())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.flatten();
|
||||
|
||||
let Some(tmdb_id) = tmdb_id else {
|
||||
@@ -203,7 +199,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
||||
.bind(tmdb_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.into_iter()
|
||||
.map(|r| CastCredit {
|
||||
movie_id: MovieId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()),
|
||||
@@ -224,7 +220,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
||||
.bind(tmdb_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.into_iter()
|
||||
.map(|r| CrewCredit {
|
||||
movie_id: MovieId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()),
|
||||
@@ -247,7 +243,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(rows.into_iter().map(PersonRow::into_person).collect())
|
||||
}
|
||||
@@ -265,7 +261,7 @@ impl PersonQuery for SqlitePersonAdapter {
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
|
||||
@@ -17,10 +17,6 @@ impl SqliteMovieProfileRepository {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -29,7 +25,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
let movie_id = p.movie_id.value().to_string();
|
||||
let enriched_at = p.enriched_at.to_rfc3339();
|
||||
|
||||
let mut tx = self.pool.begin().await.map_err(Self::map_err)?;
|
||||
let mut tx = self.pool.begin().await.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"INSERT INTO movie_profiles
|
||||
@@ -62,13 +58,13 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
.bind(&enriched_at)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
sqlx::query("DELETE FROM movie_genres WHERE movie_id = ?")
|
||||
.bind(&movie_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
for g in &p.genres {
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO movie_genres (movie_id, tmdb_id, name) VALUES (?,?,?)",
|
||||
@@ -78,14 +74,14 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
.bind(&g.name)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM movie_keywords WHERE movie_id = ?")
|
||||
.bind(&movie_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
for k in &p.keywords {
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO movie_keywords (movie_id, tmdb_id, name) VALUES (?,?,?)",
|
||||
@@ -95,14 +91,14 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
.bind(&k.name)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM movie_cast WHERE movie_id = ?")
|
||||
.bind(&movie_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
for c in &p.cast {
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO movie_cast \
|
||||
@@ -117,14 +113,14 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
.bind(&c.profile_path)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM movie_crew WHERE movie_id = ?")
|
||||
.bind(&movie_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
for cr in &p.crew {
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO movie_crew \
|
||||
@@ -139,10 +135,10 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
.bind(&cr.profile_path)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
}
|
||||
|
||||
tx.commit().await.map_err(Self::map_err)
|
||||
tx.commit().await.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
|
||||
async fn get_by_movie_id(&self, id: &MovieId) -> Result<Option<MovieProfile>, DomainError> {
|
||||
@@ -157,7 +153,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
.bind(&movie_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let row = match row {
|
||||
Some(r) => r,
|
||||
@@ -175,7 +171,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
.bind(&movie_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.into_iter()
|
||||
.map(|r| Genre {
|
||||
tmdb_id: r.try_get::<i64, _>("tmdb_id").unwrap_or(0) as u32,
|
||||
@@ -187,7 +183,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
.bind(&movie_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.into_iter()
|
||||
.map(|r| Keyword {
|
||||
tmdb_id: r.try_get::<i64, _>("tmdb_id").unwrap_or(0) as u32,
|
||||
@@ -202,7 +198,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
.bind(&movie_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.into_iter()
|
||||
.map(|r| CastMember {
|
||||
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
||||
@@ -220,7 +216,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
.bind(&movie_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.into_iter()
|
||||
.map(|r| CrewMember {
|
||||
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
||||
@@ -273,7 +269,7 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
.bind(&threshold)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
|
||||
@@ -15,10 +15,6 @@ impl SqliteProfileFieldsRepository {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -31,7 +27,7 @@ impl UserProfileFieldsRepository for SqliteProfileFieldsRepository {
|
||||
.bind(&id_str)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
@@ -53,7 +49,7 @@ impl UserProfileFieldsRepository for SqliteProfileFieldsRepository {
|
||||
.bind(&id_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
for (i, field) in fields.into_iter().enumerate() {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
@@ -68,7 +64,7 @@ impl UserProfileFieldsRepository for SqliteProfileFieldsRepository {
|
||||
.bind(position)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -16,10 +16,6 @@ impl SqliteRefreshSessionAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
DomainError::InfrastructureError(e.to_string())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
||||
async fn create(&self, session: &RefreshSession) -> Result<(), DomainError> {
|
||||
@@ -34,7 +30,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
||||
.bind(session.created_at.to_rfc3339())
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -45,7 +41,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
||||
.bind(token)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
row.map(RefreshSessionRow::into_domain).transpose()
|
||||
}
|
||||
@@ -55,7 +51,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
||||
.bind(token)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -64,7 +60,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
||||
.bind(user_id.value().to_string())
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -74,7 +70,7 @@ impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
|
||||
.bind(&now)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ use domain::{
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::models::{ReviewRow, datetime_to_str};
|
||||
use adapter_common::datetime_to_str;
|
||||
use crate::models::ReviewRow;
|
||||
|
||||
pub struct SqliteReviewRepository {
|
||||
pool: SqlitePool,
|
||||
@@ -18,10 +19,6 @@ impl SqliteReviewRepository {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -54,7 +51,7 @@ impl ReviewRepository for SqliteReviewRepository {
|
||||
.bind(review.watch_medium().map(|wm| wm.to_string()))
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -68,7 +65,7 @@ impl ReviewRepository for SqliteReviewRepository {
|
||||
.bind(&id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.map(ReviewRow::into_domain)
|
||||
.transpose()
|
||||
}
|
||||
@@ -90,7 +87,7 @@ impl ReviewRepository for SqliteReviewRepository {
|
||||
.bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -101,7 +98,7 @@ impl ReviewRepository for SqliteReviewRepository {
|
||||
.bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -114,7 +111,7 @@ impl ReviewRepository for SqliteReviewRepository {
|
||||
.bind(&uid)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.into_iter()
|
||||
.map(ReviewRow::into_domain)
|
||||
.collect()
|
||||
|
||||
@@ -18,11 +18,6 @@ impl SqliteStatsRepository {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
|
||||
async fn fetch_user_totals(&self, user_id: &str) -> Result<UserTotalsRow, DomainError> {
|
||||
sqlx::query_as::<_, UserTotalsRow>(
|
||||
"SELECT COUNT(DISTINCT movie_id) AS total,
|
||||
@@ -32,7 +27,7 @@ impl SqliteStatsRepository {
|
||||
.bind(user_id)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)
|
||||
.map_err(adapter_common::map_sqlx_error)
|
||||
}
|
||||
|
||||
async fn fetch_user_favorite_director(
|
||||
@@ -51,7 +46,7 @@ impl SqliteStatsRepository {
|
||||
.bind(user_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
@@ -70,7 +65,7 @@ impl SqliteStatsRepository {
|
||||
.bind(user_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(row)
|
||||
}
|
||||
}
|
||||
@@ -86,7 +81,7 @@ impl StatsRepository for SqliteStatsRepository {
|
||||
self.fetch_user_most_active_month(&uid)
|
||||
)?;
|
||||
|
||||
let most_active_month = most_active.map(|ym| crate::format_year_month(&ym));
|
||||
let most_active_month = most_active.map(|ym| adapter_common::format_year_month(&ym));
|
||||
|
||||
Ok(UserStats {
|
||||
total_movies: totals.total,
|
||||
@@ -128,14 +123,14 @@ impl StatsRepository for SqliteStatsRepository {
|
||||
.bind(&uid)
|
||||
.fetch_all(&self.pool)
|
||||
)
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let max_director_count = director_rows.iter().map(|d| d.count).max().unwrap_or(1);
|
||||
|
||||
let monthly_ratings = rating_rows
|
||||
.into_iter()
|
||||
.map(|r| MonthlyRating {
|
||||
month_label: crate::format_year_month(&r.month),
|
||||
month_label: adapter_common::format_year_month(&r.month),
|
||||
year_month: r.month,
|
||||
avg_rating: r.avg_rating,
|
||||
count: r.count,
|
||||
|
||||
@@ -16,10 +16,6 @@ impl SqliteUserSettingsRepository {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -33,7 +29,7 @@ impl UserSettingsRepository for SqliteUserSettingsRepository {
|
||||
.bind(&uid)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
match row {
|
||||
Some(r) => {
|
||||
@@ -68,7 +64,7 @@ impl UserSettingsRepository for SqliteUserSettingsRepository {
|
||||
})
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -84,7 +80,7 @@ impl UserFederationSettingsQuery for SqliteUserSettingsRepository {
|
||||
.bind(&uid)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
match row {
|
||||
Some(r) => {
|
||||
|
||||
@@ -19,11 +19,6 @@ impl SqliteUserRepository {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
|
||||
fn parse_role(s: &str) -> UserRole {
|
||||
match s {
|
||||
"admin" => UserRole::Admin,
|
||||
@@ -73,7 +68,7 @@ impl UserRepository for SqliteUserRepository {
|
||||
.bind(email_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
row.as_ref()
|
||||
.map(|r| Self::row_to_user(r, vec![]))
|
||||
@@ -86,7 +81,7 @@ impl UserRepository for SqliteUserRepository {
|
||||
.bind(username_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
row.as_ref()
|
||||
.map(|r| Self::row_to_user(r, vec![]))
|
||||
@@ -126,7 +121,7 @@ impl UserRepository for SqliteUserRepository {
|
||||
.bind(role)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -137,7 +132,7 @@ impl UserRepository for SqliteUserRepository {
|
||||
.bind(&id_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let Some(r) = row else { return Ok(None) };
|
||||
|
||||
@@ -194,7 +189,7 @@ impl UserRepository for SqliteUserRepository {
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.into_iter()
|
||||
.map(UserSummaryRow::into_domain)
|
||||
.collect()
|
||||
|
||||
@@ -7,12 +7,7 @@ use domain::{
|
||||
};
|
||||
use sqlx::{Row, SqlitePool};
|
||||
|
||||
use crate::models::datetime_to_str;
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
use adapter_common::datetime_to_str;
|
||||
|
||||
fn parse_uuid(s: &str) -> Result<uuid::Uuid, DomainError> {
|
||||
s.parse()
|
||||
@@ -65,7 +60,7 @@ impl WatchEventCommand for SqliteWatchEventRepository {
|
||||
.bind(&created_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -83,7 +78,7 @@ impl WatchEventCommand for SqliteWatchEventRepository {
|
||||
.bind(&id_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -105,7 +100,7 @@ impl WatchEventCommand for SqliteWatchEventRepository {
|
||||
for id in ids {
|
||||
q = q.bind(id.value().to_string());
|
||||
}
|
||||
let result = q.execute(&self.pool).await.map_err(map_err)?;
|
||||
let result = q.execute(&self.pool).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
@@ -119,7 +114,7 @@ impl WatchEventCommand for SqliteWatchEventRepository {
|
||||
.bind(&before_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
@@ -139,7 +134,7 @@ impl WatchEventQuery for SqliteWatchEventRepository {
|
||||
.bind(&uid)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
rows.iter().map(row_to_watch_event).collect()
|
||||
}
|
||||
@@ -155,7 +150,7 @@ impl WatchEventQuery for SqliteWatchEventRepository {
|
||||
.bind(&id_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
row.as_ref().map(row_to_watch_event).transpose()
|
||||
}
|
||||
@@ -175,7 +170,7 @@ impl WatchEventQuery for SqliteWatchEventRepository {
|
||||
for id in ids {
|
||||
q = q.bind(id.value().to_string());
|
||||
}
|
||||
let rows = q.fetch_all(&self.pool).await.map_err(map_err)?;
|
||||
let rows = q.fetch_all(&self.pool).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
rows.iter().map(row_to_watch_event).collect()
|
||||
}
|
||||
|
||||
@@ -197,7 +192,7 @@ impl WatchEventQuery for SqliteWatchEventRepository {
|
||||
.bind(&after_str)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.try_get(0)
|
||||
.unwrap_or(0);
|
||||
|
||||
@@ -206,16 +201,16 @@ impl WatchEventQuery for SqliteWatchEventRepository {
|
||||
}
|
||||
|
||||
fn row_to_watch_event(row: &sqlx::sqlite::SqliteRow) -> Result<WatchEvent, DomainError> {
|
||||
let id_str: &str = row.try_get("id").map_err(map_err)?;
|
||||
let user_id_str: &str = row.try_get("user_id").map_err(map_err)?;
|
||||
let movie_id_str: Option<&str> = row.try_get("movie_id").map_err(map_err)?;
|
||||
let title: String = row.try_get("title").map_err(map_err)?;
|
||||
let year: Option<i64> = row.try_get("year").map_err(map_err)?;
|
||||
let ext_id: Option<String> = row.try_get("external_metadata_id").map_err(map_err)?;
|
||||
let source_str: String = row.try_get("source").map_err(map_err)?;
|
||||
let watched_at_str: String = row.try_get("watched_at").map_err(map_err)?;
|
||||
let status_str: String = row.try_get("status").map_err(map_err)?;
|
||||
let created_at_str: String = row.try_get("created_at").map_err(map_err)?;
|
||||
let id_str: &str = row.try_get("id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let user_id_str: &str = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let movie_id_str: Option<&str> = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let title: String = row.try_get("title").map_err(adapter_common::map_sqlx_error)?;
|
||||
let year: Option<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(adapter_common::map_sqlx_error)?;
|
||||
let source_str: String = row.try_get("source").map_err(adapter_common::map_sqlx_error)?;
|
||||
let watched_at_str: String = row.try_get("watched_at").map_err(adapter_common::map_sqlx_error)?;
|
||||
let status_str: String = row.try_get("status").map_err(adapter_common::map_sqlx_error)?;
|
||||
let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let source: WatchEventSource = source_str
|
||||
.parse()
|
||||
@@ -278,7 +273,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
||||
.bind(&last_used)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -291,7 +286,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
||||
.bind(hash)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
row.as_ref().map(row_to_webhook_token).transpose()
|
||||
}
|
||||
@@ -306,7 +301,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
||||
.bind(&uid)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
rows.iter().map(row_to_webhook_token).collect()
|
||||
}
|
||||
@@ -320,7 +315,7 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
||||
.bind(&uid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(DomainError::NotFound(format!("Webhook token {id_str}")));
|
||||
@@ -337,20 +332,20 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
||||
.bind(&id_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_webhook_token(row: &sqlx::sqlite::SqliteRow) -> Result<WebhookToken, DomainError> {
|
||||
let id_str: &str = row.try_get("id").map_err(map_err)?;
|
||||
let user_id_str: &str = row.try_get("user_id").map_err(map_err)?;
|
||||
let token_hash: String = row.try_get("token_hash").map_err(map_err)?;
|
||||
let provider_str: String = row.try_get("provider").map_err(map_err)?;
|
||||
let label: Option<String> = row.try_get("label").map_err(map_err)?;
|
||||
let created_at_str: String = row.try_get("created_at").map_err(map_err)?;
|
||||
let last_used_str: Option<String> = row.try_get("last_used_at").map_err(map_err)?;
|
||||
let id_str: &str = row.try_get("id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let user_id_str: &str = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let token_hash: String = row.try_get("token_hash").map_err(adapter_common::map_sqlx_error)?;
|
||||
let provider_str: String = row.try_get("provider").map_err(adapter_common::map_sqlx_error)?;
|
||||
let label: Option<String> = row.try_get("label").map_err(adapter_common::map_sqlx_error)?;
|
||||
let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?;
|
||||
let last_used_str: Option<String> = row.try_get("last_used_at").map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let provider: WatchEventSource = provider_str
|
||||
.parse()
|
||||
|
||||
@@ -10,7 +10,8 @@ use domain::{
|
||||
};
|
||||
use sqlx::{Row, SqlitePool};
|
||||
|
||||
use crate::models::{WatchlistRow, datetime_to_str};
|
||||
use adapter_common::datetime_to_str;
|
||||
use crate::models::WatchlistRow;
|
||||
|
||||
pub struct SqliteWatchlistRepository {
|
||||
pool: SqlitePool,
|
||||
@@ -21,10 +22,6 @@ impl SqliteWatchlistRepository {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -45,7 +42,7 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
||||
.bind(&added_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -60,7 +57,7 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
||||
.bind(&mid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(DomainError::NotFound(format!(
|
||||
@@ -84,7 +81,7 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
||||
.bind(&mid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
@@ -112,13 +109,13 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let total: i64 = sqlx::query("SELECT COUNT(*) FROM watchlist_entries WHERE user_id = ?")
|
||||
.bind(&uid)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.try_get(0)
|
||||
.unwrap_or(0);
|
||||
|
||||
@@ -145,7 +142,7 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
||||
.bind(&mid)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.map_err(adapter_common::map_sqlx_error)?
|
||||
.try_get(0)
|
||||
.unwrap_or(0);
|
||||
Ok(count > 0)
|
||||
|
||||
@@ -13,12 +13,7 @@ use domain::{
|
||||
use sqlx::{Row, SqlitePool};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{parse_datetime, parse_uuid};
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
use adapter_common::{parse_datetime, parse_uuid};
|
||||
|
||||
fn status_to_str(s: &WrapUpStatus) -> &'static str {
|
||||
match s {
|
||||
@@ -87,7 +82,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
||||
.bind(&completed)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -107,7 +102,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
||||
.bind(&id_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -126,7 +121,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
||||
.bind(&id_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -142,7 +137,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
||||
.bind(&id_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
row.as_ref().map(row_to_record).transpose()
|
||||
}
|
||||
@@ -158,7 +153,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
||||
.bind(&uid)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
rows.iter().map(row_to_record).collect()
|
||||
}
|
||||
@@ -171,7 +166,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
rows.iter().map(row_to_record).collect()
|
||||
}
|
||||
@@ -200,7 +195,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
||||
.bind(&end_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
row.as_ref().map(row_to_record).transpose()
|
||||
}
|
||||
@@ -210,7 +205,7 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
||||
.bind(id.value().to_string())
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -224,21 +219,21 @@ impl WrapUpRepository for SqliteWrapUpRepository {
|
||||
.bind(&before_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
.map_err(adapter_common::map_sqlx_error)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_record(row: &sqlx::sqlite::SqliteRow) -> Result<WrapUpRecord, DomainError> {
|
||||
let id_str: String = row.try_get("id").map_err(map_err)?;
|
||||
let user_id_str: Option<String> = row.try_get("user_id").map_err(map_err)?;
|
||||
let start_date_str: String = row.try_get("start_date").map_err(map_err)?;
|
||||
let end_date_str: String = row.try_get("end_date").map_err(map_err)?;
|
||||
let status_str: String = row.try_get("status").map_err(map_err)?;
|
||||
let report_json: Option<String> = row.try_get("report_json").map_err(map_err)?;
|
||||
let error_message: Option<String> = row.try_get("error_message").map_err(map_err)?;
|
||||
let created_at_str: String = row.try_get("created_at").map_err(map_err)?;
|
||||
let completed_at_str: Option<String> = row.try_get("completed_at").map_err(map_err)?;
|
||||
let id_str: String = row.try_get("id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let user_id_str: Option<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(adapter_common::map_sqlx_error)?;
|
||||
let end_date_str: String = row.try_get("end_date").map_err(adapter_common::map_sqlx_error)?;
|
||||
let status_str: String = row.try_get("status").map_err(adapter_common::map_sqlx_error)?;
|
||||
let report_json: Option<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(adapter_common::map_sqlx_error)?;
|
||||
let created_at_str: String = row.try_get("created_at").map_err(adapter_common::map_sqlx_error)?;
|
||||
let completed_at_str: Option<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()?;
|
||||
|
||||
@@ -304,7 +299,7 @@ impl WrapUpStatsQuery for SqliteWrapUpStatsQuery {
|
||||
q = q.bind(uid);
|
||||
}
|
||||
|
||||
let rows = q.fetch_all(&self.pool).await.map_err(map_err)?;
|
||||
let rows = q.fetch_all(&self.pool).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
if rows.is_empty() {
|
||||
return Ok(vec![]);
|
||||
@@ -314,7 +309,7 @@ impl WrapUpStatsQuery for SqliteWrapUpStatsQuery {
|
||||
let mut movie_ids: Vec<String> = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for row in &rows {
|
||||
let mid: String = row.try_get("movie_id").map_err(map_err)?;
|
||||
let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?;
|
||||
if seen.insert(mid.clone()) {
|
||||
movie_ids.push(mid);
|
||||
}
|
||||
@@ -330,18 +325,18 @@ impl WrapUpStatsQuery for SqliteWrapUpStatsQuery {
|
||||
// 3) Build result
|
||||
let mut result = Vec::with_capacity(rows.len());
|
||||
for row in &rows {
|
||||
let movie_id_str: String = row.try_get("movie_id").map_err(map_err)?;
|
||||
let title: String = row.try_get("title").map_err(map_err)?;
|
||||
let release_year: i64 = row.try_get("release_year").map_err(map_err)?;
|
||||
let director: Option<String> = row.try_get("director").map_err(map_err)?;
|
||||
let poster_path: Option<String> = row.try_get("poster_path").map_err(map_err)?;
|
||||
let rating: i64 = row.try_get("rating").map_err(map_err)?;
|
||||
let watched_at_str: String = row.try_get("watched_at").map_err(map_err)?;
|
||||
let user_id_str: String = row.try_get("user_id").map_err(map_err)?;
|
||||
let runtime_minutes: Option<i32> = row.try_get("runtime_minutes").map_err(map_err)?;
|
||||
let budget_usd: Option<i64> = row.try_get("budget_usd").map_err(map_err)?;
|
||||
let movie_id_str: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let title: String = row.try_get("title").map_err(adapter_common::map_sqlx_error)?;
|
||||
let release_year: i64 = row.try_get("release_year").map_err(adapter_common::map_sqlx_error)?;
|
||||
let director: Option<String> = row.try_get("director").map_err(adapter_common::map_sqlx_error)?;
|
||||
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(adapter_common::map_sqlx_error)?;
|
||||
let watched_at_str: String = row.try_get("watched_at").map_err(adapter_common::map_sqlx_error)?;
|
||||
let user_id_str: String = row.try_get("user_id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let runtime_minutes: Option<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(adapter_common::map_sqlx_error)?;
|
||||
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 keywords = keywords_map.get(&movie_id_str).cloned().unwrap_or_default();
|
||||
@@ -411,12 +406,12 @@ async fn fetch_genres_sqlite(
|
||||
for id in movie_ids {
|
||||
q = q.bind(id);
|
||||
}
|
||||
let rows = q.fetch_all(pool).await.map_err(map_err)?;
|
||||
let rows = q.fetch_all(pool).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for row in rows {
|
||||
let mid: String = row.try_get("movie_id").map_err(map_err)?;
|
||||
let name: String = row.try_get("name").map_err(map_err)?;
|
||||
let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?;
|
||||
map.entry(mid).or_default().push(name);
|
||||
}
|
||||
Ok(map)
|
||||
@@ -437,12 +432,12 @@ async fn fetch_keywords_sqlite(
|
||||
for id in movie_ids {
|
||||
q = q.bind(id);
|
||||
}
|
||||
let rows = q.fetch_all(pool).await.map_err(map_err)?;
|
||||
let rows = q.fetch_all(pool).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for row in rows {
|
||||
let mid: String = row.try_get("movie_id").map_err(map_err)?;
|
||||
let name: String = row.try_get("name").map_err(map_err)?;
|
||||
let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?;
|
||||
map.entry(mid).or_default().push(name);
|
||||
}
|
||||
Ok(map)
|
||||
@@ -466,15 +461,15 @@ async fn fetch_cast_sqlite(
|
||||
for id in movie_ids {
|
||||
q = q.bind(id);
|
||||
}
|
||||
let rows = q.fetch_all(pool).await.map_err(map_err)?;
|
||||
let rows = q.fetch_all(pool).await.map_err(adapter_common::map_sqlx_error)?;
|
||||
|
||||
let mut map: HashMap<String, Vec<CastEntry>> = HashMap::new();
|
||||
for row in rows {
|
||||
let mid: String = row.try_get("movie_id").map_err(map_err)?;
|
||||
let name: String = row.try_get("name").map_err(map_err)?;
|
||||
let billing_order: i32 = row.try_get("billing_order").map_err(map_err)?;
|
||||
let tmdb_person_id: i64 = row.try_get("tmdb_person_id").map_err(map_err)?;
|
||||
let profile_path: Option<String> = row.try_get("profile_path").map_err(map_err)?;
|
||||
let mid: String = row.try_get("movie_id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let name: String = row.try_get("name").map_err(adapter_common::map_sqlx_error)?;
|
||||
let billing_order: i32 = row.try_get("billing_order").map_err(adapter_common::map_sqlx_error)?;
|
||||
let tmdb_person_id: i64 = row.try_get("tmdb_person_id").map_err(adapter_common::map_sqlx_error)?;
|
||||
let profile_path: Option<String> = row.try_get("profile_path").map_err(adapter_common::map_sqlx_error)?;
|
||||
map.entry(mid).or_default().push(CastEntry {
|
||||
name,
|
||||
billing_order: billing_order as u32,
|
||||
|
||||
Reference in New Issue
Block a user