use fraiseql_core::{db::dialect::SqlDialect, schema::TypeDefinition};
use super::proto_gen::graphql_to_row_view_type;
pub fn generate_row_view_sql(dialect: &dyn SqlDialect, type_def: &TypeDefinition) -> String {
let source_table = format!("tb_{}", type_def.sql_source);
let view_name = format!("vr_{}", type_def.sql_source);
let columns: Vec<(String, String)> = type_def
.fields
.iter()
.filter(|f| f.field_type.is_scalar() && is_safe_field_name(f.name.as_ref()))
.map(|f| {
let col_type = graphql_to_row_view_type(&f.field_type.to_graphql_string());
let expr =
dialect.row_view_column_expr(&type_def.jsonb_column, f.name.as_ref(), &col_type);
(f.name.to_string(), expr)
})
.collect();
dialect.create_row_view_ddl(&view_name, &source_table, &columns)
}
pub fn generate_all_row_views(
dialect: &dyn SqlDialect,
types: &[TypeDefinition],
include_types: &[String],
exclude_types: &[String],
) -> String {
let mut ddl_parts = Vec::new();
for td in types {
let name: &str = td.name.as_ref();
if !include_types.is_empty() && !include_types.iter().any(|t| t == name) {
continue;
}
if exclude_types.iter().any(|t| t == name) {
continue;
}
let has_scalars = td.fields.iter().any(|f| f.field_type.is_scalar());
if !has_scalars {
continue;
}
ddl_parts.push(generate_row_view_sql(dialect, td));
}
ddl_parts.join("\n\n")
}
fn is_safe_field_name(name: &str) -> bool {
!name.is_empty()
&& name.len() <= 128
&& name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
}
#[cfg(test)]
mod field_name_tests {
use super::is_safe_field_name;
#[test]
fn is_safe_field_name_accepts_identifiers_rejects_injection() {
assert!(is_safe_field_name("email"));
assert!(is_safe_field_name("created_at"));
assert!(is_safe_field_name("_private"));
assert!(!is_safe_field_name(""));
assert!(!is_safe_field_name("evil'; DROP VIEW vr_user; --"));
assert!(!is_safe_field_name("a'b"));
assert!(!is_safe_field_name("with space"));
assert!(!is_safe_field_name("dotted.path"));
}
}