photoncloud-monorepo/flaredb/crates/flaredb-server/src/rest.rs

340 lines
10 KiB
Rust

//! 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<String>,
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<serde_json::Value>,
}
#[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<T> {
pub data: T,
pub meta: ResponseMeta,
}
impl<T> SuccessResponse<T> {
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<u64>,
pub rows: Option<Vec<serde_json::Value>>,
}
/// 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<String>,
}
/// Query parameters for scan
#[derive(Debug, Deserialize)]
pub struct ScanQuery {
pub start: Option<String>,
pub end: Option<String>,
#[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<KvItem>,
}
/// 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<u64>,
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<SuccessResponse<serde_json::Value>>) {
(
StatusCode::OK,
Json(SuccessResponse::new(serde_json::json!({ "status": "healthy" }))),
)
}
/// POST /api/v1/sql - Execute SQL query
async fn execute_sql(
State(_state): State<RestApiState>,
Json(req): Json<SqlRequest>,
) -> Result<Json<SuccessResponse<SqlResponse>>, (StatusCode, Json<ErrorResponse>)> {
// SQL execution requires Arc<Mutex<RdbClient>> 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<Mutex<RdbClient>> sharing"
})]),
})))
}
/// GET /api/v1/tables - List tables
async fn list_tables(
State(_state): State<RestApiState>,
) -> Result<Json<SuccessResponse<TablesResponse>>, (StatusCode, Json<ErrorResponse>)> {
// Listing tables requires SQL executor with Arc<Mutex<RdbClient>>
// 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<RestApiState>,
Path(key): Path<String>,
) -> Result<Json<SuccessResponse<GetResponse>>, (StatusCode, Json<ErrorResponse>)> {
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<RestApiState>,
Path(key): Path<String>,
Json(req): Json<PutRequest>,
) -> Result<(StatusCode, Json<SuccessResponse<serde_json::Value>>), (StatusCode, Json<ErrorResponse>)> {
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<RestApiState>,
Query(params): Query<ScanQuery>,
) -> Result<Json<SuccessResponse<ScanResponse>>, (StatusCode, Json<ErrorResponse>)> {
let mut client = RdbClient::connect_direct(state.server_addr.clone(), &params.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<KvItem> = 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<RestApiState>,
Path(id): Path<u64>,
) -> Result<Json<SuccessResponse<RegionResponse>>, (StatusCode, Json<ErrorResponse>)> {
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<RestApiState>,
Path(id): Path<u64>,
Json(req): Json<AddPeerRequest>,
) -> Result<Json<SuccessResponse<RegionResponse>>, (StatusCode, Json<ErrorResponse>)> {
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<ErrorResponse>) {
(
status,
Json(ErrorResponse {
error: ErrorDetail {
code: code.to_string(),
message: message.to_string(),
details: None,
},
meta: ResponseMeta::new(),
}),
)
}