//! 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 serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; /// REST API state #[derive(Clone)] pub struct RestApiState { pub server_addr: String, pub pd_endpoints: Vec, pub store_id: u64, pub configured_peers: HashMap, } /// 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, } /// Legacy/admin add member request for first-boot compatibility. #[derive(Debug, Deserialize)] pub struct AddMemberRequestLegacy { pub id: String, pub raft_addr: String, #[serde(default)] pub addr: Option, } /// 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("/admin/member/add", post(add_member_legacy)) .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, }))) } /// POST /admin/member/add - first-boot compatible cluster join hook. async fn add_member_legacy( State(state): State, Json(req): Json, ) -> Result<(StatusCode, Json>), (StatusCode, Json)> { let (peer_id, peer_addr) = resolve_join_peer(&state, &req).ok_or_else(|| { error_response( StatusCode::BAD_REQUEST, "INVALID_MEMBER", "Unable to resolve FlareDB peer id/address from join request", ) })?; 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 stores = pd_client.list_stores().await; let already_registered = stores.iter().any(|store| store.id == peer_id); pd_client .register_store(peer_id, peer_addr.clone()) .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; let mut regions = pd_client.list_regions().await; if regions.is_empty() { pd_client .init_default_region(vec![state.store_id, peer_id]) .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; regions = vec![crate::pd_client::RegionInfo { id: 1, start_key: Vec::new(), end_key: Vec::new(), peers: vec![state.store_id, peer_id], leader_id: 0, }]; } let mut updated_regions = Vec::new(); for mut region in regions { if !region.peers.contains(&peer_id) { region.peers.push(peer_id); region.peers.sort_unstable(); pd_client .put_region(region.clone()) .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; updated_regions.push(region.id); } } let status = if already_registered && updated_regions.is_empty() { StatusCode::CONFLICT } else if already_registered { StatusCode::OK } else { StatusCode::CREATED }; Ok(( status, Json(SuccessResponse::new(serde_json::json!({ "peer_id": peer_id, "addr": peer_addr, "updated_regions": updated_regions, "already_registered": already_registered, }))), )) } fn resolve_join_peer( state: &RestApiState, req: &AddMemberRequestLegacy, ) -> Option<(u64, String)> { if let Ok(peer_id) = req.id.parse::() { if let Some(addr) = req .addr .clone() .or_else(|| state.configured_peers.get(&peer_id).cloned()) { return Some((peer_id, addr)); } } let candidate_host = socket_host(req.addr.as_deref().unwrap_or(&req.raft_addr)); state .configured_peers .iter() .find(|(_, addr)| socket_host(addr) == candidate_host) .map(|(peer_id, addr)| (*peer_id, addr.clone())) } fn socket_host(addr: &str) -> String { let normalized = addr .trim() .trim_start_matches("http://") .trim_start_matches("https://") .split('/') .next() .unwrap_or(addr) .to_string(); normalized .parse::() .map(|socket_addr| socket_addr.ip().to_string()) .unwrap_or_else(|_| { normalized .rsplit_once(':') .map(|(host, _)| host.trim_matches(['[', ']']).to_string()) .unwrap_or(normalized) }) } /// 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(), }), ) } #[cfg(test)] mod tests { use super::*; fn test_state() -> RestApiState { RestApiState { server_addr: "127.0.0.1:50052".to_string(), pd_endpoints: vec!["127.0.0.1:2479".to_string()], store_id: 1, configured_peers: HashMap::from([ (1, "10.100.0.11:50052".to_string()), (2, "10.100.0.12:50052".to_string()), (3, "10.100.0.13:50052".to_string()), ]), } } #[test] fn resolve_join_peer_uses_numeric_id_when_available() { let state = test_state(); let req = AddMemberRequestLegacy { id: "2".to_string(), raft_addr: "10.100.0.12:2380".to_string(), addr: None, }; assert_eq!( resolve_join_peer(&state, &req), Some((2, "10.100.0.12:50052".to_string())) ); } #[test] fn resolve_join_peer_matches_host_from_raft_addr() { let state = test_state(); let req = AddMemberRequestLegacy { id: "node02".to_string(), raft_addr: "10.100.0.12:2380".to_string(), addr: None, }; assert_eq!( resolve_join_peer(&state, &req), Some((2, "10.100.0.12:50052".to_string())) ); } }