|
|
|
|
@@ -1,10 +1,9 @@
|
|
|
|
|
// 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};
|
|
|
|
|
use regex::Regex;
|
|
|
|
|
use std::collections::HashSet;
|
|
|
|
|
|
|
|
|
|
pub async fn post_table_script(
|
|
|
|
|
db_pool: &PgPool,
|
|
|
|
|
@@ -12,7 +11,7 @@ pub async fn post_table_script(
|
|
|
|
|
) -> Result<TableScriptResponse, Status> {
|
|
|
|
|
// Fetch table definition
|
|
|
|
|
let table_def = sqlx::query!(
|
|
|
|
|
r#"SELECT table_name, columns, linked_table_id
|
|
|
|
|
r#"SELECT table_name, columns, linked_table_id
|
|
|
|
|
FROM table_definitions WHERE id = $1"#,
|
|
|
|
|
request.table_definition_id
|
|
|
|
|
)
|
|
|
|
|
@@ -21,48 +20,22 @@ pub async fn post_table_script(
|
|
|
|
|
.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)))?;
|
|
|
|
|
// Extract source tables and columns from the script
|
|
|
|
|
let (source_tables, source_columns) = extract_sources(&request.script, &db_pool).await
|
|
|
|
|
.map_err(|e| Status::invalid_argument(format!("Source extraction 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"#,
|
|
|
|
|
r#"INSERT INTO table_scripts
|
|
|
|
|
(table_definitions_id, target_column, script, source_tables, source_columns, description)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
|
|
|
RETURNING id"#,
|
|
|
|
|
request.table_definition_id,
|
|
|
|
|
request.target_column,
|
|
|
|
|
request.rhai_script,
|
|
|
|
|
bincode::serialize(&ast).map_err(|e| Status::internal(format!("Serialization failed: {}", e)))?,
|
|
|
|
|
request.script,
|
|
|
|
|
&source_tables as &[String],
|
|
|
|
|
&source_columns as &[String],
|
|
|
|
|
request.description.unwrap_or_default()
|
|
|
|
|
)
|
|
|
|
|
.fetch_one(db_pool)
|
|
|
|
|
.await
|
|
|
|
|
@@ -75,45 +48,64 @@ pub async fn post_table_script(
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
warnings: String::new(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
// Extract source tables and columns from the script
|
|
|
|
|
async fn extract_sources(script: &str, pool: &PgPool) -> Result<(Vec<String>, Vec<String>), String> {
|
|
|
|
|
let mut tables = HashSet::new();
|
|
|
|
|
let mut columns = HashSet::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())?;
|
|
|
|
|
// Regular expression to find table/column references
|
|
|
|
|
// In Steel, we're looking for patterns like:
|
|
|
|
|
// - (get-table-column "table_name" "column_name")
|
|
|
|
|
// - column8 (for the current table)
|
|
|
|
|
|
|
|
|
|
// Validate script structure
|
|
|
|
|
if !script.contains("set_result") {
|
|
|
|
|
return Err("Script must call set_result()".into());
|
|
|
|
|
// Pattern for explicit table.column references
|
|
|
|
|
let table_column_pattern = Regex::new(r#"\(get-table-column\s+"([^"]+)"\s+"([^"]+)"\)"#)
|
|
|
|
|
.map_err(|e| format!("Regex error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
// Add current table's columns (simple column references)
|
|
|
|
|
let column_pattern = Regex::new(r#"column\d+"#)
|
|
|
|
|
.map_err(|e| format!("Regex error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
// Find all table.column references
|
|
|
|
|
for cap in table_column_pattern.captures_iter(script) {
|
|
|
|
|
if let (Some(table_match), Some(column_match)) = (cap.get(1), cap.get(2)) {
|
|
|
|
|
let table_name = table_match.as_str().to_string();
|
|
|
|
|
let column_name = column_match.as_str().to_string();
|
|
|
|
|
|
|
|
|
|
// Verify table exists in the database
|
|
|
|
|
let table_exists = sqlx::query!(
|
|
|
|
|
"SELECT EXISTS(SELECT 1 FROM pg_tables WHERE tablename = $1) as exists",
|
|
|
|
|
table_name
|
|
|
|
|
)
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| format!("Database error: {}", e))?
|
|
|
|
|
.exists
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
if !table_exists {
|
|
|
|
|
return Err(format!("Referenced table '{}' does not exist", table_name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tables.insert(table_name);
|
|
|
|
|
columns.insert(format!("{}.{}", table_name, column_name));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
// Find simple column references (assumed to be from the current table)
|
|
|
|
|
for column_match in column_pattern.find_iter(script) {
|
|
|
|
|
columns.insert(column_match.as_str().to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure at least one table is identified
|
|
|
|
|
if tables.is_empty() {
|
|
|
|
|
// If no explicit tables found, assume it's using the current table
|
|
|
|
|
tables.insert("current".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok((tables.into_iter().collect(), columns.into_iter().collect()))
|
|
|
|
|
}
|
|
|
|
|
|