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 tonic::Status;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
@@ -206,6 +208,8 @@ impl DependencyAnalyzer {
|
|||||||
table_id: i64,
|
table_id: i64,
|
||||||
new_dependencies: &[Dependency],
|
new_dependencies: &[Dependency],
|
||||||
) -> Result<(), DependencyError> {
|
) -> 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
|
// Get current dependency graph for this schema
|
||||||
let current_deps = sqlx::query!(
|
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
|
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(())
|
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
|
/// Proper DFS-based cycle detection with state tracking
|
||||||
fn detect_cycles_dfs(
|
fn detect_cycles_dfs(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// src/table_script/handlers/post_table_script.rs
|
// src/table_script/handlers/post_table_script.rs
|
||||||
|
// TODO MAKE THE SCRIPTS PUSH ONLY TO THE EMPTY FILES
|
||||||
|
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use sqlx::{PgPool, Error as SqlxError};
|
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"];
|
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.
|
/// Validates the target column and ensures it is not a system column.
|
||||||
/// Returns the column type if valid.
|
/// Returns the column type if valid.
|
||||||
fn validate_target_column(
|
fn validate_target_column(
|
||||||
@@ -159,7 +159,7 @@ fn generate_warnings(dependencies: &[crate::table_script::handlers::dependency_a
|
|||||||
|
|
||||||
if sql_deps_count > 0 {
|
if sql_deps_count > 0 {
|
||||||
warnings.push(format!(
|
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,
|
sql_deps_count,
|
||||||
if sql_deps_count == 1 { "y" } else { "ies" }
|
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(" ")
|
warnings.join(" ")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user