refactor: fix HIGH+MEDIUM architectural violations from code review
HIGH: fix watch_medium data-loss bug, standardize error handling on ApiError, fix dep direction (rss/template-askama no longer dep on application), extract ImageFetcher port (remove reqwest from app layer), move event construction from save_review to use case, extract infra-wiring crate (DbPool/EventBusBackend dedup), deduplicate presentation helpers (encode_error, export streaming, multipart parsing) MEDIUM: split LocalApContentQuery god-trait 10→3 methods, dedup movie resolution orchestration, add RemoteActorDto/PersonDto mappers, move AppConfig to infra-wiring, fix SocialQueryPort Uuid→UserId, replace stringly-typed api-types with domain enums, move count_reviews_in_year to StatsRepository, dedup event publisher cfg blocks, extract should_enrich, move group_by_month to application, dedup count_local_posts, add FederationFlags Default, TUI input helper + ShowError rename + typed auth errors, api-types cleanup (UserSettingsDto/UserProfileBase/PreviewRowData) 102 files changed, -681 lines net
This commit is contained in:
@@ -5,9 +5,9 @@ edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["sqlite", "sqlite-federation"]
|
||||
sqlite = ["dep:sqlite", "dep:sqlite-event-queue", "dep:sqlite-search"]
|
||||
postgres = ["dep:postgres", "dep:postgres-event-queue", "dep:postgres-search"]
|
||||
nats = ["dep:nats"]
|
||||
sqlite = ["dep:sqlite", "dep:sqlite-event-queue", "dep:sqlite-search", "infra-wiring/sqlite"]
|
||||
postgres = ["dep:postgres", "dep:postgres-event-queue", "dep:postgres-search", "infra-wiring/postgres"]
|
||||
nats = ["dep:nats", "infra-wiring/nats"]
|
||||
federation = ["application/federation"]
|
||||
sqlite-federation = ["sqlite", "dep:sqlite-federation", "dep:activitypub", "federation"]
|
||||
postgres-federation = ["postgres", "dep:postgres-federation", "dep:activitypub", "federation"]
|
||||
@@ -31,6 +31,7 @@ importer = { workspace = true }
|
||||
image-converter = { workspace = true }
|
||||
nats = { workspace = true, optional = true }
|
||||
sqlx = { workspace = true }
|
||||
infra-wiring = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
|
||||
# Optional — database backends
|
||||
|
||||
@@ -2,20 +2,20 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use domain::ports::{
|
||||
ImageRefCommand, ImageRefQuery, ImportSessionRepository, LocalApContentQuery,
|
||||
MovieDeduplicator, MovieProfileRepository, MovieRepository, PersonCommand, PersonQuery,
|
||||
SearchCommand, UserRepository, WatchEventRepository,
|
||||
DiaryRepository, GoalRepository, ImageRefCommand, ImageRefQuery, ImportSessionRepository,
|
||||
LocalApContentQuery, MovieDeduplicator, MovieProfileRepository, MovieRepository, PersonCommand,
|
||||
PersonQuery, ReviewRepository, SearchCommand, StatsRepository, UserRepository,
|
||||
WatchEventRepository,
|
||||
};
|
||||
|
||||
pub enum DbPool {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Sqlite(sqlx::SqlitePool),
|
||||
#[cfg(feature = "postgres")]
|
||||
Postgres(sqlx::PgPool),
|
||||
}
|
||||
pub use infra_wiring::DbPool;
|
||||
|
||||
pub struct WorkerDbOutput {
|
||||
pub movie: Arc<dyn MovieRepository>,
|
||||
pub review: Arc<dyn ReviewRepository>,
|
||||
pub diary: Arc<dyn DiaryRepository>,
|
||||
pub stats: Arc<dyn StatsRepository>,
|
||||
pub goal: Arc<dyn GoalRepository>,
|
||||
pub user: Arc<dyn UserRepository>,
|
||||
pub import_session: Arc<dyn ImportSessionRepository>,
|
||||
pub movie_profile: Arc<dyn MovieProfileRepository>,
|
||||
@@ -50,6 +50,10 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
|
||||
Arc::new(postgres::PostgresWatchEventRepository::new(w.pool.clone()));
|
||||
Ok(WorkerDbOutput {
|
||||
movie: w.movie,
|
||||
review: w.review,
|
||||
diary: w.diary,
|
||||
stats: w.stats,
|
||||
goal: w.goal,
|
||||
user: w.user,
|
||||
import_session: w.import_session,
|
||||
movie_profile: w.movie_profile,
|
||||
@@ -84,6 +88,10 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
|
||||
Arc::new(sqlite::SqliteWatchEventRepository::new(w.pool.clone()));
|
||||
Ok(WorkerDbOutput {
|
||||
movie: w.movie,
|
||||
review: w.review,
|
||||
diary: w.diary,
|
||||
stats: w.stats,
|
||||
goal: w.goal,
|
||||
user: w.user,
|
||||
import_session: w.import_session,
|
||||
movie_profile: w.movie_profile,
|
||||
|
||||
@@ -5,31 +5,7 @@ use anyhow::Context;
|
||||
use domain::ports::{EventConsumer, EventPublisher};
|
||||
|
||||
use crate::db::DbPool;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum EventBusBackend {
|
||||
Db,
|
||||
#[cfg(feature = "nats")]
|
||||
Nats,
|
||||
}
|
||||
|
||||
impl EventBusBackend {
|
||||
pub fn from_env() -> anyhow::Result<Self> {
|
||||
match std::env::var("EVENT_BUS_BACKEND")
|
||||
.unwrap_or_else(|_| "db".to_string())
|
||||
.as_str()
|
||||
{
|
||||
"db" => Ok(Self::Db),
|
||||
#[cfg(feature = "nats")]
|
||||
"nats" => Ok(Self::Nats),
|
||||
#[cfg(not(feature = "nats"))]
|
||||
"nats" => {
|
||||
anyhow::bail!("EVENT_BUS_BACKEND=nats requires the nats feature to be compiled in")
|
||||
}
|
||||
other => anyhow::bail!("unknown EVENT_BUS_BACKEND={other}, expected 'db' or 'nats'"),
|
||||
}
|
||||
}
|
||||
}
|
||||
use infra_wiring::EventBusBackend;
|
||||
|
||||
pub async fn create(
|
||||
db_pool: &DbPool,
|
||||
|
||||
@@ -38,8 +38,23 @@ async fn main() -> anyhow::Result<()> {
|
||||
let image_ref_query = Arc::clone(&db.image_ref_query);
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
let (fed_ap_content, fed_user_repo, base_url, allow_registration) = (
|
||||
let (
|
||||
fed_ap_content,
|
||||
fed_movie_repo,
|
||||
fed_review_repo,
|
||||
fed_diary_repo,
|
||||
fed_goal_repo,
|
||||
fed_stats_repo,
|
||||
fed_user_repo,
|
||||
base_url,
|
||||
allow_registration,
|
||||
) = (
|
||||
Arc::clone(&db.ap_content),
|
||||
Arc::clone(&db.movie),
|
||||
Arc::clone(&db.review),
|
||||
Arc::clone(&db.diary),
|
||||
Arc::clone(&db.goal),
|
||||
Arc::clone(&db.stats),
|
||||
Arc::clone(&db.user),
|
||||
app_config.base_url.clone(),
|
||||
app_config.allow_registration,
|
||||
@@ -93,6 +108,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
Ok(client) => {
|
||||
tracing::info!("TMDb enrichment enabled");
|
||||
let client = Arc::new(client);
|
||||
let image_fetcher = poster_fetcher::create_image_fetcher()?;
|
||||
let handler = Arc::new(application::movies::MovieEnrichmentHandler::new(
|
||||
Arc::clone(&client) as Arc<dyn MovieEnrichmentClient>,
|
||||
Arc::clone(&movie),
|
||||
@@ -100,6 +116,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
Arc::clone(&person_command),
|
||||
Arc::clone(&search_command),
|
||||
Arc::clone(&object_storage),
|
||||
image_fetcher,
|
||||
)) as Arc<dyn EventHandler>;
|
||||
let person_enrichment_arc = Arc::clone(&client) as Arc<dyn PersonEnrichmentClient>;
|
||||
let person_handler = Arc::new(application::person::PersonEnrichmentHandler::new(
|
||||
@@ -189,48 +206,38 @@ async fn main() -> anyhow::Result<()> {
|
||||
&object_storage,
|
||||
))) as Arc<dyn EventHandler>;
|
||||
|
||||
#[cfg(not(feature = "federation"))]
|
||||
{
|
||||
let search_cleanup = Arc::new(SearchCleanupHandler::new(
|
||||
Arc::clone(&search_command),
|
||||
Arc::clone(&person_query),
|
||||
)) as Arc<dyn EventHandler>;
|
||||
let discovery_indexer = Arc::new(MovieDiscoveryIndexer::new(
|
||||
Arc::clone(&movie),
|
||||
Arc::clone(&search_command),
|
||||
)) as Arc<dyn EventHandler>;
|
||||
let wrapup_handler =
|
||||
Arc::new(application::wrapup::event_handler::WrapUpEventHandler::new(
|
||||
Arc::clone(&wrapup_repo),
|
||||
Arc::clone(&event_publisher),
|
||||
Arc::clone(&wrapup_stats),
|
||||
)) as Arc<dyn EventHandler>;
|
||||
let reindex_handler = Arc::new(SearchReindexHandler::new(ReindexSearchDeps {
|
||||
movie: Arc::clone(&movie),
|
||||
movie_profile: Arc::clone(&movie_profile),
|
||||
search_command: Arc::clone(&search_command),
|
||||
person_command: Arc::clone(&person_command),
|
||||
person_query: Arc::clone(&person_query),
|
||||
})) as Arc<dyn EventHandler>;
|
||||
let mut h = vec![
|
||||
poster,
|
||||
cleanup,
|
||||
search_cleanup,
|
||||
discovery_indexer,
|
||||
wrapup_handler,
|
||||
reindex_handler,
|
||||
];
|
||||
if let Some(e) = enrichment_handler {
|
||||
h.push(e);
|
||||
}
|
||||
if let Some(e) = person_enrichment_handler {
|
||||
h.push(e);
|
||||
}
|
||||
if let Some((ref conv_handler, _)) = conversion {
|
||||
h.push(Arc::clone(conv_handler));
|
||||
}
|
||||
h
|
||||
}
|
||||
let search_cleanup = Arc::new(SearchCleanupHandler::new(
|
||||
Arc::clone(&search_command),
|
||||
Arc::clone(&person_query),
|
||||
)) as Arc<dyn EventHandler>;
|
||||
|
||||
let discovery_indexer = Arc::new(MovieDiscoveryIndexer::new(
|
||||
Arc::clone(&movie),
|
||||
Arc::clone(&search_command),
|
||||
)) as Arc<dyn EventHandler>;
|
||||
|
||||
let wrapup_handler = Arc::new(application::wrapup::event_handler::WrapUpEventHandler::new(
|
||||
Arc::clone(&wrapup_repo),
|
||||
Arc::clone(&event_publisher),
|
||||
Arc::clone(&wrapup_stats),
|
||||
)) as Arc<dyn EventHandler>;
|
||||
|
||||
let reindex_handler = Arc::new(SearchReindexHandler::new(ReindexSearchDeps {
|
||||
movie: Arc::clone(&movie),
|
||||
movie_profile: Arc::clone(&movie_profile),
|
||||
search_command: Arc::clone(&search_command),
|
||||
person_command: Arc::clone(&person_command),
|
||||
person_query: Arc::clone(&person_query),
|
||||
})) as Arc<dyn EventHandler>;
|
||||
|
||||
let mut h = vec![
|
||||
poster,
|
||||
cleanup,
|
||||
search_cleanup,
|
||||
discovery_indexer,
|
||||
wrapup_handler,
|
||||
reindex_handler,
|
||||
];
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
{
|
||||
@@ -243,6 +250,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
remote_watchlist_repo: fed_remote_watchlist_repo,
|
||||
remote_goal_repo: Arc::clone(&remote_goal),
|
||||
local_ap_content: fed_ap_content,
|
||||
movie_repo: fed_movie_repo,
|
||||
review_repo: fed_review_repo,
|
||||
diary_repo: fed_diary_repo,
|
||||
goal_repo: fed_goal_repo,
|
||||
stats_repo: fed_stats_repo,
|
||||
user_repo: fed_user_repo,
|
||||
base_url,
|
||||
allow_registration,
|
||||
@@ -251,54 +263,24 @@ async fn main() -> anyhow::Result<()> {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let ap_event_handler = ap_wire.event_handler;
|
||||
let backfill = Arc::new(follow_backfill_handler::FollowBackfillHandler {
|
||||
ap_service: ap_wire.service,
|
||||
}) as Arc<dyn EventHandler>;
|
||||
|
||||
let search_cleanup = Arc::new(SearchCleanupHandler::new(
|
||||
Arc::clone(&search_command),
|
||||
Arc::clone(&person_query),
|
||||
)) as Arc<dyn EventHandler>;
|
||||
let discovery_indexer = Arc::new(MovieDiscoveryIndexer::new(
|
||||
Arc::clone(&movie),
|
||||
Arc::clone(&search_command),
|
||||
)) as Arc<dyn EventHandler>;
|
||||
tracing::info!("federation event handler registered");
|
||||
let wrapup_handler =
|
||||
Arc::new(application::wrapup::event_handler::WrapUpEventHandler::new(
|
||||
Arc::clone(&wrapup_repo),
|
||||
Arc::clone(&event_publisher),
|
||||
Arc::clone(&wrapup_stats),
|
||||
)) as Arc<dyn EventHandler>;
|
||||
let reindex_handler = Arc::new(SearchReindexHandler::new(ReindexSearchDeps {
|
||||
movie: Arc::clone(&movie),
|
||||
movie_profile: Arc::clone(&movie_profile),
|
||||
search_command: Arc::clone(&search_command),
|
||||
person_command: Arc::clone(&person_command),
|
||||
person_query: Arc::clone(&person_query),
|
||||
})) as Arc<dyn EventHandler>;
|
||||
let mut h = vec![
|
||||
poster,
|
||||
cleanup,
|
||||
ap_event_handler,
|
||||
backfill,
|
||||
search_cleanup,
|
||||
discovery_indexer,
|
||||
wrapup_handler,
|
||||
reindex_handler,
|
||||
];
|
||||
if let Some(e) = enrichment_handler {
|
||||
h.push(e);
|
||||
}
|
||||
if let Some(e) = person_enrichment_handler {
|
||||
h.push(e);
|
||||
}
|
||||
if let Some((ref conv_handler, _)) = conversion {
|
||||
h.push(Arc::clone(conv_handler));
|
||||
}
|
||||
h
|
||||
h.push(ap_wire.event_handler);
|
||||
h.push(Arc::new(follow_backfill_handler::FollowBackfillHandler {
|
||||
ap_service: ap_wire.service,
|
||||
}) as Arc<dyn EventHandler>);
|
||||
}
|
||||
|
||||
if let Some(e) = enrichment_handler {
|
||||
h.push(e);
|
||||
}
|
||||
if let Some(e) = person_enrichment_handler {
|
||||
h.push(e);
|
||||
}
|
||||
if let Some((ref conv_handler, _)) = conversion {
|
||||
h.push(Arc::clone(conv_handler));
|
||||
}
|
||||
|
||||
h
|
||||
};
|
||||
|
||||
// ── Run ───────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user