1use std::collections::HashMap;
4use std::path::PathBuf;
5use std::sync::{Arc, Mutex};
6
7use axum::extract::{Path as AxumPath, State};
8use axum::http::{header, StatusCode, Uri};
9use axum::response::{IntoResponse, Json};
10use axum::routing::{get, post};
11use axum::Router;
12use rust_embed::Embed;
13use serde::{Deserialize, Serialize};
14use tower_http::cors::CorsLayer;
15
16use mdql_core::executor::{self, QueryResult};
17use mdql_core::loader;
18use mdql_core::model::{Row, Value};
19use mdql_core::projector::format_results;
20use mdql_core::schema::Schema;
21
22#[derive(Embed)]
23#[folder = "static/"]
24struct StaticFiles;
25
26#[derive(Clone)]
27struct AppState {
28 db_path: PathBuf,
29 tables: Arc<Mutex<HashMap<String, (Schema, Vec<Row>)>>>,
30 fk_errors: Arc<Mutex<Vec<mdql_core::errors::ValidationError>>>,
31}
32
33#[derive(Serialize)]
34struct TableInfo {
35 name: String,
36 row_count: usize,
37}
38
39#[derive(Serialize)]
40struct TablesResponse {
41 tables: Vec<TableInfo>,
42}
43
44#[derive(Serialize)]
45struct TableDetailResponse {
46 table: String,
47 primary_key: String,
48 row_count: usize,
49 frontmatter: HashMap<String, FieldInfo>,
50 sections: HashMap<String, SectionInfo>,
51}
52
53#[derive(Serialize)]
54struct FieldInfo {
55 #[serde(rename = "type")]
56 field_type: String,
57 required: bool,
58 enum_values: Option<Vec<String>>,
59}
60
61#[derive(Serialize)]
62struct SectionInfo {
63 content_type: String,
64 required: bool,
65}
66
67#[derive(Deserialize)]
68struct QueryRequest {
69 sql: String,
70 #[serde(default = "default_format")]
71 format: String,
72}
73
74fn default_format() -> String {
75 "table".into()
76}
77
78#[derive(Serialize)]
79struct QueryResponse {
80 columns: Option<Vec<String>>,
81 rows: Option<Vec<HashMap<String, serde_json::Value>>>,
82 output: Option<String>,
83 error: Option<String>,
84 row_count: Option<usize>,
85}
86
87pub async fn run_server(db_path: PathBuf, port: u16) {
88 let tables = match load_all_tables(&db_path) {
90 Ok(t) => t,
91 Err(e) => {
92 eprintln!("Failed to load database: {}", e);
93 std::process::exit(1);
94 }
95 };
96
97 let fk_errors: Arc<Mutex<Vec<mdql_core::errors::ValidationError>>> =
98 Arc::new(Mutex::new(Vec::new()));
99
100 let state = AppState {
101 db_path: db_path.clone(),
102 tables: Arc::new(Mutex::new(tables)),
103 fk_errors: fk_errors.clone(),
104 };
105
106 {
108 let tables_clone = state.tables.clone();
109 let fk_errors_clone = fk_errors.clone();
110 let db_path_clone = db_path.clone();
111 tokio::task::spawn_blocking(move || {
112 let watcher = match mdql_core::watcher::FkWatcher::start(db_path_clone.clone()) {
113 Ok(w) => w,
114 Err(e) => {
115 eprintln!("Warning: could not start FK watcher: {}", e);
116 return;
117 }
118 };
119 loop {
120 if let Some(errors) = watcher.poll() {
121 *fk_errors_clone.lock().unwrap() = errors;
122 if let Ok(new_tables) = load_all_tables(&db_path_clone) {
124 *tables_clone.lock().unwrap() = new_tables;
125 }
126 }
127 std::thread::sleep(std::time::Duration::from_millis(200));
128 }
129 });
130 }
131
132 let app = Router::new()
133 .route("/api/tables", get(list_tables))
134 .route("/api/tables/{name}", get(table_detail))
135 .route("/api/query", post(execute_query))
136 .route("/api/reload", post(reload_tables))
137 .route("/api/fk-errors", get(get_fk_errors))
138 .fallback(static_handler)
139 .layer(CorsLayer::permissive())
140 .with_state(state);
141
142 let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))
143 .await
144 .expect("Failed to bind");
145
146 println!("MDQL client running at http://localhost:{}", port);
147
148 axum::serve(listener, app).await.expect("Server failed");
149}
150
151fn load_all_tables(db_path: &std::path::Path) -> Result<HashMap<String, (Schema, Vec<Row>)>, String> {
152 if let Ok((_config, tables, _errors)) = loader::load_database(db_path) {
154 return Ok(tables);
155 }
156
157 match loader::load_table(db_path) {
159 Ok((schema, rows, _errors)) => {
160 let mut map = HashMap::new();
161 map.insert(schema.table.clone(), (schema, rows));
162 Ok(map)
163 }
164 Err(e) => Err(format!("Failed to load: {}", e)),
165 }
166}
167
168async fn list_tables(State(state): State<AppState>) -> Json<TablesResponse> {
169 let tables = state.tables.lock().unwrap();
170 let mut infos: Vec<TableInfo> = tables
171 .iter()
172 .map(|(name, (_schema, rows))| TableInfo {
173 name: name.clone(),
174 row_count: rows.len(),
175 })
176 .collect();
177 infos.sort_by(|a, b| a.name.cmp(&b.name));
178 Json(TablesResponse { tables: infos })
179}
180
181async fn table_detail(
182 State(state): State<AppState>,
183 AxumPath(name): AxumPath<String>,
184) -> Result<Json<TableDetailResponse>, StatusCode> {
185 let tables = state.tables.lock().unwrap();
186 let (schema, rows) = tables.get(&name).ok_or(StatusCode::NOT_FOUND)?;
187
188 let frontmatter: HashMap<String, FieldInfo> = schema
189 .frontmatter
190 .iter()
191 .map(|(k, v)| {
192 (
193 k.clone(),
194 FieldInfo {
195 field_type: format!("{:?}", v.field_type),
196 required: v.required,
197 enum_values: v.enum_values.clone(),
198 },
199 )
200 })
201 .collect();
202
203 let sections: HashMap<String, SectionInfo> = schema
204 .sections
205 .iter()
206 .map(|(k, v)| {
207 (
208 k.clone(),
209 SectionInfo {
210 content_type: v.content_type.clone(),
211 required: v.required,
212 },
213 )
214 })
215 .collect();
216
217 Ok(Json(TableDetailResponse {
218 table: schema.table.clone(),
219 primary_key: schema.primary_key.clone(),
220 row_count: rows.len(),
221 frontmatter,
222 sections,
223 }))
224}
225
226async fn execute_query(
227 State(state): State<AppState>,
228 Json(req): Json<QueryRequest>,
229) -> Json<QueryResponse> {
230 let result = executor::execute(&state.db_path, &req.sql);
231
232 match result {
233 Ok((QueryResult::Rows { rows, columns }, _warnings)) => {
234 if req.format == "json" || req.format == "csv" {
235 let output = format_results(&rows, Some(&columns), &req.format, 80);
236 Json(QueryResponse {
237 columns: None,
238 rows: None,
239 output: Some(output),
240 error: None,
241 row_count: Some(rows.len()),
242 })
243 } else {
244 let json_rows: Vec<HashMap<String, serde_json::Value>> = rows
245 .iter()
246 .map(|row| {
247 columns
248 .iter()
249 .map(|col| {
250 let val = row.get(col).unwrap_or(&Value::Null);
251 (col.clone(), value_to_json(val))
252 })
253 .collect()
254 })
255 .collect();
256
257 Json(QueryResponse {
258 columns: Some(columns),
259 rows: Some(json_rows.clone()),
260 output: None,
261 error: None,
262 row_count: Some(json_rows.len()),
263 })
264 }
265 }
266 Ok((QueryResult::Message(msg), _warnings)) => {
267 if let Ok(new_tables) = load_all_tables(&state.db_path) {
269 let mut tables = state.tables.lock().unwrap();
270 *tables = new_tables;
271 }
272 Json(QueryResponse {
273 columns: None,
274 rows: None,
275 output: Some(msg),
276 error: None,
277 row_count: None,
278 })
279 }
280 Err(e) => Json(QueryResponse {
281 columns: None,
282 rows: None,
283 output: None,
284 error: Some(e.to_string()),
285 row_count: None,
286 }),
287 }
288}
289
290async fn get_fk_errors(State(state): State<AppState>) -> Json<serde_json::Value> {
291 let errors = state.fk_errors.lock().unwrap();
292 let error_list: Vec<serde_json::Value> = errors
293 .iter()
294 .map(|e| {
295 serde_json::json!({
296 "file": e.file_path,
297 "field": e.field,
298 "message": e.message,
299 })
300 })
301 .collect();
302 Json(serde_json::json!({ "errors": error_list }))
303}
304
305async fn reload_tables(State(state): State<AppState>) -> Json<serde_json::Value> {
306 match load_all_tables(&state.db_path) {
307 Ok(new_tables) => {
308 let mut tables = state.tables.lock().unwrap();
309 *tables = new_tables;
310 Json(serde_json::json!({ "status": "ok" }))
311 }
312 Err(e) => Json(serde_json::json!({ "status": "error", "message": e })),
313 }
314}
315
316fn value_to_json(val: &Value) -> serde_json::Value {
317 match val {
318 Value::Null => serde_json::Value::Null,
319 Value::String(s) => serde_json::Value::String(s.clone()),
320 Value::Int(n) => serde_json::json!(n),
321 Value::Float(f) => serde_json::json!(f),
322 Value::Bool(b) => serde_json::json!(b),
323 Value::Date(d) => serde_json::Value::String(d.format("%Y-%m-%d").to_string()),
324 Value::DateTime(dt) => serde_json::Value::String(dt.format("%Y-%m-%dT%H:%M:%S").to_string()),
325 Value::List(items) => serde_json::json!(items),
326 Value::Dict(map) => {
327 let obj: serde_json::Map<String, serde_json::Value> = map.iter()
328 .map(|(k, v)| (k.clone(), value_to_json(v)))
329 .collect();
330 serde_json::Value::Object(obj)
331 }
332 }
333}
334
335async fn static_handler(uri: Uri) -> impl IntoResponse {
336 let path = uri.path().trim_start_matches('/');
337 let path = if path.is_empty() { "index.html" } else { path };
338
339 match StaticFiles::get(path) {
340 Some(content) => {
341 let mime = mime_guess::from_path(path).first_or_octet_stream();
342 (
343 StatusCode::OK,
344 [(header::CONTENT_TYPE, mime.as_ref().to_string())],
345 content.data.into_owned(),
346 )
347 .into_response()
348 }
349 None => {
350 match StaticFiles::get("index.html") {
352 Some(content) => (
353 StatusCode::OK,
354 [(header::CONTENT_TYPE, "text/html".to_string())],
355 content.data.into_owned(),
356 )
357 .into_response(),
358 None => (StatusCode::NOT_FOUND, "Not found").into_response(),
359 }
360 }
361 }
362}
363