From d2053b1d5a0440057beac26e8df4caff02e611bc Mon Sep 17 00:00:00 2001 From: filipriec Date: Fri, 11 Jul 2025 08:55:32 +0200 Subject: [PATCH] SCRIPTS only scripts reference to a linked table from this commit --- .../handlers/dependency_analyzer.rs | 75 ++++++++++++++++++- .../handlers/post_table_script.rs | 21 +++++- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/server/src/table_script/handlers/dependency_analyzer.rs b/server/src/table_script/handlers/dependency_analyzer.rs index 084c875..98a2a32 100644 --- a/server/src/table_script/handlers/dependency_analyzer.rs +++ b/server/src/table_script/handlers/dependency_analyzer.rs @@ -1,4 +1,6 @@ -use std::collections::HashMap; +// src/table_script/handlers/dependency_analyzer.rs + +use std::collections::{HashMap, HashSet}; use tonic::Status; use sqlx::PgPool; use serde_json::{json, Value}; @@ -206,6 +208,8 @@ impl DependencyAnalyzer { table_id: i64, new_dependencies: &[Dependency], ) -> Result<(), DependencyError> { + // FIRST: Validate that structured table access respects link constraints + self.validate_link_constraints(tx, table_id, new_dependencies).await?; // Get current dependency graph for this schema let current_deps = sqlx::query!( r#"SELECT sd.source_table_id, sd.target_table_id, st.table_name as source_name, tt.table_name as target_name @@ -267,6 +271,75 @@ impl DependencyAnalyzer { Ok(()) } + /// Validates that structured table access (steel_get_column functions) respects link constraints + /// Raw SQL access (steel_query_sql) is allowed to reference any table + /// + /// Example: + /// - Table A is linked to Table B via table_definition_links + /// - Script for Table A can use: (steel_get_column "table_b" "column_name") ✅ + /// - Script for Table A CANNOT use: (steel_get_column "table_c" "column_name") ❌ + /// - Script for Table A CAN use: (steel_query_sql "SELECT * FROM table_c") ✅ + async fn validate_link_constraints( + &self, + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + source_table_id: i64, + dependencies: &[Dependency], + ) -> Result<(), DependencyError> { + // Get all valid linked tables for the source table + let linked_tables = sqlx::query!( + r#"SELECT td.table_name, tdl.is_required + FROM table_definition_links tdl + JOIN table_definitions td ON tdl.linked_table_id = td.id + WHERE tdl.source_table_id = $1"#, + source_table_id + ) + .fetch_all(&mut **tx) + .await + .map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?; + + // Create a set of allowed table names for quick lookup + let allowed_tables: std::collections::HashSet = linked_tables + .into_iter() + .map(|row| row.table_name) + .collect(); + + // Get the current table name for better error messages + let current_table_name = sqlx::query_scalar!( + "SELECT table_name FROM table_definitions WHERE id = $1", + source_table_id + ) + .fetch_one(&mut **tx) + .await + .map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?; + + // Validate each dependency + for dep in dependencies { + match &dep.dependency_type { + // Structured access must respect link constraints + DependencyType::ColumnAccess { column } | DependencyType::IndexedAccess { column, .. } => { + if !allowed_tables.contains(&dep.target_table) { + return Err(DependencyError::InvalidTableReference { + table_name: dep.target_table.clone(), + script_context: format!( + "Table '{}' is not linked to '{}'. Add a link in the table definition to access '{}' via steel_get_column functions. Column attempted: '{}'", + dep.target_table, + current_table_name, + dep.target_table, + column + ), + }); + } + } + // Raw SQL access is unrestricted + DependencyType::SqlQuery { .. } => { + // No validation - raw SQL can access any table + } + } + } + + Ok(()) + } + /// Proper DFS-based cycle detection with state tracking fn detect_cycles_dfs( &self, diff --git a/server/src/table_script/handlers/post_table_script.rs b/server/src/table_script/handlers/post_table_script.rs index ea29323..eb14c86 100644 --- a/server/src/table_script/handlers/post_table_script.rs +++ b/server/src/table_script/handlers/post_table_script.rs @@ -1,4 +1,5 @@ // src/table_script/handlers/post_table_script.rs +// TODO MAKE THE SCRIPTS PUSH ONLY TO THE EMPTY FILES use tonic::Status; use sqlx::{PgPool, Error as SqlxError}; @@ -10,7 +11,6 @@ use crate::table_script::handlers::dependency_analyzer::DependencyAnalyzer; const SYSTEM_COLUMNS: &[&str] = &["id", "deleted", "created_at"]; -// TODO MAKE THE SCRIPTS PUSH ONLY TO THE EMPTY FILES /// Validates the target column and ensures it is not a system column. /// Returns the column type if valid. fn validate_target_column( @@ -159,7 +159,7 @@ fn generate_warnings(dependencies: &[crate::table_script::handlers::dependency_a if sql_deps_count > 0 { warnings.push(format!( - "Warning: Script contains {} SQL quer{}, ensure they are read-only and reference valid tables.", + "Warning: Script contains {} raw SQL quer{}, ensure they are read-only and reference valid tables.", sql_deps_count, if sql_deps_count == 1 { "y" } else { "ies" } )); @@ -173,5 +173,22 @@ fn generate_warnings(dependencies: &[crate::table_script::handlers::dependency_a )); } + // Count structured access dependencies + let structured_deps_count = dependencies.iter() + .filter(|d| matches!( + d.dependency_type, + crate::table_script::handlers::dependency_analyzer::DependencyType::ColumnAccess { .. } | + crate::table_script::handlers::dependency_analyzer::DependencyType::IndexedAccess { .. } + )) + .count(); + + if structured_deps_count > 0 { + warnings.push(format!( + "Info: Script uses {} linked table{} via steel_get_column functions.", + structured_deps_count, + if structured_deps_count == 1 { "" } else { "s" } + )); + } + warnings.join(" ") }