diff --git a/common/proto/table_structure.proto b/common/proto/table_structure.proto index e9c246f..00e98b2 100644 --- a/common/proto/table_structure.proto +++ b/common/proto/table_structure.proto @@ -4,18 +4,22 @@ package multieko2.table_structure; import "common.proto"; +message GetTableStructureRequest { + string profile_name = 1; // e.g., "default" + string table_name = 2; // e.g., "2025_adresar6" +} + message TableStructureResponse { repeated TableColumn columns = 1; } message TableColumn { string name = 1; - string data_type = 2; + string data_type = 2; // e.g., "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ" bool is_nullable = 3; bool is_primary_key = 4; } service TableStructureService { - rpc GetAdresarTableStructure (common.Empty) returns (TableStructureResponse); - rpc GetUctovnictvoTableStructure (common.Empty) returns (TableStructureResponse); + rpc GetTableStructure (GetTableStructureRequest) returns (TableStructureResponse); } diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 2bc3df3..60f3d32 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_structure.rs b/common/src/proto/multieko2.table_structure.rs index 2112d73..513e62b 100644 --- a/common/src/proto/multieko2.table_structure.rs +++ b/common/src/proto/multieko2.table_structure.rs @@ -1,5 +1,14 @@ // This file is @generated by prost-build. #[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetTableStructureRequest { + /// e.g., "default" + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + /// e.g., "2025_adresar6" + #[prost(string, tag = "2")] + pub table_name: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] pub struct TableStructureResponse { #[prost(message, repeated, tag = "1")] pub columns: ::prost::alloc::vec::Vec, @@ -8,6 +17,7 @@ pub struct TableStructureResponse { pub struct TableColumn { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, + /// e.g., "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ" #[prost(string, tag = "2")] pub data_type: ::prost::alloc::string::String, #[prost(bool, tag = "3")] @@ -106,9 +116,9 @@ pub mod table_structure_service_client { self.inner = self.inner.max_encoding_message_size(limit); self } - pub async fn get_adresar_table_structure( + pub async fn get_table_structure( &mut self, - request: impl tonic::IntoRequest, + request: impl tonic::IntoRequest, ) -> std::result::Result< tonic::Response, tonic::Status, @@ -123,43 +133,14 @@ pub mod table_structure_service_client { })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( - "/multieko2.table_structure.TableStructureService/GetAdresarTableStructure", + "/multieko2.table_structure.TableStructureService/GetTableStructure", ); let mut req = request.into_request(); req.extensions_mut() .insert( GrpcMethod::new( "multieko2.table_structure.TableStructureService", - "GetAdresarTableStructure", - ), - ); - self.inner.unary(req, path, codec).await - } - pub async fn get_uctovnictvo_table_structure( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/multieko2.table_structure.TableStructureService/GetUctovnictvoTableStructure", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert( - GrpcMethod::new( - "multieko2.table_structure.TableStructureService", - "GetUctovnictvoTableStructure", + "GetTableStructure", ), ); self.inner.unary(req, path, codec).await @@ -179,16 +160,9 @@ pub mod table_structure_service_server { /// Generated trait containing gRPC methods that should be implemented for use with TableStructureServiceServer. #[async_trait] pub trait TableStructureService: std::marker::Send + std::marker::Sync + 'static { - async fn get_adresar_table_structure( + async fn get_table_structure( &self, - request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; - async fn get_uctovnictvo_table_structure( - &self, - request: tonic::Request, + request: tonic::Request, ) -> std::result::Result< tonic::Response, tonic::Status, @@ -271,15 +245,13 @@ pub mod table_structure_service_server { } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { - "/multieko2.table_structure.TableStructureService/GetAdresarTableStructure" => { + "/multieko2.table_structure.TableStructureService/GetTableStructure" => { #[allow(non_camel_case_types)] - struct GetAdresarTableStructureSvc( - pub Arc, - ); + struct GetTableStructureSvc(pub Arc); impl< T: TableStructureService, - > tonic::server::UnaryService - for GetAdresarTableStructureSvc { + > tonic::server::UnaryService + for GetTableStructureSvc { type Response = super::TableStructureResponse; type Future = BoxFuture< tonic::Response, @@ -287,11 +259,11 @@ pub mod table_structure_service_server { >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_adresar_table_structure( + ::get_table_structure( &inner, request, ) @@ -306,58 +278,7 @@ pub mod table_structure_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetAdresarTableStructureSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec) - .apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ) - .apply_max_message_size_config( - max_decoding_message_size, - max_encoding_message_size, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/multieko2.table_structure.TableStructureService/GetUctovnictvoTableStructure" => { - #[allow(non_camel_case_types)] - struct GetUctovnictvoTableStructureSvc( - pub Arc, - ); - impl< - T: TableStructureService, - > tonic::server::UnaryService - for GetUctovnictvoTableStructureSvc { - type Response = super::TableStructureResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = Arc::clone(&self.0); - let fut = async move { - ::get_uctovnictvo_table_structure( - &inner, - request, - ) - .await - }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let max_decoding_message_size = self.max_decoding_message_size; - let max_encoding_message_size = self.max_encoding_message_size; - let inner = self.inner.clone(); - let fut = async move { - let method = GetUctovnictvoTableStructureSvc(inner); + let method = GetTableStructureSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( diff --git a/server/src/server/services/table_structure_service.rs b/server/src/server/services/table_structure_service.rs index f9722b9..71eb753 100644 --- a/server/src/server/services/table_structure_service.rs +++ b/server/src/server/services/table_structure_service.rs @@ -1,11 +1,12 @@ // src/server/services/table_structure_service.rs use tonic::{Request, Response, Status}; +// Correct the import path for the TableStructureService trait use common::proto::multieko2::table_structure::table_structure_service_server::TableStructureService; -use common::proto::multieko2::table_structure::TableStructureResponse; -use common::proto::multieko2::common::Empty; -use crate::table_structure::handlers::{ - get_adresar_table_structure, get_uctovnictvo_table_structure, +use common::proto::multieko2::table_structure::{ + GetTableStructureRequest, + TableStructureResponse, }; +use crate::table_structure::handlers::get_table_structure; use sqlx::PgPool; #[derive(Debug)] @@ -13,22 +14,21 @@ pub struct TableStructureHandler { pub db_pool: PgPool, } -#[tonic::async_trait] -impl TableStructureService for TableStructureHandler { - async fn get_adresar_table_structure( - &self, - request: Request, - ) -> Result, Status> { - let response = get_adresar_table_structure(&self.db_pool, request.into_inner()) - .await?; - Ok(Response::new(response)) +impl TableStructureHandler { + pub fn new(db_pool: PgPool) -> Self { + Self { db_pool } } +} - async fn get_uctovnictvo_table_structure( +#[tonic::async_trait] +impl TableStructureService for TableStructureHandler { // This line should now be correct + async fn get_table_structure( &self, - request: Request, + request: Request, ) -> Result, Status> { - let response = get_uctovnictvo_table_structure(&self.db_pool, request.into_inner()).await?; + let req_payload = request.into_inner(); + let response = + get_table_structure(&self.db_pool, req_payload).await?; Ok(Response::new(response)) } } diff --git a/server/src/table_structure/docs/response.txt b/server/src/table_structure/docs/response.txt index 7c628c3..74114d1 100644 --- a/server/src/table_structure/docs/response.txt +++ b/server/src/table_structure/docs/response.txt @@ -1,83 +1,39 @@ -Adresar response: -❯ grpcurl -plaintext \ - -proto proto/table_structure.proto \ - -import-path proto \ +grpcurl -plaintext \ + -d '{ + "profile_name": "default", + "table_name": "2025_customer" + }' \ localhost:50051 \ - multieko2.table_structure.TableStructureService/GetAdresarTableStructure + multieko2.table_structure.TableStructureService/GetTableStructure { "columns": [ { - "name": "firma", - "dataType": "TEXT" + "name": "id", + "dataType": "INT8", + "isPrimaryKey": true }, { - "name": "kz", + "name": "deleted", + "dataType": "BOOL" + }, + { + "name": "full_name", "dataType": "TEXT", "isNullable": true }, { - "name": "drc", - "dataType": "TEXT", + "name": "email", + "dataType": "VARCHAR(255)", "isNullable": true }, { - "name": "ulica", - "dataType": "TEXT", + "name": "loyalty_status", + "dataType": "BOOL", "isNullable": true }, { - "name": "psc", - "dataType": "TEXT", - "isNullable": true - }, - { - "name": "mesto", - "dataType": "TEXT", - "isNullable": true - }, - { - "name": "stat", - "dataType": "TEXT", - "isNullable": true - }, - { - "name": "banka", - "dataType": "TEXT", - "isNullable": true - }, - { - "name": "ucet", - "dataType": "TEXT", - "isNullable": true - }, - { - "name": "skladm", - "dataType": "TEXT", - "isNullable": true - }, - { - "name": "ico", - "dataType": "TEXT", - "isNullable": true - }, - { - "name": "kontakt", - "dataType": "TEXT", - "isNullable": true - }, - { - "name": "telefon", - "dataType": "TEXT", - "isNullable": true - }, - { - "name": "skladu", - "dataType": "TEXT", - "isNullable": true - }, - { - "name": "fax", - "dataType": "TEXT", + "name": "created_at", + "dataType": "TIMESTAMPTZ", "isNullable": true } ] diff --git a/server/src/table_structure/handlers.rs b/server/src/table_structure/handlers.rs index 921d11a..499c0f2 100644 --- a/server/src/table_structure/handlers.rs +++ b/server/src/table_structure/handlers.rs @@ -1,4 +1,4 @@ // src/table_structure/handlers.rs pub mod table_structure; -pub use table_structure::{get_adresar_table_structure, get_uctovnictvo_table_structure}; +pub use table_structure::get_table_structure; diff --git a/server/src/table_structure/handlers/table_structure.rs b/server/src/table_structure/handlers/table_structure.rs index fdcd9c5..afe22f3 100644 --- a/server/src/table_structure/handlers/table_structure.rs +++ b/server/src/table_structure/handlers/table_structure.rs @@ -1,181 +1,134 @@ // src/table_structure/handlers/table_structure.rs -use tonic::Status; -use sqlx::PgPool; -use common::proto::multieko2::{ - table_structure::{TableStructureResponse, TableColumn}, - common::Empty +use common::proto::multieko2::table_structure::{ + GetTableStructureRequest, TableColumn, TableStructureResponse, }; +use sqlx::{PgPool, Row}; +use tonic::Status; -pub async fn get_adresar_table_structure( - _db_pool: &PgPool, - _request: Empty, -) -> Result { - let columns = vec![ - TableColumn { - name: "firma".to_string(), - data_type: "TEXT".to_string(), - is_nullable: false, - is_primary_key: false, - }, - TableColumn { - name: "kz".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "drc".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "ulica".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "psc".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "mesto".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "stat".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "banka".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "ucet".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "skladm".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "ico".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "kontakt".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "telefon".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "skladu".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "fax".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - ]; - Ok(TableStructureResponse { columns }) +// Helper struct to map query results +#[derive(sqlx::FromRow, Debug)] +struct DbColumnInfo { + column_name: String, + formatted_data_type: String, + is_nullable: bool, + is_primary_key: bool, } -pub async fn get_uctovnictvo_table_structure( - _db_pool: &PgPool, - _request: Empty, +pub async fn get_table_structure( + db_pool: &PgPool, + request: GetTableStructureRequest, ) -> Result { - let columns = vec![ - TableColumn { - name: "adresar_id".to_string(), - data_type: "BIGINT".to_string(), - is_nullable: false, - is_primary_key: false, - }, - TableColumn { - name: "c_dokladu".to_string(), - data_type: "TEXT".to_string(), - is_nullable: false, - is_primary_key: false, - }, - TableColumn { - name: "datum".to_string(), - data_type: "DATE".to_string(), - is_nullable: false, - is_primary_key: false, - }, - TableColumn { - name: "c_faktury".to_string(), - data_type: "TEXT".to_string(), - is_nullable: false, - is_primary_key: false, - }, - TableColumn { - name: "obsah".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "stredisko".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "c_uctu".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "md".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "identif".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "poznanka".to_string(), - data_type: "TEXT".to_string(), - is_nullable: true, - is_primary_key: false, - }, - TableColumn { - name: "firma".to_string(), - data_type: "TEXT".to_string(), - is_nullable: false, - is_primary_key: false, - }, - ]; + let profile_name = request.profile_name; + let table_name = request.table_name; // This should be the full table name, e.g., "2025_adresar6" + let table_schema = "public"; // Assuming tables are in the 'public' schema + + // 1. Validate Profile + let profile = sqlx::query!( + "SELECT id FROM profiles WHERE name = $1", + profile_name + ) + .fetch_optional(db_pool) + .await + .map_err(|e| { + Status::internal(format!( + "Failed to query profile '{}': {}", + profile_name, e + )) + })?; + + let profile_id = match profile { + Some(p) => p.id, + None => { + return Err(Status::not_found(format!( + "Profile '{}' not found", + profile_name + ))); + } + }; + + // 2. Validate Table within Profile + sqlx::query!( + "SELECT id FROM table_definitions WHERE profile_id = $1 AND table_name = $2", + profile_id, + table_name + ) + .fetch_optional(db_pool) + .await + .map_err(|e| Status::internal(format!("Failed to query table_definitions: {}", e)))? + .ok_or_else(|| Status::not_found(format!( + "Table '{}' not found in profile '{}'", + table_name, + profile_name + )))?; + + // 3. Query information_schema for column details + let query_str = r#" + SELECT + c.column_name, + CASE + WHEN c.udt_name = 'varchar' AND c.character_maximum_length IS NOT NULL THEN + 'VARCHAR(' || c.character_maximum_length || ')' + WHEN c.udt_name = 'bpchar' AND c.character_maximum_length IS NOT NULL THEN + 'CHAR(' || c.character_maximum_length || ')' + WHEN c.udt_name = 'numeric' AND c.numeric_precision IS NOT NULL AND c.numeric_scale IS NOT NULL THEN + 'NUMERIC(' || c.numeric_precision || ',' || c.numeric_scale || ')' + WHEN c.udt_name = 'numeric' AND c.numeric_precision IS NOT NULL THEN + 'NUMERIC(' || c.numeric_precision || ')' + WHEN STARTS_WITH(c.udt_name, '_') THEN + UPPER(SUBSTRING(c.udt_name FROM 2)) || '[]' + ELSE + UPPER(c.udt_name) + END AS formatted_data_type, + c.is_nullable = 'YES' AS is_nullable, + EXISTS ( + SELECT 1 + FROM information_schema.key_column_usage kcu + JOIN information_schema.table_constraints tc + ON kcu.constraint_name = tc.constraint_name + AND kcu.table_schema = tc.table_schema + AND kcu.table_name = tc.table_name + WHERE tc.table_schema = c.table_schema + AND tc.table_name = c.table_name + AND tc.constraint_type = 'PRIMARY KEY' + AND kcu.column_name = c.column_name + ) AS is_primary_key + FROM + information_schema.columns c + WHERE + c.table_schema = $1 + AND c.table_name = $2 + ORDER BY + c.ordinal_position; + "#; + + let db_columns = sqlx::query_as::<_, DbColumnInfo>(query_str) + .bind(table_schema) + .bind(&table_name) // Use the validated table_name + .fetch_all(db_pool) + .await + .map_err(|e| { + Status::internal(format!( + "Failed to query column information for table '{}': {}", + table_name, e + )) + })?; + + if db_columns.is_empty() { + // This could mean the table exists in table_definitions but not in information_schema, + // or it has no columns. The latter is unlikely for a created table. + // Depending on desired behavior, you could return an error or an empty list. + // For now, returning an empty list if the table was validated. + } + + let columns = db_columns + .into_iter() + .map(|db_col| TableColumn { + name: db_col.column_name, + data_type: db_col.formatted_data_type, + is_nullable: db_col.is_nullable, + is_primary_key: db_col.is_primary_key, + }) + .collect(); + Ok(TableStructureResponse { columns }) }