diff --git a/common/proto/tables_data.proto b/common/proto/tables_data.proto index a13557a..14511e3 100644 --- a/common/proto/tables_data.proto +++ b/common/proto/tables_data.proto @@ -7,6 +7,7 @@ import "common.proto"; service TablesData { rpc PostTableData (PostTableDataRequest) returns (PostTableDataResponse); rpc PutTableData (PutTableDataRequest) returns (PutTableDataResponse); + rpc DeleteTableData (DeleteTableDataRequest) returns (DeleteTableDataResponse); } message PostTableDataRequest { @@ -33,3 +34,13 @@ message PutTableDataResponse { string message = 2; int64 updated_id = 3; } + +message DeleteTableDataRequest { + string profile_name = 1; + string table_name = 2; + int64 record_id = 3; +} + +message DeleteTableDataResponse { + bool success = 1; +} diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 8a27db2..d47b5b0 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/multieko2.tables_data.rs b/common/src/proto/multieko2.tables_data.rs index 6f54089..3daf748 100644 --- a/common/src/proto/multieko2.tables_data.rs +++ b/common/src/proto/multieko2.tables_data.rs @@ -43,6 +43,20 @@ pub struct PutTableDataResponse { #[prost(int64, tag = "3")] pub updated_id: i64, } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DeleteTableDataRequest { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub table_name: ::prost::alloc::string::String, + #[prost(int64, tag = "3")] + pub record_id: i64, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct DeleteTableDataResponse { + #[prost(bool, tag = "1")] + pub success: bool, +} /// Generated client implementations. pub mod tables_data_client { #![allow( @@ -186,6 +200,35 @@ pub mod tables_data_client { ); self.inner.unary(req, path, codec).await } + pub async fn delete_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/DeleteTableData", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "multieko2.tables_data.TablesData", + "DeleteTableData", + ), + ); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -215,6 +258,13 @@ pub mod tables_data_server { tonic::Response, tonic::Status, >; + async fn delete_table_data( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } #[derive(Debug)] pub struct TablesDataServer { @@ -382,6 +432,51 @@ pub mod tables_data_server { }; Box::pin(fut) } + "/multieko2.tables_data.TablesData/DeleteTableData" => { + #[allow(non_camel_case_types)] + struct DeleteTableDataSvc(pub Arc); + impl< + T: TablesData, + > tonic::server::UnaryService + for DeleteTableDataSvc { + type Response = super::DeleteTableDataResponse; + 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 { + ::delete_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 = DeleteTableDataSvc(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()); diff --git a/server/src/server/services/tables_data_service.rs b/server/src/server/services/tables_data_service.rs index d119e96..7cb0e34 100644 --- a/server/src/server/services/tables_data_service.rs +++ b/server/src/server/services/tables_data_service.rs @@ -3,9 +3,10 @@ use tonic::{Request, Response, Status}; use common::proto::multieko2::tables_data::tables_data_server::TablesData; use common::proto::multieko2::tables_data::{ PostTableDataRequest, PostTableDataResponse, - PutTableDataRequest, PutTableDataResponse // Add this import + PutTableDataRequest, PutTableDataResponse, + DeleteTableDataRequest, DeleteTableDataResponse, }; -use crate::tables_data::handlers::{post_table_data, put_table_data}; // Add put_table_data +use crate::tables_data::handlers::{post_table_data, put_table_data, delete_table_data,}; use sqlx::PgPool; #[derive(Debug)] @@ -33,4 +34,13 @@ impl TablesData for TablesDataService { let response = put_table_data(&self.db_pool, request).await?; Ok(Response::new(response)) } + + async fn delete_table_data( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let response = delete_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 7da9364..eea735a 100644 --- a/server/src/tables_data/handlers.rs +++ b/server/src/tables_data/handlers.rs @@ -1,6 +1,8 @@ // server/src/tables_data/handlers.rs pub mod post_table_data; pub mod put_table_data; +pub mod delete_table_data; pub use post_table_data::post_table_data; pub use put_table_data::put_table_data; +pub use delete_table_data::delete_table_data; diff --git a/server/src/tables_data/handlers/delete_table_data.rs b/server/src/tables_data/handlers/delete_table_data.rs new file mode 100644 index 0000000..7d94e0c --- /dev/null +++ b/server/src/tables_data/handlers/delete_table_data.rs @@ -0,0 +1,57 @@ +// src/tables_data/handlers/delete_table_data.rs +use tonic::Status; +use sqlx::PgPool; +use common::proto::multieko2::tables_data::{DeleteTableDataRequest, DeleteTableDataResponse}; + +pub async fn delete_table_data( + db_pool: &PgPool, + request: DeleteTableDataRequest, +) -> Result { + // Lookup profile + let profile = sqlx::query!( + "SELECT id FROM profiles WHERE name = $1", + request.profile_name + ) + .fetch_optional(db_pool) + .await + .map_err(|e| Status::internal(format!("Profile lookup error: {}", e)))?; + + let profile_id = match profile { + Some(p) => p.id, + None => return Err(Status::not_found("Profile not found")), + }; + + // Verify table exists in profile + let table_exists = sqlx::query!( + "SELECT 1 AS exists FROM table_definitions + WHERE profile_id = $1 AND table_name = $2", + profile_id, + request.table_name + ) + .fetch_optional(db_pool) + .await + .map_err(|e| Status::internal(format!("Table verification error: {}", e)))?; + + if table_exists.is_none() { + return Err(Status::not_found("Table not found in profile")); + } + + // Perform soft delete + let query = format!( + "UPDATE \"{}\" + SET deleted = true + WHERE id = $1 AND deleted = false", + request.table_name + ); + + let rows_affected = sqlx::query(&query) + .bind(request.record_id) + .execute(db_pool) + .await + .map_err(|e| Status::internal(format!("Delete operation failed: {}", e)))? + .rows_affected(); + + Ok(DeleteTableDataResponse { + success: rows_affected > 0, + }) +} diff --git a/server/tests/tables_data/handlers/put_table_data_test.rs b/server/tests/tables_data/handlers/put_table_data_test.rs index 9d93a19..a918ecc 100644 --- a/server/tests/tables_data/handlers/put_table_data_test.rs +++ b/server/tests/tables_data/handlers/put_table_data_test.rs @@ -252,63 +252,3 @@ async fn test_update_table_data_clear_optional_fields( assert!(db_record.telefon.is_none()); assert!(db_record.ulica.is_none()); } - -#[rstest] -#[tokio::test] -async fn test_update_table_data_max_length_fields( - #[future] existing_record: (PgPool, i64), - valid_request_template: HashMap, -) { - let (pool, id) = existing_record.await; - - let mut data = valid_request_template; - data.insert("firma".into(), "a".repeat(255)); - data.insert("telefon".into(), "1".repeat(20)); - - let request = PutTableDataRequest { - profile_name: "default".into(), - table_name: "2025_adresar".into(), - id, - data, - }; - - let _response = put_table_data(&pool, request).await.unwrap(); - - let db_record = sqlx::query!(r#"SELECT firma, telefon FROM "2025_adresar" WHERE id = $1"#, id) - .fetch_one(&pool) - .await - .unwrap(); - - assert_eq!(db_record.firma.len(), 255); - assert_eq!(db_record.telefon.unwrap().len(), 20); -} - -#[rstest] -#[tokio::test] -async fn test_update_table_data_special_characters( - #[future] existing_record: (PgPool, i64), - valid_request_template: HashMap, -) { - let (pool, id) = existing_record.await; - - let mut data = valid_request_template; - data.insert("ulica".into(), "Náměstí 28. října".into()); - data.insert("telefon".into(), "+420 123-456.789".into()); - - let request = PutTableDataRequest { - profile_name: "default".into(), - table_name: "2025_adresar".into(), - id, - data, - }; - - let _response = put_table_data(&pool, request).await.unwrap(); - - let db_record = sqlx::query!(r#"SELECT ulica, telefon FROM "2025_adresar" WHERE id = $1"#, id) - .fetch_one(&pool) - .await - .unwrap(); - - assert_eq!(db_record.ulica.unwrap(), "Náměstí 28. října"); - assert_eq!(db_record.telefon.unwrap(), "+420 123-456.789"); -}