From a55bc6b895ffb4cfb06aa149462648c637509167 Mon Sep 17 00:00:00 2001 From: filipriec Date: Sat, 1 Mar 2025 11:48:32 +0100 Subject: [PATCH] table definitions added --- common/proto/table_definition.proto | 20 +++ ...0250301093450_create_table_definitions.sql | 12 ++ .../services/table_definition_service.rs | 24 ++++ server/src/table_definition/handlers.rs | 8 ++ .../handlers/post_table_definition.rs | 125 ++++++++++++++++++ server/src/table_definition/mod.rs | 4 + server/src/table_definition/models.rs | 0 7 files changed, 193 insertions(+) create mode 100644 common/proto/table_definition.proto create mode 100644 server/migrations/20250301093450_create_table_definitions.sql create mode 100644 server/src/server/services/table_definition_service.rs create mode 100644 server/src/table_definition/handlers.rs create mode 100644 server/src/table_definition/handlers/post_table_definition.rs create mode 100644 server/src/table_definition/mod.rs create mode 100644 server/src/table_definition/models.rs diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto new file mode 100644 index 0000000..adc4fe9 --- /dev/null +++ b/common/proto/table_definition.proto @@ -0,0 +1,20 @@ +// common/proto/table_definition.proto +syntax = "proto3"; +package multieko2.table_definition; + +import "common.proto"; + +service TableDefinition { + rpc PostTableDefinition (PostTableDefinitionRequest) returns (TableDefinitionResponse); +} + +message PostTableDefinitionRequest { + string table_name = 1; + repeated string columns = 2; + repeated string indexes = 3; +} + +message TableDefinitionResponse { + bool success = 1; + string sql = 2; +} diff --git a/server/migrations/20250301093450_create_table_definitions.sql b/server/migrations/20250301093450_create_table_definitions.sql new file mode 100644 index 0000000..f75837f --- /dev/null +++ b/server/migrations/20250301093450_create_table_definitions.sql @@ -0,0 +1,12 @@ +-- Add migration script here +CREATE TABLE table_definitions ( + id BIGSERIAL PRIMARY KEY, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + firma TEXT NOT NULL, + table_name TEXT NOT NULL UNIQUE, + columns JSONB NOT NULL, + indexes JSONB NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_table_definitions_table_name ON table_definitions (table_name); diff --git a/server/src/server/services/table_definition_service.rs b/server/src/server/services/table_definition_service.rs new file mode 100644 index 0000000..950e90d --- /dev/null +++ b/server/src/server/services/table_definition_service.rs @@ -0,0 +1,24 @@ +// src/server/services/table_definition_service.rs +use tonic::{Request, Response, Status}; +use common::proto::multieko2::table_definition::{ + table_definition_server::TableDefinition, + PostTableDefinitionRequest, TableDefinitionResponse +}; +use sqlx::PgPool; +use crate::table_definition::handlers::post_table_definition; + +#[derive(Debug)] +pub struct TableDefinitionService { + pub db_pool: PgPool, +} + +#[tonic::async_trait] +impl TableDefinition for TableDefinitionService { + async fn post_table_definition( + &self, + request: Request, + ) -> Result, Status> { + let response = post_table_definition(&self.db_pool, request.into_inner()).await?; + Ok(Response::new(response)) + } +} diff --git a/server/src/table_definition/handlers.rs b/server/src/table_definition/handlers.rs new file mode 100644 index 0000000..08b1087 --- /dev/null +++ b/server/src/table_definition/handlers.rs @@ -0,0 +1,8 @@ +// src/table_definition/handlers.rs +pub mod post_table_definition; +// pub mod get_table_definition; +// pub mod list_table_definitions; + +pub use post_table_definition::post_table_definition; +// pub use get_table_definition::get_table_definition; +// pub use list_table_definitions::list_table_definitions; diff --git a/server/src/table_definition/handlers/post_table_definition.rs b/server/src/table_definition/handlers/post_table_definition.rs new file mode 100644 index 0000000..49a101b --- /dev/null +++ b/server/src/table_definition/handlers/post_table_definition.rs @@ -0,0 +1,125 @@ +// src/table_definition/handlers/post_table_definition.rs +use tonic::Status; +use sqlx::{PgPool, types::Json}; +use common::proto::multieko2::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse}; + +// Validate SQL identifiers +fn is_valid_identifier(s: &str) -> bool { + !s.is_empty() && + s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') && + !s.starts_with('_') && + !s.chars().next().unwrap().is_ascii_digit() +} + +// Sanitize SQL identifiers +fn sanitize_identifier(s: &str) -> String { + s.replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "") + .trim() + .to_lowercase() +} + +pub async fn post_table_definition( + db_pool: &PgPool, + mut request: PostTableDefinitionRequest, +) -> Result { + // Validate and sanitize table name + let table_name = sanitize_identifier(&request.table_name); + if !is_valid_identifier(&table_name) { + return Err(Status::invalid_argument("Invalid table name")); + } + + // Validate and sanitize columns + let mut columns = Vec::with_capacity(request.columns.len()); + for col in request.columns.drain(..) { + let clean_col = sanitize_identifier(&col); + if !is_valid_identifier(&clean_col) { + return Err(Status::invalid_argument(format!("Invalid column name: {}", col))); + } + columns.push(clean_col); + } + + // Validate and sanitize indexes + let mut indexes = Vec::with_capacity(request.indexes.len()); + for idx in request.indexes.drain(..) { + let clean_idx = sanitize_identifier(&idx); + if !is_valid_identifier(&clean_idx) { + return Err(Status::invalid_argument(format!("Invalid index name: {}", idx))); + } + indexes.push(clean_idx); + } + + // Generate SQL with proper quoting + let (create_sql, index_sql) = generate_table_sql(&table_name, &columns, &indexes); + + // Store definition in table_definitions + sqlx::query!( + r#"INSERT INTO table_definitions + (firma, table_name, columns, indexes) + VALUES ($1, $2, $3, $4)"#, + "system", // Or get from auth context + &table_name, + Json(&columns), + Json(&indexes) + ) + .execute(db_pool) + .await + .map_err(|e| { + if let Some(db_err) = e.as_database_error() { + if db_err.constraint() == Some("table_definitions_table_name_key") { + return Status::already_exists("Table already exists"); + } + } + Status::internal(format!("Database error: {}", e)) + })?; + + // Execute the generated SQL + sqlx::query(&create_sql) + .execute(db_pool) + .await + .map_err(|e| Status::internal(format!("Table creation failed: {}", e)))?; + + for sql in index_sql { + sqlx::query(&sql) + .execute(db_pool) + .await + .map_err(|e| Status::internal(format!("Index creation failed: {}", e)))?; + } + + Ok(TableDefinitionResponse { + success: true, + sql: format!("{}\n{}", create_sql, index_sql.join("\n")), + }) +} + +fn generate_table_sql(table_name: &str, columns: &[String], indexes: &[String]) -> (String, Vec) { + // Generate quoted column definitions + let columns_sql = columns.iter() + .map(|c| format!("\"{}\" TEXT", c)) + .collect::>() + .join(",\n "); + + // Create table with proper quoting + let create_sql = format!( + "CREATE TABLE \"{}\" ( + id BIGSERIAL PRIMARY KEY, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + firma TEXT NOT NULL, + {}, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + )", table_name, columns_sql); + + // Always include firma index + let mut all_indexes = vec!["firma".to_string()]; + all_indexes.extend(indexes.iter().cloned()); + all_indexes.dedup(); + + // Generate safe index SQL + let index_sql = all_indexes.iter() + .map(|i| format!( + "CREATE INDEX idx_{}_{} ON \"{}\" (\"{}\")", + table_name, i, table_name, i + )) + .collect(); + + (create_sql, index_sql) +} diff --git a/server/src/table_definition/mod.rs b/server/src/table_definition/mod.rs new file mode 100644 index 0000000..4347c98 --- /dev/null +++ b/server/src/table_definition/mod.rs @@ -0,0 +1,4 @@ +// src/table_definition/mod.rs + +pub mod models; +pub mod handlers; diff --git a/server/src/table_definition/models.rs b/server/src/table_definition/models.rs new file mode 100644 index 0000000..e69de29