From d59e5b60cf2813b9c3c948dd8db7656d20df34f8 Mon Sep 17 00:00:00 2001 From: filipriec Date: Mon, 3 Mar 2025 16:14:31 +0100 Subject: [PATCH] post to the user defined table, broken push --- common/build.rs | 3 +- common/proto/tables_data.proto | 21 ++ common/src/lib.rs | 3 + common/src/proto/descriptor.bin | Bin 13562 -> 14617 bytes common/src/proto/multieko2.tables_data.rs | 321 ++++++++++++++++++ server/src/server/run.rs | 14 +- server/src/server/services/mod.rs | 2 + .../server/services/tables_data_service.rs | 22 ++ server/src/tables_data/handlers.rs | 4 +- .../tables_data/handlers/post_table_data.rs | 151 ++++++++ .../src/tables_data/handlers/post_tables.rs | 0 11 files changed, 533 insertions(+), 8 deletions(-) create mode 100644 common/proto/tables_data.proto create mode 100644 common/src/proto/multieko2.tables_data.rs create mode 100644 server/src/server/services/tables_data_service.rs create mode 100644 server/src/tables_data/handlers/post_table_data.rs delete mode 100644 server/src/tables_data/handlers/post_tables.rs diff --git a/common/build.rs b/common/build.rs index 370eeea..5bcf5f9 100644 --- a/common/build.rs +++ b/common/build.rs @@ -10,7 +10,8 @@ fn main() -> Result<(), Box> { "proto/adresar.proto", "proto/uctovnictvo.proto", "proto/table_structure.proto", - "proto/table_definition.proto" + "proto/table_definition.proto", + "proto/tables_data.proto", ], &["proto"], )?; diff --git a/common/proto/tables_data.proto b/common/proto/tables_data.proto new file mode 100644 index 0000000..53cbbe6 --- /dev/null +++ b/common/proto/tables_data.proto @@ -0,0 +1,21 @@ +// common/proto/tables_data.proto +syntax = "proto3"; +package multieko2.tables_data; + +import "common.proto"; + +service TablesData { + rpc PostTableData (PostTableDataRequest) returns (PostTableDataResponse); +} + +message PostTableDataRequest { + string profile_name = 1; + string table_name = 2; + map data = 3; +} + +message PostTableDataResponse { + bool success = 1; + string message = 2; + int64 inserted_id = 3; +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 3d7cfa2..1605e69 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -16,6 +16,9 @@ pub mod proto { pub mod table_definition { include!("proto/multieko2.table_definition.rs"); } + pub mod tables_data { + include!("proto/multieko2.tables_data.rs"); + } pub const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("proto/descriptor.bin"); } diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 59e840ce19bddabb4d311fb66bc7cefc6d3e5a47..47fd50819ca0dfd8856e914956cfa5e894507dd5 100644 GIT binary patch delta 916 zcmb7>&u-H|5XQZK;@D%SipNQ645d*HD3sz#p$Nnogi=645%(%3K7=N58#{=e{0`)V z#6$2LNW1}0(i5|rb=4p)-1ggFDhUQfR*e_S8GgYOOm#rSNN=BJZ!F-|U)S&?CT zzM2)&^gJ6Rw~Dd#zYd2b4nZnXB)X#S4W1_dL?v6s zQF^&b^CD4@SBqkKHPVRq8MrrB*aUH&UPY|S`tC>_u?4ly9>z{WYlnw&`VOtD^?I3s{ zZHrqd>iiA(P*Z|RbvvvSC)IvfDM9LZNC+-1lwj>>oKzlg?kSwqIFHB5qC^da#~q_e z)#Hq)$ delta 7 OcmbPP^ec12FB1R}pae|- diff --git a/common/src/proto/multieko2.tables_data.rs b/common/src/proto/multieko2.tables_data.rs new file mode 100644 index 0000000..f97efb1 --- /dev/null +++ b/common/src/proto/multieko2.tables_data.rs @@ -0,0 +1,321 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PostTableDataRequest { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub table_name: ::prost::alloc::string::String, + #[prost(map = "string, string", tag = "3")] + pub data: ::std::collections::HashMap< + ::prost::alloc::string::String, + ::prost::alloc::string::String, + >, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PostTableDataResponse { + #[prost(bool, tag = "1")] + pub success: bool, + #[prost(string, tag = "2")] + pub message: ::prost::alloc::string::String, + #[prost(int64, tag = "3")] + pub inserted_id: i64, +} +/// Generated client implementations. +pub mod tables_data_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct TablesDataClient { + inner: tonic::client::Grpc, + } + impl TablesDataClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl TablesDataClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> TablesDataClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + TablesDataClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn post_table_data( + &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.tables_data.TablesData/PostTableData", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("multieko2.tables_data.TablesData", "PostTableData"), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod tables_data_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with TablesDataServer. + #[async_trait] + pub trait TablesData: std::marker::Send + std::marker::Sync + 'static { + async fn post_table_data( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + #[derive(Debug)] + pub struct TablesDataServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl TablesDataServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for TablesDataServer + where + T: TablesData, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/multieko2.tables_data.TablesData/PostTableData" => { + #[allow(non_camel_case_types)] + struct PostTableDataSvc(pub Arc); + impl< + T: TablesData, + > tonic::server::UnaryService + for PostTableDataSvc { + type Response = super::PostTableDataResponse; + 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 { + ::post_table_data(&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 = PostTableDataSvc(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) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new(empty_body()); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for TablesDataServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "multieko2.tables_data.TablesData"; + impl tonic::server::NamedService for TablesDataServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/server/src/server/run.rs b/server/src/server/run.rs index 7085b42..f9ff935 100644 --- a/server/src/server/run.rs +++ b/server/src/server/run.rs @@ -4,15 +4,17 @@ use tonic_reflection::server::Builder as ReflectionBuilder; use common::proto::multieko2::FILE_DESCRIPTOR_SET; use crate::server::services::{ - AdresarService, - UctovnictvoService, + AdresarService, + UctovnictvoService, TableStructureHandler, - TableDefinitionService + TableDefinitionService, + TablesDataService, // Add this }; use common::proto::multieko2::adresar::adresar_server::AdresarServer; use common::proto::multieko2::uctovnictvo::uctovnictvo_server::UctovnictvoServer; use common::proto::multieko2::table_structure::table_structure_service_server::TableStructureServiceServer; use common::proto::multieko2::table_definition::table_definition_server::TableDefinitionServer; +use common::proto::multieko2::tables_data::tables_data_server::TablesDataServer; // Add this pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box> { let addr = "[::1]:50051".parse()?; @@ -21,14 +23,16 @@ pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box, + ) -> Result, Status> { + let request = request.into_inner(); + let response = post_table_data(&self.db_pool, request).await?; + Ok(Response::new(response)) + } +} diff --git a/server/src/tables_data/handlers.rs b/server/src/tables_data/handlers.rs index 16cdda5..282fe4c 100644 --- a/server/src/tables_data/handlers.rs +++ b/server/src/tables_data/handlers.rs @@ -1,4 +1,4 @@ // server/src/tables_data/handlers.rs -pub mod post_tables; +pub mod post_table_data; -pub use post_tables::post_tables; +pub use post_table_data::post_table_data; diff --git a/server/src/tables_data/handlers/post_table_data.rs b/server/src/tables_data/handlers/post_table_data.rs new file mode 100644 index 0000000..a0ead03 --- /dev/null +++ b/server/src/tables_data/handlers/post_table_data.rs @@ -0,0 +1,151 @@ +// src/tables_data/handlers/post_table_data.rs +use tonic::{Status, Response}; +use sqlx::{PgPool, Postgres, Arguments, Row}; +use sqlx::postgres::PgArguments; +use chrono::{DateTime, Utc}; +use std::collections::HashMap; +use common::proto::multieko2::tables_data::{PostTableDataRequest, PostTableDataResponse}; + +pub async fn post_table_data( + db_pool: &PgPool, + request: PostTableDataRequest, +) -> Result, Status> { + let profile_name = request.profile_name; + let table_name = request.table_name; + let data = request.data; + + // Lookup 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!("Profile lookup error: {}", e)))?; + + let profile_id = profile.ok_or_else(|| Status::not_found("Profile not found"))?.id; + + // Lookup table_definition + let table_def = sqlx::query!( + r#"SELECT id, columns, linked_table_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!("Table lookup error: {}", e)))?; + + let table_def = table_def.ok_or_else(|| Status::not_found("Table not found"))?; + + // Parse columns from JSON + let columns_json: Vec = serde_json::from_value(table_def.columns.clone()) + .map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?; + + let mut columns = Vec::new(); + for col_def in columns_json { + let parts: Vec<&str> = col_def.splitn(2, ' ').collect(); + if parts.len() != 2 { + return Err(Status::internal("Invalid column format")); + } + let name = parts[0].trim_matches('"').to_string(); + let sql_type = parts[1].to_string(); + columns.push((name, sql_type)); + } + + // Check required system columns + let mut required_columns = vec!["firma".to_string()]; + if let Some(linked_table_id) = table_def.linked_table_id { + let linked_table = sqlx::query!( + "SELECT table_name FROM table_definitions WHERE id = $1", + linked_table_id + ) + .fetch_one(db_pool) + .await + .map_err(|e| Status::internal(format!("Linked table error: {}", e)))?; + + let base_name = linked_table.table_name.splitn(2, '_').last().unwrap_or(&linked_table.table_name); + required_columns.push(format!("{}_id", base_name)); + } + + // Validate required columns + for col in &required_columns { + if !data.contains_key(col) { + return Err(Status::invalid_argument(format!("Missing required column: {}", col))); + } + } + + // Validate all data columns + let system_columns = ["firma", "deleted"]; + let user_columns: Vec<&String> = columns.iter().map(|(name, _)| name).collect(); + for key in data.keys() { + if !system_columns.contains(&key.as_str()) && !user_columns.contains(&key) { + return Err(Status::invalid_argument(format!("Invalid column: {}", key))); + } + } + + // Prepare SQL parameters + let mut params = PgArguments::default(); + let mut columns_list = Vec::new(); + let mut placeholders = Vec::new(); + let mut param_idx = 1; + + for (col, value) in data { + let sql_type = if system_columns.contains(&col.as_str()) { + match col.as_str() { + "firma" => "TEXT", + "deleted" => "BOOLEAN", + _ => return Err(Status::invalid_argument("Invalid system column")), + } + } else { + columns.iter() + .find(|(name, _)| name == &col) + .map(|(_, sql_type)| sql_type.as_str()) + .ok_or_else(|| Status::invalid_argument(format!("Column not found: {}", col)))? + }; + + match sql_type { + "TEXT" | "VARCHAR(15)" | "VARCHAR(255)" => { + if let Some(max_len) = sql_type.strip_prefix("VARCHAR(").and_then(|s| s.strip_suffix(')')).and_then(|s| s.parse::().ok()) { + if value.len() > max_len { + return Err(Status::invalid_argument(format!("Value too long for {}", col))); + } + } + params.add(value); + }, + "BOOLEAN" => { + let val = value.parse::() + .map_err(|_| Status::invalid_argument(format!("Invalid boolean for {}", col)))?; + params.add(val); + }, + "TIMESTAMPTZ" => { + let dt = DateTime::parse_from_rfc3339(&value) + .map_err(|_| Status::invalid_argument(format!("Invalid timestamp for {}", col)))?; + params.add(dt.with_timezone(&Utc)); + }, + _ => return Err(Status::invalid_argument(format!("Unsupported type {}", sql_type))), + } + + columns_list.push(format!("\"{}\"", col)); + placeholders.push(format!("${}", param_idx)); + param_idx += 1; + } + + let sql = format!( + "INSERT INTO \"{}\" ({}) VALUES ({}) RETURNING id", + table_name, + columns_list.join(", "), + placeholders.join(", ") + ); + + let inserted_id: i64 = sqlx::query_scalar_with(&sql, params) + .fetch_one(db_pool) + .await + .map_err(|e| Status::internal(format!("Insert failed: {}", e)))?; + + Ok(Response::new(PostTableDataResponse { + success: true, + message: "Data inserted successfully".into(), + inserted_id, + })) +} diff --git a/server/src/tables_data/handlers/post_tables.rs b/server/src/tables_data/handlers/post_tables.rs deleted file mode 100644 index e69de29..0000000