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::SqlitePool;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -15,23 +18,34 @@ impl SqliteImageRefAdapter {
|
||||
|
||||
pub fn create_image_ref(pool: SqlitePool) -> (Arc<dyn ImageRefCommand>, Arc<dyn ImageRefQuery>) {
|
||||
let adapter = Arc::new(SqliteImageRefAdapter::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 SqliteImageRefAdapter {
|
||||
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 = ? WHERE avatar_path = ?")
|
||||
.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 = ? WHERE poster_path = ?")
|
||||
.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::SqlitePool;
|
||||
|
||||
#[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 SqliteImportProfileRepository {
|
||||
}
|
||||
|
||||
impl SqliteImportProfileRepository {
|
||||
pub fn new(pool: SqlitePool) -> Self { Self { pool } }
|
||||
pub fn new(pool: SqlitePool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("DB error: {:?}", e);
|
||||
@@ -95,7 +105,9 @@ impl SqliteImportProfileRepository {
|
||||
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"))
|
||||
.map_err(|e| DomainError::InfrastructureError(format!("invalid datetime '{}': {}", s, e)))
|
||||
.map_err(|e| {
|
||||
DomainError::InfrastructureError(format!("invalid datetime '{}': {}", s, e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +121,11 @@ impl ImportProfileRepository for SqliteImportProfileRepository {
|
||||
sqlx::query!(
|
||||
"INSERT OR REPLACE INTO import_profiles (id, user_id, name, field_mappings, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)",
|
||||
id, user_id, p.name, field_mappings, created_at
|
||||
id,
|
||||
user_id,
|
||||
p.name,
|
||||
field_mappings,
|
||||
created_at
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
@@ -127,18 +143,31 @@ impl ImportProfileRepository for SqliteImportProfileRepository {
|
||||
.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: Self::parse_dt(&r.created_at)?,
|
||||
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: Self::parse_dt(&r.created_at)?,
|
||||
})
|
||||
})
|
||||
}).collect()
|
||||
.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();
|
||||
let row = sqlx::query!(
|
||||
@@ -151,13 +180,21 @@ impl ImportProfileRepository for SqliteImportProfileRepository {
|
||||
|
||||
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()))?),
|
||||
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: Self::parse_dt(&r.created_at)?,
|
||||
})
|
||||
}).transpose()
|
||||
})
|
||||
.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,7 +198,9 @@ pub struct SqliteImportSessionRepository {
|
||||
}
|
||||
|
||||
impl SqliteImportSessionRepository {
|
||||
pub fn new(pool: SqlitePool) -> Self { Self { pool } }
|
||||
pub fn new(pool: SqlitePool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("DB error: {:?}", e);
|
||||
@@ -192,18 +210,33 @@ impl SqliteImportSessionRepository {
|
||||
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"))
|
||||
.map_err(|e| DomainError::InfrastructureError(format!("invalid datetime '{}': {}", s, e)))
|
||||
.map_err(|e| {
|
||||
DomainError::InfrastructureError(format!("invalid datetime '{}': {}", s, e))
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
@@ -222,15 +255,20 @@ impl SqliteImportSessionRepository {
|
||||
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())
|
||||
@@ -239,10 +277,13 @@ impl SqliteImportSessionRepository {
|
||||
|
||||
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,
|
||||
@@ -272,22 +313,35 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
||||
.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();
|
||||
let row = sqlx::query!(
|
||||
"SELECT id, user_id, parsed_data, field_mappings, row_results, created_at, expires_at
|
||||
FROM import_sessions WHERE id = ? AND user_id = ?",
|
||||
id_str, uid_str
|
||||
id_str,
|
||||
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> {
|
||||
@@ -295,7 +349,9 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
||||
let (_, field_mappings, row_results) = Self::serialize_session(s)?;
|
||||
sqlx::query!(
|
||||
"UPDATE import_sessions SET field_mappings = ?, row_results = ? WHERE id = ?",
|
||||
field_mappings, row_results, id
|
||||
field_mappings,
|
||||
row_results,
|
||||
id
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
@@ -322,10 +378,13 @@ impl ImportSessionRepository for SqliteImportSessionRepository {
|
||||
|
||||
async fn delete_expired_for_user(&self, user_id: &UserId) -> Result<(), DomainError> {
|
||||
let uid = user_id.value().to_string();
|
||||
sqlx::query!("DELETE FROM import_sessions WHERE user_id = ? AND expires_at < datetime('now')", uid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
sqlx::query!(
|
||||
"DELETE FROM import_sessions WHERE user_id = ? AND expires_at < datetime('now')",
|
||||
uid
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,11 +402,15 @@ impl MovieRepository for SqliteMovieRepository {
|
||||
&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();
|
||||
|
||||
@@ -694,10 +698,7 @@ impl DiaryRepository for SqliteMovieRepository {
|
||||
}
|
||||
|
||||
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 +801,10 @@ impl DiaryRepository for SqliteMovieRepository {
|
||||
let limit = page.limit as i64;
|
||||
let offset = page.offset as i64;
|
||||
|
||||
let total = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM reviews WHERE movie_id = ?",
|
||||
id_str
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
let total = sqlx::query_scalar!("SELECT COUNT(*) FROM reviews WHERE movie_id = ?", 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,
|
||||
@@ -843,12 +841,11 @@ impl DiaryRepository for SqliteMovieRepository {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -934,7 +931,9 @@ impl StatsRepository for SqliteMovieRepository {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wire(database_url: &str) -> anyhow::Result<(
|
||||
pub async fn wire(
|
||||
database_url: &str,
|
||||
) -> anyhow::Result<(
|
||||
sqlx::SqlitePool,
|
||||
std::sync::Arc<dyn domain::ports::MovieRepository>,
|
||||
std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
||||
@@ -946,9 +945,9 @@ pub async fn wire(database_url: &str) -> anyhow::Result<(
|
||||
std::sync::Arc<dyn domain::ports::MovieProfileRepository>,
|
||||
std::sync::Arc<dyn domain::ports::WatchlistRepository>,
|
||||
)> {
|
||||
use std::str::FromStr;
|
||||
use anyhow::Context;
|
||||
use sqlx::sqlite::SqliteConnectOptions;
|
||||
use std::str::FromStr;
|
||||
|
||||
let opts = SqliteConnectOptions::from_str(database_url)
|
||||
.context("Invalid DATABASE_URL")?
|
||||
@@ -1073,8 +1072,9 @@ mod feed_filter_tests {
|
||||
let repo = SqliteMovieRepository::new(pool);
|
||||
|
||||
let filter = FollowingFilter {
|
||||
local_user_ids: vec![uuid::Uuid::parse_str("11111111-1111-1111-1111-111111111111")
|
||||
.unwrap()],
|
||||
local_user_ids: vec![
|
||||
uuid::Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap(),
|
||||
],
|
||||
remote_actor_urls: vec!["https://remote.social/users/carol".to_string()],
|
||||
};
|
||||
let page = PageParams::new(Some(10), Some(0)).unwrap();
|
||||
@@ -1147,7 +1147,10 @@ mod feed_filter_tests {
|
||||
assert_eq!(result.total_count, 1);
|
||||
assert_eq!(result.items.len(), 1);
|
||||
assert!(result.items[0].review().is_remote());
|
||||
assert_eq!(result.items[0].user_email(), "https://remote.social/users/carol");
|
||||
assert_eq!(
|
||||
result.items[0].user_email(),
|
||||
"https://remote.social/users/carol"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1209,8 +1212,12 @@ mod diary_count_tests {
|
||||
.bind(&user_id).bind("a@b.com").bind("hash").bind("2024-01-01 00:00:00").bind("alice")
|
||||
.execute(&pool).await.unwrap();
|
||||
sqlx::query("INSERT INTO movies (id, title, release_year) VALUES (?, ?, ?)")
|
||||
.bind(&movie_id).bind("Test Movie").bind(2024i32)
|
||||
.execute(&pool).await.unwrap();
|
||||
.bind(&movie_id)
|
||||
.bind("Test Movie")
|
||||
.bind(2024i32)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Local review (remote_actor_url IS NULL)
|
||||
let r1 = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{DiaryEntry, FeedEntry, Movie, MovieSummary, Review, ReviewSource, UserSummary, WatchlistEntry, WatchlistWithMovie},
|
||||
models::{
|
||||
DiaryEntry, FeedEntry, Movie, MovieSummary, Review, ReviewSource, UserSummary,
|
||||
WatchlistEntry, WatchlistWithMovie,
|
||||
},
|
||||
value_objects::{
|
||||
Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear,
|
||||
ReviewId, UserId, WatchlistEntryId,
|
||||
|
||||
@@ -20,7 +20,10 @@ impl SqlitePersonAdapter {
|
||||
|
||||
pub fn create_person_adapter(pool: SqlitePool) -> (Arc<dyn PersonCommand>, Arc<dyn PersonQuery>) {
|
||||
let adapter = Arc::new(SqlitePersonAdapter::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 {
|
||||
@@ -70,7 +73,10 @@ impl PersonQuery for SqlitePersonAdapter {
|
||||
Ok(row.map(PersonRow::into_person))
|
||||
}
|
||||
|
||||
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> {
|
||||
let row = sqlx::query_as::<_, PersonRow>(
|
||||
"SELECT id, external_id, name, known_for_department, profile_path FROM persons WHERE external_id = ?",
|
||||
)
|
||||
@@ -83,21 +89,25 @@ impl PersonQuery for SqlitePersonAdapter {
|
||||
}
|
||||
|
||||
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 = ?",
|
||||
)
|
||||
.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 = ?")
|
||||
.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![],
|
||||
});
|
||||
};
|
||||
|
||||
let cast = sqlx::query_as::<_, CastRow>(
|
||||
|
||||
@@ -66,48 +66,80 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
|
||||
sqlx::query("DELETE FROM movie_genres WHERE movie_id = ?")
|
||||
.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 OR IGNORE INTO movie_genres (movie_id, tmdb_id, name) VALUES (?,?,?)")
|
||||
.bind(&movie_id).bind(g.tmdb_id as i64).bind(&g.name)
|
||||
.execute(&mut *tx).await.map_err(Self::map_err)?;
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO movie_genres (movie_id, tmdb_id, name) VALUES (?,?,?)",
|
||||
)
|
||||
.bind(&movie_id)
|
||||
.bind(g.tmdb_id as i64)
|
||||
.bind(&g.name)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM movie_keywords WHERE movie_id = ?")
|
||||
.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 OR IGNORE INTO movie_keywords (movie_id, tmdb_id, name) VALUES (?,?,?)")
|
||||
.bind(&movie_id).bind(k.tmdb_id as i64).bind(&k.name)
|
||||
.execute(&mut *tx).await.map_err(Self::map_err)?;
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO movie_keywords (movie_id, tmdb_id, name) VALUES (?,?,?)",
|
||||
)
|
||||
.bind(&movie_id)
|
||||
.bind(k.tmdb_id as i64)
|
||||
.bind(&k.name)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM movie_cast WHERE movie_id = ?")
|
||||
.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 OR IGNORE INTO movie_cast \
|
||||
(movie_id, tmdb_person_id, name, character, billing_order, profile_path) \
|
||||
VALUES (?,?,?,?,?,?)",
|
||||
)
|
||||
.bind(&movie_id).bind(c.tmdb_person_id as i64).bind(&c.name)
|
||||
.bind(&c.character).bind(c.billing_order as i64).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 i64)
|
||||
.bind(&c.profile_path)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM movie_crew WHERE movie_id = ?")
|
||||
.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 OR IGNORE INTO movie_crew \
|
||||
(movie_id, tmdb_person_id, name, job, department, profile_path) \
|
||||
VALUES (?,?,?,?,?,?)",
|
||||
)
|
||||
.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)
|
||||
@@ -132,7 +164,8 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let enriched_at_str: String = row.try_get("enriched_at")
|
||||
let enriched_at_str: String = row
|
||||
.try_get("enriched_at")
|
||||
.map_err(|_| DomainError::InfrastructureError("invalid enriched_at".into()))?;
|
||||
let enriched_at: DateTime<Utc> = enriched_at_str
|
||||
.parse()
|
||||
@@ -140,7 +173,9 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
|
||||
let genres = sqlx::query("SELECT tmdb_id, name FROM movie_genres WHERE movie_id = ?")
|
||||
.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::<i64, _>("tmdb_id").unwrap_or(0) as u32,
|
||||
@@ -150,7 +185,9 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
|
||||
let keywords = sqlx::query("SELECT tmdb_id, name FROM movie_keywords WHERE movie_id = ?")
|
||||
.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::<i64, _>("tmdb_id").unwrap_or(0) as u32,
|
||||
@@ -163,7 +200,9 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
FROM movie_cast WHERE movie_id = ? 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,
|
||||
@@ -179,7 +218,9 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
FROM movie_crew WHERE movie_id = ?",
|
||||
)
|
||||
.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,
|
||||
@@ -196,11 +237,19 @@ impl MovieProfileRepository for SqliteMovieProfileRepository {
|
||||
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<i64>, _>("runtime_minutes").ok().flatten().map(|v| v as u32),
|
||||
runtime_minutes: row
|
||||
.try_get::<Option<i64>, _>("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<i64>, _>("vote_count").ok().flatten().map(|v| v as u32),
|
||||
vote_count: row
|
||||
.try_get::<Option<i64>, _>("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::SqlitePool;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::ProfileField,
|
||||
ports::UserProfileFieldsRepository,
|
||||
errors::DomainError, models::ProfileField, ports::UserProfileFieldsRepository,
|
||||
value_objects::UserId,
|
||||
};
|
||||
|
||||
@@ -30,10 +28,20 @@ impl UserProfileFieldsRepository for SqliteProfileFieldsRepository {
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
|
||||
Ok(rows.into_iter().map(|r| ProfileField { name: r.name, value: r.value }).collect())
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| ProfileField {
|
||||
name: r.name,
|
||||
value: r.value,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn set_fields(&self, user_id: &UserId, fields: Vec<ProfileField>) -> Result<(), DomainError> {
|
||||
async fn set_fields(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
fields: Vec<ProfileField>,
|
||||
) -> Result<(), DomainError> {
|
||||
let id_str = user_id.value().to_string();
|
||||
|
||||
sqlx::query!("DELETE FROM user_profile_fields WHERE user_id = ?", id_str)
|
||||
|
||||
@@ -40,7 +40,9 @@ async fn list_keys_returns_both_avatar_and_poster_paths() {
|
||||
sqlx::query("INSERT INTO users VALUES ('u1','e@e.com','u','h','2024-01-01','standard',NULL,'avatars/u1')")
|
||||
.execute(&pool).await.unwrap();
|
||||
sqlx::query("INSERT INTO movies VALUES ('m1','tt1','Title',2020,'Dir','posters/m1')")
|
||||
.execute(&pool).await.unwrap();
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let adapter = SqliteImageRefAdapter::new(pool);
|
||||
let mut keys = adapter.list_keys().await.unwrap();
|
||||
@@ -54,8 +56,12 @@ async fn list_keys_excludes_nulls() {
|
||||
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||
setup(&pool).await;
|
||||
|
||||
sqlx::query("INSERT INTO users VALUES ('u1','e@e.com','u','h','2024-01-01','standard',NULL,NULL)")
|
||||
.execute(&pool).await.unwrap();
|
||||
sqlx::query(
|
||||
"INSERT INTO users VALUES ('u1','e@e.com','u','h','2024-01-01','standard',NULL,NULL)",
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let adapter = SqliteImageRefAdapter::new(pool);
|
||||
assert_eq!(adapter.list_keys().await.unwrap(), Vec::<String>::new());
|
||||
@@ -73,7 +79,9 @@ async fn swap_updates_avatar_path() {
|
||||
adapter.swap("avatars/u1", "avatars/u1.avif").await.unwrap();
|
||||
|
||||
let row: (Option<String>,) = sqlx::query_as("SELECT avatar_path FROM users WHERE id='u1'")
|
||||
.fetch_one(&pool).await.unwrap();
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(row.0.as_deref(), Some("avatars/u1.avif"));
|
||||
}
|
||||
|
||||
@@ -83,13 +91,17 @@ async fn swap_updates_poster_path() {
|
||||
setup(&pool).await;
|
||||
|
||||
sqlx::query("INSERT INTO movies VALUES ('m1','tt1','Title',2020,'Dir','posters/m1')")
|
||||
.execute(&pool).await.unwrap();
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let adapter = SqliteImageRefAdapter::new(pool.clone());
|
||||
adapter.swap("posters/m1", "posters/m1.avif").await.unwrap();
|
||||
|
||||
let row: (Option<String>,) = sqlx::query_as("SELECT poster_path FROM movies WHERE id='m1'")
|
||||
.fetch_one(&pool).await.unwrap();
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(row.0.as_deref(), Some("posters/m1.avif"));
|
||||
}
|
||||
|
||||
@@ -99,5 +111,8 @@ async fn swap_noop_when_key_not_found() {
|
||||
setup(&pool).await;
|
||||
|
||||
let adapter = SqliteImageRefAdapter::new(pool);
|
||||
adapter.swap("missing/key", "missing/key.avif").await.unwrap();
|
||||
adapter
|
||||
.swap("missing/key", "missing/key.avif")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -61,11 +61,16 @@ async fn upsert_batch_inserts_persons() {
|
||||
let pool = pool_with_schema().await;
|
||||
let adapter = SqlitePersonAdapter::new(pool.clone());
|
||||
|
||||
let persons = vec![make_person(1, "Alice", Some("Acting")), make_person(2, "Bob", Some("Directing"))];
|
||||
let persons = vec![
|
||||
make_person(1, "Alice", Some("Acting")),
|
||||
make_person(2, "Bob", Some("Directing")),
|
||||
];
|
||||
adapter.upsert_batch(&persons).await.unwrap();
|
||||
|
||||
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM persons")
|
||||
.fetch_one(&pool).await.unwrap();
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count.0, 2);
|
||||
}
|
||||
|
||||
@@ -79,7 +84,9 @@ async fn upsert_batch_is_idempotent() {
|
||||
adapter.upsert_batch(&persons).await.unwrap();
|
||||
|
||||
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM persons")
|
||||
.fetch_one(&pool).await.unwrap();
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count.0, 1);
|
||||
}
|
||||
|
||||
@@ -114,9 +121,13 @@ async fn get_credits_returns_cast_and_crew() {
|
||||
adapter.upsert_batch(&[p.clone()]).await.unwrap();
|
||||
|
||||
sqlx::query("INSERT INTO movies VALUES ('m1', 'The Film', 2020, 'Dir', NULL, NULL)")
|
||||
.execute(&pool).await.unwrap();
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query("INSERT INTO movie_cast VALUES ('m1', 7, 'Diana', 'Hero', 1, NULL)")
|
||||
.execute(&pool).await.unwrap();
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let credits = adapter.get_credits(p.id()).await.unwrap();
|
||||
assert_eq!(credits.person.name(), "Diana");
|
||||
|
||||
@@ -36,7 +36,7 @@ async fn find_by_id_returns_user_when_found() {
|
||||
let (pool, repo) = setup().await;
|
||||
let id = uuid::Uuid::new_v4();
|
||||
sqlx::query(
|
||||
"INSERT INTO users (id, email, username, password_hash, created_at) VALUES (?, ?, ?, ?, ?)"
|
||||
"INSERT INTO users (id, email, username, password_hash, created_at) VALUES (?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(id.to_string())
|
||||
.bind("test@example.com")
|
||||
@@ -88,10 +88,18 @@ async fn update_profile_clears_fields_with_none() {
|
||||
UserRole::Standard,
|
||||
);
|
||||
repo.save(&user).await.unwrap();
|
||||
repo.update_profile(user.id(), Some("bio".to_string()), Some("path".to_string()), None, None)
|
||||
repo.update_profile(
|
||||
user.id(),
|
||||
Some("bio".to_string()),
|
||||
Some("path".to_string()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
repo.update_profile(user.id(), None, None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
repo.update_profile(user.id(), None, None, None, None).await.unwrap();
|
||||
|
||||
let found = repo.find_by_id(user.id()).await.unwrap().unwrap();
|
||||
assert_eq!(found.bio(), None);
|
||||
|
||||
@@ -177,7 +177,13 @@ impl UserRepository for SqliteUserRepository {
|
||||
.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.unwrap_or_default(),
|
||||
@@ -190,7 +196,8 @@ impl UserRepository for SqliteUserRepository {
|
||||
r.banner_path,
|
||||
r.also_known_as,
|
||||
profile_fields,
|
||||
).map(Some)
|
||||
)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
async fn update_profile(
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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},
|
||||
};
|
||||
@@ -51,14 +54,13 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
||||
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 = ? AND movie_id = ?",
|
||||
)
|
||||
.bind(&uid)
|
||||
.bind(&mid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
let result =
|
||||
sqlx::query("DELETE FROM watchlist_entries WHERE user_id = ? AND movie_id = ?")
|
||||
.bind(&uid)
|
||||
.bind(&mid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(DomainError::NotFound(format!(
|
||||
@@ -76,14 +78,13 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
||||
) -> 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 = ? AND movie_id = ?",
|
||||
)
|
||||
.bind(&uid)
|
||||
.bind(&mid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
let result =
|
||||
sqlx::query("DELETE FROM watchlist_entries WHERE user_id = ? AND movie_id = ?")
|
||||
.bind(&uid)
|
||||
.bind(&mid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
@@ -113,15 +114,13 @@ impl WatchlistRepository for SqliteWatchlistRepository {
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
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)?
|
||||
.try_get(0)
|
||||
.unwrap_or(0);
|
||||
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)?
|
||||
.try_get(0)
|
||||
.unwrap_or(0);
|
||||
|
||||
let items = rows
|
||||
.into_iter()
|
||||
|
||||
Reference in New Issue
Block a user