diff options
Diffstat (limited to 'makima/src/server/handlers/directive_documents.rs')
| -rw-r--r-- | makima/src/server/handlers/directive_documents.rs | 110 |
1 files changed, 98 insertions, 12 deletions
diff --git a/makima/src/server/handlers/directive_documents.rs b/makima/src/server/handlers/directive_documents.rs index 48f314f..ed38ee4 100644 --- a/makima/src/server/handlers/directive_documents.rs +++ b/makima/src/server/handlers/directive_documents.rs @@ -40,15 +40,27 @@ pub struct CreateDirectiveDocumentRequest { pub body: Option<String>, } -/// Body for `PATCH /api/v1/directive-documents/{document_id}`. +/// Body for `PATCH /api/v1/contracts/{document_id}`. #[derive(Debug, Default, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateDirectiveDocumentRequest { pub title: Option<String>, pub body: Option<String>, + /// Per-contract merge mode. `shared` lands commits on the directive's + /// branch; `own_pr` carves out a contract-specific branch + PR. The + /// queue scheduler reads this when activating the contract. + pub merge_mode: Option<String>, } -/// Body for `POST /api/v1/directive-documents/{document_id}/ship`. +/// Body for `POST /api/v1/contracts/{document_id}/reorder`. +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ReorderDirectiveDocumentRequest { + /// New 0-indexed queue position within the parent directive. + pub position: i32, +} + +/// Body for `POST /api/v1/contracts/{document_id}/ship`. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ShipDirectiveDocumentRequest { @@ -88,10 +100,10 @@ async fn load_owned_document( /// List all documents under a directive. #[utoipa::path( get, - path = "/api/v1/directives/{directive_id}/documents", + path = "/api/v1/directives/{directive_id}/contracts", params(("directive_id" = Uuid, Path, description = "Directive ID")), responses( - (status = 200, description = "List of directive documents", body = Vec<crate::db::models::DirectiveDocument>), + (status = 200, description = "List of contracts under the directive", body = Vec<crate::db::models::DirectiveDocument>), (status = 404, description = "Directive not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), @@ -146,11 +158,11 @@ pub async fn list_documents( /// Create a new directive document. The new document starts in `draft` status. #[utoipa::path( post, - path = "/api/v1/directives/{directive_id}/documents", + path = "/api/v1/directives/{directive_id}/contracts", params(("directive_id" = Uuid, Path, description = "Directive ID")), request_body = CreateDirectiveDocumentRequest, responses( - (status = 201, description = "Document created", body = crate::db::models::DirectiveDocument), + (status = 201, description = "Contract created", body = crate::db::models::DirectiveDocument), (status = 404, description = "Directive not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), @@ -213,7 +225,7 @@ pub async fn create_document( /// Get a single directive document by ID. #[utoipa::path( get, - path = "/api/v1/directive-documents/{document_id}", + path = "/api/v1/contracts/{document_id}", params(("document_id" = Uuid, Path, description = "Directive document ID")), responses( (status = 200, description = "Directive document", body = crate::db::models::DirectiveDocument), @@ -263,7 +275,7 @@ pub async fn get_document( /// returns the updated row — the reactivation is automatic. #[utoipa::path( patch, - path = "/api/v1/directive-documents/{document_id}", + path = "/api/v1/contracts/{document_id}", params(("document_id" = Uuid, Path, description = "Directive document ID")), request_body = UpdateDirectiveDocumentRequest, responses( @@ -312,6 +324,7 @@ pub async fn update_document( document_id, req.title.as_deref(), req.body.as_deref(), + req.merge_mode.as_deref(), ) .await { @@ -336,7 +349,7 @@ pub async fn update_document( /// pr_branch, sets status='shipped', and stamps shipped_at. #[utoipa::path( post, - path = "/api/v1/directive-documents/{document_id}/ship", + path = "/api/v1/contracts/{document_id}/ship", params(("document_id" = Uuid, Path, description = "Directive document ID")), request_body = ShipDirectiveDocumentRequest, responses( @@ -408,7 +421,7 @@ pub async fn ship_document( /// Archive a directive document. #[utoipa::path( post, - path = "/api/v1/directive-documents/{document_id}/archive", + path = "/api/v1/contracts/{document_id}/archive", params(("document_id" = Uuid, Path, description = "Directive document ID")), responses( (status = 200, description = "Document archived", body = crate::db::models::DirectiveDocument), @@ -476,7 +489,7 @@ pub async fn archive_document( // not to the directive). // ============================================================================= -/// Response body for `GET /api/v1/directive-documents/{document_id}/tasks`. +/// Response body for `GET /api/v1/contracts/{document_id}/tasks`. /// /// We return BOTH steps and tasks. Steps are the planned units of work in the /// directive's DAG; tasks are the actual execution records (orchestrator, @@ -501,7 +514,7 @@ pub struct DocumentTasksResponse { /// the parent directive. #[utoipa::path( get, - path = "/api/v1/directive-documents/{document_id}/tasks", + path = "/api/v1/contracts/{document_id}/tasks", params(("document_id" = Uuid, Path, description = "Directive document ID")), responses( (status = 200, description = "Steps and tasks attached to the document", body = DocumentTasksResponse), @@ -570,3 +583,76 @@ pub async fn list_document_tasks( Json(DocumentTasksResponse { steps, tasks }).into_response() } + +// ============================================================================= +// Reorder a contract within its parent directive's queue. +// +// Drag-to-reorder in the sidebar lands here. The `position` field on each +// contract drives the ORDER BY in `list_directive_documents`, so the +// repository function does the bookkeeping (shift siblings, set new +// position) inside a single transaction. The handler only owns auth. +// ============================================================================= + +/// Move a contract to a new queue position within its parent directive. +#[utoipa::path( + post, + path = "/api/v1/contracts/{document_id}/reorder", + params(("document_id" = Uuid, Path, description = "Contract ID")), + request_body = ReorderDirectiveDocumentRequest, + responses( + (status = 200, description = "Contract reordered", body = crate::db::models::DirectiveDocument), + (status = 404, description = "Not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + ), + security(("bearer_auth" = []), ("api_key" = [])), + tag = "Directive Documents" +)] +pub async fn reorder_contract( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(document_id): Path<Uuid>, + Json(req): Json<ReorderDirectiveDocumentRequest>, +) -> 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 load_owned_document(pool, auth.owner_id, document_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Contract not found")), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GET_FAILED", &e.to_string())), + ) + .into_response(); + } + } + + match repository::reorder_directive_document_position(pool, document_id, req.position).await { + Ok(Some(doc)) => Json(doc).into_response(), + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Contract not found")), + ) + .into_response(), + Err(e) => { + tracing::error!("Failed to reorder contract: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("REORDER_FAILED", &e.to_string())), + ) + .into_response() + } + } +} |
