diff options
Diffstat (limited to 'makima/src/server/handlers')
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 395 |
1 files changed, 391 insertions, 4 deletions
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs index d48ff74..f624d82 100644 --- a/makima/src/server/handlers/directives.rs +++ b/makima/src/server/handlers/directives.rs @@ -1,23 +1,31 @@ //! HTTP handlers for directive CRUD and DAG progression. use axum::{ - extract::{Path, State}, + extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, Json, }; +use serde::Deserialize; use uuid::Uuid; use crate::db::models::{ - CreateDirectiveRequest, CreateDirectiveStepRequest, Directive, DirectiveListResponse, - DirectiveStep, DirectiveWithSteps, UpdateDirectiveRequest, UpdateDirectiveStepRequest, - UpdateGoalRequest, + BatchSetDirectiveMemoryRequest, CreateDirectiveRequest, CreateDirectiveStepRequest, + Directive, DirectiveListResponse, DirectiveMemory, DirectiveMemoryListResponse, + DirectiveStep, DirectiveWithSteps, SetDirectiveMemoryRequest, UpdateDirectiveRequest, + UpdateDirectiveStepRequest, UpdateGoalRequest, }; use crate::db::repository; use crate::server::auth::Authenticated; use crate::server::messages::ApiError; use crate::server::state::SharedState; +/// Query parameters for the memory list endpoint. +#[derive(Debug, Deserialize)] +pub struct MemoryListQuery { + pub category: Option<String>, +} + // ============================================================================= // Directive CRUD // ============================================================================= @@ -839,3 +847,382 @@ pub async fn update_goal( } } } + +// ============================================================================= +// Directive Memory CRUD +// ============================================================================= + +/// List all memories for a directive, optionally filtered by category. +#[utoipa::path( + get, + path = "/api/v1/directives/{id}/memories", + params( + ("id" = Uuid, Path, description = "Directive ID"), + ("category" = Option<String>, Query, description = "Filter by category"), + ), + responses( + (status = 200, description = "List of memories", body = DirectiveMemoryListResponse), + (status = 404, description = "Directive not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + ), + security(("bearer_auth" = []), ("api_key" = [])), + tag = "Directives" +)] +pub async fn list_memories( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, + Query(query): Query<MemoryListQuery>, +) -> 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 directive ownership + match repository::get_directive_for_owner(pool, auth.owner_id, id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Directive not found")), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GET_FAILED", &e.to_string())), + ) + .into_response(); + } + } + + match repository::list_directive_memories(pool, id, query.category.as_deref()).await { + Ok(memories) => { + let total = memories.len() as i64; + Json(DirectiveMemoryListResponse { memories, total }).into_response() + } + Err(e) => { + tracing::error!("Failed to list memories: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("LIST_FAILED", &e.to_string())), + ) + .into_response() + } + } +} + +/// Get a single memory entry by key. +#[utoipa::path( + get, + path = "/api/v1/directives/{id}/memories/{key}", + params( + ("id" = Uuid, Path, description = "Directive ID"), + ("key" = String, Path, description = "Memory key"), + ), + responses( + (status = 200, description = "Memory entry", body = DirectiveMemory), + (status = 404, description = "Not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + ), + security(("bearer_auth" = []), ("api_key" = [])), + tag = "Directives" +)] +pub async fn get_memory( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path((id, key)): Path<(Uuid, String)>, +) -> 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 directive ownership + match repository::get_directive_for_owner(pool, auth.owner_id, id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Directive not found")), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GET_FAILED", &e.to_string())), + ) + .into_response(); + } + } + + match repository::get_directive_memory(pool, id, &key).await { + Ok(Some(memory)) => Json(memory).into_response(), + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Memory entry not found")), + ) + .into_response(), + Err(e) => { + tracing::error!("Failed to get memory: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GET_FAILED", &e.to_string())), + ) + .into_response() + } + } +} + +/// Set (upsert) a single memory entry. +#[utoipa::path( + post, + path = "/api/v1/directives/{id}/memories", + params(("id" = Uuid, Path, description = "Directive ID")), + request_body = SetDirectiveMemoryRequest, + responses( + (status = 200, description = "Memory entry set", body = DirectiveMemory), + (status = 404, description = "Directive not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + ), + security(("bearer_auth" = []), ("api_key" = [])), + tag = "Directives" +)] +pub async fn set_memory( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, + Json(req): Json<SetDirectiveMemoryRequest>, +) -> 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 directive ownership + match repository::get_directive_for_owner(pool, auth.owner_id, id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Directive not found")), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GET_FAILED", &e.to_string())), + ) + .into_response(); + } + } + + match repository::set_directive_memory(pool, id, &req).await { + Ok(memory) => Json(memory).into_response(), + Err(e) => { + tracing::error!("Failed to set memory: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("SET_FAILED", &e.to_string())), + ) + .into_response() + } + } +} + +/// Batch set multiple memory entries. +#[utoipa::path( + post, + path = "/api/v1/directives/{id}/memories/batch", + params(("id" = Uuid, Path, description = "Directive ID")), + request_body = BatchSetDirectiveMemoryRequest, + responses( + (status = 200, description = "Memory entries set", body = Vec<DirectiveMemory>), + (status = 404, description = "Directive not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + ), + security(("bearer_auth" = []), ("api_key" = [])), + tag = "Directives" +)] +pub async fn batch_set_memories( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, + Json(req): Json<BatchSetDirectiveMemoryRequest>, +) -> 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 directive ownership + match repository::get_directive_for_owner(pool, auth.owner_id, id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Directive not found")), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GET_FAILED", &e.to_string())), + ) + .into_response(); + } + } + + match repository::batch_set_directive_memories(pool, id, &req.memories).await { + Ok(memories) => Json(memories).into_response(), + Err(e) => { + tracing::error!("Failed to batch set memories: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("SET_FAILED", &e.to_string())), + ) + .into_response() + } + } +} + +/// Delete a single memory entry by key. +#[utoipa::path( + delete, + path = "/api/v1/directives/{id}/memories/{key}", + params( + ("id" = Uuid, Path, description = "Directive ID"), + ("key" = String, Path, description = "Memory key"), + ), + responses( + (status = 204, description = "Deleted"), + (status = 404, description = "Not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + ), + security(("bearer_auth" = []), ("api_key" = [])), + tag = "Directives" +)] +pub async fn delete_memory( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path((id, key)): Path<(Uuid, String)>, +) -> 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 directive ownership + match repository::get_directive_for_owner(pool, auth.owner_id, id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Directive not found")), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GET_FAILED", &e.to_string())), + ) + .into_response(); + } + } + + match repository::delete_directive_memory(pool, id, &key).await { + Ok(true) => StatusCode::NO_CONTENT.into_response(), + Ok(false) => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Memory entry not found")), + ) + .into_response(), + Err(e) => { + tracing::error!("Failed to delete memory: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DELETE_FAILED", &e.to_string())), + ) + .into_response() + } + } +} + +/// Clear all memories for a directive. +#[utoipa::path( + delete, + path = "/api/v1/directives/{id}/memories", + params(("id" = Uuid, Path, description = "Directive ID")), + responses( + (status = 204, description = "All memories cleared"), + (status = 404, description = "Directive not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + ), + security(("bearer_auth" = []), ("api_key" = [])), + tag = "Directives" +)] +pub async fn clear_memories( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(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 directive ownership + match repository::get_directive_for_owner(pool, auth.owner_id, id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Directive not found")), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GET_FAILED", &e.to_string())), + ) + .into_response(); + } + } + + match repository::clear_directive_memories(pool, id).await { + Ok(_) => StatusCode::NO_CONTENT.into_response(), + Err(e) => { + tracing::error!("Failed to clear memories: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("CLEAR_FAILED", &e.to_string())), + ) + .into_response() + } + } +} |
