diff options
Diffstat (limited to 'makima/src/server/handlers')
| -rw-r--r-- | makima/src/server/handlers/chains.rs | 335 |
1 files changed, 331 insertions, 4 deletions
diff --git a/makima/src/server/handlers/chains.rs b/makima/src/server/handlers/chains.rs index 6cef72c..9b32495 100644 --- a/makima/src/server/handlers/chains.rs +++ b/makima/src/server/handlers/chains.rs @@ -13,10 +13,10 @@ use utoipa::ToSchema; use uuid::Uuid; use crate::db::models::{ - AddContractDefinitionRequest, ChainContractDefinition, ChainContractDetail, - ChainDefinitionGraphResponse, ChainEditorData, ChainEvent, ChainGraphResponse, ChainSummary, - ChainWithContracts, CreateChainRequest, StartChainRequest, StartChainResponse, - UpdateChainRequest, UpdateContractDefinitionRequest, + AddChainRepositoryRequest, AddContractDefinitionRequest, ChainContractDefinition, + ChainContractDetail, ChainDefinitionGraphResponse, ChainEditorData, ChainEvent, + ChainGraphResponse, ChainRepository, ChainSummary, ChainWithContracts, CreateChainRequest, + StartChainRequest, StartChainResponse, UpdateChainRequest, UpdateContractDefinitionRequest, }; use crate::db::repository::{self, RepositoryError}; use crate::server::auth::Authenticated; @@ -1255,3 +1255,330 @@ pub async fn stop_chain( } } } + +// ============================================================================= +// Chain Repository Handlers +// ============================================================================= + +/// List repositories for a chain. +/// +/// GET /api/v1/chains/{id}/repositories +#[utoipa::path( + get, + path = "/api/v1/chains/{id}/repositories", + params( + ("id" = Uuid, Path, description = "Chain ID") + ), + responses( + (status = 200, description = "List of repositories", body = Vec<ChainRepository>), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Chain not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError) + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Chains" +)] +pub async fn list_chain_repositories( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(chain_id): Path<Uuid>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Verify ownership + match repository::get_chain_for_owner(pool, chain_id, auth.owner_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Chain not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to verify chain ownership: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + match repository::list_chain_repositories(pool, chain_id).await { + Ok(repos) => Json(repos).into_response(), + Err(e) => { + tracing::error!("Failed to list chain repositories: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Add a repository to a chain. +/// +/// POST /api/v1/chains/{id}/repositories +#[utoipa::path( + post, + path = "/api/v1/chains/{id}/repositories", + params( + ("id" = Uuid, Path, description = "Chain ID") + ), + request_body = AddChainRepositoryRequest, + responses( + (status = 201, description = "Repository added", body = ChainRepository), + (status = 400, description = "Invalid request", body = ApiError), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Chain not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError) + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Chains" +)] +pub async fn add_chain_repository( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(chain_id): Path<Uuid>, + Json(req): Json<AddChainRepositoryRequest>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Validate request + if req.name.trim().is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::new("VALIDATION_ERROR", "Repository name cannot be empty")), + ) + .into_response(); + } + + // Must have either repository_url or local_path + if req.repository_url.is_none() && req.local_path.is_none() { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::new( + "VALIDATION_ERROR", + "Repository must have either repository_url or local_path", + )), + ) + .into_response(); + } + + // Verify ownership + match repository::get_chain_for_owner(pool, chain_id, auth.owner_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Chain not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to verify chain ownership: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + match repository::add_chain_repository(pool, chain_id, &req).await { + Ok(repo) => (StatusCode::CREATED, Json(repo)).into_response(), + Err(e) => { + tracing::error!("Failed to add chain repository: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Delete a repository from a chain. +/// +/// DELETE /api/v1/chains/{chain_id}/repositories/{repository_id} +#[utoipa::path( + delete, + path = "/api/v1/chains/{chain_id}/repositories/{repository_id}", + params( + ("chain_id" = Uuid, Path, description = "Chain ID"), + ("repository_id" = Uuid, Path, description = "Repository ID") + ), + responses( + (status = 200, description = "Repository deleted"), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Chain or repository not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError) + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Chains" +)] +pub async fn delete_chain_repository( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path((chain_id, repository_id)): Path<(Uuid, Uuid)>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Verify ownership + match repository::get_chain_for_owner(pool, chain_id, auth.owner_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Chain not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to verify chain ownership: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + match repository::delete_chain_repository(pool, chain_id, repository_id).await { + Ok(true) => Json(serde_json::json!({"deleted": true})).into_response(), + Ok(false) => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Repository not found")), + ) + .into_response(), + Err(e) => { + tracing::error!("Failed to delete chain repository: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Set a repository as primary for a chain. +/// +/// PUT /api/v1/chains/{chain_id}/repositories/{repository_id}/primary +#[utoipa::path( + put, + path = "/api/v1/chains/{chain_id}/repositories/{repository_id}/primary", + params( + ("chain_id" = Uuid, Path, description = "Chain ID"), + ("repository_id" = Uuid, Path, description = "Repository ID") + ), + responses( + (status = 200, description = "Repository set as primary", body = ChainRepository), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Chain or repository not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError) + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Chains" +)] +pub async fn set_chain_repository_primary( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path((chain_id, repository_id)): Path<(Uuid, Uuid)>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Verify ownership + match repository::get_chain_for_owner(pool, chain_id, auth.owner_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Chain not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to verify chain ownership: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + // Verify repository exists for this chain + match repository::get_chain_repository(pool, chain_id, repository_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Repository not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get chain repository: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + match repository::set_chain_repository_primary(pool, chain_id, repository_id).await { + Ok(repo) => Json(repo).into_response(), + Err(e) => { + tracing::error!("Failed to set chain repository as primary: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} |
