refactor: clean up presentation layer — AppState grouping, multipart extractor, thin handlers

This commit is contained in:
2026-05-31 06:14:19 +02:00
parent 34b231a8f6
commit 2d9dd2c2d0
14 changed files with 199 additions and 258 deletions

View File

@@ -68,22 +68,7 @@ impl AssetResponse {
let meta_map = metadata let meta_map = metadata
.inner() .inner()
.iter() .iter()
.map(|(k, v)| { .map(|(k, v)| (k.clone(), serde_json::Value::from(v)))
let json_val = match v {
domain::value_objects::MetadataValue::String(s) => {
serde_json::Value::String(s.clone())
}
domain::value_objects::MetadataValue::Integer(i) => {
serde_json::json!(*i)
}
domain::value_objects::MetadataValue::Float(f) => {
serde_json::json!(*f)
}
domain::value_objects::MetadataValue::Boolean(b) => serde_json::Value::Bool(*b),
domain::value_objects::MetadataValue::Null => serde_json::Value::Null,
};
(k.clone(), json_val)
})
.collect(); .collect();
Self { Self {

View File

@@ -17,13 +17,7 @@ async fn returns_asset_with_resolved_metadata() {
checksum: Checksum::new("a".repeat(64)).unwrap(), checksum: Checksum::new("a".repeat(64)).unwrap(),
}; };
let owner = SystemId::new(); let owner = SystemId::new();
let asset = Asset::new( let asset = Asset::new(source, AssetType::Image, "image/jpeg", 1024, owner);
source,
AssetType::Image,
"image/jpeg",
1024,
owner,
);
asset_repo.save(&asset).await.unwrap(); asset_repo.save(&asset).await.unwrap();
// Add exif layer // Add exif layer

View File

@@ -77,7 +77,6 @@ impl Harness {
} }
} }
#[tokio::test] #[tokio::test]
async fn ingests_successfully() { async fn ingests_successfully() {
let h = Harness::new(); let h = Harness::new();

View File

@@ -16,15 +16,18 @@ use adapters_postgres::{
connect, run_migrations, connect, run_migrations,
}; };
use adapters_storage::{LocalFileStorage, ObjectStorageAdapter, StorageConfig, build_store}; use adapters_storage::LocalFileStorage;
use application::{ use application::{
catalog::{GetAssetHandler, GetTimelineHandler, UpdateMetadataHandler}, catalog::{GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, UpdateMetadataHandler},
identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler}, identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler},
organization::{CreateAlbumHandler, GetAlbumHandler, ManageAlbumEntriesHandler}, organization::{CreateAlbumHandler, GetAlbumHandler, ManageAlbumEntriesHandler},
storage::{IngestAssetHandler, RegisterLibraryPathHandler, RegisterVolumeHandler}, storage::{IngestAssetHandler, RegisterLibraryPathHandler, RegisterVolumeHandler},
}; };
use presentation::{routes::app_router, state::AppState}; use presentation::{
routes::app_router,
state::{AppState, CatalogHandlers, IdentityHandlers, OrganizationHandlers, StorageHandlers},
};
use crate::config::Config; use crate::config::Config;
use crate::log_event_publisher::LogEventPublisher; use crate::log_event_publisher::LogEventPublisher;
@@ -46,11 +49,6 @@ pub async fn build_app(config: &Config) -> Result<Router> {
)); ));
let get_profile_handler = Arc::new(GetProfileHandler::new(user_repo)); 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 // Repos
let album_repo = Arc::new(PostgresAlbumRepository::new(pool.clone())); let album_repo = Arc::new(PostgresAlbumRepository::new(pool.clone()));
let asset_repo = Arc::new(PostgresAssetRepository::new(pool.clone())); let asset_repo = Arc::new(PostgresAssetRepository::new(pool.clone()));
@@ -62,7 +60,7 @@ pub async fn build_app(config: &Config) -> Result<Router> {
let ledger_repo = Arc::new(PostgresUsageLedgerRepository::new(pool.clone())); let ledger_repo = Arc::new(PostgresUsageLedgerRepository::new(pool.clone()));
let event_publisher: Arc<LogEventPublisher> = Arc::new(LogEventPublisher); let event_publisher: Arc<LogEventPublisher> = Arc::new(LogEventPublisher);
// File storage for ingest // File storage
let storage_path = std::env::var("STORAGE_PATH").unwrap_or_else(|_| "./data/media".to_string()); let storage_path = std::env::var("STORAGE_PATH").unwrap_or_else(|_| "./data/media".to_string());
let file_storage: Arc<LocalFileStorage> = Arc::new(LocalFileStorage::new(&storage_path)); let file_storage: Arc<LocalFileStorage> = Arc::new(LocalFileStorage::new(&storage_path));
@@ -94,30 +92,45 @@ pub async fn build_app(config: &Config) -> Result<Router> {
metadata_repo, metadata_repo,
event_publisher, event_publisher,
)); ));
let read_asset_file_handler = Arc::new(ReadAssetFileHandler::new(asset_repo, file_storage));
// Storage handlers // Storage handlers
let register_volume_handler = Arc::new(RegisterVolumeHandler::new(volume_repo.clone())); let register_volume_handler = Arc::new(RegisterVolumeHandler::new(volume_repo.clone()));
let register_library_path_handler = let register_library_path_handler =
Arc::new(RegisterLibraryPathHandler::new(volume_repo, path_repo)); Arc::new(RegisterLibraryPathHandler::new(volume_repo, path_repo));
let state = AppState::new( let identity = IdentityHandlers {
register_handler, register: register_handler,
login_handler, login: login_handler,
get_profile_handler, get_profile: get_profile_handler,
issuer, };
storage,
create_album_handler, let catalog = CatalogHandlers {
get_album_handler, ingest_asset: ingest_asset_handler,
manage_album_entries_handler, get_asset: get_asset_handler,
ingest_asset_handler, get_timeline: get_timeline_handler,
get_asset_handler, update_metadata: update_metadata_handler,
get_timeline_handler, read_asset_file: read_asset_file_handler,
update_metadata_handler, };
register_volume_handler,
register_library_path_handler, let organization = OrganizationHandlers {
file_storage, create_album: create_album_handler,
asset_repo, get_album: get_album_handler,
); manage_album_entries: manage_album_entries_handler,
};
let storage_handlers = StorageHandlers {
register_volume: register_volume_handler,
register_library_path: register_library_path_handler,
};
let state = AppState {
identity,
catalog,
organization,
storage: storage_handlers,
token_issuer: issuer,
};
let cors = CorsLayer::new() let cors = CorsLayer::new()
.allow_origin( .allow_origin(

View File

@@ -0,0 +1,3 @@
pub const DEFAULT_PAGE_SIZE: u32 = 50;
pub const MAX_PAGE_SIZE: u32 = 1000;
pub const DEFAULT_DEVICE_ID: &str = "web";

View File

@@ -1,5 +1,7 @@
pub mod auth; pub mod auth;
pub mod json; pub mod json;
pub mod multipart_upload;
pub use auth::JwtClaims; pub use auth::JwtClaims;
pub use json::ValidatedJson; pub use json::ValidatedJson;
pub use multipart_upload::UploadedAsset;

View File

@@ -0,0 +1,74 @@
use axum::extract::Multipart;
use bytes::Bytes;
use domain::errors::DomainError;
use domain::value_objects::SystemId;
use crate::constants::DEFAULT_DEVICE_ID;
use crate::errors::AppError;
pub struct UploadedAsset {
pub filename: String,
pub data: Bytes,
pub target_path_id: SystemId,
pub client_device_id: String,
}
impl UploadedAsset {
pub async fn from_multipart(mut multipart: Multipart) -> Result<Self, AppError> {
let mut file_data: Option<Bytes> = None;
let mut filename: Option<String> = None;
let mut target_path_id: Option<uuid::Uuid> = None;
let mut client_device_id = DEFAULT_DEVICE_ID.to_string();
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| AppError::from(DomainError::Validation(e.to_string())))?
{
let name = field.name().unwrap_or("").to_string();
match name.as_str() {
"file" => {
filename = field.file_name().map(|s| s.to_string());
file_data = Some(
field
.bytes()
.await
.map_err(|e| AppError::from(DomainError::Internal(e.to_string())))?,
);
}
"target_path_id" => {
let text = field
.text()
.await
.map_err(|e| AppError::from(DomainError::Validation(e.to_string())))?;
target_path_id = Some(
text.parse::<uuid::Uuid>()
.map_err(|e| AppError::from(DomainError::Validation(e.to_string())))?,
);
}
"client_device_id" => {
client_device_id = field
.text()
.await
.map_err(|e| AppError::from(DomainError::Validation(e.to_string())))?;
}
_ => {}
}
}
let data = file_data
.ok_or_else(|| AppError::from(DomainError::Validation("Missing file field".into())))?;
let fname = filename
.ok_or_else(|| AppError::from(DomainError::Validation("Missing filename".into())))?;
let path_id = target_path_id.ok_or_else(|| {
AppError::from(DomainError::Validation("Missing target_path_id".into()))
})?;
Ok(Self {
filename: fname,
data,
target_path_id: SystemId::from_uuid(path_id),
client_device_id,
})
}
}

View File

@@ -22,7 +22,7 @@ pub async fn create_album(
title: req.title, title: req.title,
creator_id: claims.user_id, creator_id: claims.user_id,
}; };
let album = state.create_album_handler.execute(cmd).await?; let album = state.organization.create_album.execute(cmd).await?;
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
Json(AlbumResponse::from_domain(&album)), Json(AlbumResponse::from_domain(&album)),
@@ -38,7 +38,7 @@ pub async fn get_album(
album_id: SystemId::from_uuid(album_id), album_id: SystemId::from_uuid(album_id),
user_id: claims.user_id, user_id: claims.user_id,
}; };
let album = state.get_album_handler.execute(query).await?; let album = state.organization.get_album.execute(query).await?;
Ok(Json(AlbumResponse::from_domain(&album))) Ok(Json(AlbumResponse::from_domain(&album)))
} }
@@ -55,7 +55,7 @@ pub async fn add_entry(
}, },
user_id: claims.user_id, user_id: claims.user_id,
}; };
let album = state.manage_album_entries_handler.execute(cmd).await?; let album = state.organization.manage_album_entries.execute(cmd).await?;
Ok((StatusCode::OK, Json(AlbumResponse::from_domain(&album)))) Ok((StatusCode::OK, Json(AlbumResponse::from_domain(&album))))
} }
@@ -71,6 +71,6 @@ pub async fn remove_entry(
}, },
user_id: claims.user_id, user_id: claims.user_id,
}; };
let album = state.manage_album_entries_handler.execute(cmd).await?; let album = state.organization.manage_album_entries.execute(cmd).await?;
Ok(Json(AlbumResponse::from_domain(&album))) Ok(Json(AlbumResponse::from_domain(&album)))
} }

View File

@@ -1,10 +1,12 @@
use crate::{errors::AppError, extractors::JwtClaims, state::AppState}; use crate::{
use api_types::{ constants::{DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE},
requests::UpdateMetadataRequest, errors::AppError,
responses::{AssetResponse, IngestResponse, TimelineResponse}, extractors::{JwtClaims, UploadedAsset},
state::AppState,
}; };
use api_types::responses::{AssetResponse, IngestResponse, TimelineResponse};
use application::{ use application::{
catalog::{GetAssetQuery, GetTimelineQuery, UpdateMetadataCommand}, catalog::{GetAssetQuery, GetTimelineQuery, ReadAssetFileQuery, UpdateMetadataCommand},
storage::IngestAssetCommand, storage::IngestAssetCommand,
}; };
use axum::{ use axum::{
@@ -25,78 +27,25 @@ pub struct TimelineParams {
pub async fn ingest( pub async fn ingest(
State(state): State<AppState>, State(state): State<AppState>,
claims: JwtClaims, claims: JwtClaims,
mut multipart: Multipart, multipart: Multipart,
) -> Result<(StatusCode, Json<IngestResponse>), AppError> { ) -> Result<(StatusCode, Json<IngestResponse>), AppError> {
let mut file_data: Option<bytes::Bytes> = None; let upload = UploadedAsset::from_multipart(multipart).await?;
let mut filename: Option<String> = None;
let mut target_path_id: Option<uuid::Uuid> = None;
let mut client_device_id = "web".to_string();
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| AppError::from(domain::errors::DomainError::Validation(e.to_string())))?
{
let name = field.name().unwrap_or("").to_string();
match name.as_str() {
"file" => {
filename = field.file_name().map(|s| s.to_string());
let data = field.bytes().await.map_err(|e| {
AppError::from(domain::errors::DomainError::Internal(e.to_string()))
})?;
file_data = Some(data);
}
"target_path_id" => {
let text = field.text().await.map_err(|e| {
AppError::from(domain::errors::DomainError::Validation(e.to_string()))
})?;
target_path_id = Some(text.parse::<uuid::Uuid>().map_err(|e| {
AppError::from(domain::errors::DomainError::Validation(e.to_string()))
})?);
}
"client_device_id" => {
client_device_id = field.text().await.map_err(|e| {
AppError::from(domain::errors::DomainError::Validation(e.to_string()))
})?;
}
_ => {}
}
}
let data = file_data.ok_or_else(|| {
AppError::from(domain::errors::DomainError::Validation(
"Missing file field".to_string(),
))
})?;
let fname = filename.ok_or_else(|| {
AppError::from(domain::errors::DomainError::Validation(
"Missing filename".to_string(),
))
})?;
let path_id = target_path_id.ok_or_else(|| {
AppError::from(domain::errors::DomainError::Validation(
"Missing target_path_id".to_string(),
))
})?;
let file_size = data.len() as u64;
let cmd = IngestAssetCommand { let cmd = IngestAssetCommand {
uploader_id: claims.user_id, uploader_id: claims.user_id,
client_device_id, client_device_id: upload.client_device_id,
filename: fname, filename: upload.filename,
target_path_id: SystemId::from_uuid(path_id), target_path_id: upload.target_path_id,
file_size, file_size: upload.data.len() as u64,
data, data: upload.data,
}; };
let (asset, session) = state.ingest_asset_handler.execute(cmd).await?; let (asset, session) = state.catalog.ingest_asset.execute(cmd).await?;
let empty_meta = StructuredData::new();
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
Json(IngestResponse { Json(IngestResponse {
asset: AssetResponse::from_domain(&asset, &empty_meta), asset: AssetResponse::from_domain(&asset, &StructuredData::new()),
session_id: *session.session_id.as_uuid(), session_id: *session.session_id.as_uuid(),
}), }),
)) ))
@@ -111,7 +60,7 @@ pub async fn get_asset(
asset_id: SystemId::from_uuid(asset_id), asset_id: SystemId::from_uuid(asset_id),
user_id: claims.user_id, user_id: claims.user_id,
}; };
let (asset, metadata) = state.get_asset_handler.execute(query).await?; let (asset, metadata) = state.catalog.get_asset.execute(query).await?;
Ok(Json(AssetResponse::from_domain(&asset, &metadata))) Ok(Json(AssetResponse::from_domain(&asset, &metadata)))
} }
@@ -122,10 +71,10 @@ pub async fn timeline(
) -> Result<Json<TimelineResponse>, AppError> { ) -> Result<Json<TimelineResponse>, AppError> {
let query = GetTimelineQuery { let query = GetTimelineQuery {
owner_id: claims.user_id, owner_id: claims.user_id,
limit: params.limit.unwrap_or(50), limit: params.limit.unwrap_or(DEFAULT_PAGE_SIZE).min(MAX_PAGE_SIZE),
offset: params.offset.unwrap_or(0), offset: params.offset.unwrap_or(0),
}; };
let results = state.get_timeline_handler.execute(query).await?; let results = state.catalog.get_timeline.execute(query).await?;
let total = results.len(); let total = results.len();
let assets = results let assets = results
.iter() .iter()
@@ -138,26 +87,11 @@ pub async fn update_metadata(
State(state): State<AppState>, State(state): State<AppState>,
claims: JwtClaims, claims: JwtClaims,
Path((asset_id,)): Path<(uuid::Uuid,)>, Path((asset_id,)): Path<(uuid::Uuid,)>,
Json(req): Json<UpdateMetadataRequest>, Json(req): Json<api_types::requests::UpdateMetadataRequest>,
) -> Result<Json<serde_json::Value>, AppError> { ) -> Result<Json<serde_json::Value>, AppError> {
let mut data = StructuredData::new(); let mut data = StructuredData::new();
for (k, v) in req.data { for (k, v) in req.data {
let mv = match v { data.insert(k, MetadataValue::from(v));
serde_json::Value::String(s) => MetadataValue::String(s),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
MetadataValue::Integer(i)
} else if let Some(f) = n.as_f64() {
MetadataValue::Float(f)
} else {
MetadataValue::Null
}
}
serde_json::Value::Bool(b) => MetadataValue::Boolean(b),
serde_json::Value::Null => MetadataValue::Null,
_ => MetadataValue::String(v.to_string()),
};
data.insert(k, mv);
} }
let cmd = UpdateMetadataCommand { let cmd = UpdateMetadataCommand {
@@ -165,7 +99,7 @@ pub async fn update_metadata(
user_id: claims.user_id, user_id: claims.user_id,
data, data,
}; };
state.update_metadata_handler.execute(cmd).await?; state.catalog.update_metadata.execute(cmd).await?;
Ok(Json(serde_json::json!({ "status": "updated" }))) Ok(Json(serde_json::json!({ "status": "updated" })))
} }
@@ -174,33 +108,19 @@ pub async fn serve_file(
_claims: JwtClaims, _claims: JwtClaims,
Path((asset_id,)): Path<(uuid::Uuid,)>, Path((asset_id,)): Path<(uuid::Uuid,)>,
) -> Result<Response, AppError> { ) -> Result<Response, AppError> {
let asset = state let query = ReadAssetFileQuery {
.asset_repo asset_id: SystemId::from_uuid(asset_id),
.find_by_id(&SystemId::from_uuid(asset_id)) };
.await? let result = state.catalog.read_asset_file.execute(query).await?;
.ok_or_else(|| domain::errors::DomainError::NotFound("Asset not found".into()))?;
let data = state Response::builder()
.file_storage
.read_file(&asset.source_reference.relative_path)
.await?;
Ok(Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header(header::CONTENT_TYPE, &asset.mime_type) .header(header::CONTENT_TYPE, &result.mime_type)
.header(header::CONTENT_LENGTH, data.len()) .header(header::CONTENT_LENGTH, result.data.len())
.header( .header(
header::CONTENT_DISPOSITION, header::CONTENT_DISPOSITION,
format!( format!("inline; filename=\"{}\"", result.filename),
"inline; filename=\"{}\"",
asset
.source_reference
.relative_path
.rsplit('/')
.next()
.unwrap_or("file")
),
) )
.body(Body::from(data)) .body(Body::from(result.data))
.unwrap()) .map_err(|e| AppError::from(domain::errors::DomainError::Internal(e.to_string())))
} }

View File

@@ -28,7 +28,7 @@ pub async fn register(
email: req.email, email: req.email,
password: req.password, password: req.password,
}; };
let user = state.register_handler.execute(cmd).await?; let user = state.identity.register.execute(cmd).await?;
let token = state let token = state
.token_issuer .token_issuer
.issue(&user.id, "user") .issue(&user.id, "user")
@@ -59,7 +59,7 @@ pub async fn login(
email: req.email, email: req.email,
password: req.password, password: req.password,
}; };
let (user, token) = state.login_handler.execute(cmd).await?; let (user, token) = state.identity.login.execute(cmd).await?;
Ok(Json(AuthResponse { Ok(Json(AuthResponse {
token, token,
user: UserResponse::from_domain(&user), user: UserResponse::from_domain(&user),
@@ -81,6 +81,6 @@ pub async fn me(
let query = GetProfileQuery { let query = GetProfileQuery {
user_id: claims.user_id, user_id: claims.user_id,
}; };
let user = state.get_profile_handler.execute(query).await?; let user = state.identity.get_profile.execute(query).await?;
Ok(Json(UserResponse::from_domain(&user))) Ok(Json(UserResponse::from_domain(&user)))
} }

View File

@@ -17,7 +17,7 @@ pub async fn register_volume(
uri_prefix: req.uri_prefix, uri_prefix: req.uri_prefix,
is_writable: req.is_writable, is_writable: req.is_writable,
}; };
let volume = state.register_volume_handler.execute(cmd).await?; let volume = state.storage.register_volume.execute(cmd).await?;
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
Json(VolumeResponse::from_domain(&volume)), Json(VolumeResponse::from_domain(&volume)),
@@ -35,7 +35,7 @@ pub async fn register_library_path(
owner_id: SystemId::from_uuid(req.owner_id), owner_id: SystemId::from_uuid(req.owner_id),
is_ingest_destination: req.is_ingest_destination, is_ingest_destination: req.is_ingest_destination,
}; };
let path = state.register_library_path_handler.execute(cmd).await?; let path = state.storage.register_library_path.execute(cmd).await?;
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
Json(LibraryPathResponse::from_domain(&path)), Json(LibraryPathResponse::from_domain(&path)),

View File

@@ -1,27 +0,0 @@
// Example: stream a stored file as an HTTP response.
// Remove this file or replace with your own handlers.
//
// To use, add to your router:
// .route("/files/*key", get(storage_example::get_file))
//
// use axum::{
// body::Body,
// extract::{Path, State},
// http::StatusCode,
// response::IntoResponse,
// };
// use futures::StreamExt;
// use crate::state::AppState;
//
// pub async fn get_file(
// Path(key): Path<String>,
// State(state): State<AppState>,
// ) -> Result<impl IntoResponse, StatusCode> {
// let stream = state
// .storage
// .get(&key)
// .await
// .map_err(|_| StatusCode::NOT_FOUND)?;
// let body = Body::from_stream(stream.map(|r| r.map_err(|e| e.to_string())));
// Ok(body)
// }

View File

@@ -1,3 +1,4 @@
pub mod constants;
pub mod errors; pub mod errors;
pub mod extractors; pub mod extractors;
pub mod handlers; pub mod handlers;

View File

@@ -1,70 +1,47 @@
use std::sync::Arc;
use application::{ use application::{
catalog::{GetAssetHandler, GetTimelineHandler, UpdateMetadataHandler}, catalog::{GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, UpdateMetadataHandler},
identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler}, identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler},
organization::{CreateAlbumHandler, GetAlbumHandler, ManageAlbumEntriesHandler}, organization::{CreateAlbumHandler, GetAlbumHandler, ManageAlbumEntriesHandler},
storage::{IngestAssetHandler, RegisterLibraryPathHandler, RegisterVolumeHandler}, storage::{IngestAssetHandler, RegisterLibraryPathHandler, RegisterVolumeHandler},
}; };
use std::sync::Arc; use domain::ports::TokenIssuer;
use domain::ports::{AssetRepository, FileStoragePort, StoragePort, TokenIssuer}; #[derive(Clone)]
pub struct IdentityHandlers {
pub register: Arc<RegisterUserHandler>,
pub login: Arc<LoginUserHandler>,
pub get_profile: Arc<GetProfileHandler>,
}
#[derive(Clone)]
pub struct CatalogHandlers {
pub ingest_asset: Arc<IngestAssetHandler>,
pub get_asset: Arc<GetAssetHandler>,
pub get_timeline: Arc<GetTimelineHandler>,
pub update_metadata: Arc<UpdateMetadataHandler>,
pub read_asset_file: Arc<ReadAssetFileHandler>,
}
#[derive(Clone)]
pub struct OrganizationHandlers {
pub create_album: Arc<CreateAlbumHandler>,
pub get_album: Arc<GetAlbumHandler>,
pub manage_album_entries: Arc<ManageAlbumEntriesHandler>,
}
#[derive(Clone)]
pub struct StorageHandlers {
pub register_volume: Arc<RegisterVolumeHandler>,
pub register_library_path: Arc<RegisterLibraryPathHandler>,
}
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
pub register_handler: Arc<RegisterUserHandler>, pub identity: IdentityHandlers,
pub login_handler: Arc<LoginUserHandler>, pub catalog: CatalogHandlers,
pub get_profile_handler: Arc<GetProfileHandler>, pub organization: OrganizationHandlers,
pub storage: StorageHandlers,
pub token_issuer: Arc<dyn TokenIssuer>, pub token_issuer: Arc<dyn TokenIssuer>,
pub storage: Arc<dyn StoragePort>,
pub create_album_handler: Arc<CreateAlbumHandler>,
pub get_album_handler: Arc<GetAlbumHandler>,
pub manage_album_entries_handler: Arc<ManageAlbumEntriesHandler>,
pub ingest_asset_handler: Arc<IngestAssetHandler>,
pub get_asset_handler: Arc<GetAssetHandler>,
pub get_timeline_handler: Arc<GetTimelineHandler>,
pub update_metadata_handler: Arc<UpdateMetadataHandler>,
pub register_volume_handler: Arc<RegisterVolumeHandler>,
pub register_library_path_handler: Arc<RegisterLibraryPathHandler>,
pub file_storage: Arc<dyn FileStoragePort>,
pub asset_repo: Arc<dyn AssetRepository>,
}
impl AppState {
#[allow(clippy::too_many_arguments)]
pub fn new(
register_handler: Arc<RegisterUserHandler>,
login_handler: Arc<LoginUserHandler>,
get_profile_handler: Arc<GetProfileHandler>,
token_issuer: Arc<dyn TokenIssuer>,
storage: Arc<dyn StoragePort>,
create_album_handler: Arc<CreateAlbumHandler>,
get_album_handler: Arc<GetAlbumHandler>,
manage_album_entries_handler: Arc<ManageAlbumEntriesHandler>,
ingest_asset_handler: Arc<IngestAssetHandler>,
get_asset_handler: Arc<GetAssetHandler>,
get_timeline_handler: Arc<GetTimelineHandler>,
update_metadata_handler: Arc<UpdateMetadataHandler>,
register_volume_handler: Arc<RegisterVolumeHandler>,
register_library_path_handler: Arc<RegisterLibraryPathHandler>,
file_storage: Arc<dyn FileStoragePort>,
asset_repo: Arc<dyn AssetRepository>,
) -> Self {
Self {
register_handler,
login_handler,
get_profile_handler,
token_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,
file_storage,
asset_repo,
}
}
} }