SCRIPTS only scripts reference to a linked table from this commit
This commit is contained in:
@@ -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<String> = 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,
|
||||
|
||||
@@ -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(" ")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user