diff --git a/Cargo.lock b/Cargo.lock index c538685..a46b7ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -595,8 +595,8 @@ dependencies = [ "dotenvy", "futures", "lazy_static", - "prost", - "prost-types", + "prost 0.13.5", + "prost-types 0.13.5", "ratatui", "rstest", "serde", @@ -637,9 +637,11 @@ dependencies = [ name = "common" version = "0.5.0" dependencies = [ - "prost", - "prost-types", + "prost 0.13.5", + "prost-build 0.14.1", + "prost-types 0.13.5", "serde", + "serde_json", "tantivy", "tonic", "tonic-build", @@ -2504,7 +2506,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive 0.14.1", ] [[package]] @@ -2520,8 +2532,28 @@ dependencies = [ "once_cell", "petgraph", "prettyplease", - "prost", - "prost-types", + "prost 0.13.5", + "prost-types 0.13.5", + "regex", + "syn 2.0.104", + "tempfile", +] + +[[package]] +name = "prost-build" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost 0.14.1", + "prost-types 0.14.1", "regex", "syn 2.0.104", "tempfile", @@ -2540,13 +2572,35 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "prost-types" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" dependencies = [ - "prost", + "prost 0.13.5", +] + +[[package]] +name = "prost-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +dependencies = [ + "prost 0.14.1", ] [[package]] @@ -3050,7 +3104,7 @@ version = "0.5.0" dependencies = [ "anyhow", "common", - "prost", + "prost 0.13.5", "serde", "serde_json", "sqlx", @@ -3156,8 +3210,9 @@ dependencies = [ "futures", "jsonwebtoken", "lazy_static", - "prost", - "prost-types", + "prost 0.13.5", + "prost-build 0.14.1", + "prost-types 0.13.5", "rand 0.9.2", "regex", "rstest", @@ -4193,7 +4248,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", + "prost 0.13.5", "socket2 0.5.10", "tokio", "tokio-stream", @@ -4211,8 +4266,8 @@ checksum = "eac6f67be712d12f0b41328db3137e0d0757645d8904b4cb7d51cd9c2279e847" dependencies = [ "prettyplease", "proc-macro2", - "prost-build", - "prost-types", + "prost-build 0.13.5", + "prost-types 0.13.5", "quote", "syn 2.0.104", ] @@ -4223,8 +4278,8 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9687bd5bfeafebdded2356950f278bba8226f0b32109537c4253406e09aafe1" dependencies = [ - "prost", - "prost-types", + "prost 0.13.5", + "prost-types 0.13.5", "tokio", "tokio-stream", "tonic", diff --git a/common/Cargo.toml b/common/Cargo.toml index 5d19597..c9756d0 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -13,6 +13,8 @@ serde = { version = "1.0.219", features = ["derive"] } # Search tantivy = { workspace = true } +serde_json.workspace = true [build-dependencies] -tonic-build = "0.13.0" +tonic-build = { version = "0.13.0", features = ["prost-build"] } +prost-build = "0.14.1" diff --git a/common/build.rs b/common/build.rs index e390ebf..2903de1 100644 --- a/common/build.rs +++ b/common/build.rs @@ -2,21 +2,57 @@ fn main() -> Result<(), Box> { tonic_build::configure() .build_server(true) - .out_dir("src/proto") .file_descriptor_set_path("src/proto/descriptor.bin") - .compile_protos( // Changed from .compile() + .out_dir("src/proto") + // Derive serde for all relevant messages + .type_attribute( + ".komp_ac.table_validation.FieldValidation", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_validation.CharacterLimits", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_validation.TableValidationResponse", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_validation.UpdateFieldValidationRequest", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_validation.UpdateFieldValidationResponse", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + // Derive serde for the enum and keep legacy string values in JSON + .type_attribute( + ".komp_ac.table_validation.CountMode", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_validation.CountMode", + r#"#[serde(rename_all = "SCREAMING_SNAKE_CASE")]"#, + ) + // Back-compat: allow old "character_limits" key to deserialize into "limits" + .field_attribute( + ".komp_ac.table_validation.FieldValidation.limits", + r#"#[serde(alias = "character_limits")]"#, + ) + // Compile all protos (same list you already have) + .compile_protos( &[ "proto/common.proto", "proto/adresar.proto", "proto/auth.proto", - "proto/uctovnictvo.proto", - "proto/table_structure.proto", - "proto/table_definition.proto", - "proto/tables_data.proto", - "proto/table_script.proto", "proto/search.proto", "proto/search2.proto", + "proto/table_definition.proto", + "proto/table_script.proto", + "proto/table_structure.proto", "proto/table_validation.proto", + "proto/tables_data.proto", + "proto/uctovnictvo.proto", ], &["proto"], )?; diff --git a/common/proto/table_validation.proto b/common/proto/table_validation.proto index b4b6026..0a1a2b3 100644 --- a/common/proto/table_validation.proto +++ b/common/proto/table_validation.proto @@ -53,4 +53,19 @@ message CharacterLimits { service TableValidationService { rpc GetTableValidation(GetTableValidationRequest) returns (TableValidationResponse); + + rpc UpdateFieldValidation(UpdateFieldValidationRequest) + returns (UpdateFieldValidationResponse); +} + +message UpdateFieldValidationRequest { + string profile_name = 1; + string table_name = 2; + string data_key = 3; + FieldValidation validation = 4; +} + +message UpdateFieldValidationResponse { + bool success = 1; + string message = 2; } diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 23e9a23..35543cc 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/komp_ac.table_validation.rs b/common/src/proto/komp_ac.table_validation.rs index c8a7a2c..f277c00 100644 --- a/common/src/proto/komp_ac.table_validation.rs +++ b/common/src/proto/komp_ac.table_validation.rs @@ -9,12 +9,14 @@ pub struct GetTableValidationRequest { } /// Response with field-level validations; if a field is omitted, /// no validation is applied (default unspecified). +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TableValidationResponse { #[prost(message, repeated, tag = "1")] pub fields: ::prost::alloc::vec::Vec, } /// Field-level validation (extensible for future kinds) +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct FieldValidation { /// MUST match your frontend FormState.data_key for the column @@ -28,9 +30,11 @@ pub struct FieldValidation { /// ExternalValidation external = 13; /// CustomFormatter formatter = 14; #[prost(message, optional, tag = "10")] + #[serde(alias = "character_limits")] pub limits: ::core::option::Option, } /// Character limit validation (Validation 1) +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct CharacterLimits { /// When zero, the field is considered "not set". If both min/max are zero, @@ -46,7 +50,29 @@ pub struct CharacterLimits { #[prost(enumeration = "CountMode", tag = "4")] pub count_mode: i32, } +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpdateFieldValidationRequest { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub table_name: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub data_key: ::prost::alloc::string::String, + #[prost(message, optional, tag = "4")] + pub validation: ::core::option::Option, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpdateFieldValidationResponse { + #[prost(bool, tag = "1")] + pub success: bool, + #[prost(string, tag = "2")] + pub message: ::prost::alloc::string::String, +} /// Character length counting mode +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum CountMode { @@ -203,6 +229,35 @@ pub mod table_validation_service_client { ); self.inner.unary(req, path, codec).await } + pub async fn update_field_validation( + &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( + "/komp_ac.table_validation.TableValidationService/UpdateFieldValidation", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.table_validation.TableValidationService", + "UpdateFieldValidation", + ), + ); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -225,6 +280,13 @@ pub mod table_validation_service_server { tonic::Response, tonic::Status, >; + async fn update_field_validation( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } /// Service to fetch validations for a table #[derive(Debug)] @@ -353,6 +415,57 @@ pub mod table_validation_service_server { }; Box::pin(fut) } + "/komp_ac.table_validation.TableValidationService/UpdateFieldValidation" => { + #[allow(non_camel_case_types)] + struct UpdateFieldValidationSvc( + pub Arc, + ); + impl< + T: TableValidationService, + > tonic::server::UnaryService + for UpdateFieldValidationSvc { + type Response = super::UpdateFieldValidationResponse; + 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 { + ::update_field_validation( + &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 = UpdateFieldValidationSvc(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( diff --git a/server/Cargo.toml b/server/Cargo.toml index d7ded2b..b0d425e 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,10 +10,10 @@ search = { path = "../search" } anyhow = { workspace = true } tantivy = { workspace = true } +prost = "0.13.5" prost-types = { workspace = true } chrono = { version = "0.4.40", features = ["serde"] } dotenvy = "0.15.7" -prost = "0.13.5" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" sqlx = { version = "0.8.5", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "rust_decimal", "time", "uuid"] } @@ -40,6 +40,9 @@ regex = { workspace = true } thiserror = { workspace = true } steel-decimal = "1.0.0" +[build-dependencies] +prost-build = "0.14.1" + [lib] name = "server" path = "src/lib.rs" diff --git a/server/src/server/run.rs b/server/src/server/run.rs index 14674fe..2357e6c 100644 --- a/server/src/server/run.rs +++ b/server/src/server/run.rs @@ -23,7 +23,7 @@ use common::proto::komp_ac::{ search2::search2_server::Search2Server, }; use search::{SearcherService, SearcherServer}; -use crate::table_validation::post::service::TableValidationSvc; +use crate::table_validation::get::service::TableValidationSvc; pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box> { // Initialize JWT for authentication diff --git a/server/src/table_validation/get/service.rs b/server/src/table_validation/get/service.rs index 089ab1f..3698e4d 100644 --- a/server/src/table_validation/get/service.rs +++ b/server/src/table_validation/get/service.rs @@ -2,28 +2,14 @@ use tonic::{Request, Response, Status}; use sqlx::PgPool; -use serde::Deserialize; use common::proto::komp_ac::table_validation::{ table_validation_service_server::TableValidationService, GetTableValidationRequest, TableValidationResponse, - FieldValidation, CharacterLimits, CountMode as PbCountMode, + UpdateFieldValidationRequest, UpdateFieldValidationResponse, + FieldValidation, }; use crate::table_validation::post::repo; // repo still lives in post -#[derive(Deserialize)] -struct FieldConfig { - #[serde(default)] - character_limits: Option, -} - -#[derive(Deserialize)] -struct CharacterLimitsCfg { - #[serde(default)] min: Option, - #[serde(default)] max: Option, - #[serde(default)] warn_at: Option, - #[serde(default)] count_mode: Option, // "CHARS"/"BYTES"/"DISPLAY_WIDTH" -} - pub struct TableValidationSvc { pub db: PgPool, } @@ -46,46 +32,71 @@ impl TableValidationService for TableValidationSvc { .await .map_err(|e| Status::internal(format!("Failed to fetch rules: {}", e)))?; - // 3. Map JSON -> proto + // 3. Parse JSON directly into proto types let mut fields_out = Vec::new(); for r in rules { - let cfg: FieldConfig = match serde_json::from_value(r.config) { - Ok(c) => c, + match serde_json::from_value::(r.config) { + Ok(mut fv) => { + // Set the data_key from the database row + fv.data_key = r.data_key; + + // Skip if limits are all zero + if let Some(lims) = &fv.limits { + if lims.min == 0 && lims.max == 0 && lims.warn_at.is_none() { + continue; + } + } + fields_out.push(fv); + } Err(e) => { - tracing::warn!("Invalid config for {}: {}", r.data_key, e); + tracing::warn!("Invalid JSON for {}: {}", r.data_key, e); continue; } - }; - - if let Some(cl) = cfg.character_limits { - let min = cl.min.unwrap_or(0); - let max = cl.max.unwrap_or(0); - - // Skip "empty" validations (min=0,max=0,warn_at=None) - if min == 0 && max == 0 && cl.warn_at.is_none() { - continue; - } - - let pb_mode = match cl.count_mode.as_deref() { - Some("BYTES") => PbCountMode::Bytes as i32, - Some("DISPLAY_WIDTH") => PbCountMode::DisplayWidth as i32, - _ => PbCountMode::Chars as i32, - }; - - let limits = CharacterLimits { - min, - max, - warn_at: cl.warn_at, - count_mode: pb_mode, - }; - - fields_out.push(FieldValidation { - data_key: r.data_key, - limits: Some(limits), - }); } } Ok(Response::new(TableValidationResponse { fields: fields_out })) } + + async fn update_field_validation( + &self, + req: Request, + ) -> Result, Status> { + let req = req.into_inner(); + + let table_def_id = repo::get_table_def_id( + &self.db, &req.profile_name, &req.table_name, + ) + .await + .map_err(|_| Status::not_found("Table definition not found"))?; + + // Check if validation is provided + if let Some(validation) = req.validation { + // Convert proto FieldValidation directly to JSON + let json_value = serde_json::to_value(&validation) + .map_err(|e| Status::internal(format!("serialize error: {e}")))?; + + sqlx::query!( + r#"UPDATE table_validation_rules + SET config = $1, updated_at = now() + WHERE table_def_id = $2 AND data_key = $3"#, + json_value, + table_def_id, + req.data_key + ) + .execute(&self.db) + .await + .map_err(|e| Status::internal(format!("DB error: {e}")))?; + + Ok(Response::new(UpdateFieldValidationResponse { + success: true, + message: format!( + "Validation rules updated for {}.{} column {}", + req.profile_name, req.table_name, req.data_key + ), + })) + } else { + Err(Status::invalid_argument("No validation provided")) + } + } } diff --git a/server/src/table_validation/post/service.rs b/server/src/table_validation/post/service.rs new file mode 100644 index 0000000..3971612 --- /dev/null +++ b/server/src/table_validation/post/service.rs @@ -0,0 +1,18 @@ +// src/table_validation/post/service.rs + +use tonic::{Request, Response, Status}; +use sqlx::PgPool; +use common::proto::komp_ac::table_validation::{ + UpdateFieldValidationRequest, UpdateFieldValidationResponse, +}; +use crate::table_validation::post::repo; + +pub struct TableValidationUpdateSvc { + pub db: PgPool, +} + +impl TableValidationUpdateSvc { + pub fn new(db: PgPool) -> Self { + Self { db } + } +}