From 2a1cc97fe2251e52dd315cccd75b28ddc83e455d Mon Sep 17 00:00:00 2001 From: ammar Date: Mon, 4 May 2026 10:13:51 -0700 Subject: [PATCH] execute query from admin page and display the result --- src/db.rs | 51 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 27 ++++++++++++++++++--- src/templates/admin.tera | 38 ++++++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/src/db.rs b/src/db.rs index ff1e910..a7cd35d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -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; @@ -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 { + 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 { + 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 = 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, 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 { + 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)) +} diff --git a/src/main.rs b/src/main.rs index 3b75c9f..569ee63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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>>, } +#[derive(Deserialize, Debug)] +struct QueryForm { + query: String, + is_read: bool, +} + impl Actor for GamesActor { type Context = actix::Context; } @@ -171,6 +178,19 @@ async fn list_of_games(games: web::Data>>>) -> impl Responde .body(games_json) } +#[post("/admin/query")] +async fn admin_query(pool: web::Data, form: web::Form) -> 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() diff --git a/src/templates/admin.tera b/src/templates/admin.tera index c6a3535..5c945a6 100644 --- a/src/templates/admin.tera +++ b/src/templates/admin.tera @@ -46,6 +46,13 @@ +
+

+ +
+ +
+

   
     

Name:

Color:

@@ -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;