340 lines
10 KiB
Rust
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(), ¶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<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(),
|
|
}),
|
|
)
|
|
}
|