diff --git a/chainfire/crates/chainfire-server/src/rest.rs b/chainfire/crates/chainfire-server/src/rest.rs index c805a75..bd06595 100644 --- a/chainfire/crates/chainfire-server/src/rest.rs +++ b/chainfire/crates/chainfire-server/src/rest.rs @@ -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, + Path(key): Path, +) -> Result>, (StatusCode, Json)> { + // 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, + Path(key): Path, + Json(req): Json, +) -> Result<(StatusCode, Json>), (StatusCode, Json)> { + // 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, + Path(key): Path, +) -> Result<(StatusCode, Json>), (StatusCode, Json)> { + // 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,