rhai implementation added

This commit is contained in:
filipriec
2025-03-06 19:32:45 +01:00
parent a52e2b05f8
commit 0e0578d158
8 changed files with 290 additions and 1 deletions

View File

@@ -18,6 +18,8 @@ tonic = "0.12.3"
tonic-reflection = "0.12.3"
tracing = "0.1.41"
time = { version = "0.3.37", features = ["local-offset"] }
rhai = "1.21.0"
bincode = "2.0.0"
[lib]
name = "server"

View File

@@ -4,7 +4,7 @@ CREATE TABLE table_scripts (
table_definitions_id BIGINT NOT NULL REFERENCES table_definitions(id),
target_column TEXT NOT NULL,
rhai_script TEXT NOT NULL,
compiled_script BYTEA NOT NULL;
compiled_script BYTEA NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(table_definitions_id, target_column)
);

View File

@@ -7,6 +7,7 @@ pub mod shared;
pub mod table_structure;
pub mod table_definition;
pub mod tables_data;
pub mod table_script;
// Re-export run_server from the inner server module:
pub use server::run_server;

View File

@@ -0,0 +1,4 @@
// src/table_script/handlers.rs
pub mod post_table_script;
pub use post_table_script::post_table_script;

View File

@@ -0,0 +1,119 @@
// src/table_script/handlers/post_table_script.rs
use tonic::Status;
use sqlx::{PgPool, Error as SqlxError};
use rhai::Engine;
use time::OffsetDateTime;
use bincode::serialize;
use common::proto::multieko2::table_script::{PostTableScriptRequest, TableScriptResponse};
pub async fn post_table_script(
db_pool: &PgPool,
request: PostTableScriptRequest,
) -> Result<TableScriptResponse, Status> {
// Fetch table definition
let table_def = sqlx::query!(
r#"SELECT table_name, columns, linked_table_id
FROM table_definitions WHERE id = $1"#,
request.table_definition_id
)
.fetch_optional(db_pool)
.await
.map_err(|e| Status::internal(format!("Database error: {}", e)))?
.ok_or_else(|| Status::not_found("Table definition not found"))?;
// Validate target column exists
let columns: Vec<String> = serde_json::from_value(table_def.columns.clone())
.map_err(|e| Status::invalid_argument(format!("Invalid columns format: {}", e)))?;
let column_names: Vec<&str> = columns.iter()
.filter_map(|col| col.split_whitespace().next())
.map(|name| name.trim_matches('"'))
.collect();
if !column_names.contains(&request.target_column.as_str()) {
return Err(Status::invalid_argument("Target column not found in table definition"));
}
// Check for existing data
let row_count: i64 = sqlx::query_scalar(&format!(
"SELECT COUNT(*) FROM {}",
sanitize_identifier(&table_def.table_name)
))
.fetch_one(db_pool)
.await
.map_err(|e| Status::internal(format!("Data check failed: {}", e)))?;
if row_count > 0 {
return Err(Status::failed_precondition(
"Cannot add scripts to tables with existing data"
));
}
// Compile Rhai script
let (ast, warnings) = compile_rhai_script(&request.rhai_script)
.map_err(|e| Status::invalid_argument(format!("Script error: {}", e)))?;
// Store script in database
let script_record = sqlx::query!(
r#"INSERT INTO table_scripts
(table_definitions_id, target_column, rhai_script, compiled_script)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at"#,
request.table_definition_id,
request.target_column,
request.rhai_script,
bincode::serialize(&ast).map_err(|e| Status::internal(format!("Serialization failed: {}", e)))?,
)
.fetch_one(db_pool)
.await
.map_err(|e| match e {
SqlxError::Database(db_err) if db_err.constraint() == Some("table_scripts_table_definitions_id_target_column_key") => {
Status::already_exists("Script already exists for this column")
}
_ => Status::internal(format!("Database error: {}", e)),
})?;
Ok(TableScriptResponse {
id: script_record.id,
created_at: Some(script_record.created_at.into()),
warnings,
})
Ok(TableScriptResponse {
id: script_record.id,
created_at: script_record.created_at.to_string(),
warnings,
})
}
fn sanitize_identifier(s: &str) -> String {
s.replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "")
.trim()
.to_lowercase()
}
fn compile_rhai_script(script: &str) -> Result<(AST, String), String> {
let mut engine = Engine::new();
let mut scope = Scope::new();
// Add custom API bindings
engine.register_fn("get_column", |_: &str, _: &str| Ok(()));
engine.register_fn("set_result", |_: &mut Scope, _: rhai::Dynamic| Ok(()));
let ast = engine.compile(script).map_err(|e| e.to_string())?;
// Validate script structure
if !script.contains("set_result") {
return Err("Script must call set_result()".into());
}
let warnings = engine.gen_ast_clear_comments(&ast)
.iter()
.filter_map(|(_, err)| match err {
rhai::ParseError(warn, _) => Some(warn.to_string()),
_ => None
})
.collect::<Vec<_>>()
.join(", ");
Ok((ast, warnings))
}

View File

@@ -0,0 +1,3 @@
// src/tables_data/mod.rs
pub mod handlers;