diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index a005be1..9d3fc8c 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -19,7 +19,7 @@ message PostTableDefinitionRequest { message ColumnDefinition { string name = 1; - string data_type = 2; + string field_type = 2; } message TableDefinitionResponse { diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 8ce08e9..ba8766b 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/multieko2.table_definition.rs b/common/src/proto/multieko2.table_definition.rs index a63e65f..30d6c05 100644 --- a/common/src/proto/multieko2.table_definition.rs +++ b/common/src/proto/multieko2.table_definition.rs @@ -17,7 +17,7 @@ pub struct ColumnDefinition { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, #[prost(string, tag = "2")] - pub data_type: ::prost::alloc::string::String, + pub field_type: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct TableDefinitionResponse { diff --git a/server/src/table_definition/docs/push_new_table.txt b/server/src/table_definition/docs/push_new_table.txt new file mode 100644 index 0000000..fa78508 --- /dev/null +++ b/server/src/table_definition/docs/push_new_table.txt @@ -0,0 +1,78 @@ +❯ grpcurl -plaintext -d '{ + "table_name": "company_data", + "columns": [ + {"name": "company_name", "field_type": "text"}, + {"name": "textfield", "field_type": "text"}, + {"name": "textfield2", "field_type": "text"}, + {"name": "textfield3", "field_type": "text"}, + {"name": "headquarters_psc", "field_type": "psc"}, + {"name": "contact_phone", "field_type": "phone"}, + {"name": "office_address", "field_type": "address"}, + {"name": "support_email", "field_type": "email"}, + {"name": "is_active", "field_type": "boolean"}, + {"name": "last_updated", "field_type": "timestamp"} + ], + "indexes": ["company_name", "is_active"], + "profile_name": "default", + "linked_table_name": "ud_2025_test_table" +}' localhost:50051 multieko2.table_definition.TableDefinition/PostTableDefinition +{ + "success": true, + "sql": "CREATE TABLE \"2025_company_data\" (\n id BIGSERIAL PRIMARY KEY,\n deleted BOOLEAN NOT NULL DEFAULT FALSE,\n firma TEXT NOT NULL,\n \"2025_test_table_id\" BIGINT NOT NULL REFERENCES \"ud_2025_test_table\"(id),\n \"2025_company_name\" TEXT,\n \"2025_textfield\" TEXT,\n \"2025_textfield2\" TEXT,\n \"2025_textfield3\" TEXT,\n \"2025_headquarters_psc\" TEXT,\n \"2025_contact_phone\" VARCHAR(15),\n \"2025_office_address\" TEXT,\n \"2025_support_email\" VARCHAR(255),\n \"2025_is_active\" BOOLEAN,\n \"2025_last_updated\" TIMESTAMPTZ,\n created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n)\nCREATE INDEX idx_2025_company_data_firma ON \"2025_company_data\" (firma)\nCREATE INDEX idx_2025_company_data_2025_test_table_id ON \"2025_company_data\" (\"2025_test_table_id\")\nCREATE INDEX idx_2025_company_data_2025_company_name ON \"2025_company_data\" (\"2025_company_name\")\nCREATE INDEX idx_2025_company_data_2025_is_active ON \"2025_company_data\" (\"2025_is_active\")" +} +❯ psql -U multi_psql_dev -d multi_rust_dev +psql (17.2) +Type "help" for help. + +multi_rust_dev=> \dt + List of relations + Schema | Name | Type | Owner +--------+--------------------------------+-------+---------------- + public | 2025_company_data | table | multi_psql_dev + public | 2025_multi_dependent_table3 | table | multi_psql_dev + public | 2025_multi_dependent_table4 | table | multi_psql_dev + public | 2025_multi_dependent_table5 | table | multi_psql_dev + public | _sqlx_migrations | table | multi_psql_dev + public | adresar | table | multi_psql_dev + public | profiles | table | multi_psql_dev + public | table_definitions | table | multi_psql_dev + public | uctovnictvo | table | multi_psql_dev + public | ud_2025_linked_test_table | table | multi_psql_dev + public | ud_2025_linked_test_table2 | table | multi_psql_dev + public | ud_2025_linked_test_table3 | table | multi_psql_dev + public | ud_2025_multi_dependent_table | table | multi_psql_dev + public | ud_2025_multi_dependent_table2 | table | multi_psql_dev + public | ud_2025_profile_table | table | multi_psql_dev + public | ud_2025_test_table | table | multi_psql_dev + public | ud_2025_test_table_no_linked | table | multi_psql_dev +(17 rows) + +multi_rust_dev=> \d 2025_company_data + Table "public.2025_company_data" + Column | Type | Collation | Nullable | Default +-----------------------+--------------------------+-----------+----------+------------------------------------------------- + id | bigint | | not null | nextval('"2025_company_data_id_seq"'::regclass) + deleted | boolean | | not null | false + firma | text | | not null | + 2025_test_table_id | bigint | | not null | + 2025_company_name | text | | | + 2025_textfield | text | | | + 2025_textfield2 | text | | | + 2025_textfield3 | text | | | + 2025_headquarters_psc | text | | | + 2025_contact_phone | character varying(15) | | | + 2025_office_address | text | | | + 2025_support_email | character varying(255) | | | + 2025_is_active | boolean | | | + 2025_last_updated | timestamp with time zone | | | + created_at | timestamp with time zone | | | CURRENT_TIMESTAMP +Indexes: + "2025_company_data_pkey" PRIMARY KEY, btree (id) + "idx_2025_company_data_2025_company_name" btree ("2025_company_name") + "idx_2025_company_data_2025_is_active" btree ("2025_is_active") + "idx_2025_company_data_2025_test_table_id" btree ("2025_test_table_id") + "idx_2025_company_data_firma" btree (firma) +Foreign-key constraints: + "2025_company_data_2025_test_table_id_fkey" FOREIGN KEY ("2025_test_table_id") REFERENCES ud_2025_test_table(id) + +multi_rust_dev=> diff --git a/server/src/table_definition/handlers/post_table_definition.rs b/server/src/table_definition/handlers/post_table_definition.rs index a3cbbac..929bc5b 100644 --- a/server/src/table_definition/handlers/post_table_definition.rs +++ b/server/src/table_definition/handlers/post_table_definition.rs @@ -5,11 +5,15 @@ use serde_json::json; use time::OffsetDateTime; use common::proto::multieko2::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse}; -const VALID_DATA_TYPES: &[&str] = &["TEXT", "INTEGER", "BIGINT", "BOOLEAN", "TIMESTAMPTZ", "NUMERIC"]; - -fn is_valid_data_type(dt: &str) -> bool { - VALID_DATA_TYPES.contains(&dt.to_uppercase().as_str()) -} +const PREDEFINED_FIELD_TYPES: &[(&str, &str)] = &[ + ("text", "TEXT"), + ("psc", "TEXT"), + ("phone", "VARCHAR(15)"), + ("address", "TEXT"), + ("email", "VARCHAR(255)"), + ("boolean", "BOOLEAN"), + ("timestamp", "TIMESTAMPTZ"), +]; fn is_valid_identifier(s: &str) -> bool { !s.is_empty() && @@ -28,6 +32,14 @@ fn sanitize_identifier(s: &str) -> String { format!("{}_{}", year, cleaned) } +fn map_field_type(field_type: &str) -> Result<&str, Status> { + PREDEFINED_FIELD_TYPES + .iter() + .find(|(key, _)| *key == field_type.to_lowercase().as_str()) + .map(|(_, sql_type)| *sql_type) + .ok_or_else(|| Status::invalid_argument(format!("Invalid field type: {}", field_type))) +} + pub async fn post_table_definition( db_pool: &PgPool, mut request: PostTableDefinitionRequest, @@ -52,7 +64,6 @@ pub async fn post_table_definition( // Validate linked table if provided let linked_table_id; if let Some(lt_name) = &request.linked_table_name { - // Lookup the table with the EXACT provided name let lt_record = sqlx::query!( "SELECT id FROM table_definitions WHERE profile_id = $1 AND table_name = $2", @@ -78,10 +89,9 @@ pub async fn post_table_definition( if !is_valid_identifier(&col_def.name) { return Err(Status::invalid_argument("Invalid column name")); } - if !is_valid_data_type(&col_def.data_type) { - return Err(Status::invalid_argument("Invalid data type")); - } - columns.push(format!("\"{}\" {}", col_name, col_def.data_type)); + + let sql_type = map_field_type(&col_def.field_type)?; + columns.push(format!("\"{}\" {}", col_name, sql_type)); } let mut indexes = Vec::new(); @@ -155,9 +165,8 @@ fn generate_table_sql( ]; if let Some(linked) = linked_table { - // Extract base name without prefix for relationship let parts: Vec<&str> = linked.splitn(2, '_').collect(); - let base_name = parts.get(1).unwrap_or(&linked); // "profile_table" + let base_name = parts.get(1).unwrap_or(&linked); system_columns.push( format!("\"{}_id\" BIGINT NOT NULL REFERENCES \"{}\"(id)",