saving steel script into the database
This commit is contained in:
@@ -7,23 +7,23 @@ license.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
|
|
||||||
chrono = { version = "0.4.39", features = ["serde"] }
|
chrono = { version = "0.4.40", features = ["serde"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
prost = "0.13.5"
|
prost = "0.13.5"
|
||||||
serde = { version = "1.0.218", features = ["derive"] }
|
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"] }
|
sqlx = { version = "0.8.3", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "time"] }
|
||||||
tokio = { version = "1.43.0", features = ["full", "macros"] }
|
tokio = { version = "1.43.0", features = ["full", "macros"] }
|
||||||
tonic = "0.12.3"
|
tonic = "0.12.3"
|
||||||
tonic-reflection = "0.12.3"
|
tonic-reflection = "0.12.3"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
time = { version = "0.3.37", features = ["local-offset"] }
|
time = { version = "0.3.39", features = ["local-offset"] }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "server"
|
name = "server"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.0", features = ["full", "test-util"] }
|
tokio = { version = "1.43", features = ["full", "test-util"] }
|
||||||
rstest = "0.24.0"
|
rstest = "0.24.0"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ CREATE TABLE table_scripts (
|
|||||||
table_definitions_id BIGINT NOT NULL REFERENCES table_definitions(id),
|
table_definitions_id BIGINT NOT NULL REFERENCES table_definitions(id),
|
||||||
target_column TEXT NOT NULL,
|
target_column TEXT NOT NULL,
|
||||||
script TEXT NOT NULL,
|
script TEXT NOT NULL,
|
||||||
source_tables TEXT[] NOT NULL, -- Added to track which tables are used in calculation
|
source_tables TEXT[],
|
||||||
source_columns TEXT[] NOT NULL, -- Added to track which columns are used in calculation
|
source_columns TEXT[],
|
||||||
description TEXT, -- Optional description of what the script does
|
description TEXT,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(table_definitions_id, target_column)
|
UNIQUE(table_definitions_id, target_column)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod table_structure;
|
|||||||
pub mod table_definition;
|
pub mod table_definition;
|
||||||
pub mod tables_data;
|
pub mod tables_data;
|
||||||
pub mod table_script;
|
pub mod table_script;
|
||||||
|
pub mod steel;
|
||||||
|
|
||||||
// Re-export run_server from the inner server module:
|
// Re-export run_server from the inner server module:
|
||||||
pub use server::run_server;
|
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
|
// 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,17 +2,16 @@
|
|||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use sqlx::{PgPool, Error as SqlxError};
|
use sqlx::{PgPool, Error as SqlxError};
|
||||||
use common::proto::multieko2::table_script::{PostTableScriptRequest, TableScriptResponse};
|
use common::proto::multieko2::table_script::{PostTableScriptRequest, TableScriptResponse};
|
||||||
use regex::Regex;
|
use crate::steel::handlers::evaluator::{validate_script, validate_target_column};
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
pub async fn post_table_script(
|
pub async fn post_table_script(
|
||||||
db_pool: &PgPool,
|
db_pool: &PgPool,
|
||||||
request: PostTableScriptRequest,
|
request: PostTableScriptRequest,
|
||||||
) -> Result<TableScriptResponse, Status> {
|
) -> Result<TableScriptResponse, Status> {
|
||||||
// Fetch table definition
|
// Basic validation
|
||||||
let table_def = sqlx::query!(
|
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"#,
|
FROM table_definitions WHERE id = $1"#,
|
||||||
request.table_definition_id
|
request.table_definition_id
|
||||||
)
|
)
|
||||||
.fetch_optional(db_pool)
|
.fetch_optional(db_pool)
|
||||||
@@ -20,22 +19,26 @@ pub async fn post_table_script(
|
|||||||
.map_err(|e| Status::internal(format!("Database error: {}", e)))?
|
.map_err(|e| Status::internal(format!("Database error: {}", e)))?
|
||||||
.ok_or_else(|| Status::not_found("Table definition not found"))?;
|
.ok_or_else(|| Status::not_found("Table definition not found"))?;
|
||||||
|
|
||||||
// Extract source tables and columns from the script
|
// Call validation functions
|
||||||
let (source_tables, source_columns) = extract_sources(&request.script, &db_pool).await
|
validate_script(&request.script) // Changed from validate_syntax to validate_script
|
||||||
.map_err(|e| Status::invalid_argument(format!("Source extraction error: {}", e)))?;
|
.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
|
// Store script in database
|
||||||
let script_record = sqlx::query!(
|
let script_record = sqlx::query!(
|
||||||
r#"INSERT INTO table_scripts
|
r#"INSERT INTO table_scripts
|
||||||
(table_definitions_id, target_column, script, source_tables, source_columns, description)
|
(table_definitions_id, target_column, script, description)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4)
|
||||||
RETURNING id"#,
|
RETURNING id"#,
|
||||||
request.table_definition_id,
|
request.table_definition_id,
|
||||||
request.target_column,
|
request.target_column,
|
||||||
request.script,
|
request.script,
|
||||||
&source_tables as &[String],
|
description
|
||||||
&source_columns as &[String],
|
|
||||||
request.description.unwrap_or_default()
|
|
||||||
)
|
)
|
||||||
.fetch_one(db_pool)
|
.fetch_one(db_pool)
|
||||||
.await
|
.await
|
||||||
@@ -51,61 +54,3 @@ pub async fn post_table_script(
|
|||||||
warnings: String::new(),
|
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