diff --git a/server/src/steel/handlers.rs b/server/src/steel/handlers.rs deleted file mode 100644 index 2e1c6a4..0000000 --- a/server/src/steel/handlers.rs +++ /dev/null @@ -1,4 +0,0 @@ -// src/steel/handlers.rs -pub mod execution; - -pub use execution::*; diff --git a/server/src/steel/mod.rs b/server/src/steel/mod.rs index 181dd66..5759769 100644 --- a/server/src/steel/mod.rs +++ b/server/src/steel/mod.rs @@ -1,4 +1,3 @@ // src/steel/mod.rs -pub mod handlers; -pub mod validation; +pub mod server; diff --git a/server/src/steel/server/data_access.rs b/server/src/steel/server/data_access.rs new file mode 100644 index 0000000..0c68458 --- /dev/null +++ b/server/src/steel/server/data_access.rs @@ -0,0 +1,92 @@ +// src/steel/server/data_access.rs +use crate::steel::server::{ + rvals::{Custom, SteelVal}, + SteelError, +}; +use sqlx::{PgPool, Row}; +use std::collections::HashMap; + +pub struct TableAccess { + db_pool: PgPool, + profile_id: i64, + current_table: String, + local_data: HashMap, +} + +impl Custom for TableAccess {} + +impl TableAccess { + pub fn new( + db_pool: PgPool, + profile_id: i64, + current_table: &str, + local_data: HashMap, + ) -> Self { + Self { + db_pool, + profile_id, + current_table: current_table.to_string(), + local_data, + } + } + + async fn validate_link(&self, table: &str) -> Result<(), SteelError> { + let is_linked: bool = sqlx::query_scalar!( + r#"SELECT EXISTS( + SELECT 1 FROM table_definition_links tdl + JOIN table_definitions src ON tdl.source_table_id = src.id + JOIN table_definitions dst ON tdl.linked_table_id = dst.id + WHERE src.table_name = $1 AND dst.table_name = $2 AND src.profile_id = $3 + )"#, + self.current_table, + table, + self.profile_id + ) + .fetch_one(&self.db_pool) + .await + .map_err(|e| SteelError::new(e.to_string()))? + .unwrap_or(false); + + if !is_linked { + return Err(SteelError::new(format!( + "Table '{}' is not linked to '{}'", + table, self.current_table + ))); + } + + Ok(()) + } + + async fn get_external_value(&self, table: &str, column: &str) -> Result { + self.validate_link(table).await?; + + let firma = self.local_data.get("firma") + .ok_or_else(|| SteelError::new("Missing 'firma' in local data"))?; + + let query = format!( + "SELECT {} FROM \"{}\" WHERE firma = $1 AND profile_id = $2", + column, table + ); + + let value: Option = sqlx::query(&query) + .bind(firma) + .bind(self.profile_id) + .fetch_optional(&self.db_pool) + .await + .map_err(|e| SteelError::new(e.to_string()))? + .and_then(|row| row.try_get(0).ok()); + + value + .map(SteelVal::StringV) + .ok_or_else(|| SteelError::new(format!("No value found for {}.{}", table, column))) + } +} + +fn table_access_module( + db_pool: PgPool, + profile_id: i64, + current_table: String, + local_data: HashMap, +) -> Result { + Ok(TableAccess::new(db_pool, profile_id, ¤t_table, local_data)) +} diff --git a/server/src/steel/handlers/execution.rs b/server/src/steel/server/execution.rs similarity index 64% rename from server/src/steel/handlers/execution.rs rename to server/src/steel/server/execution.rs index c8418ab..ac77583 100644 --- a/server/src/steel/handlers/execution.rs +++ b/server/src/steel/server/execution.rs @@ -1,4 +1,4 @@ -// src/steel/handlers/execution.rs +// src/steel/server/execution.rs use std::fmt; use std::collections::HashMap; use sqlx::{PgPool, Row}; @@ -99,16 +99,21 @@ pub async fn resolve_value( let external_table = table.strip_prefix('@') .ok_or_else(|| ScriptExecutionError::InvalidReference(format!("Invalid external reference: {}", source)))?; - // Get linking key (assuming firma is the common key) - let firma = current_data.get("firma") - .ok_or_else(|| ScriptExecutionError::MissingLinkKey("firma".into()))?; + // Get foreign key relationship info + let (fk_column, referenced_column) = get_relationship_info(db_pool, current_table, external_table).await?; - // Query external table - let query = format!("SELECT {} FROM \"{}\" WHERE firma = $1 AND profile_id = $2", column, external_table); + // Get foreign key value from current data + let fk_value = current_data.get(&fk_column) + .ok_or_else(|| ScriptExecutionError::MissingLinkKey(fk_column.clone()))?; + + // Build and execute query + let query = format!( + "SELECT {} FROM \"{}\" WHERE {} = $1", + column, external_table, referenced_column + ); let result: Option = sqlx::query(&query) - .bind(firma) - .bind(profile_id) + .bind(fk_value) .fetch_optional(db_pool) .await .map_err(|e| ScriptExecutionError::DatabaseError(e.to_string()))? @@ -118,9 +123,58 @@ pub async fn resolve_value( format!("No data found for {} in {}", column, external_table) )) } else { - // Local column reference + // Local column reference remains the same current_data.get(source) .cloned() .ok_or_else(|| ScriptExecutionError::MissingSourceColumn(source.into())) } } + +async fn get_relationship_info( + db_pool: &PgPool, + current_table: &str, + external_table: &str, +) -> Result<(String, String), ScriptExecutionError> { + let query = r#" + SELECT + kcu.column_name AS fk_column, + ccu.column_name AS referenced_column + FROM + information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage AS ccu + ON tc.constraint_name = ccu.constraint_name + AND tc.table_schema = ccu.table_schema + WHERE + tc.constraint_type = 'FOREIGN KEY' + AND tc.table_name = $1 + AND ccu.table_name = $2 + "#; + + let rows = sqlx::query(query) + .bind(current_table) + .bind(external_table) + .fetch_all(db_pool) + .await + .map_err(|e| ScriptExecutionError::DatabaseError(e.to_string()))?; + + match rows.len() { + 0 => Err(ScriptExecutionError::InvalidReference(format!( + "No foreign key relationship found from {} to {}", + current_table, external_table + ))), + 1 => { + let row = &rows[0]; + Ok(( + row.get::("fk_column"), + row.get::("referenced_column"), + )) + }, + _ => Err(ScriptExecutionError::InvalidReference(format!( + "Multiple foreign keys between {} and {} - cannot resolve ambiguity", + current_table, external_table + ))) + } +} diff --git a/server/src/steel/server/mod.rs b/server/src/steel/server/mod.rs new file mode 100644 index 0000000..366096d --- /dev/null +++ b/server/src/steel/server/mod.rs @@ -0,0 +1,10 @@ +// src/steel/server/mod.rs +pub mod rvals; +pub mod data_access; +pub mod execution; +pub mod script; + +pub use data_access::*; +pub use execution::*; +pub use script::*; +pub use rvals::*; diff --git a/server/src/steel/server/rvals.rs b/server/src/steel/server/rvals.rs new file mode 100644 index 0000000..8de96a1 --- /dev/null +++ b/server/src/steel/server/rvals.rs @@ -0,0 +1,25 @@ +// src/steel/server/rvals.rs +use std::fmt; + +#[derive(Debug)] +pub struct SteelError { + pub message: String, +} + +impl SteelError { + pub fn new(message: impl Into) -> Self { + Self { message: message.into() } + } +} + +impl fmt::Display for SteelError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +pub trait Custom {} +pub enum SteelVal { + StringV(String), + // Add other variants as needed +} diff --git a/server/src/steel/validation/script.rs b/server/src/steel/server/script.rs similarity index 95% rename from server/src/steel/validation/script.rs rename to server/src/steel/server/script.rs index cd834f6..10b69db 100644 --- a/server/src/steel/validation/script.rs +++ b/server/src/steel/server/script.rs @@ -1,4 +1,4 @@ -// src/steel/validation/script.rs +// src/steel/server/script.rs use std::fmt; #[derive(Debug)] diff --git a/server/src/steel/validation.rs b/server/src/steel/validation.rs deleted file mode 100644 index dd7047a..0000000 --- a/server/src/steel/validation.rs +++ /dev/null @@ -1,2 +0,0 @@ -// src/steel/validation.rs -pub mod script; diff --git a/server/src/table_script/handlers/post_table_script.rs b/server/src/table_script/handlers/post_table_script.rs index 4663fcb..580704f 100644 --- a/server/src/table_script/handlers/post_table_script.rs +++ b/server/src/table_script/handlers/post_table_script.rs @@ -2,11 +2,11 @@ use tonic::Status; use sqlx::{PgPool, Error as SqlxError}; use common::proto::multieko2::table_script::{PostTableScriptRequest, TableScriptResponse}; -use crate::steel::validation::script::validate_script; +use crate::steel::server::script::validate_script; use serde_json::Value; // Add these imports for the execution module and ScriptOperation -use crate::steel::handlers::execution::{self, ScriptOperation}; +use crate::steel::server::execution::{self, ScriptOperation}; const SYSTEM_COLUMNS: &[&str] = &["id", "deleted", "created_at"]; diff --git a/server/src/tables_data/handlers/post_table_data.rs b/server/src/tables_data/handlers/post_table_data.rs index 85a0479..fd3202b 100644 --- a/server/src/tables_data/handlers/post_table_data.rs +++ b/server/src/tables_data/handlers/post_table_data.rs @@ -4,7 +4,7 @@ use sqlx::{PgPool, Arguments}; use sqlx::postgres::PgArguments; use chrono::{DateTime, Utc}; use common::proto::multieko2::tables_data::{PostTableDataRequest, PostTableDataResponse}; -use crate::steel::handlers::execution::{self, ScriptOperation}; +use crate::steel::server::execution::{self, ScriptOperation}; use std::collections::HashMap; pub async fn post_table_data(