execute query from admin page and display the result

This commit is contained in:
2026-05-04 10:13:51 -07:00
parent 8b6b4cd4f5
commit 2a1cc97fe2
3 changed files with 111 additions and 5 deletions

View File

@ -1,5 +1,7 @@
use crate::data::{Game, Player, Team};
use actix_web::{Error, error, web};
use rusqlite::{Row, params};
use serde_json::{Map, Value};
use std::collections::HashMap;
pub type Pool = r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>;
@ -12,6 +14,37 @@ const GAMES_QUERY: &str =
const TEAMS_QUERY: &str =
"select game, team, min(color), group_concat(player) from teams group by game, team";
pub async fn execute_query(pool: &Pool, query: &str) -> Result<String, Error> {
let pool = pool.clone();
let conn = web::block(move || pool.get())
.await?
.map_err(error::ErrorInternalServerError)?;
match conn.execute(query, params![]) {
Ok(val) => println!("{val:?}"),
Err(er) => println!("error: {er:?}"),
}
Ok(String::from("success"))
}
pub async fn execute_stmt(pool: &Pool, query: &str) -> Result<String, Error> {
let pool = pool.clone();
let conn = web::block(move || pool.get())
.await?
.map_err(|e| error::ErrorInternalServerError(e))?;
let mut stmt = conn
.prepare(query)
.map_err(|e| error::ErrorInternalServerError(e))?;
let column_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
let rows = stmt
.query_map([], |row| row_to_json(row, &column_names))
.map_err(|e| error::ErrorInternalServerError(e))?;
let mut res = vec![];
for row in rows {
res.push(row.map_err(|e| error::ErrorInternalServerError(e))?);
}
Ok(serde_json::to_string(&res).unwrap())
}
pub async fn games(pool: &Pool) -> Result<Vec<Game>, Error> {
let pool = pool.clone();
let teams_result = teams(&pool).await?;
@ -83,3 +116,21 @@ fn get_all_teams(conn: Connection) -> TeamsResult {
})
.and_then(Iterator::collect)
}
fn row_to_json(row: &Row, column_names: &[String]) -> rusqlite::Result<Value> {
let mut obj = Map::new();
for (i, name) in column_names.iter().enumerate() {
let value: rusqlite::types::Value = row.get(i)?;
let json_value = match value {
rusqlite::types::Value::Null => Value::Null,
rusqlite::types::Value::Integer(i) => Value::from(i),
rusqlite::types::Value::Real(f) => Value::from(f),
rusqlite::types::Value::Text(t) => Value::from(t),
rusqlite::types::Value::Blob(_) => panic!("blob not supported"),
};
obj.insert(name.clone(), json_value);
}
Ok(Value::Object(obj))
}

View File

@ -15,11 +15,12 @@ use actix::WrapFuture;
use actix::dev::MessageResponse;
use actix::dev::OneshotSender;
use actix::prelude::Actor;
use actix_web::{App, HttpResponse, HttpServer, Responder, get, web};
use actix_web::{App, HttpResponse, HttpServer, Responder, get, post, web};
use chrono::NaiveDateTime;
use db::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use reqwest::Client;
use serde::Deserialize;
use std::error::Error;
use std::sync::Arc;
use std::time::Duration;
@ -61,6 +62,12 @@ struct GamesActor {
games: Arc<Mutex<Vec<Game>>>,
}
#[derive(Deserialize, Debug)]
struct QueryForm {
query: String,
is_read: bool,
}
impl Actor for GamesActor {
type Context = actix::Context<Self>;
}
@ -171,6 +178,19 @@ async fn list_of_games(games: web::Data<Arc<Mutex<Vec<Game>>>>) -> impl Responde
.body(games_json)
}
#[post("/admin/query")]
async fn admin_query(pool: web::Data<Pool>, form: web::Form<QueryForm>) -> impl Responder {
let res: String;
if form.is_read {
res = db::execute_stmt(&pool, &form.query).await.unwrap();
} else {
res = db::execute_query(&pool, &form.query).await.unwrap();
}
HttpResponse::Ok()
.content_type("application/json")
.body(res)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
unsafe {
@ -197,8 +217,9 @@ async fn main() -> std::io::Result<()> {
.service(game_page)
.service(geojson_endpoint)
.service(legend_data)
.service(admin)
.service(list_of_games)
//.service(admin)
//.service(admin_query)
//.service(list_of_games)
})
.bind(("0.0.0.0", 8080))?
.run()

View File

@ -46,6 +46,13 @@
</tr></thead>
<tbody id="games_table"></tbody>
</table>
<form id="query_form">
<p><label for="query">Compose a sql query:</label></p>
<textarea id="query" name="query" rows="6" cols="60">select * from games limit 10;</textarea>
<br>
<input type="submit" value="Execute">
</form>
<pre id="query_result"></pre>
<dialog id="team_dialog">
<p><bold>Name: <span id="td_name"></span></bold></p>
<p>Color: <span id="td_color"></span></p>
@ -58,6 +65,34 @@
const td_name = document.getElementById("td_name");
const td_color = document.getElementById("td_color");
const td_players = document.getElementById("td_players");
const query_form = document.getElementById("query_form");
games_form.addEventListener("submit", refresh_games);
query_form.addEventListener("submit", submit_query);
function submit_query(event) {
event.preventDefault();
fetch("/admin/query", {
method: "POST",
body: new URLSearchParams({
'query': document.getElementById("query").value,
'is_read': true,
})
})
.then(res => {
if (!res.ok) {
throw new Error(`Response status: ${res.status}`);
}
return res.json();
})
.then((data) => {
document.getElementById("query_result").textContent = JSON.stringify(data, null, 2);
console.log(data);
})
.catch(error => {
console.error("Fetching data failed: ", error);
});
}
function refresh_games(event) {
event.preventDefault();
fetch("/admin/games")
@ -102,10 +137,9 @@
});
})
.catch(error => {
console.error("Fetching legend data failed:", error);
console.error("Fetching data failed:", error);
});
}
games_form.addEventListener("submit", refresh_games);
function showModal(name, color, players) {
td_name.textContent = name;
td_color.textContent = color;