diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index 9d3fc8c..01ef71c 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -7,6 +7,7 @@ import "common.proto"; service TableDefinition { rpc PostTableDefinition (PostTableDefinitionRequest) returns (TableDefinitionResponse); rpc GetProfileTree (multieko2.common.Empty) returns (ProfileTreeResponse); + rpc DeleteTable (DeleteTableRequest) returns (DeleteTableResponse); } message PostTableDefinitionRequest { @@ -40,3 +41,13 @@ message ProfileTreeResponse { repeated Profile profiles = 1; } + +message DeleteTableRequest { + string profile_name = 1; + string table_name = 2; +} + +message DeleteTableResponse { + bool success = 1; + string message = 2; +} diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index ba8766b..59e840c 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 30d6c05..bd5870d 100644 --- a/common/src/proto/multieko2.table_definition.rs +++ b/common/src/proto/multieko2.table_definition.rs @@ -48,6 +48,20 @@ pub mod profile_tree_response { pub tables: ::prost::alloc::vec::Vec, } } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DeleteTableRequest { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub table_name: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DeleteTableResponse { + #[prost(bool, tag = "1")] + pub success: bool, + #[prost(string, tag = "2")] + pub message: ::prost::alloc::string::String, +} /// Generated client implementations. pub mod table_definition_client { #![allow( @@ -197,6 +211,35 @@ pub mod table_definition_client { ); self.inner.unary(req, path, codec).await } + pub async fn delete_table( + &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_definition.TableDefinition/DeleteTable", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "multieko2.table_definition.TableDefinition", + "DeleteTable", + ), + ); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -226,6 +269,13 @@ pub mod table_definition_server { tonic::Response, tonic::Status, >; + async fn delete_table( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } #[derive(Debug)] pub struct TableDefinitionServer { @@ -398,6 +448,51 @@ pub mod table_definition_server { }; Box::pin(fut) } + "/multieko2.table_definition.TableDefinition/DeleteTable" => { + #[allow(non_camel_case_types)] + struct DeleteTableSvc(pub Arc); + impl< + T: TableDefinition, + > tonic::server::UnaryService + for DeleteTableSvc { + type Response = super::DeleteTableResponse; + 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(&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 = DeleteTableSvc(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/table_definition_service.rs b/server/src/server/services/table_definition_service.rs index a96cd6a..919e877 100644 --- a/server/src/server/services/table_definition_service.rs +++ b/server/src/server/services/table_definition_service.rs @@ -1,20 +1,27 @@ -// server/src/server/services/table_definition_service.rs +// src/server/services/table_definition_service.rs use tonic::{Request, Response, Status}; use common::proto::multieko2::{ common::Empty, table_definition::{ table_definition_server::TableDefinition, - PostTableDefinitionRequest, TableDefinitionResponse, ProfileTreeResponse - } + PostTableDefinitionRequest, TableDefinitionResponse, + ProfileTreeResponse, DeleteTableRequest, DeleteTableResponse, + }, }; use sqlx::PgPool; -use crate::table_definition::handlers::{post_table_definition, get_profile_tree}; +use crate::table_definition::handlers::{post_table_definition, get_profile_tree, delete_table}; #[derive(Debug)] pub struct TableDefinitionService { pub db_pool: PgPool, } +impl TableDefinitionService { + pub fn new(db_pool: PgPool) -> Self { + Self { db_pool } + } +} + #[tonic::async_trait] impl TableDefinition for TableDefinitionService { async fn post_table_definition( @@ -27,8 +34,16 @@ impl TableDefinition for TableDefinitionService { async fn get_profile_tree( &self, - request: Request, // Changed from Request<()> + request: Request, ) -> Result, Status> { get_profile_tree::get_profile_tree(&self.db_pool, request).await } + + async fn delete_table( + &self, + request: Request, + ) -> Result, Status> { + let response = delete_table(&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 index db5b807..d66aa88 100644 --- a/server/src/table_definition/handlers.rs +++ b/server/src/table_definition/handlers.rs @@ -1,6 +1,8 @@ // server/src/table_definition/handlers.rs pub mod post_table_definition; pub mod get_profile_tree; +pub mod delete_table; pub use post_table_definition::post_table_definition; pub use get_profile_tree::get_profile_tree; +pub use delete_table::delete_table; diff --git a/server/src/table_definition/handlers/delete_table.rs b/server/src/table_definition/handlers/delete_table.rs new file mode 100644 index 0000000..0af789a --- /dev/null +++ b/server/src/table_definition/handlers/delete_table.rs @@ -0,0 +1,84 @@ +// server/src/table_definition/handlers/delete_table.rs +use tonic::Status; +use sqlx::{PgPool, Postgres, Transaction}; +use common::proto::multieko2::table_definition::{DeleteTableRequest, DeleteTableResponse}; + +pub async fn delete_table( + db_pool: &PgPool, + request: DeleteTableRequest, +) -> Result { + let mut transaction = db_pool.begin().await + .map_err(|e| Status::internal(format!("Failed to start transaction: {}", e)))?; + + // Step 1: Get profile and validate existence + let profile = sqlx::query!( + "SELECT id FROM profiles WHERE name = $1", + request.profile_name + ) + .fetch_optional(&mut *transaction) + .await + .map_err(|e| Status::internal(format!("Profile lookup failed: {}", e)))?; + + let profile_id = match profile { + Some(p) => p.id, + None => return Err(Status::not_found("Profile not found")), + }; + + // Step 2: Get table definition and validate existence + let table_def = sqlx::query!( + "SELECT id FROM table_definitions + WHERE profile_id = $1 AND table_name = $2", + profile_id, + request.table_name + ) + .fetch_optional(&mut *transaction) + .await + .map_err(|e| Status::internal(format!("Table lookup failed: {}", e)))?; + + let table_def_id = match table_def { + Some(t) => t.id, + None => return Err(Status::not_found("Table not found in profile")), + }; + + // Step 3: Drop the actual PostgreSQL table with CASCADE + sqlx::query(&format!(r#"DROP TABLE IF EXISTS "{}" CASCADE"#, request.table_name)) + .execute(&mut *transaction) + .await + .map_err(|e| Status::internal(format!("Table drop failed: {}", e)))?; + + // Step 4: Delete from table_definitions + sqlx::query!( + "DELETE FROM table_definitions WHERE id = $1", + table_def_id + ) + .execute(&mut *transaction) + .await + .map_err(|e| Status::internal(format!("Definition deletion failed: {}", e)))?; + + // Step 5: Check and clean up profile if empty + let remaining = sqlx::query!( + "SELECT COUNT(*) as count FROM table_definitions WHERE profile_id = $1", + profile_id + ) + .fetch_one(&mut *transaction) + .await + .map_err(|e| Status::internal(format!("Count query failed: {}", e)))?; + + if remaining.count.unwrap_or(1) == 0 { + sqlx::query!( + "DELETE FROM profiles WHERE id = $1", + profile_id + ) + .execute(&mut *transaction) + .await + .map_err(|e| Status::internal(format!("Profile cleanup failed: {}", e)))?; + } + + transaction.commit().await + .map_err(|e| Status::internal(format!("Transaction commit failed: {}", e)))?; + + Ok(DeleteTableResponse { + success: true, + message: format!("Table '{}' and its definition were successfully removed", request.table_name), + }) +} diff --git a/server/src/table_definition/handlers/post_table_definition.rs b/server/src/table_definition/handlers/post_table_definition.rs index 3794ae9..b3fbf3a 100644 --- a/server/src/table_definition/handlers/post_table_definition.rs +++ b/server/src/table_definition/handlers/post_table_definition.rs @@ -101,15 +101,6 @@ pub async fn post_table_definition( } // Process indexes without year prefix - let mut indexes = Vec::new(); - for idx in request.indexes.drain(..) { - let idx_name = sanitize_identifier(&idx); // No year prefix - if !is_valid_identifier(&idx) { - return Err(Status::invalid_argument(format!("Invalid index name: {}", idx))); - } - indexes.push(idx_name); - } - let mut indexes = Vec::new(); for idx in request.indexes.drain(..) { let idx_name = sanitize_identifier(&idx);