feat: add presentation layer + bootstrap wiring for vertical slice

This commit is contained in:
2026-05-31 05:51:09 +02:00
parent 8c1a0e4519
commit 201eff717d
21 changed files with 726 additions and 51 deletions

View File

@@ -21,6 +21,7 @@ adapters-postgres = { path = "../adapters/postgres" }
tokio = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
dotenvy = { workspace = true }

View File

@@ -9,21 +9,32 @@ use tower_http::{
use adapters_auth::{BcryptPasswordHasher, JwtTokenIssuer};
use adapters_postgres::{PostgresUserRepository, connect, run_migrations};
use adapters_postgres::{
PostgresAlbumRepository, PostgresAssetMetadataRepository, PostgresAssetRepository,
PostgresIngestSessionRepository, PostgresLibraryPathRepository, PostgresQuotaRepository,
PostgresStorageVolumeRepository, PostgresUsageLedgerRepository, PostgresUserRepository,
connect, run_migrations,
};
use adapters_storage::{ObjectStorageAdapter, StorageConfig, build_store};
use adapters_storage::{LocalFileStorage, ObjectStorageAdapter, StorageConfig, build_store};
use application::identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler};
use application::{
catalog::{GetAssetHandler, GetTimelineHandler, UpdateMetadataHandler},
identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler},
organization::{CreateAlbumHandler, GetAlbumHandler, ManageAlbumEntriesHandler},
storage::{IngestAssetHandler, RegisterLibraryPathHandler, RegisterVolumeHandler},
};
use presentation::{routes::app_router, state::AppState};
use crate::config::Config;
use crate::log_event_publisher::LogEventPublisher;
pub async fn build_app(config: &Config) -> Result<Router> {
let pool = connect(&config.database_url).await?;
run_migrations(&pool).await?;
let user_repo = Arc::new(PostgresUserRepository::new(pool));
// Identity
let user_repo = Arc::new(PostgresUserRepository::new(pool.clone()));
let hasher = Arc::new(BcryptPasswordHasher);
let issuer = Arc::new(JwtTokenIssuer::new(&config.jwt_secret));
@@ -35,16 +46,75 @@ pub async fn build_app(config: &Config) -> Result<Router> {
));
let get_profile_handler = Arc::new(GetProfileHandler::new(user_repo));
// Object storage
let storage_cfg = StorageConfig::from_env()?;
let store = build_store(&storage_cfg)?;
let storage = Arc::new(ObjectStorageAdapter::new(store, &storage_cfg.prefix)?);
// Repos
let album_repo = Arc::new(PostgresAlbumRepository::new(pool.clone()));
let asset_repo = Arc::new(PostgresAssetRepository::new(pool.clone()));
let metadata_repo = Arc::new(PostgresAssetMetadataRepository::new(pool.clone()));
let volume_repo = Arc::new(PostgresStorageVolumeRepository::new(pool.clone()));
let path_repo = Arc::new(PostgresLibraryPathRepository::new(pool.clone()));
let session_repo = Arc::new(PostgresIngestSessionRepository::new(pool.clone()));
let quota_repo = Arc::new(PostgresQuotaRepository::new(pool.clone()));
let ledger_repo = Arc::new(PostgresUsageLedgerRepository::new(pool.clone()));
let event_publisher: Arc<LogEventPublisher> = Arc::new(LogEventPublisher);
// File storage for ingest
let storage_path = std::env::var("STORAGE_PATH").unwrap_or_else(|_| "./data/media".to_string());
let file_storage = Arc::new(LocalFileStorage::new(&storage_path));
// Album handlers
let create_album_handler = Arc::new(CreateAlbumHandler::new(album_repo.clone()));
let get_album_handler = Arc::new(GetAlbumHandler::new(album_repo.clone()));
let manage_album_entries_handler = Arc::new(ManageAlbumEntriesHandler::new(album_repo));
// Asset handlers
let ingest_asset_handler = Arc::new(IngestAssetHandler::new(
session_repo,
path_repo.clone(),
quota_repo,
ledger_repo,
asset_repo.clone(),
file_storage,
event_publisher.clone(),
));
let get_asset_handler = Arc::new(GetAssetHandler::new(
asset_repo.clone(),
metadata_repo.clone(),
));
let get_timeline_handler = Arc::new(GetTimelineHandler::new(
asset_repo.clone(),
metadata_repo.clone(),
));
let update_metadata_handler = Arc::new(UpdateMetadataHandler::new(
asset_repo,
metadata_repo,
event_publisher,
));
// Storage handlers
let register_volume_handler = Arc::new(RegisterVolumeHandler::new(volume_repo.clone()));
let register_library_path_handler =
Arc::new(RegisterLibraryPathHandler::new(volume_repo, path_repo));
let state = AppState::new(
register_handler,
login_handler,
get_profile_handler,
issuer,
storage,
create_album_handler,
get_album_handler,
manage_album_entries_handler,
ingest_asset_handler,
get_asset_handler,
get_timeline_handler,
update_metadata_handler,
register_volume_handler,
register_library_path_handler,
);
let cors = CorsLayer::new()

View File

@@ -1 +1,3 @@
pub mod config;
pub mod factory;
pub mod log_event_publisher;

View File

@@ -0,0 +1,12 @@
use async_trait::async_trait;
use domain::{errors::DomainError, events::DomainEvent, ports::EventPublisher};
pub struct LogEventPublisher;
#[async_trait]
impl EventPublisher for LogEventPublisher {
async fn publish(&self, event: DomainEvent) -> Result<(), DomainError> {
tracing::info!(?event, "domain event published");
Ok(())
}
}

View File

@@ -3,6 +3,7 @@ use tracing::info;
mod config;
mod factory;
mod log_event_publisher;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
@@ -20,8 +21,8 @@ async fn main() -> anyhow::Result<()> {
let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
let listener = tokio::net::TcpListener::bind(addr).await?;
info!("🚀 Server running at http://{addr}");
info!("📖 Scalar docs at http://{addr}/scalar");
info!("Server running at http://{addr}");
info!("Scalar docs at http://{addr}/scalar");
axum::serve(listener, app).await?;
Ok(())