summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers')
-rw-r--r--makima/src/server/handlers/directives.rs395
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()
+ }
+ }
+}