refactor: remaining MEDIUM — CQRS splits, DI Deps, profile dedup, event Value, response enum
M1: MovieRepository→MovieCommand/MovieQuery, WatchEventRepository→ WatchEventCommand/WatchEventQuery M2: goals/ and import/ use Deps structs M7: extract upload_image helper in update_profile M8: FederationDeliveryRequested activity_json String→serde_json::Value M11: UserProfileResponse uses ProfileViewData enum
This commit is contained in:
@@ -5,7 +5,7 @@ use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
ports::{
|
||||
GoalRepository, LocalApContentQuery, MovieRepository, ReviewRepository, StatsRepository,
|
||||
GoalRepository, LocalApContentQuery, MovieQuery, ReviewRepository, StatsRepository,
|
||||
UserFederationSettingsQuery,
|
||||
},
|
||||
value_objects::{MovieId, ReviewId, UserId},
|
||||
@@ -21,7 +21,7 @@ pub struct ActivityPubEventHandler {
|
||||
ap_service: Arc<ActivityPubService>,
|
||||
content_query: Arc<dyn LocalApContentQuery>,
|
||||
review_repo: Arc<dyn ReviewRepository>,
|
||||
movie_repo: Arc<dyn MovieRepository>,
|
||||
movie_repo: Arc<dyn MovieQuery>,
|
||||
goal_repo: Arc<dyn GoalRepository>,
|
||||
stats_repo: Arc<dyn StatsRepository>,
|
||||
federation_settings: Arc<dyn UserFederationSettingsQuery>,
|
||||
@@ -34,7 +34,7 @@ impl ActivityPubEventHandler {
|
||||
ap_service: Arc<ActivityPubService>,
|
||||
content_query: Arc<dyn LocalApContentQuery>,
|
||||
review_repo: Arc<dyn ReviewRepository>,
|
||||
movie_repo: Arc<dyn MovieRepository>,
|
||||
movie_repo: Arc<dyn MovieQuery>,
|
||||
goal_repo: Arc<dyn GoalRepository>,
|
||||
stats_repo: Arc<dyn StatsRepository>,
|
||||
federation_settings: Arc<dyn UserFederationSettingsQuery>,
|
||||
@@ -108,12 +108,8 @@ impl EventHandler for ActivityPubEventHandler {
|
||||
let inbox: url::Url = inbox_url
|
||||
.parse()
|
||||
.map_err(|e| DomainError::InfrastructureError(format!("bad inbox URL: {e}")))?;
|
||||
let activity: serde_json::Value =
|
||||
serde_json::from_str(activity_json).map_err(|e| {
|
||||
DomainError::InfrastructureError(format!("bad activity JSON: {e}"))
|
||||
})?;
|
||||
self.ap_service
|
||||
.deliver_to_inbox(inbox, activity, *signing_actor_id)
|
||||
.deliver_to_inbox(inbox, activity_json.clone(), *signing_actor_id)
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))
|
||||
}
|
||||
|
||||
@@ -34,12 +34,10 @@ impl k_ap::EventPublisher for FederationEventBridge {
|
||||
activity,
|
||||
signing_actor_id,
|
||||
} => {
|
||||
let json = serde_json::to_string(&activity)
|
||||
.map_err(|e| anyhow::anyhow!("serialize activity: {e}"))?;
|
||||
self.domain_publisher
|
||||
.publish(&DomainEvent::FederationDeliveryRequested {
|
||||
inbox_url: inbox.to_string(),
|
||||
activity_json: json,
|
||||
activity_json: activity,
|
||||
signing_actor_id,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -51,7 +51,7 @@ pub struct ActivityPubDeps {
|
||||
pub remote_watchlist_repo: std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
|
||||
pub remote_goal_repo: std::sync::Arc<dyn domain::ports::RemoteGoalRepository>,
|
||||
pub local_ap_content: std::sync::Arc<dyn domain::ports::LocalApContentQuery>,
|
||||
pub movie_repo: std::sync::Arc<dyn domain::ports::MovieRepository>,
|
||||
pub movie_repo: std::sync::Arc<dyn domain::ports::MovieQuery>,
|
||||
pub review_repo: std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
||||
pub diary_repo: std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
||||
pub goal_repo: std::sync::Arc<dyn domain::ports::GoalRepository>,
|
||||
|
||||
@@ -4,7 +4,7 @@ use async_trait::async_trait;
|
||||
use domain::{
|
||||
events::DomainEvent,
|
||||
models::ReviewSource,
|
||||
ports::{DiaryRepository, EventPublisher, LocalApContentQuery, MovieRepository},
|
||||
ports::{DiaryRepository, EventPublisher, LocalApContentQuery, MovieQuery},
|
||||
value_objects::{Comment, ExternalMetadataId, MovieId, Rating, ReviewId, UserId},
|
||||
};
|
||||
use k_ap::{ApContentReader, ApObjectHandler};
|
||||
@@ -16,7 +16,7 @@ use crate::urls::{actor_url, review_url};
|
||||
|
||||
pub struct ReviewObjectHandler {
|
||||
pub content_query: Arc<dyn LocalApContentQuery>,
|
||||
pub movie_repo: Arc<dyn MovieRepository>,
|
||||
pub movie_repo: Arc<dyn MovieQuery>,
|
||||
pub diary_repo: Arc<dyn DiaryRepository>,
|
||||
pub review_store: Arc<dyn RemoteReviewRepository>,
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
|
||||
@@ -72,7 +72,7 @@ pub enum EventPayload {
|
||||
},
|
||||
FederationDeliveryRequested {
|
||||
inbox_url: String,
|
||||
activity_json: String,
|
||||
activity_json: serde_json::Value,
|
||||
signing_actor_id: String,
|
||||
},
|
||||
WatchEventIngested {
|
||||
|
||||
@@ -5,14 +5,15 @@ use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
ports::{
|
||||
EventHandler, EventPublisher, MetadataClient, MovieRepository, ObjectStorage,
|
||||
EventHandler, EventPublisher, MetadataClient, MovieCommand, MovieQuery, ObjectStorage,
|
||||
PosterFetcherClient,
|
||||
},
|
||||
value_objects::{ExternalMetadataId, MovieId, PosterPath},
|
||||
};
|
||||
|
||||
pub struct PosterSyncHandler {
|
||||
movie_repository: Arc<dyn MovieRepository>,
|
||||
movie_command: Arc<dyn MovieCommand>,
|
||||
movie_query: Arc<dyn MovieQuery>,
|
||||
metadata_client: Arc<dyn MetadataClient>,
|
||||
poster_fetcher: Arc<dyn PosterFetcherClient>,
|
||||
object_storage: Arc<dyn ObjectStorage>,
|
||||
@@ -22,7 +23,8 @@ pub struct PosterSyncHandler {
|
||||
|
||||
impl PosterSyncHandler {
|
||||
pub fn new(
|
||||
movie_repository: Arc<dyn MovieRepository>,
|
||||
movie_command: Arc<dyn MovieCommand>,
|
||||
movie_query: Arc<dyn MovieQuery>,
|
||||
metadata_client: Arc<dyn MetadataClient>,
|
||||
poster_fetcher: Arc<dyn PosterFetcherClient>,
|
||||
object_storage: Arc<dyn ObjectStorage>,
|
||||
@@ -30,7 +32,8 @@ impl PosterSyncHandler {
|
||||
max_retries: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
movie_repository,
|
||||
movie_command,
|
||||
movie_query,
|
||||
metadata_client,
|
||||
poster_fetcher,
|
||||
object_storage,
|
||||
@@ -44,7 +47,7 @@ impl PosterSyncHandler {
|
||||
movie_id: MovieId,
|
||||
external_metadata_id: ExternalMetadataId,
|
||||
) -> Result<(), DomainError> {
|
||||
let mut movie = match self.movie_repository.get_movie_by_id(&movie_id).await? {
|
||||
let mut movie = match self.movie_query.get_movie_by_id(&movie_id).await? {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
tracing::warn!("Sync cancelled: Movie {} not found", movie_id.value());
|
||||
@@ -82,7 +85,7 @@ impl PosterSyncHandler {
|
||||
let poster_path = PosterPath::new(stored_path)?;
|
||||
|
||||
movie.update_poster(poster_path);
|
||||
self.movie_repository.upsert_movie(&movie).await?;
|
||||
self.movie_command.upsert_movie(&movie).await?;
|
||||
|
||||
if let Err(e) = self
|
||||
.event_publisher
|
||||
@@ -115,7 +118,7 @@ impl EventHandler for PosterSyncHandler {
|
||||
} => {
|
||||
// Only sync poster if the movie doesn't have one yet
|
||||
let already_has_poster = self
|
||||
.movie_repository
|
||||
.movie_query
|
||||
.get_movie_by_id(&MovieId::from_uuid(movie_id.value()))
|
||||
.await?
|
||||
.map(|m| m.poster_path().is_some())
|
||||
|
||||
@@ -79,7 +79,8 @@ pub fn create_profile_fields_repo(
|
||||
|
||||
pub struct PostgresWireOutput {
|
||||
pub pool: PgPool,
|
||||
pub movie: std::sync::Arc<dyn domain::ports::MovieRepository>,
|
||||
pub movie_command: std::sync::Arc<dyn domain::ports::MovieCommand>,
|
||||
pub movie_query: std::sync::Arc<dyn domain::ports::MovieQuery>,
|
||||
pub review: std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
||||
pub diary: std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
||||
pub stats: std::sync::Arc<dyn domain::ports::StatsRepository>,
|
||||
@@ -114,9 +115,12 @@ pub async fn wire(database_url: &str) -> anyhow::Result<PostgresWireOutput> {
|
||||
user_settings::PostgresUserSettingsRepository::new(pool.clone()),
|
||||
);
|
||||
|
||||
let movie_repo = std::sync::Arc::new(PostgresMovieRepository::new(pool.clone()));
|
||||
|
||||
Ok(PostgresWireOutput {
|
||||
pool: pool.clone(),
|
||||
movie: std::sync::Arc::new(PostgresMovieRepository::new(pool.clone())) as _,
|
||||
movie_command: movie_repo.clone() as _,
|
||||
movie_query: movie_repo as _,
|
||||
review: std::sync::Arc::new(PostgresReviewRepository::new(pool.clone())) as _,
|
||||
diary: std::sync::Arc::new(PostgresDiaryRepository::new(pool.clone())) as _,
|
||||
stats: std::sync::Arc::new(PostgresStatsRepository::new(pool.clone())) as _,
|
||||
|
||||
@@ -5,7 +5,7 @@ use domain::{
|
||||
Movie, MovieFilter, MovieSummary,
|
||||
collections::{PageParams, Paginated},
|
||||
},
|
||||
ports::MovieRepository,
|
||||
ports::{MovieCommand, MovieQuery},
|
||||
value_objects::{ExternalMetadataId, MovieId, MovieTitle, ReleaseYear},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
@@ -28,7 +28,51 @@ impl PostgresMovieRepository {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MovieRepository for PostgresMovieRepository {
|
||||
impl MovieCommand for PostgresMovieRepository {
|
||||
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError> {
|
||||
let id = movie.id().value().to_string();
|
||||
let external_metadata_id = movie.external_metadata_id().map(|e| e.value().to_string());
|
||||
let title = movie.title().value();
|
||||
let release_year = movie.release_year().value() as i64;
|
||||
let director = movie.director();
|
||||
let poster_path = movie.poster_path().map(|p| p.value().to_string());
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO movies (id, external_metadata_id, title, release_year, director, poster_path)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
external_metadata_id = excluded.external_metadata_id,
|
||||
title = excluded.title,
|
||||
release_year = excluded.release_year,
|
||||
director = excluded.director,
|
||||
poster_path = excluded.poster_path",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&external_metadata_id)
|
||||
.bind(title)
|
||||
.bind(release_year)
|
||||
.bind(director)
|
||||
.bind(&poster_path)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError> {
|
||||
let id = movie_id.value().to_string();
|
||||
sqlx::query("DELETE FROM movies WHERE id = $1")
|
||||
.bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MovieQuery for PostgresMovieRepository {
|
||||
async fn get_movie_by_external_id(
|
||||
&self,
|
||||
external_metadata_id: &ExternalMetadataId,
|
||||
@@ -81,47 +125,6 @@ impl MovieRepository for PostgresMovieRepository {
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError> {
|
||||
let id = movie.id().value().to_string();
|
||||
let external_metadata_id = movie.external_metadata_id().map(|e| e.value().to_string());
|
||||
let title = movie.title().value();
|
||||
let release_year = movie.release_year().value() as i64;
|
||||
let director = movie.director();
|
||||
let poster_path = movie.poster_path().map(|p| p.value().to_string());
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO movies (id, external_metadata_id, title, release_year, director, poster_path)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
external_metadata_id = excluded.external_metadata_id,
|
||||
title = excluded.title,
|
||||
release_year = excluded.release_year,
|
||||
director = excluded.director,
|
||||
poster_path = excluded.poster_path",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&external_metadata_id)
|
||||
.bind(title)
|
||||
.bind(release_year)
|
||||
.bind(director)
|
||||
.bind(&poster_path)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError> {
|
||||
let id = movie_id.value().to_string();
|
||||
sqlx::query("DELETE FROM movies WHERE id = $1")
|
||||
.bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn existing_external_ids(
|
||||
&self,
|
||||
ids: &[ExternalMetadataId],
|
||||
|
||||
@@ -2,7 +2,7 @@ use async_trait::async_trait;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{PersistedWatchEvent, WatchEvent, WatchEventSource, WatchEventStatus, WebhookToken},
|
||||
ports::{WatchEventRepository, WebhookTokenRepository},
|
||||
ports::{WatchEventCommand, WatchEventQuery, WebhookTokenRepository},
|
||||
value_objects::{MovieId, UserId, WatchEventId, WebhookTokenId},
|
||||
};
|
||||
use sqlx::{PgPool, Row};
|
||||
@@ -27,7 +27,7 @@ impl PostgresWatchEventRepository {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WatchEventRepository for PostgresWatchEventRepository {
|
||||
impl WatchEventCommand for PostgresWatchEventRepository {
|
||||
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError> {
|
||||
let id = event.id().value().to_string();
|
||||
let user_id = event.user_id().value().to_string();
|
||||
@@ -75,6 +75,41 @@ impl WatchEventRepository for PostgresWatchEventRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_status_batch(
|
||||
&self,
|
||||
ids: &[WatchEventId],
|
||||
status: WatchEventStatus,
|
||||
) -> Result<u64, DomainError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let id_strs: Vec<String> = ids.iter().map(|id| id.value().to_string()).collect();
|
||||
let status_str = status.to_string();
|
||||
let result = sqlx::query("UPDATE watch_events SET status = $1 WHERE id = ANY($2)")
|
||||
.bind(&status_str)
|
||||
.bind(&id_strs)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
async fn delete_non_pending_older_than(
|
||||
&self,
|
||||
before: chrono::NaiveDateTime,
|
||||
) -> Result<u64, DomainError> {
|
||||
let result =
|
||||
sqlx::query("DELETE FROM watch_events WHERE status != 'pending' AND created_at < $1")
|
||||
.bind(before)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WatchEventQuery for PostgresWatchEventRepository {
|
||||
async fn list_pending(&self, user_id: &UserId) -> Result<Vec<WatchEvent>, DomainError> {
|
||||
let uid = user_id.value().to_string();
|
||||
|
||||
@@ -135,25 +170,6 @@ impl WatchEventRepository for PostgresWatchEventRepository {
|
||||
rows.iter().map(row_to_watch_event).collect()
|
||||
}
|
||||
|
||||
async fn update_status_batch(
|
||||
&self,
|
||||
ids: &[WatchEventId],
|
||||
status: WatchEventStatus,
|
||||
) -> Result<u64, DomainError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let id_strs: Vec<String> = ids.iter().map(|id| id.value().to_string()).collect();
|
||||
let status_str = status.to_string();
|
||||
let result = sqlx::query("UPDATE watch_events SET status = $1 WHERE id = ANY($2)")
|
||||
.bind(&status_str)
|
||||
.bind(&id_strs)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
async fn find_duplicate(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
@@ -175,19 +191,6 @@ impl WatchEventRepository for PostgresWatchEventRepository {
|
||||
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
async fn delete_non_pending_older_than(
|
||||
&self,
|
||||
before: chrono::NaiveDateTime,
|
||||
) -> Result<u64, DomainError> {
|
||||
let result =
|
||||
sqlx::query("DELETE FROM watch_events WHERE status != 'pending' AND created_at < $1")
|
||||
.bind(before)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_watch_event(row: &sqlx::postgres::PgRow) -> Result<WatchEvent, DomainError> {
|
||||
|
||||
@@ -75,7 +75,8 @@ pub async fn migrate(pool: &SqlitePool) -> Result<(), domain::errors::DomainErro
|
||||
|
||||
pub struct SqliteWireOutput {
|
||||
pub pool: SqlitePool,
|
||||
pub movie: std::sync::Arc<dyn domain::ports::MovieRepository>,
|
||||
pub movie_command: std::sync::Arc<dyn domain::ports::MovieCommand>,
|
||||
pub movie_query: std::sync::Arc<dyn domain::ports::MovieQuery>,
|
||||
pub review: std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
||||
pub diary: std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
||||
pub stats: std::sync::Arc<dyn domain::ports::StatsRepository>,
|
||||
@@ -119,9 +120,12 @@ pub async fn wire(database_url: &str) -> anyhow::Result<SqliteWireOutput> {
|
||||
pool.clone(),
|
||||
));
|
||||
|
||||
let movie_repo = std::sync::Arc::new(SqliteMovieRepository::new(pool.clone()));
|
||||
|
||||
Ok(SqliteWireOutput {
|
||||
pool: pool.clone(),
|
||||
movie: std::sync::Arc::new(SqliteMovieRepository::new(pool.clone())) as _,
|
||||
movie_command: movie_repo.clone() as _,
|
||||
movie_query: movie_repo as _,
|
||||
review: std::sync::Arc::new(SqliteReviewRepository::new(pool.clone())) as _,
|
||||
diary: std::sync::Arc::new(SqliteDiaryRepository::new(pool.clone())) as _,
|
||||
stats: std::sync::Arc::new(SqliteStatsRepository::new(pool.clone())) as _,
|
||||
|
||||
@@ -5,7 +5,7 @@ use domain::{
|
||||
Movie, MovieFilter, MovieSummary,
|
||||
collections::{PageParams, Paginated},
|
||||
},
|
||||
ports::MovieRepository,
|
||||
ports::{MovieCommand, MovieQuery},
|
||||
value_objects::{ExternalMetadataId, MovieId, MovieTitle, ReleaseYear},
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
@@ -28,7 +28,51 @@ impl SqliteMovieRepository {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MovieRepository for SqliteMovieRepository {
|
||||
impl MovieCommand for SqliteMovieRepository {
|
||||
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError> {
|
||||
let id = movie.id().value().to_string();
|
||||
let external_metadata_id = movie.external_metadata_id().map(|e| e.value().to_string());
|
||||
let title = movie.title().value();
|
||||
let release_year = movie.release_year().value() as i64;
|
||||
let director = movie.director();
|
||||
let poster_path = movie.poster_path().map(|p| p.value().to_string());
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO movies (id, external_metadata_id, title, release_year, director, poster_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
external_metadata_id = excluded.external_metadata_id,
|
||||
title = excluded.title,
|
||||
release_year = excluded.release_year,
|
||||
director = excluded.director,
|
||||
poster_path = excluded.poster_path",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&external_metadata_id)
|
||||
.bind(title)
|
||||
.bind(release_year)
|
||||
.bind(director)
|
||||
.bind(&poster_path)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError> {
|
||||
let id = movie_id.value().to_string();
|
||||
sqlx::query("DELETE FROM movies WHERE id = ?")
|
||||
.bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MovieQuery for SqliteMovieRepository {
|
||||
async fn get_movie_by_external_id(
|
||||
&self,
|
||||
external_metadata_id: &ExternalMetadataId,
|
||||
@@ -81,47 +125,6 @@ impl MovieRepository for SqliteMovieRepository {
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn upsert_movie(&self, movie: &Movie) -> Result<(), DomainError> {
|
||||
let id = movie.id().value().to_string();
|
||||
let external_metadata_id = movie.external_metadata_id().map(|e| e.value().to_string());
|
||||
let title = movie.title().value();
|
||||
let release_year = movie.release_year().value() as i64;
|
||||
let director = movie.director();
|
||||
let poster_path = movie.poster_path().map(|p| p.value().to_string());
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO movies (id, external_metadata_id, title, release_year, director, poster_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
external_metadata_id = excluded.external_metadata_id,
|
||||
title = excluded.title,
|
||||
release_year = excluded.release_year,
|
||||
director = excluded.director,
|
||||
poster_path = excluded.poster_path",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&external_metadata_id)
|
||||
.bind(title)
|
||||
.bind(release_year)
|
||||
.bind(director)
|
||||
.bind(&poster_path)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_movie(&self, movie_id: &MovieId) -> Result<(), DomainError> {
|
||||
let id = movie_id.value().to_string();
|
||||
sqlx::query("DELETE FROM movies WHERE id = ?")
|
||||
.bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn existing_external_ids(
|
||||
&self,
|
||||
ids: &[ExternalMetadataId],
|
||||
|
||||
@@ -2,7 +2,7 @@ use async_trait::async_trait;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{PersistedWatchEvent, WatchEvent, WatchEventSource, WatchEventStatus, WebhookToken},
|
||||
ports::{WatchEventRepository, WebhookTokenRepository},
|
||||
ports::{WatchEventCommand, WatchEventQuery, WebhookTokenRepository},
|
||||
value_objects::{MovieId, UserId, WatchEventId, WebhookTokenId},
|
||||
};
|
||||
use sqlx::{Row, SqlitePool};
|
||||
@@ -38,7 +38,7 @@ impl SqliteWatchEventRepository {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WatchEventRepository for SqliteWatchEventRepository {
|
||||
impl WatchEventCommand for SqliteWatchEventRepository {
|
||||
async fn save(&self, event: &WatchEvent) -> Result<(), DomainError> {
|
||||
let id = event.id().value().to_string();
|
||||
let user_id = event.user_id().value().to_string();
|
||||
@@ -88,6 +88,44 @@ impl WatchEventRepository for SqliteWatchEventRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_status_batch(
|
||||
&self,
|
||||
ids: &[WatchEventId],
|
||||
status: WatchEventStatus,
|
||||
) -> Result<u64, DomainError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let placeholders: Vec<&str> = ids.iter().map(|_| "?").collect();
|
||||
let sql = format!(
|
||||
"UPDATE watch_events SET status = ? WHERE id IN ({})",
|
||||
placeholders.join(",")
|
||||
);
|
||||
let mut q = sqlx::query(&sql).bind(status.to_string());
|
||||
for id in ids {
|
||||
q = q.bind(id.value().to_string());
|
||||
}
|
||||
let result = q.execute(&self.pool).await.map_err(map_err)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
async fn delete_non_pending_older_than(
|
||||
&self,
|
||||
before: chrono::NaiveDateTime,
|
||||
) -> Result<u64, DomainError> {
|
||||
let before_str = datetime_to_str(&before);
|
||||
let result =
|
||||
sqlx::query("DELETE FROM watch_events WHERE status != 'pending' AND created_at < ?")
|
||||
.bind(&before_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WatchEventQuery for SqliteWatchEventRepository {
|
||||
async fn list_pending(&self, user_id: &UserId) -> Result<Vec<WatchEvent>, DomainError> {
|
||||
let uid = user_id.value().to_string();
|
||||
|
||||
@@ -141,27 +179,6 @@ impl WatchEventRepository for SqliteWatchEventRepository {
|
||||
rows.iter().map(row_to_watch_event).collect()
|
||||
}
|
||||
|
||||
async fn update_status_batch(
|
||||
&self,
|
||||
ids: &[WatchEventId],
|
||||
status: WatchEventStatus,
|
||||
) -> Result<u64, DomainError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let placeholders: Vec<&str> = ids.iter().map(|_| "?").collect();
|
||||
let sql = format!(
|
||||
"UPDATE watch_events SET status = ? WHERE id IN ({})",
|
||||
placeholders.join(",")
|
||||
);
|
||||
let mut q = sqlx::query(&sql).bind(status.to_string());
|
||||
for id in ids {
|
||||
q = q.bind(id.value().to_string());
|
||||
}
|
||||
let result = q.execute(&self.pool).await.map_err(map_err)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
async fn find_duplicate(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
@@ -186,20 +203,6 @@ impl WatchEventRepository for SqliteWatchEventRepository {
|
||||
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
async fn delete_non_pending_older_than(
|
||||
&self,
|
||||
before: chrono::NaiveDateTime,
|
||||
) -> Result<u64, DomainError> {
|
||||
let before_str = datetime_to_str(&before);
|
||||
let result =
|
||||
sqlx::query("DELETE FROM watch_events WHERE status != 'pending' AND created_at < ?")
|
||||
.bind(&before_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_err)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_watch_event(row: &sqlx::sqlite::SqliteRow) -> Result<WatchEvent, DomainError> {
|
||||
|
||||
Reference in New Issue
Block a user