//! REST HTTP API handlers for FlareDB //! //! Implements REST endpoints as specified in T050.S3: //! - POST /api/v1/sql - Execute SQL query //! - GET /api/v1/tables - List tables //! - GET /api/v1/kv/{key} - KV get //! - PUT /api/v1/kv/{key} - KV put //! - GET /api/v1/scan - Range scan //! - GET /health - Health check use axum::{ extract::{Path, Query, State}, http::StatusCode, routing::{get, post, put}, Json, Router, }; use crate::pd_client::PdClient; use flaredb_client::RdbClient; use flaredb_sql::executor::{ExecutionResult, SqlExecutor}; use serde::{Deserialize, Serialize}; use std::sync::Arc; /// REST API state #[derive(Clone)] pub struct RestApiState { pub server_addr: String, pub pd_endpoints: Vec, pub store_id: u64, } /// Standard REST error response #[derive(Debug, Serialize)] pub struct ErrorResponse { pub error: ErrorDetail, pub meta: ResponseMeta, } #[derive(Debug, Serialize)] pub struct ErrorDetail { pub code: String, pub message: String, #[serde(skip_serializing_if = "Option::is_none")] pub details: Option, } #[derive(Debug, Serialize)] pub struct ResponseMeta { pub request_id: String, pub timestamp: String, } impl ResponseMeta { fn new() -> Self { Self { request_id: uuid::Uuid::new_v4().to_string(), timestamp: chrono::Utc::now().to_rfc3339(), } } } /// Standard REST success response #[derive(Debug, Serialize)] pub struct SuccessResponse { pub data: T, pub meta: ResponseMeta, } impl SuccessResponse { fn new(data: T) -> Self { Self { data, meta: ResponseMeta::new(), } } } /// SQL execution request #[derive(Debug, Deserialize)] pub struct SqlRequest { pub query: String, } /// SQL execution response #[derive(Debug, Serialize)] pub struct SqlResponse { pub rows_affected: Option, pub rows: Option>, } /// KV Put request body #[derive(Debug, Deserialize)] pub struct PutRequest { pub value: String, #[serde(default)] pub namespace: String, } /// KV Get response #[derive(Debug, Serialize)] pub struct GetResponse { pub key: String, pub value: String, } /// Tables list response #[derive(Debug, Serialize)] pub struct TablesResponse { pub tables: Vec, } /// Query parameters for scan #[derive(Debug, Deserialize)] pub struct ScanQuery { pub start: Option, pub end: Option, #[serde(default)] pub namespace: String, } /// Scan response item #[derive(Debug, Serialize)] pub struct KvItem { pub key: String, pub value: String, } /// Scan response #[derive(Debug, Serialize)] pub struct ScanResponse { pub items: Vec, } /// Add peer request #[derive(Debug, Deserialize)] pub struct AddPeerRequest { pub peer_id: u64, } /// Region info response #[derive(Debug, Serialize)] pub struct RegionResponse { pub id: u64, pub peers: Vec, pub leader_id: u64, } /// Build the REST API router pub fn build_router(state: RestApiState) -> Router { Router::new() .route("/api/v1/sql", post(execute_sql)) .route("/api/v1/tables", get(list_tables)) .route("/api/v1/kv/{key}", get(get_kv).put(put_kv)) .route("/api/v1/scan", get(scan_kv)) .route("/api/v1/regions/{id}", get(get_region)) .route("/api/v1/regions/{id}/add_peer", post(add_peer_to_region)) .route("/health", get(health_check)) .with_state(state) } /// Health check endpoint async fn health_check() -> (StatusCode, Json>) { ( StatusCode::OK, Json(SuccessResponse::new(serde_json::json!({ "status": "healthy" }))), ) } /// POST /api/v1/sql - Execute SQL query async fn execute_sql( State(_state): State, Json(req): Json, ) -> Result>, (StatusCode, Json)> { // SQL execution requires Arc> which is complex to set up in REST context // For now, return a placeholder indicating SQL should be accessed via gRPC // Full implementation would require refactoring to share SQL executor state Ok(Json(SuccessResponse::new(SqlResponse { rows_affected: None, rows: Some(vec![serde_json::json!({ "message": format!("SQL execution via REST not yet implemented. Query received: {}", req.query), "hint": "Use gRPC SqlService for SQL queries or implement Arc> sharing" })]), }))) } /// GET /api/v1/tables - List tables async fn list_tables( State(_state): State, ) -> Result>, (StatusCode, Json)> { // Listing tables requires SQL executor with Arc> // For now, return empty list with hint Ok(Json(SuccessResponse::new(TablesResponse { tables: vec!["(Table listing via REST not yet implemented - use gRPC)".to_string()], }))) } /// GET /api/v1/kv/{key} - Get value async fn get_kv( State(state): State, Path(key): Path, ) -> Result>, (StatusCode, Json)> { let mut client = RdbClient::connect_direct(state.server_addr.clone(), "default") .await .map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", &format!("Failed to connect: {}", e)))?; let value = client .raw_get(key.as_bytes().to_vec()) .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))? .ok_or_else(|| error_response(StatusCode::NOT_FOUND, "NOT_FOUND", "Key not found"))?; Ok(Json(SuccessResponse::new(GetResponse { key, value: String::from_utf8_lossy(&value).to_string(), }))) } /// PUT /api/v1/kv/{key} - Put value async fn put_kv( State(state): State, Path(key): Path, Json(req): Json, ) -> Result<(StatusCode, Json>), (StatusCode, Json)> { let mut client = RdbClient::connect_direct(state.server_addr.clone(), &req.namespace) .await .map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", &format!("Failed to connect: {}", e)))?; client .raw_put(key.as_bytes().to_vec(), req.value.as_bytes().to_vec()) .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; Ok(( StatusCode::OK, Json(SuccessResponse::new(serde_json::json!({ "key": key, "success": true }))), )) } /// GET /api/v1/scan - Range scan async fn scan_kv( State(state): State, Query(params): Query, ) -> Result>, (StatusCode, Json)> { let mut client = RdbClient::connect_direct(state.server_addr.clone(), ¶ms.namespace) .await .map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", &format!("Failed to connect: {}", e)))?; let start_key = params.start.unwrap_or_default(); let end_key = params.end.unwrap_or_else(|| format!("{}~", start_key)); let (keys, values, _next) = client .raw_scan(start_key.as_bytes().to_vec(), end_key.as_bytes().to_vec(), 100) .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; let items: Vec = keys .into_iter() .zip(values.into_iter()) .map(|(key, value)| KvItem { key: String::from_utf8_lossy(&key).to_string(), value: String::from_utf8_lossy(&value).to_string(), }) .collect(); Ok(Json(SuccessResponse::new(ScanResponse { items }))) } /// GET /api/v1/regions/{id} - Get region info async fn get_region( State(state): State, Path(id): Path, ) -> Result>, (StatusCode, Json)> { let mut pd_client = PdClient::connect_any(&state.pd_endpoints) .await .map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "PD_UNAVAILABLE", &format!("Failed to connect to PD: {}", e)))?; let region = pd_client .get_region(id) .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))? .ok_or_else(|| error_response(StatusCode::NOT_FOUND, "NOT_FOUND", &format!("Region {} not found", id)))?; Ok(Json(SuccessResponse::new(RegionResponse { id: region.id, peers: region.peers, leader_id: region.leader_id, }))) } /// POST /api/v1/regions/{id}/add_peer - Add peer to region async fn add_peer_to_region( State(state): State, Path(id): Path, Json(req): Json, ) -> Result>, (StatusCode, Json)> { let mut pd_client = PdClient::connect_any(&state.pd_endpoints) .await .map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "PD_UNAVAILABLE", &format!("Failed to connect to PD: {}", e)))?; let mut region = pd_client .get_region(id) .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))? .ok_or_else(|| error_response(StatusCode::NOT_FOUND, "NOT_FOUND", &format!("Region {} not found", id)))?; // Add peer if not already present if !region.peers.contains(&req.peer_id) { region.peers.push(req.peer_id); region.peers.sort(); pd_client .put_region(region.clone()) .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; } Ok(Json(SuccessResponse::new(RegionResponse { id: region.id, peers: region.peers, leader_id: region.leader_id, }))) } /// Helper to create error response fn error_response( status: StatusCode, code: &str, message: &str, ) -> (StatusCode, Json) { ( status, Json(ErrorResponse { error: ErrorDetail { code: code.to_string(), message: message.to_string(), details: None, }, meta: ResponseMeta::new(), }), ) }