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:
2026-07-10 03:50:43 +02:00
parent 12da356a40
commit dee013c7eb
99 changed files with 1262 additions and 896 deletions

View File

@@ -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 _,

View File

@@ -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],

View File

@@ -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> {