This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
use async_trait::async_trait;
|
||||
use domain::{errors::DomainError, ports::{ImageRefCommand, ImageRefQuery}};
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
ports::{ImageRefCommand, ImageRefQuery},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -15,23 +18,34 @@ impl PostgresImageRefAdapter {
|
||||
|
||||
pub fn create_image_ref(pool: PgPool) -> (Arc<dyn ImageRefCommand>, Arc<dyn ImageRefQuery>) {
|
||||
let adapter = Arc::new(PostgresImageRefAdapter::new(pool));
|
||||
(Arc::clone(&adapter) as Arc<dyn ImageRefCommand>, adapter as Arc<dyn ImageRefQuery>)
|
||||
(
|
||||
Arc::clone(&adapter) as Arc<dyn ImageRefCommand>,
|
||||
adapter as Arc<dyn ImageRefQuery>,
|
||||
)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ImageRefCommand for PostgresImageRefAdapter {
|
||||
async fn swap(&self, old_key: &str, new_key: &str) -> Result<(), DomainError> {
|
||||
let mut tx = self.pool.begin().await
|
||||
let mut tx = self
|
||||
.pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
sqlx::query("UPDATE users SET avatar_path = $1 WHERE avatar_path = $2")
|
||||
.bind(new_key).bind(old_key)
|
||||
.execute(&mut *tx).await
|
||||
.bind(new_key)
|
||||
.bind(old_key)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
sqlx::query("UPDATE movies SET poster_path = $1 WHERE poster_path = $2")
|
||||
.bind(new_key).bind(old_key)
|
||||
.execute(&mut *tx).await
|
||||
.bind(new_key)
|
||||
.bind(old_key)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
tx.commit().await
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,20 @@ use sqlx::PgPool;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum DomainFieldJson {
|
||||
Title, ReleaseYear, Director, Rating, WatchedAt, Comment, ExternalMetadataId,
|
||||
Title,
|
||||
ReleaseYear,
|
||||
Director,
|
||||
Rating,
|
||||
WatchedAt,
|
||||
Comment,
|
||||
ExternalMetadataId,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum TransformJson {
|
||||
RatingScale(f64), DateFormat(String), Identity,
|
||||
RatingScale(f64),
|
||||
DateFormat(String),
|
||||
Identity,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -75,8 +83,8 @@ fn serialize_mappings(ms: &[FieldMapping]) -> Result<String, DomainError> {
|
||||
}
|
||||
|
||||
fn deserialize_mappings(s: &str) -> Result<Vec<FieldMapping>, DomainError> {
|
||||
let js: Vec<FieldMappingJson> = serde_json::from_str(s)
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
let js: Vec<FieldMappingJson> =
|
||||
serde_json::from_str(s).map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
Ok(js.into_iter().map(mapping_from_json).collect())
|
||||
}
|
||||
|
||||
@@ -85,7 +93,9 @@ pub struct PostgresImportProfileRepository {
|
||||
}
|
||||
|
||||
impl PostgresImportProfileRepository {
|
||||
pub fn new(pool: PgPool) -> Self { Self { pool } }
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("DB error: {:?}", e);
|
||||
@@ -115,7 +125,13 @@ impl ImportProfileRepository for PostgresImportProfileRepository {
|
||||
let uid = user_id.value().to_string();
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct Row { id: String, user_id: String, name: String, field_mappings: String, created_at: NaiveDateTime }
|
||||
struct Row {
|
||||
id: String,
|
||||
user_id: String,
|
||||
name: String,
|
||||
field_mappings: String,
|
||||
created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
let rows = sqlx::query_as::<_, Row>(
|
||||
"SELECT id, user_id, name, field_mappings, created_at FROM import_profiles WHERE user_id = $1 ORDER BY created_at DESC",
|
||||
@@ -125,25 +141,42 @@ impl ImportProfileRepository for PostgresImportProfileRepository {
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
rows.into_iter().map(|r| Ok(ImportProfile {
|
||||
id: ImportProfileId::from_uuid(
|
||||
r.id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
),
|
||||
user_id: UserId::from_uuid(
|
||||
r.user_id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
),
|
||||
name: r.name,
|
||||
field_mappings: deserialize_mappings(&r.field_mappings)?,
|
||||
created_at: r.created_at,
|
||||
})).collect()
|
||||
rows.into_iter()
|
||||
.map(|r| {
|
||||
Ok(ImportProfile {
|
||||
id: ImportProfileId::from_uuid(
|
||||
r.id.parse::<uuid::Uuid>()
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
),
|
||||
user_id: UserId::from_uuid(
|
||||
r.user_id
|
||||
.parse::<uuid::Uuid>()
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
),
|
||||
name: r.name,
|
||||
field_mappings: deserialize_mappings(&r.field_mappings)?,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn get(&self, id: &ImportProfileId, user_id: &UserId) -> Result<Option<ImportProfile>, DomainError> {
|
||||
async fn get(
|
||||
&self,
|
||||
id: &ImportProfileId,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<ImportProfile>, DomainError> {
|
||||
let id_str = id.value().to_string();
|
||||
let uid_str = user_id.value().to_string();
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct Row { id: String, user_id: String, name: String, field_mappings: String, created_at: NaiveDateTime }
|
||||
struct Row {
|
||||
id: String,
|
||||
user_id: String,
|
||||
name: String,
|
||||
field_mappings: String,
|
||||
created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
let row = sqlx::query_as::<_, Row>(
|
||||
"SELECT id, user_id, name, field_mappings, created_at FROM import_profiles WHERE id = $1 AND user_id = $2",
|
||||
@@ -153,17 +186,23 @@ impl ImportProfileRepository for PostgresImportProfileRepository {
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
row.map(|r| Ok(ImportProfile {
|
||||
id: ImportProfileId::from_uuid(
|
||||
r.id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
),
|
||||
user_id: UserId::from_uuid(
|
||||
r.user_id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
),
|
||||
name: r.name,
|
||||
field_mappings: deserialize_mappings(&r.field_mappings)?,
|
||||
created_at: r.created_at,
|
||||
})).transpose()
|
||||
row.map(|r| {
|
||||
Ok(ImportProfile {
|
||||
id: ImportProfileId::from_uuid(
|
||||
r.id.parse::<uuid::Uuid>()
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
),
|
||||
user_id: UserId::from_uuid(
|
||||
r.user_id
|
||||
.parse::<uuid::Uuid>()
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
),
|
||||
name: r.name,
|
||||
field_mappings: deserialize_mappings(&r.field_mappings)?,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &ImportProfileId) -> Result<(), DomainError> {
|
||||
|
||||
@@ -22,7 +22,13 @@ struct ParsedFileJson {
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum DomainFieldJson {
|
||||
Title, ReleaseYear, Director, Rating, WatchedAt, Comment, ExternalMetadataId,
|
||||
Title,
|
||||
ReleaseYear,
|
||||
Director,
|
||||
Rating,
|
||||
WatchedAt,
|
||||
Comment,
|
||||
ExternalMetadataId,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -41,19 +47,29 @@ struct FieldMappingJson {
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
struct ImportRowJson {
|
||||
#[serde(skip_serializing_if = "Option::is_none")] title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")] release_year: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")] director: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")] rating: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")] watched_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")] comment: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")] external_metadata_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
release_year: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
director: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
rating: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
watched_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
comment: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
external_metadata_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum RowResultJson {
|
||||
Valid(ImportRowJson),
|
||||
Invalid { errors: Vec<String>, raw: Vec<(String, String)> },
|
||||
Invalid {
|
||||
errors: Vec<String>,
|
||||
raw: Vec<(String, String)>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -182,22 +198,37 @@ pub struct PostgresImportSessionRepository {
|
||||
}
|
||||
|
||||
impl PostgresImportSessionRepository {
|
||||
pub fn new(pool: PgPool) -> Self { Self { pool } }
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("DB error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
|
||||
fn serialize_session(s: &ImportSession) -> Result<(String, Option<String>, Option<String>), DomainError> {
|
||||
let parsed = s.parsed_file.as_ref()
|
||||
.map(|f| ser(&ParsedFileJson { columns: f.columns.clone(), rows: f.rows.clone() }))
|
||||
fn serialize_session(
|
||||
s: &ImportSession,
|
||||
) -> Result<(String, Option<String>, Option<String>), DomainError> {
|
||||
let parsed = s
|
||||
.parsed_file
|
||||
.as_ref()
|
||||
.map(|f| {
|
||||
ser(&ParsedFileJson {
|
||||
columns: f.columns.clone(),
|
||||
rows: f.rows.clone(),
|
||||
})
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let mappings = s.field_mappings.as_ref()
|
||||
let mappings = s
|
||||
.field_mappings
|
||||
.as_ref()
|
||||
.map(|ms| ser(&ms.iter().map(mapping_to_json).collect::<Vec<_>>()))
|
||||
.transpose()?;
|
||||
let results = s.row_results.as_ref()
|
||||
let results = s
|
||||
.row_results
|
||||
.as_ref()
|
||||
.map(|rs| ser(&rs.iter().map(annotated_to_json).collect::<Vec<_>>()))
|
||||
.transpose()?;
|
||||
Ok((parsed, mappings, results))
|
||||
@@ -216,15 +247,20 @@ impl PostgresImportSessionRepository {
|
||||
None
|
||||
} else {
|
||||
let j: ParsedFileJson = de(&parsed_data)?;
|
||||
Some(ParsedFile { columns: j.columns, rows: j.rows })
|
||||
Some(ParsedFile {
|
||||
columns: j.columns,
|
||||
rows: j.rows,
|
||||
})
|
||||
};
|
||||
let field_mappings = field_mappings.as_deref()
|
||||
let field_mappings = field_mappings
|
||||
.as_deref()
|
||||
.map(|s| -> Result<Vec<FieldMapping>, DomainError> {
|
||||
let js: Vec<FieldMappingJson> = de(s)?;
|
||||
Ok(js.into_iter().map(mapping_from_json).collect())
|
||||
})
|
||||
.transpose()?;
|
||||
let row_results = row_results.as_deref()
|
||||
let row_results = row_results
|
||||
.as_deref()
|
||||
.map(|s| -> Result<Vec<AnnotatedRow>, DomainError> {
|
||||
let js: Vec<AnnotatedRowJson> = de(s)?;
|
||||
Ok(js.into_iter().map(annotated_from_json).collect())
|
||||
@@ -232,10 +268,13 @@ impl PostgresImportSessionRepository {
|
||||
.transpose()?;
|
||||
Ok(ImportSession {
|
||||
id: ImportSessionId::from_uuid(
|
||||
id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
id.parse::<uuid::Uuid>()
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
),
|
||||
user_id: UserId::from_uuid(
|
||||
user_id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
user_id
|
||||
.parse::<uuid::Uuid>()
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
),
|
||||
parsed_file,
|
||||
field_mappings,
|
||||
@@ -265,7 +304,11 @@ impl ImportSessionRepository for PostgresImportSessionRepository {
|
||||
.map_err(Self::map_err)
|
||||
}
|
||||
|
||||
async fn get(&self, id: &ImportSessionId, user_id: &UserId) -> Result<Option<ImportSession>, DomainError> {
|
||||
async fn get(
|
||||
&self,
|
||||
id: &ImportSessionId,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<ImportSession>, DomainError> {
|
||||
let id_str = id.value().to_string();
|
||||
let uid_str = user_id.value().to_string();
|
||||
|
||||
@@ -284,26 +327,39 @@ impl ImportSessionRepository for PostgresImportSessionRepository {
|
||||
"SELECT id, user_id, parsed_data, field_mappings, row_results, created_at, expires_at
|
||||
FROM import_sessions WHERE id = $1 AND user_id = $2",
|
||||
)
|
||||
.bind(&id_str).bind(&uid_str)
|
||||
.bind(&id_str)
|
||||
.bind(&uid_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
row.map(|r| Self::deserialize_session(
|
||||
r.id, r.user_id, r.parsed_data, r.field_mappings, r.row_results,
|
||||
r.created_at, r.expires_at,
|
||||
)).transpose()
|
||||
row.map(|r| {
|
||||
Self::deserialize_session(
|
||||
r.id,
|
||||
r.user_id,
|
||||
r.parsed_data,
|
||||
r.field_mappings,
|
||||
r.row_results,
|
||||
r.created_at,
|
||||
r.expires_at,
|
||||
)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
async fn update(&self, s: &ImportSession) -> Result<(), DomainError> {
|
||||
let id = s.id.value().to_string();
|
||||
let (_, field_mappings, row_results) = Self::serialize_session(s)?;
|
||||
sqlx::query("UPDATE import_sessions SET field_mappings = $1, row_results = $2 WHERE id = $3")
|
||||
.bind(&field_mappings).bind(&row_results).bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
sqlx::query(
|
||||
"UPDATE import_sessions SET field_mappings = $1, row_results = $2 WHERE id = $3",
|
||||
)
|
||||
.bind(&field_mappings)
|
||||
.bind(&row_results)
|
||||
.bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &ImportSessionId) -> Result<(), DomainError> {
|
||||
|
||||
@@ -388,11 +388,15 @@ impl MovieRepository for PostgresRepository {
|
||||
&self,
|
||||
page: &domain::models::collections::PageParams,
|
||||
filter: &domain::models::MovieFilter,
|
||||
) -> Result<domain::models::collections::Paginated<domain::models::MovieSummary>, DomainError> {
|
||||
) -> Result<domain::models::collections::Paginated<domain::models::MovieSummary>, DomainError>
|
||||
{
|
||||
use sqlx::Row;
|
||||
let limit = page.limit as i64;
|
||||
let offset = page.offset as i64;
|
||||
let pattern = filter.search.as_deref().map(|s| format!("%{}%", s.to_lowercase()));
|
||||
let pattern = filter
|
||||
.search
|
||||
.as_deref()
|
||||
.map(|s| format!("%{}%", s.to_lowercase()));
|
||||
let genre = filter.genre.as_deref();
|
||||
let language = filter.language.as_deref();
|
||||
|
||||
@@ -612,8 +616,7 @@ impl DiaryRepository for PostgresRepository {
|
||||
}
|
||||
|
||||
if let Some(f) = following {
|
||||
let local_params: Vec<String> =
|
||||
f.local_user_ids.iter().map(|_| next_param()).collect();
|
||||
let local_params: Vec<String> = f.local_user_ids.iter().map(|_| next_param()).collect();
|
||||
let remote_params: Vec<String> =
|
||||
f.remote_actor_urls.iter().map(|_| next_param()).collect();
|
||||
|
||||
@@ -691,10 +694,7 @@ impl DiaryRepository for PostgresRepository {
|
||||
}
|
||||
|
||||
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(Self::map_err)?;
|
||||
|
||||
let rows_q = bind_filter_params!(sqlx::query_as::<_, FeedRow>(&select_sql));
|
||||
let rows = rows_q
|
||||
@@ -800,13 +800,11 @@ impl DiaryRepository for PostgresRepository {
|
||||
let limit = page.limit as i64;
|
||||
let offset = page.offset as i64;
|
||||
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM reviews WHERE movie_id = $1",
|
||||
)
|
||||
.bind(&id_str)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
let total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM reviews WHERE movie_id = $1")
|
||||
.bind(&id_str)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
let rows = sqlx::query_as::<_, FeedRow>(
|
||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||
@@ -845,12 +843,11 @@ impl DiaryRepository for PostgresRepository {
|
||||
}
|
||||
|
||||
async fn count_local_posts(&self) -> Result<u64, DomainError> {
|
||||
let count: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM reviews WHERE remote_actor_url IS NULL"
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
let count: i64 =
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM reviews WHERE remote_actor_url IS NULL")
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
Ok(count as u64)
|
||||
}
|
||||
}
|
||||
@@ -939,7 +936,9 @@ pub fn create_profile_fields_repo(
|
||||
std::sync::Arc::new(profile_fields::PostgresProfileFieldsRepository::new(pool))
|
||||
}
|
||||
|
||||
pub async fn wire(database_url: &str) -> anyhow::Result<(
|
||||
pub async fn wire(
|
||||
database_url: &str,
|
||||
) -> anyhow::Result<(
|
||||
sqlx::PgPool,
|
||||
std::sync::Arc<dyn domain::ports::MovieRepository>,
|
||||
std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
||||
@@ -963,8 +962,10 @@ pub async fn wire(database_url: &str) -> anyhow::Result<(
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
.context("Database migration failed")?;
|
||||
|
||||
let import_session_repo = std::sync::Arc::new(PostgresImportSessionRepository::new(pool.clone()));
|
||||
let import_profile_repo = std::sync::Arc::new(PostgresImportProfileRepository::new(pool.clone()));
|
||||
let import_session_repo =
|
||||
std::sync::Arc::new(PostgresImportSessionRepository::new(pool.clone()));
|
||||
let import_profile_repo =
|
||||
std::sync::Arc::new(PostgresImportProfileRepository::new(pool.clone()));
|
||||
let movie_profile_repo = std::sync::Arc::new(PostgresMovieProfileRepository::new(pool.clone()));
|
||||
let watchlist_repo = std::sync::Arc::new(PostgresWatchlistRepository::new(pool.clone()));
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ impl PostgresPersonAdapter {
|
||||
|
||||
pub fn create_person_adapter(pool: PgPool) -> (Arc<dyn PersonCommand>, Arc<dyn PersonQuery>) {
|
||||
let adapter = Arc::new(PostgresPersonAdapter::new(pool));
|
||||
(Arc::clone(&adapter) as Arc<dyn PersonCommand>, adapter as Arc<dyn PersonQuery>)
|
||||
(
|
||||
Arc::clone(&adapter) as Arc<dyn PersonCommand>,
|
||||
adapter as Arc<dyn PersonQuery>,
|
||||
)
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
@@ -88,7 +91,10 @@ impl PersonQuery for PostgresPersonAdapter {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_by_external_id(&self, id: &ExternalPersonId) -> Result<Option<Person>, DomainError> {
|
||||
async fn get_by_external_id(
|
||||
&self,
|
||||
id: &ExternalPersonId,
|
||||
) -> Result<Option<Person>, DomainError> {
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct Row {
|
||||
id: String,
|
||||
@@ -119,21 +125,25 @@ impl PersonQuery for PostgresPersonAdapter {
|
||||
}
|
||||
|
||||
async fn get_credits(&self, id: &PersonId) -> Result<PersonCredits, DomainError> {
|
||||
let person = self.get_by_id(id).await?.ok_or_else(|| {
|
||||
DomainError::NotFound(format!("Person {} not found", id.value()))
|
||||
})?;
|
||||
let person = self
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound(format!("Person {} not found", id.value())))?;
|
||||
|
||||
let tmdb_id: Option<i64> = sqlx::query_scalar(
|
||||
"SELECT tmdb_person_id FROM persons WHERE id = $1",
|
||||
)
|
||||
.bind(id.value().to_string())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?
|
||||
.flatten();
|
||||
let tmdb_id: Option<i64> =
|
||||
sqlx::query_scalar("SELECT tmdb_person_id FROM persons WHERE id = $1")
|
||||
.bind(id.value().to_string())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?
|
||||
.flatten();
|
||||
|
||||
let Some(tmdb_id) = tmdb_id else {
|
||||
return Ok(PersonCredits { person, cast: vec![], crew: vec![] });
|
||||
return Ok(PersonCredits {
|
||||
person,
|
||||
cast: vec![],
|
||||
crew: vec![],
|
||||
});
|
||||
};
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
|
||||
@@ -65,7 +65,9 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
||||
|
||||
sqlx::query("DELETE FROM movie_genres WHERE movie_id = $1")
|
||||
.bind(&movie_id)
|
||||
.execute(&mut *tx).await.map_err(Self::map_err)?;
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
for g in &p.genres {
|
||||
sqlx::query("INSERT INTO movie_genres (movie_id, tmdb_id, name) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING")
|
||||
.bind(&movie_id).bind(g.tmdb_id as i32).bind(&g.name)
|
||||
@@ -74,7 +76,9 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
||||
|
||||
sqlx::query("DELETE FROM movie_keywords WHERE movie_id = $1")
|
||||
.bind(&movie_id)
|
||||
.execute(&mut *tx).await.map_err(Self::map_err)?;
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
for k in &p.keywords {
|
||||
sqlx::query("INSERT INTO movie_keywords (movie_id, tmdb_id, name) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING")
|
||||
.bind(&movie_id).bind(k.tmdb_id as i32).bind(&k.name)
|
||||
@@ -83,30 +87,46 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
||||
|
||||
sqlx::query("DELETE FROM movie_cast WHERE movie_id = $1")
|
||||
.bind(&movie_id)
|
||||
.execute(&mut *tx).await.map_err(Self::map_err)?;
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
for c in &p.cast {
|
||||
sqlx::query(
|
||||
"INSERT INTO movie_cast \
|
||||
(movie_id, tmdb_person_id, name, character, billing_order, profile_path) \
|
||||
VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT DO NOTHING",
|
||||
)
|
||||
.bind(&movie_id).bind(c.tmdb_person_id as i64).bind(&c.name)
|
||||
.bind(&c.character).bind(c.billing_order as i32).bind(&c.profile_path)
|
||||
.execute(&mut *tx).await.map_err(Self::map_err)?;
|
||||
.bind(&movie_id)
|
||||
.bind(c.tmdb_person_id as i64)
|
||||
.bind(&c.name)
|
||||
.bind(&c.character)
|
||||
.bind(c.billing_order as i32)
|
||||
.bind(&c.profile_path)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM movie_crew WHERE movie_id = $1")
|
||||
.bind(&movie_id)
|
||||
.execute(&mut *tx).await.map_err(Self::map_err)?;
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
for cr in &p.crew {
|
||||
sqlx::query(
|
||||
"INSERT INTO movie_crew \
|
||||
(movie_id, tmdb_person_id, name, job, department, profile_path) \
|
||||
VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT DO NOTHING",
|
||||
)
|
||||
.bind(&movie_id).bind(cr.tmdb_person_id as i64).bind(&cr.name)
|
||||
.bind(&cr.job).bind(&cr.department).bind(&cr.profile_path)
|
||||
.execute(&mut *tx).await.map_err(Self::map_err)?;
|
||||
.bind(&movie_id)
|
||||
.bind(cr.tmdb_person_id as i64)
|
||||
.bind(&cr.name)
|
||||
.bind(&cr.job)
|
||||
.bind(&cr.department)
|
||||
.bind(&cr.profile_path)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
}
|
||||
|
||||
tx.commit().await.map_err(Self::map_err)
|
||||
@@ -131,12 +151,15 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let enriched_at: DateTime<Utc> = row.try_get("enriched_at")
|
||||
let enriched_at: DateTime<Utc> = row
|
||||
.try_get("enriched_at")
|
||||
.map_err(|_| DomainError::InfrastructureError("invalid enriched_at".into()))?;
|
||||
|
||||
let genres = sqlx::query("SELECT tmdb_id, name FROM movie_genres WHERE movie_id = $1")
|
||||
.bind(&movie_id)
|
||||
.fetch_all(&self.pool).await.map_err(Self::map_err)?
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.into_iter()
|
||||
.map(|r| Genre {
|
||||
tmdb_id: r.try_get::<i32, _>("tmdb_id").unwrap_or(0) as u32,
|
||||
@@ -146,7 +169,9 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
||||
|
||||
let keywords = sqlx::query("SELECT tmdb_id, name FROM movie_keywords WHERE movie_id = $1")
|
||||
.bind(&movie_id)
|
||||
.fetch_all(&self.pool).await.map_err(Self::map_err)?
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.into_iter()
|
||||
.map(|r| Keyword {
|
||||
tmdb_id: r.try_get::<i32, _>("tmdb_id").unwrap_or(0) as u32,
|
||||
@@ -159,7 +184,9 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
||||
FROM movie_cast WHERE movie_id = $1 ORDER BY billing_order",
|
||||
)
|
||||
.bind(&movie_id)
|
||||
.fetch_all(&self.pool).await.map_err(Self::map_err)?
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.into_iter()
|
||||
.map(|r| CastMember {
|
||||
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
||||
@@ -175,7 +202,9 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
||||
FROM movie_crew WHERE movie_id = $1",
|
||||
)
|
||||
.bind(&movie_id)
|
||||
.fetch_all(&self.pool).await.map_err(Self::map_err)?
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
.into_iter()
|
||||
.map(|r| CrewMember {
|
||||
tmdb_person_id: r.try_get::<i64, _>("tmdb_person_id").unwrap_or(0) as u64,
|
||||
@@ -192,11 +221,19 @@ impl MovieProfileRepository for PostgresMovieProfileRepository {
|
||||
imdb_id: row.try_get("imdb_id").ok(),
|
||||
overview: row.try_get("overview").ok(),
|
||||
tagline: row.try_get("tagline").ok(),
|
||||
runtime_minutes: row.try_get::<Option<i32>, _>("runtime_minutes").ok().flatten().map(|v| v as u32),
|
||||
runtime_minutes: row
|
||||
.try_get::<Option<i32>, _>("runtime_minutes")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| v as u32),
|
||||
budget_usd: row.try_get("budget_usd").ok(),
|
||||
revenue_usd: row.try_get("revenue_usd").ok(),
|
||||
vote_average: row.try_get("vote_average").ok(),
|
||||
vote_count: row.try_get::<Option<i32>, _>("vote_count").ok().flatten().map(|v| v as u32),
|
||||
vote_count: row
|
||||
.try_get::<Option<i32>, _>("vote_count")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| v as u32),
|
||||
original_language: row.try_get("original_language").ok(),
|
||||
collection_name: row.try_get("collection_name").ok(),
|
||||
genres,
|
||||
|
||||
@@ -2,9 +2,7 @@ use async_trait::async_trait;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::ProfileField,
|
||||
ports::UserProfileFieldsRepository,
|
||||
errors::DomainError, models::ProfileField, ports::UserProfileFieldsRepository,
|
||||
value_objects::UserId,
|
||||
};
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ impl PostgresUserRepository {
|
||||
) -> Result<User, DomainError> {
|
||||
let id = uuid::Uuid::parse_str(&id_str)
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
let email = Email::new(email_str)
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
let email =
|
||||
Email::new(email_str).map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
let username = Username::new(username_str)
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
let hash = PasswordHash::new(hash_str)
|
||||
@@ -208,7 +208,10 @@ impl UserRepository for PostgresUserRepository {
|
||||
let Some(r) = row else { return Ok(None) };
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct FieldRow { name: String, value: String }
|
||||
struct FieldRow {
|
||||
name: String,
|
||||
value: String,
|
||||
}
|
||||
let field_rows = sqlx::query_as::<_, FieldRow>(
|
||||
"SELECT name, value FROM user_profile_fields WHERE user_id = $1 ORDER BY position ASC",
|
||||
)
|
||||
@@ -217,7 +220,13 @@ impl UserRepository for PostgresUserRepository {
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
|
||||
let profile_fields = field_rows.into_iter().map(|f| ProfileField { name: f.name, value: f.value }).collect();
|
||||
let profile_fields = field_rows
|
||||
.into_iter()
|
||||
.map(|f| ProfileField {
|
||||
name: f.name,
|
||||
value: f.value,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self::row_to_user(
|
||||
r.id,
|
||||
@@ -230,7 +239,8 @@ impl UserRepository for PostgresUserRepository {
|
||||
r.banner_path,
|
||||
r.also_known_as,
|
||||
profile_fields,
|
||||
).map(Some)
|
||||
)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
async fn update_profile(
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{WatchlistEntry, WatchlistWithMovie, collections::{PageParams, Paginated}},
|
||||
models::{
|
||||
WatchlistEntry, WatchlistWithMovie,
|
||||
collections::{PageParams, Paginated},
|
||||
},
|
||||
ports::WatchlistRepository,
|
||||
value_objects::{MovieId, UserId, WatchlistEntryId},
|
||||
};
|
||||
use sqlx::{PgPool, Row};
|
||||
|
||||
use crate::models::{parse_uuid, parse_datetime, MovieRow};
|
||||
use crate::models::{MovieRow, parse_datetime, parse_uuid};
|
||||
|
||||
pub struct PostgresWatchlistRepository {
|
||||
pool: PgPool,
|
||||
@@ -52,14 +55,13 @@ impl WatchlistRepository for PostgresWatchlistRepository {
|
||||
let uid = user_id.value().to_string();
|
||||
let mid = movie_id.value().to_string();
|
||||
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM watchlist_entries WHERE user_id = $1 AND movie_id = $2",
|
||||
)
|
||||
.bind(&uid)
|
||||
.bind(&mid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
let result =
|
||||
sqlx::query("DELETE FROM watchlist_entries WHERE user_id = $1 AND movie_id = $2")
|
||||
.bind(&uid)
|
||||
.bind(&mid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(DomainError::NotFound(format!(
|
||||
@@ -77,14 +79,13 @@ impl WatchlistRepository for PostgresWatchlistRepository {
|
||||
) -> Result<bool, DomainError> {
|
||||
let uid = user_id.value().to_string();
|
||||
let mid = movie_id.value().to_string();
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM watchlist_entries WHERE user_id = $1 AND movie_id = $2",
|
||||
)
|
||||
.bind(&uid)
|
||||
.bind(&mid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
let result =
|
||||
sqlx::query("DELETE FROM watchlist_entries WHERE user_id = $1 AND movie_id = $2")
|
||||
.bind(&uid)
|
||||
.bind(&mid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
@@ -115,30 +116,53 @@ impl WatchlistRepository for PostgresWatchlistRepository {
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM watchlist_entries WHERE user_id = $1",
|
||||
)
|
||||
.bind(&uid)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
let total: i64 =
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM watchlist_entries WHERE user_id = $1")
|
||||
.bind(&uid)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
let items = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let entry = WatchlistEntry {
|
||||
id: WatchlistEntryId::from_uuid(parse_uuid(&row.try_get::<String, _>("id").map_err(|e| DomainError::InfrastructureError(e.to_string()))?)?),
|
||||
user_id: UserId::from_uuid(parse_uuid(&row.try_get::<String, _>("user_id").map_err(|e| DomainError::InfrastructureError(e.to_string()))?)?),
|
||||
movie_id: MovieId::from_uuid(parse_uuid(&row.try_get::<String, _>("movie_id").map_err(|e| DomainError::InfrastructureError(e.to_string()))?)?),
|
||||
added_at: parse_datetime(&row.try_get::<String, _>("added_at").map_err(|e| DomainError::InfrastructureError(e.to_string()))?)?,
|
||||
id: WatchlistEntryId::from_uuid(parse_uuid(
|
||||
&row.try_get::<String, _>("id")
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
)?),
|
||||
user_id: UserId::from_uuid(parse_uuid(
|
||||
&row.try_get::<String, _>("user_id")
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
)?),
|
||||
movie_id: MovieId::from_uuid(parse_uuid(
|
||||
&row.try_get::<String, _>("movie_id")
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
)?),
|
||||
added_at: parse_datetime(
|
||||
&row.try_get::<String, _>("added_at")
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
)?,
|
||||
};
|
||||
let movie = MovieRow {
|
||||
id: row.try_get("m_id").map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
external_metadata_id: row.try_get("external_metadata_id").map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
title: row.try_get("title").map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
release_year: row.try_get("release_year").map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
director: row.try_get("director").map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
poster_path: row.try_get("poster_path").map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
id: row
|
||||
.try_get("m_id")
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
external_metadata_id: row
|
||||
.try_get("external_metadata_id")
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
title: row
|
||||
.try_get("title")
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
release_year: row
|
||||
.try_get("release_year")
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
director: row
|
||||
.try_get("director")
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
poster_path: row
|
||||
.try_get("poster_path")
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||
}
|
||||
.into_domain()?;
|
||||
Ok(WatchlistWithMovie { entry, movie })
|
||||
|
||||
Reference in New Issue
Block a user