diff --git a/Cargo.lock b/Cargo.lock index 551131d..ce4e12a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,7 +493,7 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "canvas" -version = "0.6.11" +version = "0.6.12" dependencies = [ "anyhow", "async-trait", @@ -586,7 +586,7 @@ dependencies = [ [[package]] name = "client" -version = "0.6.11" +version = "0.6.12" dependencies = [ "anyhow", "async-trait", @@ -642,7 +642,7 @@ dependencies = [ [[package]] name = "common" -version = "0.6.11" +version = "0.6.12" dependencies = [ "prost 0.13.5", "prost-build 0.14.1", @@ -3117,7 +3117,7 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "search" -version = "0.6.11" +version = "0.6.12" dependencies = [ "anyhow", "common", @@ -3216,7 +3216,7 @@ dependencies = [ [[package]] name = "server" -version = "0.6.11" +version = "0.6.12" dependencies = [ "anyhow", "bcrypt", @@ -4549,7 +4549,7 @@ dependencies = [ [[package]] name = "validation-core" -version = "0.6.11" +version = "0.6.12" dependencies = [ "regex", "serde", diff --git a/client b/client index 4f8c712..3badee2 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 4f8c71274ac36a32cf61715f675da0e3acf80d3b +Subproject commit 3badee28b165e53f9b9968d762a23edeb48a7dcb diff --git a/common/build.rs b/common/build.rs index 9189ae8..aeefe9a 100644 --- a/common/build.rs +++ b/common/build.rs @@ -8,6 +8,10 @@ fn main() -> Result<(), Box> { ".komp_ac.table_validation.FieldValidation", "#[derive(serde::Serialize, serde::Deserialize)]", ) + .field_attribute( + ".komp_ac.table_validation.FieldValidation.locked", + "#[serde(default)]", + ) .type_attribute( ".komp_ac.table_validation.CharacterLimits", "#[derive(serde::Serialize, serde::Deserialize)]", @@ -136,6 +140,14 @@ fn main() -> Result<(), Box> { ".komp_ac.table_validation.ApplyValidationSetResponse", "#[derive(serde::Serialize, serde::Deserialize)]", ) + .type_attribute( + ".komp_ac.table_validation.LockFieldValidationRequest", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_validation.LockFieldValidationResponse", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) // Enum -> readable strings in JSON ("BYTES", "DISPLAY_WIDTH") .type_attribute( ".komp_ac.table_validation.CountMode", @@ -161,6 +173,10 @@ fn main() -> Result<(), Box> { ".komp_ac.table_definition.PostTableDefinitionRequest", "#[derive(serde::Serialize, serde::Deserialize)]", ) + .type_attribute( + ".komp_ac.table_definition.AddTableColumnsRequest", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) .type_attribute( ".komp_ac.table_definition.TableDefinitionResponse", "#[derive(serde::Serialize, serde::Deserialize)]" diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index aeab0de..d535eca 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -14,6 +14,10 @@ service TableDefinition { // Also inserts metadata and default validation rules. Entirely transactional. rpc PostTableDefinition(PostTableDefinitionRequest) returns (TableDefinitionResponse); + // Appends new user-defined columns to an existing table. + // Existing columns, links, and table logic are never changed by this call. + rpc AddTableColumns(AddTableColumnsRequest) returns (TableDefinitionResponse); + // Lists all profiles (schemas) and their tables with declared dependencies. // This provides a tree-like overview of table relationships. rpc GetProfileTree(komp_ac.common.Empty) returns (ProfileTreeResponse); @@ -72,6 +76,21 @@ message PostTableDefinitionRequest { string profile_name = 5; } +// Defines append-only column additions for an existing table. +message AddTableColumnsRequest { + // Existing profile/schema name. + string profile_name = 1; + + // Existing table name in the profile. + string table_name = 2; + + // New user-defined columns only. Existing columns cannot be changed here. + repeated ColumnDefinition columns = 3; + + // Optional indexes for the new columns only. + repeated string indexes = 4; +} + // Describes one user-defined column for a table. message ColumnDefinition { // Column name that follows the same validation rules as table_name. diff --git a/common/proto/table_validation.proto b/common/proto/table_validation.proto index bcfb8ab..a60015b 100644 --- a/common/proto/table_validation.proto +++ b/common/proto/table_validation.proto @@ -52,6 +52,9 @@ message FieldValidation { // Field must be provided / treated as required by clients and server enforcement layers. bool required = 4; + + // Once locked, this field's validation config cannot be changed. + bool locked = 15; } // Character length counting mode @@ -191,6 +194,9 @@ service TableValidationService { // Snapshot a reusable set onto a concrete table field. rpc ApplyValidationSet(ApplyValidationSetRequest) returns (ApplyValidationSetResponse); + + // Permanently lock one field's validation config. + rpc LockFieldValidation(LockFieldValidationRequest) returns (LockFieldValidationResponse); } message UpdateFieldValidationRequest { @@ -320,3 +326,14 @@ message ApplyValidationSetResponse { string message = 2; FieldValidation validation = 3; } + +message LockFieldValidationRequest { + string profileName = 1; + string tableName = 2; + string dataKey = 3; +} + +message LockFieldValidationResponse { + bool success = 1; + string message = 2; +} diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index fae9cd0..0985e48 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_definition.rs b/common/src/proto/komp_ac.table_definition.rs index a137362..aa908ad 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -43,6 +43,23 @@ pub struct PostTableDefinitionRequest { #[prost(string, tag = "5")] pub profile_name: ::prost::alloc::string::String, } +/// Defines append-only column additions for an existing table. +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddTableColumnsRequest { + /// Existing profile/schema name. + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + /// Existing table name in the profile. + #[prost(string, tag = "2")] + pub table_name: ::prost::alloc::string::String, + /// New user-defined columns only. Existing columns cannot be changed here. + #[prost(message, repeated, tag = "3")] + pub columns: ::prost::alloc::vec::Vec, + /// Optional indexes for the new columns only. + #[prost(string, repeated, tag = "4")] + pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} /// Describes one user-defined column for a table. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -359,6 +376,37 @@ pub mod table_definition_client { ); self.inner.unary(req, path, codec).await } + /// Appends new user-defined columns to an existing table. + /// Existing columns, links, and table logic are never changed by this call. + pub async fn add_table_columns( + &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_definition.TableDefinition/AddTableColumns", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.table_definition.TableDefinition", + "AddTableColumns", + ), + ); + self.inner.unary(req, path, codec).await + } /// Lists all profiles (schemas) and their tables with declared dependencies. /// This provides a tree-like overview of table relationships. pub async fn get_profile_tree( @@ -536,6 +584,15 @@ pub mod table_definition_server { tonic::Response, tonic::Status, >; + /// Appends new user-defined columns to an existing table. + /// Existing columns, links, and table logic are never changed by this call. + async fn add_table_columns( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; /// Lists all profiles (schemas) and their tables with declared dependencies. /// This provides a tree-like overview of table relationships. async fn get_profile_tree( @@ -708,6 +765,52 @@ pub mod table_definition_server { }; Box::pin(fut) } + "/komp_ac.table_definition.TableDefinition/AddTableColumns" => { + #[allow(non_camel_case_types)] + struct AddTableColumnsSvc(pub Arc); + impl< + T: TableDefinition, + > tonic::server::UnaryService + for AddTableColumnsSvc { + type Response = super::TableDefinitionResponse; + 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 { + ::add_table_columns(&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 = AddTableColumnsSvc(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) + } "/komp_ac.table_definition.TableDefinition/GetProfileTree" => { #[allow(non_camel_case_types)] struct GetProfileTreeSvc(pub Arc); diff --git a/common/src/proto/komp_ac.table_validation.rs b/common/src/proto/komp_ac.table_validation.rs index 51baf6c..80e9326 100644 --- a/common/src/proto/komp_ac.table_validation.rs +++ b/common/src/proto/komp_ac.table_validation.rs @@ -43,6 +43,10 @@ pub struct FieldValidation { /// Field must be provided / treated as required by clients and server enforcement layers. #[prost(bool, tag = "4")] pub required: bool, + /// Once locked, this field's validation config cannot be changed. + #[prost(bool, tag = "15")] + #[serde(default)] + pub locked: bool, } /// Character limit validation (Validation 1). /// These rules map directly to canvas CharacterLimits. @@ -368,6 +372,24 @@ pub struct ApplyValidationSetResponse { #[prost(message, optional, tag = "3")] pub validation: ::core::option::Option, } +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LockFieldValidationRequest { + #[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, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LockFieldValidationResponse { + #[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")] @@ -877,6 +899,36 @@ pub mod table_validation_service_client { ); self.inner.unary(req, path, codec).await } + /// Permanently lock one field's validation config. + pub async fn lock_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/LockFieldValidation", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.table_validation.TableValidationService", + "LockFieldValidation", + ), + ); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -967,6 +1019,14 @@ pub mod table_validation_service_server { tonic::Response, tonic::Status, >; + /// Permanently lock one field's validation config. + async fn lock_field_validation( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } /// Service for storing and fetching field-validation definitions. #[derive(Debug)] @@ -1544,6 +1604,55 @@ pub mod table_validation_service_server { }; Box::pin(fut) } + "/komp_ac.table_validation.TableValidationService/LockFieldValidation" => { + #[allow(non_camel_case_types)] + struct LockFieldValidationSvc(pub Arc); + impl< + T: TableValidationService, + > tonic::server::UnaryService + for LockFieldValidationSvc { + type Response = super::LockFieldValidationResponse; + 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 { + ::lock_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 = LockFieldValidationSvc(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 b/server index 2f933e4..aa0f9a3 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 2f933e4e34e1417f2eeb1024f98a95e1c598b06f +Subproject commit aa0f9a310897adf1560b3a364ae83d38ae0a0f68