diff --git a/AGENTS.md b/AGENTS.md index 51dd9027..57d2eab8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ - When creating a new Rust file, format ONLY that new file with the Cargo formatter (`rustfmt --edition 2024 --config skip_children=true path/to/new_file.rs`). Use the file’s actual Rust edition. Never run workspace-wide or package-wide `cargo fmt`, and never format existing files unless explicitly requested. - Do not run other automatic code-formatting commands unless explicitly requested. -- Write new or changed SQL in a human-readable layout. Use indented multiline queries, put SQL clauses and JOIN conditions on separate lines, and indent subqueries. Rust formatting does not format SQL inside strings; review that SQL manually. +- Keep SQL easy to scan and preserve existing readable layouts. Short queries and simple subqueries may stay on one line; for example, keep each simple `EXISTS (SELECT 1 FROM ... WHERE ...)` condition together, with successive `OR EXISTS` conditions on separate lines. Use multiline layout and indentation when query complexity or line length warrants it, especially for long SELECT lists, multiple JOINs, or nested logic. Do not mechanically put every clause on its own line or expand compact SQL just to satisfy a formatting rule. Review SQL inside Rust strings manually; do not reformat unrelated queries. - Preserve existing formatting. Make only the smallest hand-edited changes required for the task. - Do not run builds, checks, linters, or pre-existing tests. Only run tests that an agent wrote or modified as part of the current task; if the agent did not write or modify any tests, run no tests. - Check if what you are doing is running. Server can be running, tauri app might be running. No need to turn on redundant systems. diff --git a/common/build.rs b/common/build.rs index 33791b95..f8bdcc54 100644 --- a/common/build.rs +++ b/common/build.rs @@ -9,6 +9,7 @@ fn main() -> Result<(), Box> { .type_attribute(".komp_ac.accounting", serde) .type_attribute(".komp_ac.analytics", serde) .type_attribute(".komp_ac.auth", serde) + .type_attribute(".komp_ac.common", serde) .type_attribute(".komp_ac.document_data", serde) .type_attribute( ".komp_ac.document_data.TypstTemplateVersion", diff --git a/common/proto/exchange_rates.proto b/common/proto/exchange_rates.proto index 7d21fdec..a38e1a25 100644 --- a/common/proto/exchange_rates.proto +++ b/common/proto/exchange_rates.proto @@ -127,6 +127,8 @@ message PreviewDirectConversionResponse { ExchangeRateDateRule applied_date_rule = 23; ExchangeRateSource applied_source = 24; string selection_reason = 25; + string module_id = 26; + string provider_evidence_json = 27; } message ListConversionEvidenceRequest { @@ -167,6 +169,8 @@ message ConversionEvidence { optional string custom_rate_note = 26; string selection_reason = 27; optional string selected_by_user_id = 28; + string module_id = 29; + string provider_evidence_json = 30; } message ListConversionEvidenceResponse { diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index 8c37b5ee..a334d495 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -9,6 +9,9 @@ import "common.proto"; // logical "profiles" (schemas). Each table has stored structure, links, and // validation rules. service TableDefinition { + rpc GetModuleConfiguration(GetModuleConfigurationRequest) returns (ModuleConfiguration); + rpc SetModuleConfiguration(SetModuleConfigurationRequest) returns (ModuleConfiguration); + // Creates a new table (and schema if missing) with system columns, // linked-table foreign keys, user-defined columns, and optional indexes. // Also inserts metadata and default validation rules. Entirely transactional. @@ -102,6 +105,30 @@ service TableDefinition { rpc DeleteProfile(DeleteProfileRequest) returns (DeleteProfileResponse); } +message ModuleScope { + oneof target { + komp_ac.common.Empty global = 1; + string profile_name = 2; + } +} + +message GetModuleConfigurationRequest { + ModuleScope scope = 1; +} + +message SetModuleConfigurationRequest { + ModuleScope scope = 1; + repeated string enabled_modules = 2; +} + +message ModuleConfiguration { + ModuleScope scope = 1; + repeated string available_modules = 2; + repeated string selected_modules = 3; + repeated string inherited_modules = 4; + repeated string effective_modules = 5; +} + message CreateCustomExchangeRatesTableRequest { string profile_name = 1; } diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 9b3568c2..001cb632 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.common.rs b/common/src/proto/komp_ac.common.rs index 087cbb6b..211fa638 100644 --- a/common/src/proto/komp_ac.common.rs +++ b/common/src/proto/komp_ac.common.rs @@ -1,11 +1,14 @@ // This file is @generated by prost-build. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct Empty {} +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct CountResponse { #[prost(int64, tag = "1")] pub count: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct PositionRequest { #[prost(int64, tag = "1")] diff --git a/common/src/proto/komp_ac.exchange_rates.rs b/common/src/proto/komp_ac.exchange_rates.rs index c20acb22..7b890acb 100644 --- a/common/src/proto/komp_ac.exchange_rates.rs +++ b/common/src/proto/komp_ac.exchange_rates.rs @@ -97,6 +97,10 @@ pub struct PreviewDirectConversionResponse { pub applied_source: i32, #[prost(string, tag = "25")] pub selection_reason: ::prost::alloc::string::String, + #[prost(string, tag = "26")] + pub module_id: ::prost::alloc::string::String, + #[prost(string, tag = "27")] + pub provider_evidence_json: ::prost::alloc::string::String, } #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -173,6 +177,10 @@ pub struct ConversionEvidence { pub selection_reason: ::prost::alloc::string::String, #[prost(string, optional, tag = "28")] pub selected_by_user_id: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, tag = "29")] + pub module_id: ::prost::alloc::string::String, + #[prost(string, tag = "30")] + pub provider_evidence_json: ::prost::alloc::string::String, } #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/common/src/proto/komp_ac.table_definition.rs b/common/src/proto/komp_ac.table_definition.rs index e824406f..7bd22837 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -1,6 +1,51 @@ // This file is @generated by prost-build. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ModuleScope { + #[prost(oneof = "module_scope::Target", tags = "1, 2")] + pub target: ::core::option::Option, +} +/// Nested message and enum types in `ModuleScope`. +pub mod module_scope { + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Target { + #[prost(message, tag = "1")] + Global(super::super::common::Empty), + #[prost(string, tag = "2")] + ProfileName(::prost::alloc::string::String), + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GetModuleConfigurationRequest { + #[prost(message, optional, tag = "1")] + pub scope: ::core::option::Option, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SetModuleConfigurationRequest { + #[prost(message, optional, tag = "1")] + pub scope: ::core::option::Option, + #[prost(string, repeated, tag = "2")] + pub enabled_modules: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ModuleConfiguration { + #[prost(message, optional, tag = "1")] + pub scope: ::core::option::Option, + #[prost(string, repeated, tag = "2")] + pub available_modules: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(string, repeated, tag = "3")] + pub selected_modules: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(string, repeated, tag = "4")] + pub inherited_modules: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(string, repeated, tag = "5")] + pub effective_modules: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct CreateCustomExchangeRatesTableRequest { #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, @@ -1314,6 +1359,64 @@ pub mod table_definition_client { self.inner = self.inner.max_encoding_message_size(limit); self } + pub async fn get_module_configuration( + &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_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.table_definition.TableDefinition/GetModuleConfiguration", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.table_definition.TableDefinition", + "GetModuleConfiguration", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn set_module_configuration( + &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_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.table_definition.TableDefinition/SetModuleConfiguration", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.table_definition.TableDefinition", + "SetModuleConfiguration", + ), + ); + self.inner.unary(req, path, codec).await + } /// Creates a new table (and schema if missing) with system columns, /// linked-table foreign keys, user-defined columns, and optional indexes. /// Also inserts metadata and default validation rules. Entirely transactional. @@ -2044,6 +2147,20 @@ pub mod table_definition_server { /// Generated trait containing gRPC methods that should be implemented for use with TableDefinitionServer. #[async_trait] pub trait TableDefinition: std::marker::Send + std::marker::Sync + 'static { + async fn get_module_configuration( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn set_module_configuration( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; /// Creates a new table (and schema if missing) with system columns, /// linked-table foreign keys, user-defined columns, and optional indexes. /// Also inserts metadata and default validation rules. Entirely transactional. @@ -2332,6 +2449,104 @@ pub mod table_definition_server { } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { + "/komp_ac.table_definition.TableDefinition/GetModuleConfiguration" => { + #[allow(non_camel_case_types)] + struct GetModuleConfigurationSvc(pub Arc); + impl< + T: TableDefinition, + > tonic::server::UnaryService + for GetModuleConfigurationSvc { + type Response = super::ModuleConfiguration; + 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 { + ::get_module_configuration( + &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 = GetModuleConfigurationSvc(inner); + let codec = tonic_prost::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/SetModuleConfiguration" => { + #[allow(non_camel_case_types)] + struct SetModuleConfigurationSvc(pub Arc); + impl< + T: TableDefinition, + > tonic::server::UnaryService + for SetModuleConfigurationSvc { + type Response = super::ModuleConfiguration; + 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 { + ::set_module_configuration( + &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 = SetModuleConfigurationSvc(inner); + let codec = tonic_prost::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/PostTableDefinition" => { #[allow(non_camel_case_types)] struct PostTableDefinitionSvc(pub Arc); diff --git a/generic_rust_todo.txt b/generic_rust_todo.txt new file mode 100644 index 00000000..1a2e0a15 --- /dev/null +++ b/generic_rust_todo.txt @@ -0,0 +1,12 @@ +1. - Your Rust module can define a column type XX, which you can add through the table definition. + - When a row is saved, the framework calls the registered modules’ row hooks. + - You implement your module’s hook and choose whether its logic runs for every table, only tables containing XX, or + another condition. + + - If you want it to affect only the XX column, your hook selects and processes that column’s value from the supplied + row. + + - The framework doesn’t automatically make those choices. You implement the filtering and column-specific behavior; + modules without a row-hook implementation default to doing nothing. + +2. diff --git a/server b/server index cc585a2a..1da33d65 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit cc585a2a9c7be3e3dfe4ab1fcfeafb7a79ef4864 +Subproject commit 1da33d65622004d0005b672a7b67b2f69d7c9492