saving steel script into the database
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
4
server/src/steel/handlers.rs
Normal file
4
server/src/steel/handlers.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
// src/steel/handlers.rs
|
||||
pub mod evaluator;
|
||||
|
||||
pub use evaluator::{validate_script, validate_target_column};
|
||||
32
server/src/steel/handlers/evaluator.rs
Normal file
32
server/src/steel/handlers/evaluator.rs
Normal file
@@ -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<String> = 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(())
|
||||
}
|
||||
3
server/src/steel/mod.rs
Normal file
3
server/src/steel/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
// src/steel/mod.rs
|
||||
|
||||
pub mod handlers;
|
||||
@@ -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;
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
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<TableScriptResponse, Status> {
|
||||
// Fetch table definition
|
||||
// Basic validation
|
||||
let table_def = sqlx::query!(
|
||||
r#"SELECT table_name, columns, linked_table_id
|
||||
r#"SELECT id, table_name, columns, profile_id
|
||||
FROM table_definitions WHERE id = $1"#,
|
||||
request.table_definition_id
|
||||
)
|
||||
@@ -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<String>, Vec<String>), 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()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user