diff --git a/server/Cargo.toml b/server/Cargo.toml index 6fc65c2..0e47340 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -7,23 +7,23 @@ license.workspace = true [dependencies] common = { path = "../common" } -chrono = { version = "0.4.39", features = ["serde"] } +chrono = { version = "0.4.40", features = ["serde"] } dotenvy = "0.15.7" prost = "0.13.5" serde = { version = "1.0.218", features = ["derive"] } -serde_json = "1.0.139" +serde_json = "1.0.140" sqlx = { version = "0.8.3", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "time"] } tokio = { version = "1.43.0", features = ["full", "macros"] } tonic = "0.12.3" tonic-reflection = "0.12.3" tracing = "0.1.41" -time = { version = "0.3.37", features = ["local-offset"] } +time = { version = "0.3.39", features = ["local-offset"] } [lib] name = "server" path = "src/lib.rs" [dev-dependencies] -tokio = { version = "1.0", features = ["full", "test-util"] } +tokio = { version = "1.43", features = ["full", "test-util"] } rstest = "0.24.0" lazy_static = "1.5.0" diff --git a/server/migrations/20250306132638_create_table_scripts.sql b/server/migrations/20250306132638_create_table_scripts.sql index 662ea0b..90567a7 100644 --- a/server/migrations/20250306132638_create_table_scripts.sql +++ b/server/migrations/20250306132638_create_table_scripts.sql @@ -4,9 +4,9 @@ CREATE TABLE table_scripts ( table_definitions_id BIGINT NOT NULL REFERENCES table_definitions(id), target_column TEXT NOT NULL, script TEXT NOT NULL, - source_tables TEXT[] NOT NULL, -- Added to track which tables are used in calculation - source_columns TEXT[] NOT NULL, -- Added to track which columns are used in calculation - description TEXT, -- Optional description of what the script does + source_tables TEXT[], + source_columns TEXT[], + description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(table_definitions_id, target_column) ); diff --git a/server/src/lib.rs b/server/src/lib.rs index 5547234..fe0295c 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -8,6 +8,7 @@ pub mod table_structure; pub mod table_definition; pub mod tables_data; pub mod table_script; +pub mod steel; // Re-export run_server from the inner server module: pub use server::run_server; diff --git a/server/src/steel/handlers.rs b/server/src/steel/handlers.rs new file mode 100644 index 0000000..a918992 --- /dev/null +++ b/server/src/steel/handlers.rs @@ -0,0 +1,4 @@ +// src/steel/handlers.rs +pub mod evaluator; + +pub use evaluator::{validate_script, validate_target_column}; diff --git a/server/src/steel/handlers/evaluator.rs b/server/src/steel/handlers/evaluator.rs new file mode 100644 index 0000000..1d686af --- /dev/null +++ b/server/src/steel/handlers/evaluator.rs @@ -0,0 +1,32 @@ +// src/steel/handlers/evaluator.rs +use serde_json::Value; + +const SYSTEM_COLUMNS: &[&str] = &["id", "deleted", "firma", "created_at"]; + +// Column validation +pub fn validate_target_column( + table_name: &str, + target: &str, + table_columns: &Value, +) -> Result<(), String> { + if SYSTEM_COLUMNS.contains(&target) { + return Err(format!("Cannot override system column: {}", target)); + } + + let columns: Vec = serde_json::from_value(table_columns.clone()) + .map_err(|e| format!("Invalid column data: {}", e))?; + + if !columns.iter().any(|c| c == target) { + return Err(format!("Target column {} not defined in table {}", target, table_name)); + } + + Ok(()) +} + +// Basic script validation +pub fn validate_script(script: &str) -> Result<(), String> { + if script.trim().is_empty() { + return Err("Script cannot be empty".to_string()); + } + Ok(()) +} diff --git a/server/src/steel/mod.rs b/server/src/steel/mod.rs new file mode 100644 index 0000000..c89ced1 --- /dev/null +++ b/server/src/steel/mod.rs @@ -0,0 +1,3 @@ +// src/steel/mod.rs + +pub mod handlers; diff --git a/server/src/table_script/handlers.rs b/server/src/table_script/handlers.rs index 4d31919..da0a988 100644 --- a/server/src/table_script/handlers.rs +++ b/server/src/table_script/handlers.rs @@ -1,4 +1,4 @@ // src/table_script/handlers.rs -// pub mod post_table_script; +pub mod post_table_script; -// pub use post_table_script::post_table_script; +pub use post_table_script::post_table_script; diff --git a/server/src/table_script/handlers/post_table_script.rs b/server/src/table_script/handlers/post_table_script.rs index 8598ee9..31b6431 100644 --- a/server/src/table_script/handlers/post_table_script.rs +++ b/server/src/table_script/handlers/post_table_script.rs @@ -2,17 +2,16 @@ use tonic::Status; use sqlx::{PgPool, Error as SqlxError}; use common::proto::multieko2::table_script::{PostTableScriptRequest, TableScriptResponse}; -use regex::Regex; -use std::collections::HashSet; +use crate::steel::handlers::evaluator::{validate_script, validate_target_column}; pub async fn post_table_script( db_pool: &PgPool, request: PostTableScriptRequest, ) -> Result { - // Fetch table definition + // Basic validation let table_def = sqlx::query!( - r#"SELECT table_name, columns, linked_table_id - FROM table_definitions WHERE id = $1"#, + r#"SELECT id, table_name, columns, profile_id + FROM table_definitions WHERE id = $1"#, request.table_definition_id ) .fetch_optional(db_pool) @@ -20,22 +19,26 @@ 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"))?; - // 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)))?; + // Call validation functions + validate_script(&request.script) // Changed from validate_syntax to validate_script + .map_err(|e| Status::invalid_argument(e))?; + + validate_target_column(&table_def.table_name, &request.target_column, &table_def.columns) + .map_err(|e| Status::invalid_argument(e))?; + + // Handle optional description + let description = request.description; // Store script in database let script_record = sqlx::query!( r#"INSERT INTO table_scripts - (table_definitions_id, target_column, script, source_tables, source_columns, description) - VALUES ($1, $2, $3, $4, $5, $6) + (table_definitions_id, target_column, script, description) + VALUES ($1, $2, $3, $4) RETURNING id"#, request.table_definition_id, request.target_column, request.script, - &source_tables as &[String], - &source_columns as &[String], - request.description.unwrap_or_default() + description ) .fetch_one(db_pool) .await @@ -51,61 +54,3 @@ pub async fn post_table_script( warnings: String::new(), }) } - -// Extract source tables and columns from the script -async fn extract_sources(script: &str, pool: &PgPool) -> Result<(Vec, Vec), String> { - let mut tables = HashSet::new(); - let mut columns = HashSet::new(); - - // 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) - - // 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)); - } - } - - // 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())) -}