fix(chainfire): Use wildcard route for KV endpoints

Replace :key with *key pattern to avoid axum route conflict.
Adds wildcard handler functions for GET/PUT/DELETE that properly
handle both simple keys and namespaced keys with slashes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
centra 2025-12-19 17:04:39 +09:00
parent 4bfe75a1d7
commit 26a4443a78

View file

@ -134,7 +134,8 @@ pub struct PrefixQuery {
/// Build the REST API router
pub fn build_router(state: RestApiState) -> Router {
Router::new()
.route("/api/v1/kv/:key", get(get_kv).put(put_kv).delete(delete_kv))
// Wildcard route handles all keys (with or without slashes)
.route("/api/v1/kv/*key", get(get_kv_wildcard).put(put_kv_wildcard).delete(delete_kv_wildcard))
.route("/api/v1/kv", get(list_kv))
.route("/api/v1/cluster/status", get(cluster_status))
.route("/api/v1/cluster/members", post(add_member))
@ -222,6 +223,95 @@ async fn delete_kv(
))
}
/// GET /api/v1/kv/*key - Get value (wildcard for all keys)
async fn get_kv_wildcard(
State(state): State<RestApiState>,
Path(key): Path<String>,
) -> Result<Json<SuccessResponse<GetResponse>>, (StatusCode, Json<ErrorResponse>)> {
// Use key as-is for simple keys, prepend / for namespaced keys
// Keys like "testkey" stay as "testkey", keys like "flaredb/stores/1" become "/flaredb/stores/1"
let full_key = if key.contains('/') {
format!("/{}", key)
} else {
key.clone()
};
let sm = state.raft.state_machine();
let key_bytes = full_key.as_bytes().to_vec();
let results = sm.kv()
.get(&key_bytes)
.map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?;
let value = results
.into_iter()
.next()
.ok_or_else(|| error_response(StatusCode::NOT_FOUND, "NOT_FOUND", "Key not found"))?;
Ok(Json(SuccessResponse::new(GetResponse {
key: full_key,
value: String::from_utf8_lossy(&value.value).to_string(),
})))
}
/// PUT /api/v1/kv/*key - Put value (wildcard for all keys)
async fn put_kv_wildcard(
State(state): State<RestApiState>,
Path(key): Path<String>,
Json(req): Json<PutRequest>,
) -> Result<(StatusCode, Json<SuccessResponse<serde_json::Value>>), (StatusCode, Json<ErrorResponse>)> {
// Use key as-is for simple keys, prepend / for namespaced keys
let full_key = if key.contains('/') {
format!("/{}", key)
} else {
key.clone()
};
let command = RaftCommand::Put {
key: full_key.as_bytes().to_vec(),
value: req.value.as_bytes().to_vec(),
lease_id: None,
prev_kv: false,
};
state
.raft
.client_write(command)
.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": full_key, "success": true }))),
))
}
/// DELETE /api/v1/kv/*key - Delete key (wildcard for all keys)
async fn delete_kv_wildcard(
State(state): State<RestApiState>,
Path(key): Path<String>,
) -> Result<(StatusCode, Json<SuccessResponse<serde_json::Value>>), (StatusCode, Json<ErrorResponse>)> {
// Use key as-is for simple keys, prepend / for namespaced keys
let full_key = if key.contains('/') {
format!("/{}", key)
} else {
key.clone()
};
let command = RaftCommand::Delete {
key: full_key.as_bytes().to_vec(),
prev_kv: false,
};
state
.raft
.client_write(command)
.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": full_key, "success": true }))),
))
}
/// GET /api/v1/kv?prefix={prefix} - Range scan
async fn list_kv(
State(state): State<RestApiState>,