summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/chains.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-06 20:06:30 +0000
committersoryu <soryu@soryu.co>2026-02-06 20:15:27 +0000
commit1b692b8cde4a888c8a35af69231f181b57bf5619 (patch)
tree74ce25ce6ee5fb4536b53404e1a0ae923e85c30d /makima/src/server/handlers/chains.rs
parent139be135c2086d725e4f040e744bb25acd436549 (diff)
downloadsoryu-1b692b8cde4a888c8a35af69231f181b57bf5619.tar.gz
soryu-1b692b8cde4a888c8a35af69231f181b57bf5619.zip
Fix: Cleanup old chain code
Diffstat (limited to 'makima/src/server/handlers/chains.rs')
-rw-r--r--makima/src/server/handlers/chains.rs1644
1 files changed, 0 insertions, 1644 deletions
diff --git a/makima/src/server/handlers/chains.rs b/makima/src/server/handlers/chains.rs
deleted file mode 100644
index b8716ca..0000000
--- a/makima/src/server/handlers/chains.rs
+++ /dev/null
@@ -1,1644 +0,0 @@
-//! HTTP handlers for chain CRUD operations.
-//!
-//! Chains are DAGs (directed acyclic graphs) of contracts for multi-contract orchestration.
-
-use axum::{
- extract::{Path, Query, State},
- http::StatusCode,
- response::IntoResponse,
- Json,
-};
-use serde::Deserialize;
-use utoipa::ToSchema;
-use uuid::Uuid;
-
-use crate::db::models::{
- AddChainRepositoryRequest, AddContractDefinitionRequest, ChainContractDefinition,
- ChainContractDetail, ChainDefinitionGraphResponse, ChainEditorData, ChainEvent,
- ChainGraphResponse, ChainRepository, ChainSummary, ChainWithContracts, CreateChainRequest,
- InitChainRequest, InitChainResponse, StartChainRequest, StartChainResponse, UpdateChainRequest,
- UpdateContractDefinitionRequest,
-};
-use crate::db::repository::{self, RepositoryError};
-use crate::server::auth::Authenticated;
-use crate::server::messages::ApiError;
-use crate::server::state::SharedState;
-
-// =============================================================================
-// Query Parameters
-// =============================================================================
-
-/// Query parameters for listing chains.
-#[derive(Debug, Deserialize, ToSchema)]
-pub struct ListChainsQuery {
- /// Filter by status (active, completed, archived)
- pub status: Option<String>,
- /// Maximum number of results
- #[serde(default = "default_limit")]
- pub limit: i32,
- /// Offset for pagination
- #[serde(default)]
- pub offset: i32,
-}
-
-fn default_limit() -> i32 {
- 50
-}
-
-// =============================================================================
-// Response Types
-// =============================================================================
-
-/// Response for listing chains.
-#[derive(Debug, serde::Serialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ChainListResponse {
- pub chains: Vec<ChainSummary>,
- pub total: i64,
-}
-
-// =============================================================================
-// Handlers
-// =============================================================================
-
-/// List chains for the authenticated user.
-///
-/// GET /api/v1/chains
-#[utoipa::path(
- get,
- path = "/api/v1/chains",
- responses(
- (status = 200, description = "List of chains", body = ChainListResponse),
- (status = 401, description = "Unauthorized", 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_chains(
- State(state): State<SharedState>,
- Authenticated(auth): Authenticated,
- Query(query): Query<ListChainsQuery>,
-) -> impl IntoResponse {
- let Some(ref pool) = state.db_pool else {
- return (
- StatusCode::SERVICE_UNAVAILABLE,
- Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
- )
- .into_response();
- };
-
- match repository::list_chains_for_owner(pool, auth.owner_id).await {
- Ok(mut chains) => {
- // Apply filters
- if let Some(status) = &query.status {
- chains.retain(|c| c.status == *status);
- }
- // Apply pagination
- let total = chains.len() as i64;
- let chains: Vec<_> = chains
- .into_iter()
- .skip(query.offset as usize)
- .take(query.limit as usize)
- .collect();
- Json(ChainListResponse { chains, total }).into_response()
- }
- Err(e) => {
- tracing::error!("Failed to list chains: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Create a new chain with contracts.
-///
-/// POST /api/v1/chains
-#[utoipa::path(
- post,
- path = "/api/v1/chains",
- request_body = CreateChainRequest,
- responses(
- (status = 201, description = "Chain created"),
- (status = 400, description = "Invalid request", body = ApiError),
- (status = 401, description = "Unauthorized", 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 create_chain(
- State(state): State<SharedState>,
- Authenticated(auth): Authenticated,
- Json(req): Json<CreateChainRequest>,
-) -> 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 the request
- if req.name.trim().is_empty() {
- return (
- StatusCode::BAD_REQUEST,
- Json(ApiError::new("VALIDATION_ERROR", "Chain name cannot be empty")),
- )
- .into_response();
- }
-
- match repository::create_chain_for_owner(pool, auth.owner_id, req).await {
- Ok(chain) => (StatusCode::CREATED, Json(chain)).into_response(),
- Err(e) => {
- tracing::error!("Failed to create chain: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Initialize a directive-driven chain.
-///
-/// Creates a directive contract that will research, plan, create, and orchestrate
-/// a chain of contracts to accomplish the given goal. The directive contract goes
-/// through Research -> Specify -> Plan -> Execute -> Review phases.
-///
-/// POST /api/v1/chains/init
-#[utoipa::path(
- post,
- path = "/api/v1/chains/init",
- request_body = InitChainRequest,
- responses(
- (status = 201, description = "Directive chain initialized", body = InitChainResponse),
- (status = 400, description = "Invalid request", body = ApiError),
- (status = 401, description = "Unauthorized", 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 init_chain(
- State(state): State<SharedState>,
- Authenticated(auth): Authenticated,
- Json(req): Json<InitChainRequest>,
-) -> 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 the request
- if req.goal.trim().is_empty() {
- return (
- StatusCode::BAD_REQUEST,
- Json(ApiError::new("VALIDATION_ERROR", "Goal cannot be empty")),
- )
- .into_response();
- }
-
- match repository::init_chain_for_owner(pool, auth.owner_id, req).await {
- Ok(response) => (StatusCode::CREATED, Json(response)).into_response(),
- Err(e) => {
- tracing::error!("Failed to initialize directive chain: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Get a chain by ID.
-///
-/// GET /api/v1/chains/{id}
-#[utoipa::path(
- get,
- path = "/api/v1/chains/{id}",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- responses(
- (status = 200, description = "Chain with contracts", body = ChainWithContracts),
- (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 get_chain(
- 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();
- };
-
- match repository::get_chain_with_contracts(pool, chain_id, auth.owner_id).await {
- Ok(Some(chain)) => Json(chain).into_response(),
- Ok(None) => (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Chain not found")),
- )
- .into_response(),
- Err(e) => {
- tracing::error!("Failed to get chain: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Update a chain.
-///
-/// PUT /api/v1/chains/{id}
-#[utoipa::path(
- put,
- path = "/api/v1/chains/{id}",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- request_body = UpdateChainRequest,
- responses(
- (status = 200, description = "Chain updated"),
- (status = 400, description = "Invalid request", body = ApiError),
- (status = 401, description = "Unauthorized", body = ApiError),
- (status = 404, description = "Chain not found", body = ApiError),
- (status = 409, description = "Version conflict", 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 update_chain(
- State(state): State<SharedState>,
- Authenticated(auth): Authenticated,
- Path(chain_id): Path<Uuid>,
- Json(req): Json<UpdateChainRequest>,
-) -> impl IntoResponse {
- let Some(ref pool) = state.db_pool else {
- return (
- StatusCode::SERVICE_UNAVAILABLE,
- Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
- )
- .into_response();
- };
-
- match repository::update_chain_for_owner(pool, chain_id, auth.owner_id, req).await {
- Ok(chain) => Json(chain).into_response(),
- Err(RepositoryError::VersionConflict { expected, actual }) => (
- StatusCode::CONFLICT,
- Json(ApiError::new(
- "VERSION_CONFLICT",
- format!("Version conflict: expected {}, found {}", expected, actual),
- )),
- )
- .into_response(),
- Err(RepositoryError::Database(e)) => {
- // Check if it's a "row not found" error
- let error_str = e.to_string();
- if error_str.contains("no rows") || error_str.contains("RowNotFound") {
- (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Chain not found")),
- )
- .into_response()
- } else {
- tracing::error!("Failed to update chain: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
- }
-}
-
-/// Delete (archive) a chain.
-///
-/// DELETE /api/v1/chains/{id}
-#[utoipa::path(
- delete,
- path = "/api/v1/chains/{id}",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- responses(
- (status = 200, description = "Chain archived"),
- (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 delete_chain(
- 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();
- };
-
- match repository::delete_chain_for_owner(pool, chain_id, auth.owner_id).await {
- Ok(true) => Json(serde_json::json!({"archived": true})).into_response(),
- Ok(false) => (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Chain not found")),
- )
- .into_response(),
- Err(e) => {
- tracing::error!("Failed to delete chain: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Get contracts in a chain.
-///
-/// GET /api/v1/chains/{id}/contracts
-#[utoipa::path(
- get,
- path = "/api/v1/chains/{id}/contracts",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- responses(
- (status = 200, description = "List of contracts in chain", body = Vec<ChainContractDetail>),
- (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 get_chain_contracts(
- 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_contracts(pool, chain_id).await {
- Ok(contracts) => Json(contracts).into_response(),
- Err(e) => {
- tracing::error!("Failed to list chain contracts: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Get chain DAG structure for visualization.
-///
-/// GET /api/v1/chains/{id}/graph
-#[utoipa::path(
- get,
- path = "/api/v1/chains/{id}/graph",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- responses(
- (status = 200, description = "Chain graph structure", body = ChainGraphResponse),
- (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 get_chain_graph(
- 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 first
- 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::get_chain_graph(pool, chain_id).await {
- Ok(Some(graph)) => Json(graph).into_response(),
- Ok(None) => (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Chain not found")),
- )
- .into_response(),
- Err(e) => {
- tracing::error!("Failed to get chain graph: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Get chain events.
-///
-/// GET /api/v1/chains/{id}/events
-#[utoipa::path(
- get,
- path = "/api/v1/chains/{id}/events",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- responses(
- (status = 200, description = "Chain events", body = Vec<ChainEvent>),
- (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 get_chain_events(
- 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_events(pool, chain_id).await {
- Ok(events) => Json(events).into_response(),
- Err(e) => {
- tracing::error!("Failed to list chain events: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Get chain editor data.
-///
-/// GET /api/v1/chains/{id}/editor
-#[utoipa::path(
- get,
- path = "/api/v1/chains/{id}/editor",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- responses(
- (status = 200, description = "Chain editor data", body = ChainEditorData),
- (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 get_chain_editor(
- 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();
- };
-
- match repository::get_chain_editor_data(pool, chain_id, auth.owner_id).await {
- Ok(Some(editor_data)) => Json(editor_data).into_response(),
- Ok(None) => (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Chain not found")),
- )
- .into_response(),
- Err(e) => {
- tracing::error!("Failed to get chain editor data: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-// =============================================================================
-// Contract Definition Handlers
-// =============================================================================
-
-/// List contract definitions for a chain.
-///
-/// GET /api/v1/chains/{id}/definitions
-#[utoipa::path(
- get,
- path = "/api/v1/chains/{id}/definitions",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- responses(
- (status = 200, description = "List of contract definitions", body = Vec<ChainContractDefinition>),
- (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_definitions(
- 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_contract_definitions(pool, chain_id).await {
- Ok(definitions) => Json(definitions).into_response(),
- Err(e) => {
- tracing::error!("Failed to list chain definitions: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Create a contract definition for a chain.
-///
-/// POST /api/v1/chains/{id}/definitions
-#[utoipa::path(
- post,
- path = "/api/v1/chains/{id}/definitions",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- request_body = AddContractDefinitionRequest,
- responses(
- (status = 201, description = "Contract definition created", body = ChainContractDefinition),
- (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 create_chain_definition(
- State(state): State<SharedState>,
- Authenticated(auth): Authenticated,
- Path(chain_id): Path<Uuid>,
- Json(req): Json<AddContractDefinitionRequest>,
-) -> 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 the request
- if req.name.trim().is_empty() {
- return (
- StatusCode::BAD_REQUEST,
- Json(ApiError::new("VALIDATION_ERROR", "Definition name cannot be empty")),
- )
- .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::create_chain_contract_definition(pool, chain_id, req).await {
- Ok(definition) => (StatusCode::CREATED, Json(definition)).into_response(),
- Err(e) => {
- tracing::error!("Failed to create chain definition: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Update a contract definition.
-///
-/// PUT /api/v1/chains/{chain_id}/definitions/{definition_id}
-#[utoipa::path(
- put,
- path = "/api/v1/chains/{chain_id}/definitions/{definition_id}",
- params(
- ("chain_id" = Uuid, Path, description = "Chain ID"),
- ("definition_id" = Uuid, Path, description = "Definition ID")
- ),
- request_body = UpdateContractDefinitionRequest,
- responses(
- (status = 200, description = "Contract definition updated", body = ChainContractDefinition),
- (status = 400, description = "Invalid request", body = ApiError),
- (status = 401, description = "Unauthorized", body = ApiError),
- (status = 404, description = "Chain or definition 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 update_chain_definition(
- State(state): State<SharedState>,
- Authenticated(auth): Authenticated,
- Path((chain_id, definition_id)): Path<(Uuid, Uuid)>,
- Json(req): Json<UpdateContractDefinitionRequest>,
-) -> 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 definition belongs to this chain
- match repository::get_chain_contract_definition(pool, definition_id).await {
- Ok(Some(def)) if def.chain_id == chain_id => {}
- Ok(Some(_)) => {
- return (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Definition not found in this chain")),
- )
- .into_response();
- }
- Ok(None) => {
- return (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Definition not found")),
- )
- .into_response();
- }
- Err(e) => {
- tracing::error!("Failed to get chain definition: {}", e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response();
- }
- }
-
- match repository::update_chain_contract_definition(pool, definition_id, req).await {
- Ok(definition) => Json(definition).into_response(),
- Err(e) => {
- tracing::error!("Failed to update chain definition: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Delete a contract definition.
-///
-/// DELETE /api/v1/chains/{chain_id}/definitions/{definition_id}
-#[utoipa::path(
- delete,
- path = "/api/v1/chains/{chain_id}/definitions/{definition_id}",
- params(
- ("chain_id" = Uuid, Path, description = "Chain ID"),
- ("definition_id" = Uuid, Path, description = "Definition ID")
- ),
- responses(
- (status = 200, description = "Contract definition deleted"),
- (status = 401, description = "Unauthorized", body = ApiError),
- (status = 404, description = "Chain or definition 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_definition(
- State(state): State<SharedState>,
- Authenticated(auth): Authenticated,
- Path((chain_id, definition_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 definition belongs to this chain before deleting
- match repository::get_chain_contract_definition(pool, definition_id).await {
- Ok(Some(def)) if def.chain_id == chain_id => {}
- Ok(Some(_)) => {
- return (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Definition not found in this chain")),
- )
- .into_response();
- }
- Ok(None) => {
- return (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Definition not found")),
- )
- .into_response();
- }
- Err(e) => {
- tracing::error!("Failed to get chain definition: {}", e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response();
- }
- }
-
- match repository::delete_chain_contract_definition(pool, definition_id).await {
- Ok(true) => Json(serde_json::json!({"deleted": true})).into_response(),
- Ok(false) => (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Definition not found")),
- )
- .into_response(),
- Err(e) => {
- tracing::error!("Failed to delete chain definition: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-/// Get definition graph for a chain (shows definitions + instantiation status).
-///
-/// GET /api/v1/chains/{id}/definitions/graph
-#[utoipa::path(
- get,
- path = "/api/v1/chains/{id}/definitions/graph",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- responses(
- (status = 200, description = "Definition graph structure", body = ChainDefinitionGraphResponse),
- (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 get_chain_definition_graph(
- 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 first
- 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::get_chain_definition_graph(pool, chain_id).await {
- Ok(Some(graph)) => Json(graph).into_response(),
- Ok(None) => (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Chain not found")),
- )
- .into_response(),
- Err(e) => {
- tracing::error!("Failed to get chain definition graph: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-// =============================================================================
-// Chain Control Handlers
-// =============================================================================
-
-/// Start a chain (spawns supervisor and creates root contracts).
-///
-/// POST /api/v1/chains/{id}/start
-#[utoipa::path(
- post,
- path = "/api/v1/chains/{id}/start",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- request_body(content = Option<StartChainRequest>, description = "Optional start options"),
- responses(
- (status = 200, description = "Chain started", body = StartChainResponse),
- (status = 400, description = "Chain cannot be started", 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 start_chain(
- State(state): State<SharedState>,
- Authenticated(auth): Authenticated,
- Path(chain_id): Path<Uuid>,
- _body: Option<Json<StartChainRequest>>,
-) -> 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 and get chain
- let chain = match repository::get_chain_for_owner(pool, chain_id, auth.owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => {
- return (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Chain not found")),
- )
- .into_response();
- }
- Err(e) => {
- tracing::error!("Failed to get chain: {}", e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response();
- }
- };
-
- // Check if chain can be started
- if chain.status == "active" {
- return (
- StatusCode::BAD_REQUEST,
- Json(ApiError::new("ALREADY_ACTIVE", "Chain is already active")),
- )
- .into_response();
- }
- if chain.status == "completed" {
- return (
- StatusCode::BAD_REQUEST,
- Json(ApiError::new("ALREADY_COMPLETED", "Chain is already completed")),
- )
- .into_response();
- }
-
- // Get definitions to check if there are any
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(d) => d,
- Err(e) => {
- tracing::error!("Failed to list chain definitions: {}", e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response();
- }
- };
-
- if definitions.is_empty() {
- return (
- StatusCode::BAD_REQUEST,
- Json(ApiError::new("NO_DEFINITIONS", "Chain has no contract definitions")),
- )
- .into_response();
- }
-
- // Update chain status to active
- match repository::update_chain_status(pool, chain_id, "active").await {
- Ok(_) => {}
- Err(e) => {
- tracing::error!("Failed to update chain status: {}", e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response();
- }
- }
-
- // Progress the chain - this creates root contracts (definitions with no dependencies)
- let progression = match repository::progress_chain(pool, chain_id, auth.owner_id).await {
- Ok(p) => p,
- Err(e) => {
- tracing::error!("Failed to progress chain: {}", e);
- // Chain is active but no contracts created - return partial success
- return Json(StartChainResponse {
- chain_id,
- contracts_created: vec![],
- status: "active".to_string(),
- })
- .into_response();
- }
- };
-
- Json(StartChainResponse {
- chain_id,
- contracts_created: progression.contracts_created,
- status: if progression.chain_completed {
- "completed".to_string()
- } else {
- "active".to_string()
- },
- })
- .into_response()
-}
-
-/// Stop a chain (kills supervisor, marks as archived).
-///
-/// POST /api/v1/chains/{id}/stop
-#[utoipa::path(
- post,
- path = "/api/v1/chains/{id}/stop",
- params(
- ("id" = Uuid, Path, description = "Chain ID")
- ),
- responses(
- (status = 200, description = "Chain stopped"),
- (status = 400, description = "Chain cannot be stopped", 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 stop_chain(
- 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 and get chain
- let chain = match repository::get_chain_for_owner(pool, chain_id, auth.owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => {
- return (
- StatusCode::NOT_FOUND,
- Json(ApiError::new("NOT_FOUND", "Chain not found")),
- )
- .into_response();
- }
- Err(e) => {
- tracing::error!("Failed to get chain: {}", e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response();
- }
- };
-
- // Check if chain can be stopped
- if chain.status != "active" {
- return (
- StatusCode::BAD_REQUEST,
- Json(ApiError::new(
- "NOT_ACTIVE",
- format!("Chain is not active (status: {})", chain.status),
- )),
- )
- .into_response();
- }
-
- // Archive the chain
- match repository::update_chain_status(pool, chain_id, "archived").await {
- Ok(_) => Json(serde_json::json!({"stopped": true, "status": "archived"})).into_response(),
- Err(e) => {
- tracing::error!("Failed to update chain status: {}", e);
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ApiError::new("DB_ERROR", e.to_string())),
- )
- .into_response()
- }
- }
-}
-
-// =============================================================================
-// 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()
- }
- }
-}