From 1b21031afc38accaad33d972dc0f8e2096ac1d7d Mon Sep 17 00:00:00 2001 From: "James A. Overton" Date: Tue, 31 Oct 2023 20:10:41 +0000 Subject: [PATCH 1/7] Fix bad comma in default database filename --- src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 0c5f4d8..acec1aa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -157,7 +157,7 @@ impl Config { .database .unwrap_or_default() .connection - .unwrap_or(".nanobot,db".into()), + .unwrap_or(".nanobot.db".into()), pool: None, valve_path: user .valve From 42cdab4ed63d3e20bb124ad5a2b042b96bfc4137 Mon Sep 17 00:00:00 2001 From: "James A. Overton" Date: Tue, 31 Oct 2023 20:11:27 +0000 Subject: [PATCH 2/7] First pass at VALVE undo/redo support --- Cargo.toml | 2 +- src/get.rs | 3 ++ src/resources/page.html | 20 +++++++---- src/serve.rs | 78 ++++++++++++++++++++++++++++++----------- 4 files changed, 74 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eb3b25e..ead6143 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ rev = "f46fbd5450505644ed9970cef1ae14164699981f" [dependencies.ontodev_valve] git = "https://github.com/ontodev/valve.rs" -rev = "074b99254e4cc956c84cf7b68bff26cae57f0d16" +rev = "7dc44f7a2292691c541b8e2d7c9ff535b4011e99" [dependencies.ontodev_sqlrest] git = "https://github.com/ontodev/sqlrest.rs" diff --git a/src/get.rs b/src/get.rs index ee118f0..dd7b91c 100644 --- a/src/get.rs +++ b/src/get.rs @@ -900,6 +900,9 @@ async fn get_page( let mut tables = Map::new(); for key in table_map.keys() { + if key == "history" { + continue; + } tables.insert(key.clone(), Value::String(key.clone())); } diff --git a/src/resources/page.html b/src/resources/page.html index f0dd201..e939f94 100644 --- a/src/resources/page.html +++ b/src/resources/page.html @@ -107,7 +107,7 @@ diff --git a/src/serve.rs b/src/serve.rs index 40f3576..37deedd 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -20,7 +20,7 @@ use ontodev_hiccup::hiccup; use ontodev_sqlrest::{parse, Filter, Select, SelectColumn}; use ontodev_valve::{ ast::Expression, - delete_row, insert_new_row, update_row, + delete_row, insert_new_row, redo, undo, update_row, validate::{get_matching_values, validate_row}, }; use regex::{Captures, Regex}; @@ -109,14 +109,33 @@ async fn post_table( query_params, form_params ); - table( - &path, - &state, - &query_params, - &form_params, - RequestType::POST, - ) - .await + let mut request_type = RequestType::POST; + if form_params.contains_key("undo") { + tracing::info!("UNDO"); + let (vconfig, dt_conds, rule_conds) = match &state.config.valve { + Some(v) => (&v.config, &v.datatype_conditions, &v.rule_conditions), + None => return Err("Missing valve configuration".into()), + }; + let pool = match state.config.pool.as_ref() { + Some(p) => p, + None => return Err("Missing database pool".into()), + }; + block_on(undo(&vconfig, dt_conds, rule_conds, pool, "Nanobot")).map_err(|e| e.to_string()); + request_type = RequestType::GET; + } else if form_params.contains_key("redo") { + tracing::info!("REDO"); + let (vconfig, dt_conds, rule_conds) = match &state.config.valve { + Some(v) => (&v.config, &v.datatype_conditions, &v.rule_conditions), + None => return Err("Missing valve configuration".into()), + }; + let pool = match state.config.pool.as_ref() { + Some(p) => p, + None => return Err("Missing database pool".into()), + }; + block_on(redo(&vconfig, dt_conds, rule_conds, pool, "Nanobot")).map_err(|e| e.to_string()); + request_type = RequestType::GET; + } + table(&path, &state, &query_params, &form_params, request_type).await } async fn get_table( @@ -256,6 +275,9 @@ fn action( let table_map = { let mut table_map = SerdeMap::new(); for table in get_tables(state.config.valve.as_ref().ok_or("No VALVE config")?)? { + if table == "history" { + continue; + } table_map.insert(table.to_string(), json!(table.clone())); } json!(table_map) @@ -379,6 +401,9 @@ async fn tree( let table_map = { let mut table_map = SerdeMap::new(); for table in get_tables(&state.config.valve.as_ref().clone().unwrap())? { + if table == "history" { + continue; + } table_map.insert(table.to_string(), json!(table.clone())); } json!(table_map) @@ -504,6 +529,9 @@ async fn tree2( let table_map = { let mut table_map = SerdeMap::new(); for table in get_tables(&state.config.valve.as_ref().clone().unwrap())? { + if table == "history" { + continue; + } table_map.insert(table.to_string(), json!(table.clone())); } json!(table_map) @@ -711,7 +739,7 @@ async fn table( match get_row_as_form_map(config, &table, &validated_row) { Ok(f) => form_map = Some(f), Err(e) => { - tracing::debug!("Rendering error {}", e); + tracing::debug!("Rendering error 1 {}", e); form_map = None } }; @@ -745,10 +773,10 @@ async fn table( // TODO: Improve handling of custom views. if view != "" { // In this case the request is to view the "insert new row" form: - if table == "message" { + if vec!["message", "history"].contains(&&*table) { return Err(( StatusCode::BAD_REQUEST, - Html("Editing the message table is not possible"), + Html(format!("Editing the {} table is not possible", table)), ) .into_response() .into()); @@ -778,7 +806,7 @@ async fn table( match get_row_as_form_map(config, &table, &new_row) { Ok(f) => form_map = Some(f), Err(e) => { - tracing::debug!("Rendering error {}", e); + tracing::debug!("Rendering error 2 {}", e); form_map = None } }; @@ -788,6 +816,9 @@ async fn table( let table_map = { let mut table_map = SerdeMap::new(); for table in get_tables(config)? { + if table == "history" { + continue; + } table_map.insert(table.to_string(), json!(table.clone())); } json!(table_map) @@ -1051,7 +1082,7 @@ fn render_row_from_database( match get_row_as_form_map(config, table, &validated_row) { Ok(f) => form_map = Some(f), Err(e) => { - tracing::debug!("Rendering error {}", e); + tracing::debug!("Rendering error 3 {}", e); form_map = None } }; @@ -1092,10 +1123,10 @@ fn render_row_from_database( // Handle a request to display a form for editing and validiating the given row: if view != "" { if let None = form_map { - if table == "message" { + if vec!["message", "history"].contains(&table) { return Err(( StatusCode::BAD_REQUEST, - Html("Editing the message table is not possible"), + Html(format!("Editing the {} table is not possible", table)), ) .into_response() .into()); @@ -1116,7 +1147,7 @@ fn render_row_from_database( match get_row_as_form_map(config, table, &metafied_row) { Ok(f) => form_map = Some(f), Err(e) => { - tracing::debug!("Rendering error {}", e); + tracing::debug!("Rendering error 4 {}", e); form_map = None } }; @@ -1137,6 +1168,9 @@ fn render_row_from_database( let table_map = { let mut table_map = SerdeMap::new(); for table in get_tables(config)? { + if table == "history" { + continue; + } table_map.insert(table.to_string(), json!(table.clone())); } json!(table_map) @@ -1406,7 +1440,7 @@ fn insert_table_row( &table_name, &row_data, None, - false, + "Nanobot", )) .map_err(|e| e.to_string()) } @@ -1433,8 +1467,7 @@ fn update_table_row( &table_name, &row_data, row_number, - false, - false, + "Nanobot", )) .map_err(|e| e.to_string()) } @@ -1459,7 +1492,7 @@ fn delete_table_row( pool, &table_name, row_number, - false, + "Nanobot", )) .map_err(|e| e.to_string()) } @@ -1615,6 +1648,9 @@ fn get_row_as_form_map( if cell_header == "row_number" { continue; } + if cell_header == "history" { + continue; + } let (valid, value, messages); match cell_value.as_object() { None => return Err(format!("Cell value: {:?} is not an object.", cell_value)), From bdb08a3b59f898a66614bf6354cee1bd6fd2f361 Mon Sep 17 00:00:00 2001 From: "James A. Overton" Date: Mon, 6 Nov 2023 15:09:21 +0000 Subject: [PATCH 3/7] Update VALVE, use SQLite initial load optimizations --- Cargo.toml | 2 +- src/config.rs | 6 ++++-- src/init.rs | 7 +++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ead6143..d9c82cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ rev = "f46fbd5450505644ed9970cef1ae14164699981f" [dependencies.ontodev_valve] git = "https://github.com/ontodev/valve.rs" -rev = "7dc44f7a2292691c541b8e2d7c9ff535b4011e99" +rev = "25dd32d0bd22a539043515e35c5b9b5803e3baf6" [dependencies.ontodev_sqlrest] git = "https://github.com/ontodev/sqlrest.rs" diff --git a/src/config.rs b/src/config.rs index acec1aa..3b2482b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -248,12 +248,14 @@ impl Config { } pub async fn load_valve_config(&mut self) -> Result<&mut Config, String> { - // TODO: Make the path configurable: + let verbose = false; + let initial_load = !Path::new(&self.valve_path).is_file(); match valve( &self.valve_path, &self.connection, &ValveCommand::Config, - false, + verbose, + initial_load, "table", ) .await diff --git a/src/init.rs b/src/init.rs index 1b0992e..a026134 100644 --- a/src/init.rs +++ b/src/init.rs @@ -192,11 +192,14 @@ pub async fn init(config: &Config) -> Result { } // load tables into database + let verbose = false; + let initial_load = !Path::new(&config.valve_path).is_file(); match valve( &config.valve_path, - &database, + &config.connection, &ValveCommand::Load, - false, + verbose, + initial_load, "table", ) .await From 4904284e63b1c9ca805d54b418c22ead0551d8ad Mon Sep 17 00:00:00 2001 From: "James A. Overton" Date: Fri, 24 Nov 2023 19:07:20 +0000 Subject: [PATCH 4/7] Rework create-only and initial-load --- src/config.rs | 20 +++++++++++++++++--- src/init.rs | 12 +++++++++--- src/main.rs | 35 +++++++++++++++++++++-------------- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/config.rs b/src/config.rs index 3b2482b..a361dbe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,8 +20,10 @@ pub struct Config { pub logging_level: LoggingLevel, pub connection: String, pub pool: Option, - pub valve_path: String, pub valve: Option, + pub valve_path: String, + pub valve_create_only: bool, + pub valve_initial_load: bool, pub asset_path: Option, pub template_path: Option, pub actions: IndexMap, @@ -159,12 +161,14 @@ impl Config { .connection .unwrap_or(".nanobot.db".into()), pool: None, + valve: None, valve_path: user .valve .unwrap_or_default() .path .unwrap_or("src/schema/table.tsv".into()), - valve: None, + valve_create_only: false, + valve_initial_load: false, asset_path: { match user.assets.unwrap_or_default().path { Some(p) => { @@ -249,7 +253,7 @@ impl Config { pub async fn load_valve_config(&mut self) -> Result<&mut Config, String> { let verbose = false; - let initial_load = !Path::new(&self.valve_path).is_file(); + let initial_load = false; match valve( &self.valve_path, &self.connection, @@ -288,6 +292,16 @@ impl Config { self.connection = connection.into(); self } + + pub fn create_only(&mut self, value: bool) -> &mut Config { + self.valve_create_only = value; + self + } + + pub fn initial_load(&mut self, value: bool) -> &mut Config { + self.valve_initial_load = value; + self + } } impl fmt::Display for Config { diff --git a/src/init.rs b/src/init.rs index a026134..1d0a294 100644 --- a/src/init.rs +++ b/src/init.rs @@ -193,13 +193,19 @@ pub async fn init(config: &Config) -> Result { // load tables into database let verbose = false; - let initial_load = !Path::new(&config.valve_path).is_file(); + let command = if config.valve_create_only { + &ValveCommand::Create + } else { + &ValveCommand::Load + }; + tracing::debug!("VALVE command {:?}", command); + tracing::debug!("VALVE initial_load {}", config.valve_initial_load); match valve( &config.valve_path, &config.connection, - &ValveCommand::Load, + command, verbose, - initial_load, + config.valve_initial_load, "table", ) .await diff --git a/src/main.rs b/src/main.rs index 8be2f94..f1998cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,13 +47,17 @@ async fn main() { .subcommand_required(false) .arg_required_else_help(true) .subcommand( - Command::new("init").about("Initialises things").arg( - arg!( - -d --database "Specifies a custom database name" + Command::new("init") + .about("Initialises things") + .arg( + arg!( + -d --database "Specifies a custom database name" + ) + .required(false) + .value_parser(value_parser!(String)), ) - .required(false) - .value_parser(value_parser!(String)), - ), + .arg(arg!(--create_only "Only create VALVE tables").required(false)) + .arg(arg!(--initial_load "Use unsafe SQLite optimizations").required(false)), ) .subcommand(Command::new("config").about("Configures things")) .subcommand( @@ -79,15 +83,18 @@ async fn main() { .get_matches(); let exit_result = match matches.subcommand() { - Some(("init", sub_matches)) => match sub_matches.get_one::("database") { - Some(x) => { - //update config - config.connection(x); - - init::init(&config).await + Some(("init", sub_matches)) => { + if sub_matches.get_flag("create_only") { + config.create_only(true); + } + if sub_matches.get_flag("initial_load") { + config.initial_load(true); } - _ => init::init(&config).await, - }, + if let Some(d) = sub_matches.get_one::("database") { + config.connection(d); + } + init::init(&config).await + } Some(("config", _sub_matches)) => Ok(config.to_string()), Some(("get", sub_matches)) => { let table = match sub_matches.get_one::("TABLE") { From 95f730c41c2cf9c8d1c2d445d21fb85d584597cc Mon Sep 17 00:00:00 2001 From: "James A. Overton" Date: Fri, 24 Nov 2023 22:09:40 +0000 Subject: [PATCH 5/7] Add undo/redo messages, cell history --- src/get.rs | 454 +++++++++++++++------------------------ src/resources/page.html | 12 +- src/resources/table.html | 17 +- src/serve.rs | 24 ++- 4 files changed, 215 insertions(+), 292 deletions(-) diff --git a/src/get.rs b/src/get.rs index dd7b91c..9150c80 100644 --- a/src/get.rs +++ b/src/get.rs @@ -10,13 +10,16 @@ use crate::sql::{ use chrono::prelude::{DateTime, Utc}; use csv::WriterBuilder; use enquote::unquote; +use futures::executor::block_on; use git2::Repository; use minijinja::{Environment, Source}; use ontodev_sqlrest::{Direction, OrderByColumn, Select}; -use ontodev_valve::get_sql_type_from_global_config; +use ontodev_valve as valve; use regex::Regex; +use serde::{Deserialize, Serialize}; use serde_json::{json, to_string_pretty, Map, Value}; -use std::collections::HashMap; +use sqlx::any::AnyRow; +use sqlx::Row; use std::error::Error; use std::fmt; use std::fs; @@ -86,6 +89,24 @@ impl From for GetError { } } +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +struct ValveMessage { + column: String, + value: String, + rule: String, + level: String, + message: String, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +struct ValveChange { + column: String, + level: String, + old_value: String, + value: String, + message: String, +} + pub async fn get_table( config: &Config, table: &str, @@ -260,7 +281,7 @@ async fn get_page( } } - let sql_type = get_sql_type_from_global_config( + let sql_type = valve::get_sql_type_from_global_config( &config.valve.as_ref().unwrap().config, &unquote(&select.table).unwrap(), &key, @@ -391,9 +412,10 @@ async fn get_page( for col in &curr_cols { view_select.add_explicit_select(col); } - // If this isn't the message table, explicitly include the message column from the table's view: + // If this isn't the message table, explicitly include the message and history columns from the table's view: if unquoted_table != "message" { view_select.add_select("message"); + view_select.add_select("history"); } // Only apply the limit to the view query if we're filtering for rows with messages: @@ -415,280 +437,10 @@ async fn get_page( Err(e) => return Err(GetError::new(e.to_string())), }; // convert value_rows to cell_rows - let mut cell_rows: Vec> = vec![]; - for row in &value_rows { - let mut crow: Map = Map::new(); - for (k, v) in row.iter() { - if unquoted_table != "message" && k == "message" { - continue; - } - let mut cell: Map = Map::new(); - let mut classes: Vec = vec![]; - - // Add the value to the cell - cell.insert("value".to_string(), v.clone()); - - // Row numbers and message ids have an integer datatype but otherwise do not need to be - // processed, so we continue: - if (unquoted_table != "message" && k == "row_number") - || (unquoted_table == "message" && k == "message_id") - { - cell.insert("datatype".to_string(), Value::String("integer".to_string())); - crow.insert(k.to_string(), Value::Object(cell)); - continue; - } - - // Handle null and nulltype - if v.is_null() { - classes.push("bg-null".to_string()); - match column_map.get(k) { - Some(column) => { - if let Some(nulltype) = column.get("nulltype") { - if nulltype.is_string() { - cell.insert("nulltype".to_string(), nulltype.clone()); - } - } - } - None => { - return Err(GetError::new(format!( - "While handling nulltype: No key '{}' in column_map {:?}", - k, column_map - ))) - } - }; - } - - // Handle the datatype: - if !cell.contains_key("nulltype") { - let datatype = match column_map.get(k) { - Some(column) => match column.get("datatype") { - Some(datatype) => datatype, - None => { - return Err(GetError::new(format!( - "While handling datatype: No 'datatype' entry in {:?}", - column - ))) - } - }, - None => { - return Err(GetError::new(format!( - "No key '{}' in column_map {:?}", - k, column_map - ))) - } - }; - cell.insert("datatype".to_string(), datatype.clone()); - } - // Handle structure - match column_map.get(k) { - Some(column) => { - let default_structure = json!(""); - let structure = column.get("structure").unwrap_or(&default_structure); - if structure == "from(table.table)" { - let href = format!("table?table=eq.{}", { - match v.as_str() { - Some(s) => s.to_string(), - None => { - return Err(GetError::new(format!( - "Could not convert '{}' to str", - v - ))) - } - } - }); - cell.insert("href".to_string(), Value::String(href)); - } else if k == "table" && unquoted_table == "table" { - // In the 'table' table, link to the other tables - let href = match v.as_str() { - Some(s) => s.to_string(), - None => { - return Err(GetError::new(format!( - "Could not convert '{}' to str", - v - ))) - } - }; - cell.insert("href".to_string(), Value::String(href)); - } - } - None => { - return Err(GetError::new(format!( - "No key '{}' in column_map {:?}", - k, column_map - ))) - } - }; - - if classes.len() > 0 { - cell.insert("classes".to_string(), json!(classes)); - } - - crow.insert(k.to_string(), Value::Object(cell)); - } - - // Handle messages associated with the row: - let mut error_values = HashMap::new(); - if unquoted_table != "message" { - if let Some(input_messages) = row.get("message") { - let input_messages = match input_messages { - Value::Array(value) => value.clone(), - Value::String(value) => { - let value = unquote(&value).unwrap_or(value.to_string()); - match serde_json::from_str::(value.as_str()) { - Err(e) => return Err(GetError::new(e.to_string())), - Ok(value) => match value.as_array() { - None => { - return Err(GetError::new(format!( - "Value '{}' is not an array.", - value - ))) - } - Some(value) => value.to_vec(), - }, - } - } - Value::Null => vec![], - _ => { - return Err(GetError::new(format!( - "'{}' is not a Value String or Value Array", - input_messages - ))) - } - }; - let mut output_messages: HashMap<&str, Vec>> = HashMap::new(); - let mut max_level: usize = 0; - let mut message_level = "info".to_string(); - for message in &input_messages { - let mut m = Map::new(); - let message_map = match message.as_object() { - Some(o) => o, - None => { - return Err(GetError::new(format!("{:?} is not an object.", message))) - } - }; - for (key, value) in message_map.iter() { - if key != "column" && key != "value" { - m.insert(key.clone(), value.clone()); - } - } - let column = match message_map.get("column") { - Some(c) => match c.as_str() { - Some(s) => s, - None => { - return Err(GetError::new(format!( - "Could not convert '{}' to str", - c - ))) - } - }, - None => { - return Err(GetError::new(format!( - "No 'column' key in {:?}", - message_map - ))) - } - }; - let value = match message.get("value") { - Some(v) => match v.as_str() { - Some(s) => s, - None => { - return Err(GetError::new(format!( - "Could not convert '{}' to str", - v - ))) - } - }, - None => { - return Err(GetError::new(format!( - "No 'value' key in {:?}", - message_map - ))) - } - }; - error_values.insert(column.to_owned(), value); - if let Some(v) = output_messages.get_mut(&column) { - v.push(m); - } else { - output_messages.insert(column, vec![m]); - } - - let level = match message.get("level") { - Some(v) => match v.as_str() { - Some(s) => s.to_string(), - None => { - return Err(GetError::new(format!( - "Could not convert '{}' to str", - v - ))) - } - }, - None => { - return Err(GetError::new(format!( - "No 'level' key in {:?}", - message_map - ))) - } - }; - let lvl = level_to_int(&level); - if lvl > max_level { - max_level = lvl; - message_level = level; - } - } - - for (column, messages) in &output_messages { - if let Some(cell) = crow.get_mut(column.to_owned()) { - if let Some(cell) = cell.as_object_mut() { - cell.remove("nulltype"); - let mut new_classes = vec![]; - if let Some(classes) = cell.get_mut("classes") { - match classes.as_array() { - None => { - return Err(GetError::new(format!( - "{:?} is not an array", - classes - ))) - } - Some(classes_array) => { - for class in classes_array { - match class.as_str() { - None => { - return Err(GetError::new(format!( - "Could not convert '{}' to str", - class - ))) - } - Some(s) => { - if s.to_string() != "bg-null" { - new_classes.push(class.clone()); - } - } - }; - } - } - }; - } - let value = match error_values.get(column.to_owned()) { - Some(v) => v, - None => { - return Err(GetError::new(format!( - "No '{}' in {:?}", - column, error_values - ))) - } - }; - cell.insert("value".to_string(), json!(value)); - cell.insert("classes".to_string(), json!(new_classes)); - cell.insert("message_level".to_string(), json!(message_level)); - cell.insert("messages".to_string(), json!(messages)); - } - } - } - } - } - - cell_rows.push(crow); - } + let cell_rows: Vec> = value_rows + .iter() + .map(|r| decorate_row(&unquoted_table, &column_map, r)) + .collect(); let mut counts = Map::new(); let count = { @@ -920,6 +672,8 @@ async fn get_page( "select": select, "select_params": select2.to_params().unwrap_or_default(), "elapsed": elapsed, + "undo": get_undo_message(&config), + "redo": get_redo_message(&config), "actions": get_action_map(&config).unwrap_or_default(), "repo": get_repo_details().unwrap_or_default(), }, @@ -931,6 +685,150 @@ async fn get_page( Ok(result) } +// Given a table name, a column map, a cell value, and message list, +// return a JSON value representing this cell. +fn decorate_cell( + column_name: &str, + column: &Value, + value: &Value, + messages: &Vec, + history: &Vec>, +) -> Map { + let mut cell: Map = Map::new(); + cell.insert("value".to_string(), value.clone()); + + let mut classes: Vec = vec![]; + + // Handle null and nulltype + if value.is_null() { + classes.push("bg-null".to_string()); + if let Some(nulltype) = column.get("nulltype") { + if nulltype.is_string() { + cell.insert("nulltype".to_string(), nulltype.clone()); + } + } + } else { + let datatype = column + .get("datatype") + .expect("Column {k} must have a datatype in column_map {column_map:?}"); + cell.insert("datatype".to_string(), datatype.clone()); + } + + if classes.len() > 0 { + cell.insert("classes".to_string(), json!(classes)); + } + + // Handle messages associated with the row: + let mut output_messages = vec![]; + let mut max_level = 0; + let mut message_level = "none"; + for message in messages.iter().filter(|m| m.column == column_name) { + output_messages.push(json!({ + "level": message.level, + "rule": message.rule, + "message": message.message, + })); + let level = level_to_int(&message.level); + if level > max_level { + max_level = level; + message_level = message.level.as_str(); + } + } + + if output_messages.len() > 0 { + cell.insert("message_level".to_string(), json!(message_level)); + cell.insert("messages".to_string(), json!(output_messages)); + } + + let mut changes = vec![]; + for record in history.iter() { + for change in record.iter().filter(|c| c.column == column_name) { + changes.push(change); + } + } + + if changes.len() > 0 { + cell.insert("history".to_string(), json!(changes)); + } + + cell +} + +fn decorate_row( + table: &str, + column_map: &Map, + row: &Map, +) -> Map { + // tracing::debug!("Decorate Row: table {table}"); + let messages: Vec = match row.get("message") { + Some(serde_json::Value::Null) => vec![], + Some(json_value) => match serde_json::from_value(json_value.clone()) { + Ok(ms) => ms, + Err(x) => { + tracing::warn!("Could not parse message '{json_value:?}': {x:?}"); + vec![] + } + }, + None => vec![], + }; + let history: Vec> = match row.get("history") { + Some(serde_json::Value::Null) => vec![], + Some(json_value) => match serde_json::from_str(&json_value.as_str().unwrap_or_default()) { + Ok(ms) => ms, + Err(x) => { + tracing::warn!("Could not parse history '{json_value:?}': {x:?}"); + vec![] + } + }, + None => vec![], + }; + let mut cell_row: Map = SerdeMap::new(); + for (column_name, value) in row.iter() { + // tracing::debug!("Decorate Row: column {column_name}"); + if table != "message" && ["message", "history"].contains(&column_name.as_str()) { + continue; + } + let default_column = json!({ + "table": table.to_string(), + "column": column_name.to_string(), + "datatype": "integer", + }); + let column = column_map.get(column_name).unwrap_or(&default_column); + let cell = decorate_cell(column_name, column, value, &messages, &history); + cell_row.insert(column_name.to_string(), serde_json::Value::Object(cell)); + } + cell_row +} + +pub fn get_change_message(record: &AnyRow) -> Option { + let table = record.try_get::<&str, &str>("table").ok()?; + let row_number = record.try_get::("row").ok()? + 1; + let from = record.try_get::<&str, &str>("from").ok()?; + let to = record.try_get::<&str, &str>("to").ok()?; + let message = match (from, to) { + ("", _) => format!("add row {row_number} to '{table}'"), + (_, "") => format!("delete row {row_number} from '{table}'"), + (_, _) => format!("update row {row_number} of '{table}'"), + }; + Some(String::from(message)) +} + +// Get the undo message, or None. +pub fn get_undo_message(config: &Config) -> Option { + let pool = config.pool.as_ref()?; + let record = block_on(valve::get_record_to_undo(pool)).ok()??; + let message = get_change_message(&record)?; + Some(String::from(format!("Undo {message}"))) +} + +// Get the redo message, or None. +pub fn get_redo_message(config: &Config) -> Option { + let pool = config.pool.as_ref()?; + let record = block_on(valve::get_record_to_redo(pool)).ok()??; + let message = get_change_message(&record)?; + Some(String::from(format!("Redo {message}"))) +} + pub fn get_action_map(config: &Config) -> Result { let action_map: SerdeMap = config .actions diff --git a/src/resources/page.html b/src/resources/page.html index e939f94..85067c0 100644 --- a/src/resources/page.html +++ b/src/resources/page.html @@ -131,13 +131,13 @@ Actions