1use std::sync::Arc;
2
3use anyhow::Result;
4use axum::{
5 extract::State,
6 http::StatusCode,
7 response::IntoResponse,
8 routing::{get, post},
9 Json, Router,
10};
11use lora_database::{ExecuteOptions, QueryRunner, ResultFormat};
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Deserialize)]
15pub struct QueryRequest {
16 pub query: String,
17 #[serde(default)]
18 pub format: Option<QueryFormat>,
19}
20
21#[derive(Debug, Clone, Copy, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub enum QueryFormat {
24 Rows,
25 RowArrays,
26 Graph,
27 Combined,
28}
29
30impl From<QueryFormat> for ResultFormat {
31 fn from(value: QueryFormat) -> Self {
32 match value {
33 QueryFormat::Rows => ResultFormat::Rows,
34 QueryFormat::RowArrays => ResultFormat::RowArrays,
35 QueryFormat::Graph => ResultFormat::Graph,
36 QueryFormat::Combined => ResultFormat::Combined,
37 }
38 }
39}
40
41#[derive(Debug, Serialize)]
42pub struct ErrorResponse {
43 pub error: String,
44}
45
46#[derive(Debug, Serialize)]
47pub struct HealthResponse {
48 pub status: &'static str,
49}
50
51pub fn build_app<R>(db: Arc<R>) -> Router
52where
53 R: QueryRunner,
54{
55 Router::new()
56 .route("/health", get(health))
57 .route("/query", post(query::<R>))
58 .with_state(db)
59}
60
61pub async fn serve<R>(listener: tokio::net::TcpListener, db: Arc<R>) -> Result<()>
62where
63 R: QueryRunner,
64{
65 let app = build_app(db);
66 axum::serve(listener, app).await?;
67 Ok(())
68}
69
70async fn health() -> Json<HealthResponse> {
71 Json(HealthResponse { status: "ok" })
72}
73
74async fn query<R>(State(db): State<Arc<R>>, Json(req): Json<QueryRequest>) -> impl IntoResponse
75where
76 R: QueryRunner,
77{
78 let options = req.format.map(|format| ExecuteOptions {
79 format: format.into(),
80 });
81
82 match db.execute(&req.query, options) {
83 Ok(result) => (StatusCode::OK, Json(result)).into_response(),
84 Err(err) => (
85 StatusCode::BAD_REQUEST,
86 Json(ErrorResponse {
87 error: err.to_string(),
88 }),
89 )
90 .into_response(),
91 }
92}