From 408187cb57a4aa7d62bedfee48b65baffd97b320 Mon Sep 17 00:00:00 2001 From: Filipriec Date: Sun, 30 Aug 2026 07:49:19 +0200 Subject: [PATCH] using unified system --- Cargo.lock | 16 +- Cargo.toml | 2 +- client | 2 +- client-gui2 | 2 +- common/build.rs | 257 +--- common/src/proto/komp_ac.accounting.rs | 48 + common/src/proto/komp_ac.analytics.rs | 11 + common/src/proto/komp_ac.auth.rs | 32 + common/src/proto/komp_ac.document_data.rs | 20 + common/src/proto/komp_ac.ecb.rs | 4 + common/src/proto/komp_ac.exchange_rates.rs | 15 + common/src/proto/komp_ac.search.rs | 9 + common/src/proto/komp_ac.search2.rs | 5 + common/src/proto/komp_ac.table_definition.rs | 23 + common/src/proto/komp_ac.table_script.rs | 8 + common/src/proto/komp_ac.table_structure.rs | 9 + common/src/proto/komp_ac.table_validation.rs | 2 + common/src/proto/komp_ac.tables_data.rs | 14 + komp-app/Cargo.toml | 16 + komp-app/src/auth.rs | 304 ++++ komp-app/src/csv.rs | 107 ++ komp-app/src/grpc.rs | 1330 ++++++++++++++++++ komp-app/src/import_export.rs | 444 ++++++ komp-app/src/lib.rs | 15 + komp-app/src/navigation.rs | 36 + komp-app/src/search.rs | 27 + komp-app/src/session.rs | 13 + komp-app/src/transport.rs | 125 ++ komp-app/src/value.rs | 81 ++ server | 2 +- 30 files changed, 2754 insertions(+), 225 deletions(-) create mode 100644 komp-app/Cargo.toml create mode 100644 komp-app/src/auth.rs create mode 100644 komp-app/src/csv.rs create mode 100644 komp-app/src/grpc.rs create mode 100644 komp-app/src/import_export.rs create mode 100644 komp-app/src/lib.rs create mode 100644 komp-app/src/navigation.rs create mode 100644 komp-app/src/search.rs create mode 100644 komp-app/src/session.rs create mode 100644 komp-app/src/transport.rs create mode 100644 komp-app/src/value.rs diff --git a/Cargo.lock b/Cargo.lock index cc4ba2d1..e816ec7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1178,6 +1178,7 @@ dependencies = [ "iban_validate", "isocountry", "jiff", + "komp-app", "lazy_static", "nucleo", "phonenumber", @@ -1189,7 +1190,6 @@ dependencies = [ "rstest", "rust_decimal", "rusty-money", - "sanitise-file-name", "serde", "serde_json", "steel-core", @@ -4246,6 +4246,20 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "komp-app" +version = "0.8.48" +dependencies = [ + "anyhow", + "common", + "jiff", + "prost-types", + "sanitise-file-name", + "serde", + "tonic", + "uuid", +] + [[package]] name = "lab" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index 4c08ab18..9267973a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["client", "server", "common", "search", "tui-canvas", "tui-canvas/tui-canvas-validation-core", "tui-pages", "tui-pages/ux-config" ] +members = ["client", "server", "common", "komp-app", "search", "tui-canvas", "tui-canvas/tui-canvas-validation-core", "tui-pages", "tui-pages/ux-config" ] resolver = "3" [workspace.package] diff --git a/client b/client index 336e48ef..a3c5853f 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 336e48efa8d3980a199b971898b67e404e9397cb +Subproject commit a3c5853f60603730cbc2d762be4bdd4222936816 diff --git a/client-gui2 b/client-gui2 index ff0bceef..4a9dc302 160000 --- a/client-gui2 +++ b/client-gui2 @@ -1 +1 @@ -Subproject commit ff0bceefb7a912eadf3d9692033c83d714d90780 +Subproject commit 4a9dc30249a333df1fdff02b3ad7e9c70886734e diff --git a/common/build.rs b/common/build.rs index cb6dfe9f..938f402e 100644 --- a/common/build.rs +++ b/common/build.rs @@ -1,235 +1,58 @@ fn main() -> Result<(), Box> { + let serde = "#[derive(serde::Serialize, serde::Deserialize)]"; tonic_prost_build::configure() .build_server(true) .file_descriptor_set_path("src/proto/descriptor.bin") .out_dir("src/proto") - // Derive serde for the messages - .type_attribute( - ".komp_ac.table_validation.FieldValidation", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) + // These packages cross the native-client boundary. Keeping the derives + // here makes the root common crate usable by TUI and Tauri alike. + .type_attribute(".komp_ac.accounting", serde) + .type_attribute(".komp_ac.analytics", serde) + .type_attribute(".komp_ac.auth", serde) + .type_attribute(".komp_ac.document_data", serde) + .type_attribute(".komp_ac.ecb", serde) + .type_attribute(".komp_ac.exchange_rates", serde) + .type_attribute(".komp_ac.search", serde) + .type_attribute(".komp_ac.search2", serde) + .type_attribute(".komp_ac.table_definition", serde) + .type_attribute(".komp_ac.table_script", serde) + .type_attribute(".komp_ac.table_structure", serde) + .type_attribute(".komp_ac.table_validation", serde) + // tables_data requests contain prost_types::Value, which does not + // implement Serde. Only response-side types cross into the GUI. + .type_attribute(".komp_ac.tables_data.PostTableDataResponse", serde) + .type_attribute(".komp_ac.tables_data.PutTableDataResponse", serde) + .type_attribute(".komp_ac.tables_data.TableUpdateImpactResponse", serde) + .type_attribute(".komp_ac.tables_data.ArchiveTableDataResponse", serde) + .type_attribute(".komp_ac.tables_data.DeleteTableDataResponse", serde) + .type_attribute(".komp_ac.tables_data.ResolvedTableLinkVersion", serde) + .type_attribute(".komp_ac.tables_data.ResolvedTableLinkUpdate", serde) + .type_attribute(".komp_ac.tables_data.ResolvedTableLink", serde) + .type_attribute(".komp_ac.tables_data.GetTableDataResponse", serde) + .type_attribute(".komp_ac.tables_data.RowNavigation", serde) + .type_attribute(".komp_ac.tables_data.GetLastTableDataResponse", serde) + .type_attribute(".komp_ac.tables_data.GetAdjacentTableDataResponse", serde) + .type_attribute(".komp_ac.tables_data.ResolvedTableLinkStatus", serde) + .type_attribute(".komp_ac.tables_data.RowNavigationMode", serde) .field_attribute( ".komp_ac.table_validation.FieldValidation.locked", "#[serde(default)]", ) - .type_attribute( - ".komp_ac.table_validation.CharacterLimits", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.DisplayMask", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.TableValidationResponse", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.PatternRule", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.PatternPosition", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.CharacterConstraint", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.PatternRules", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.AllowedValues", - "#[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)]", - ) - .type_attribute( - ".komp_ac.table_validation.ReplaceTableValidationRequest", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.ReplaceTableValidationResponse", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.ValidationRuleDefinition", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.ValidationSetRuleItem", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.ValidationSetRuleItem.Source", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.ValidationSetDefinition", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.UpsertValidationRuleRequest", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.UpsertValidationRuleResponse", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.ListValidationRulesRequest", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.ListValidationRulesResponse", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.DeleteValidationRuleRequest", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.DeleteValidationRuleResponse", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.UpsertValidationSetRequest", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.UpsertValidationSetResponse", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.ListValidationSetsRequest", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.ListValidationSetsResponse", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.DeleteValidationSetRequest", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.DeleteValidationSetResponse", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_validation.ApplyValidationSetRequest", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".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", - "#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]", - ) - .type_attribute( - ".komp_ac.table_validation.PatternPositionKind", - "#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]", - ) - .type_attribute( - ".komp_ac.table_validation.CharacterConstraintKind", - "#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]", - ) - .type_attribute( - ".komp_ac.table_definition.ColumnDefinition", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) .field_attribute( ".komp_ac.table_definition.ColumnDefinition.boolean_ledger", "#[serde(default, alias = \"boolean_ledger_operator\")]", ) .type_attribute( - ".komp_ac.table_definition.PostTableDefinitionRequest", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - // Carried by the two requests that create columns, both of which are - // serialised, so it has to be serialisable with them. - .type_attribute( - ".komp_ac.table_definition.GeneratedColumnAlias", - "#[derive(serde::Serialize, serde::Deserialize)]", + ".komp_ac.table_validation.CountMode", + "#[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]", ) .type_attribute( - ".komp_ac.table_definition.CreateInvoiceTemplateTableRequest", - "#[derive(serde::Serialize, serde::Deserialize)]", + ".komp_ac.table_validation.PatternPositionKind", + "#[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]", ) .type_attribute( - ".komp_ac.table_definition.GeneratedInvoiceTemplateTable", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_definition.CreateInvoiceTemplateTableResponse", - "#[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)]" - ) - .type_attribute( - ".komp_ac.table_definition.CopyProfileRequest", - "#[derive(serde::Serialize, serde::Deserialize)]" - ) - .type_attribute( - ".komp_ac.table_definition.CopyProfileResponse", - "#[derive(serde::Serialize, serde::Deserialize)]" - ) - .type_attribute( - ".komp_ac.table_definition.GetColumnAliasRenameHistoryRequest", - "#[derive(serde::Serialize, serde::Deserialize)]" - ) - .type_attribute( - ".komp_ac.table_definition.ColumnAliasRenameHistoryEntry", - "#[derive(serde::Serialize, serde::Deserialize)]" - ) - .type_attribute( - ".komp_ac.table_definition.GetColumnAliasRenameHistoryResponse", - "#[derive(serde::Serialize, serde::Deserialize)]" - ) - .type_attribute( - ".komp_ac.table_definition.ColumnPresentation", - "#[derive(serde::Serialize, serde::Deserialize)]" - ) - .type_attribute( - ".komp_ac.table_definition.SetColumnPresentationRequest", - "#[derive(serde::Serialize, serde::Deserialize)]" - ) - .type_attribute( - ".komp_ac.table_definition.SetColumnPresentationResponse", - "#[derive(serde::Serialize, serde::Deserialize)]" - ) - .type_attribute( - ".komp_ac.table_script.PostTableScriptRequest", - "#[derive(serde::Serialize, serde::Deserialize)]", - ) - .type_attribute( - ".komp_ac.table_script.TableScriptResponse", - "#[derive(serde::Serialize, serde::Deserialize)]", + ".komp_ac.table_validation.CharacterConstraintKind", + "#[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]", ) .compile_protos( &[ @@ -252,12 +75,6 @@ fn main() -> Result<(), Box> { &["proto"], )?; - // Scope build-script reruns to the actual inputs. Without this, the script - // emits no `rerun-if-changed` directives, so Cargo reruns it whenever any - // file in the package changes. Since codegen writes into `src/proto/`, that - // self-write retriggers the script on every build (an infinite loop under - // `cargo watch`). tonic_build 0.13 emitted these automatically; 0.14 - // (tonic_prost_build) no longer does. println!("cargo:rerun-if-changed=build.rs"); for proto in [ "proto/accounting.proto", diff --git a/common/src/proto/komp_ac.accounting.rs b/common/src/proto/komp_ac.accounting.rs index c38af7d6..27efa1f6 100644 --- a/common/src/proto/komp_ac.accounting.rs +++ b/common/src/proto/komp_ac.accounting.rs @@ -1,4 +1,5 @@ // This file is @generated by prost-build. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct EnsureAccountRequest { #[prost(string, tag = "1")] @@ -12,6 +13,7 @@ pub struct EnsureAccountRequest { #[prost(string, tag = "3")] pub denomination_currency: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Account { #[prost(int64, tag = "1")] @@ -26,6 +28,7 @@ pub struct Account { #[prost(string, tag = "5")] pub denomination_currency: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PostJournalRequest { #[prost(string, tag = "1")] @@ -57,6 +60,7 @@ pub struct PostJournalRequest { #[prost(string, tag = "7")] pub accounting_date: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct JournalLineInput { #[prost(enumeration = "JournalSide", tag = "1")] @@ -75,6 +79,7 @@ pub struct JournalLineInput { super::exchange_rates::ExchangeRateSelection, >, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct CloseJournalRequest { #[prost(string, tag = "1")] @@ -82,6 +87,7 @@ pub struct CloseJournalRequest { #[prost(int64, tag = "2")] pub journal_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ReopenJournalRequest { #[prost(string, tag = "1")] @@ -89,6 +95,7 @@ pub struct ReopenJournalRequest { #[prost(int64, tag = "2")] pub journal_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetJournalRequest { #[prost(string, tag = "1")] @@ -100,11 +107,13 @@ pub struct GetJournalRequest { #[prost(bool, tag = "3")] pub include_deleted: bool, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetAccountingStatusRequest { #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct SearchJournalsRequest { #[prost(string, tag = "1")] @@ -119,6 +128,7 @@ pub struct SearchJournalsRequest { #[prost(string, tag = "4")] pub page_token: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct JournalSummary { #[prost(int64, tag = "1")] @@ -144,6 +154,7 @@ pub struct JournalSummary { #[prost(string, tag = "11")] pub accounting_date: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SearchJournalsResponse { #[prost(message, repeated, tag = "1")] @@ -153,6 +164,7 @@ pub struct SearchJournalsResponse { #[prost(string, tag = "3")] pub next_page_token: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AccountingStatus { #[prost(bool, tag = "1")] @@ -166,6 +178,7 @@ pub struct AccountingStatus { #[prost(string, tag = "5")] pub calculated_at: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListUnbalancedJournalsRequest { #[prost(string, tag = "1")] @@ -181,6 +194,7 @@ pub struct ListUnbalancedJournalsRequest { #[prost(string, tag = "4")] pub currency: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct UnbalancedJournalSummary { #[prost(int64, tag = "1")] @@ -210,6 +224,7 @@ pub struct UnbalancedJournalSummary { #[prost(string, tag = "12")] pub accounting_date: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListUnbalancedJournalsResponse { #[prost(message, repeated, tag = "1")] @@ -219,6 +234,7 @@ pub struct ListUnbalancedJournalsResponse { #[prost(string, tag = "3")] pub next_page_token: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct SoftDeleteJournalLineRequest { #[prost(string, tag = "1")] @@ -228,6 +244,7 @@ pub struct SoftDeleteJournalLineRequest { #[prost(int64, tag = "3")] pub journal_line_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CorrectJournalRequest { #[prost(string, tag = "1")] @@ -239,6 +256,7 @@ pub struct CorrectJournalRequest { #[prost(message, repeated, tag = "3")] pub corrections: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct JournalLineCorrection { #[prost(int64, tag = "1")] @@ -253,6 +271,7 @@ pub struct JournalLineCorrection { #[prost(string, tag = "4")] pub correction_reason: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct JournalLine { #[prost(int64, tag = "1")] @@ -296,6 +315,7 @@ pub struct JournalLine { #[prost(int64, tag = "18")] pub conversion_evidence_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Journal { #[prost(int64, tag = "1")] @@ -328,6 +348,7 @@ pub struct Journal { #[prost(string, tag = "14")] pub accounting_date: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ConfigureAccountingPeriodRequest { #[prost(string, tag = "1")] @@ -346,27 +367,32 @@ pub struct ConfigureAccountingPeriodRequest { #[prost(string, tag = "6")] pub period_end: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct CloseAccountingPeriodRequest { #[prost(int64, tag = "1")] pub period_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct ReopenAccountingPeriodRequest { #[prost(int64, tag = "1")] pub period_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ApproveProfileRequest { #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetProfileApprovalRequest { #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, } /// Approval state of one profile's financial statement. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ProfileApproval { #[prost(string, tag = "1")] @@ -379,21 +405,25 @@ pub struct ProfileApproval { #[prost(string, tag = "4")] pub approved_by_user_id: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetAccountingPeriodRequest { #[prost(int64, tag = "1")] pub period_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListAccountingPeriodsRequest { #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListAccountingPeriodsResponse { #[prost(message, repeated, tag = "1")] pub periods: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AccountingPeriod { #[prost(int64, tag = "1")] @@ -415,11 +445,13 @@ pub struct AccountingPeriod { #[prost(enumeration = "AccountingPeriodType", tag = "11")] pub period_type: i32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListPeriodBalancesRequest { #[prost(int64, tag = "1")] pub period_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PeriodBalance { #[prost(int64, tag = "1")] @@ -439,6 +471,7 @@ pub struct PeriodBalance { #[prost(string, tag = "7")] pub closing_balance: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PeriodDenominationBalance { #[prost(int64, tag = "1")] @@ -460,6 +493,7 @@ pub struct PeriodDenominationBalance { #[prost(string, tag = "7")] pub closing_quantity: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListPeriodBalancesResponse { #[prost(message, repeated, tag = "1")] @@ -467,6 +501,7 @@ pub struct ListPeriodBalancesResponse { #[prost(message, repeated, tag = "2")] pub denomination_balances: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GenerateAccountingTransferRequest { #[prost(string, tag = "1")] @@ -478,6 +513,7 @@ pub struct GenerateAccountingTransferRequest { #[prost(int64, tag = "4")] pub target_period_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct GenerateAccountingTransferResponse { #[prost(int64, tag = "1")] @@ -485,6 +521,7 @@ pub struct GenerateAccountingTransferResponse { #[prost(int64, tag = "2")] pub generated_row_count: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct MapOpeningBalanceAccountRequest { /// Open period whose profile receives the opening balance. Its configured @@ -496,6 +533,7 @@ pub struct MapOpeningBalanceAccountRequest { #[prost(string, tag = "3")] pub target_account: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct UnmapOpeningBalanceAccountRequest { #[prost(int64, tag = "1")] @@ -503,16 +541,19 @@ pub struct UnmapOpeningBalanceAccountRequest { #[prost(string, tag = "2")] pub target_account: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct UnmapOpeningBalanceAccountResponse { #[prost(bool, tag = "1")] pub removed: bool, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListOpeningBalanceAccountsRequest { #[prost(int64, tag = "1")] pub target_period_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct OpeningBalanceAccountMapping { #[prost(int64, tag = "1")] @@ -558,6 +599,7 @@ pub struct OpeningBalanceAccountMapping { /// A portion of a predecessor account's value that no mapping carries into the /// target period. Closing the target period drops this value, so each entry must /// either be covered by a broader mapping or deliberately closed out. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct UnmappedSourceBalance { #[prost(int64, tag = "1")] @@ -576,6 +618,7 @@ pub struct UnmappedSourceBalance { #[prost(enumeration = "OpeningBalanceStatus", tag = "6")] pub status: i32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct UnmappedSourceDenominationBalance { #[prost(int64, tag = "1")] @@ -592,6 +635,7 @@ pub struct UnmappedSourceDenominationBalance { #[prost(enumeration = "OpeningBalanceStatus", tag = "6")] pub status: i32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListOpeningBalanceAccountsResponse { #[prost(message, repeated, tag = "1")] @@ -607,6 +651,7 @@ pub struct ListOpeningBalanceAccountsResponse { UnmappedSourceDenominationBalance, >, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum JournalSide { @@ -638,6 +683,7 @@ impl JournalSide { } /// A period is only ever open or closed. Approval is a fact about the whole /// profile, because the profile is the accounting period being reported on. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum AccountingPeriodStatus { @@ -670,6 +716,7 @@ impl AccountingPeriodStatus { /// How long one accounting period lasts. MONTH and YEAR span whole months, so /// the caller states only the start and the end is derived. PARTIAL is the /// exception for periods real life cut short. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum AccountingPeriodType { @@ -707,6 +754,7 @@ impl AccountingPeriodType { } } } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum OpeningBalanceStatus { diff --git a/common/src/proto/komp_ac.analytics.rs b/common/src/proto/komp_ac.analytics.rs index 8d66dd7a..32c910d7 100644 --- a/common/src/proto/komp_ac.analytics.rs +++ b/common/src/proto/komp_ac.analytics.rs @@ -1,10 +1,12 @@ // This file is @generated by prost-build. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetAnalyticsCatalogRequest { /// Required PostgreSQL profile/schema name. #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetAnalyticsCatalogResponse { #[prost(string, tag = "1")] @@ -12,6 +14,7 @@ pub struct GetAnalyticsCatalogResponse { #[prost(message, repeated, tag = "2")] pub tables: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AnalyticsTable { #[prost(int64, tag = "1")] @@ -23,6 +26,7 @@ pub struct AnalyticsTable { #[prost(message, repeated, tag = "4")] pub links: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AnalyticsCatalogColumn { /// Current public alias. Physical ordinal names are intentionally omitted. @@ -38,6 +42,7 @@ pub struct AnalyticsCatalogColumn { #[prost(string, tag = "5")] pub currency: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AnalyticsTableLink { /// Public foreign-key column on the source table, e.g. department_id. @@ -48,6 +53,7 @@ pub struct AnalyticsTableLink { #[prost(bool, tag = "3")] pub required: bool, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExecuteAnalyticsQueryRequest { /// Required profile whose tables form the query's complete visible catalog. @@ -60,6 +66,7 @@ pub struct ExecuteAnalyticsQueryRequest { #[prost(uint32, tag = "3")] pub max_rows: u32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AnalyticsResultColumn { #[prost(string, tag = "1")] @@ -67,6 +74,7 @@ pub struct AnalyticsResultColumn { #[prost(string, tag = "2")] pub data_type: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AnalyticsValue { #[prost(oneof = "analytics_value::Kind", tags = "1, 2, 3, 4, 5, 6, 7")] @@ -74,6 +82,7 @@ pub struct AnalyticsValue { } /// Nested message and enum types in `AnalyticsValue`. pub mod analytics_value { + #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Kind { #[prost(enumeration = "::prost_types::NullValue", tag = "1")] @@ -92,11 +101,13 @@ pub mod analytics_value { BytesValue(::prost::alloc::vec::Vec), } } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AnalyticsResultRow { #[prost(message, repeated, tag = "1")] pub values: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AnalyticsResultBatch { /// Present on the first batch and omitted from later data batches. diff --git a/common/src/proto/komp_ac.auth.rs b/common/src/proto/komp_ac.auth.rs index b943e170..372b82d1 100644 --- a/common/src/proto/komp_ac.auth.rs +++ b/common/src/proto/komp_ac.auth.rs @@ -1,4 +1,5 @@ // This file is @generated by prost-build. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RegisterRequest { #[prost(string, tag = "1")] @@ -16,6 +17,7 @@ pub struct RegisterRequest { #[prost(string, tag = "6")] pub phone_country: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AuthResponse { /// UUID in string format @@ -31,6 +33,7 @@ pub struct AuthResponse { #[prost(string, tag = "4")] pub role: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ChangePasswordRequest { #[prost(string, tag = "1")] @@ -40,8 +43,10 @@ pub struct ChangePasswordRequest { #[prost(string, tag = "3")] pub new_password_confirmation: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct PasswordOperationResponse {} +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct LoginRequest { /// Can be username or email @@ -50,6 +55,7 @@ pub struct LoginRequest { #[prost(string, tag = "2")] pub password: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct LoginResponse { /// JWT token @@ -76,30 +82,38 @@ pub struct LoginResponse { #[prost(string, tag = "9")] pub phone_country: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct SetTimezoneRequest { /// IANA timezone, for example Europe/Bratislava #[prost(string, tag = "1")] pub timezone: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct UserPreferences { #[prost(string, tag = "1")] pub timezone: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct LogoutRequest {} +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct LogoutResponse {} +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RevokeUserSessionsRequest { #[prost(string, tag = "1")] pub username: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct RevokeUserSessionsResponse {} +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetAuthorizationRequest {} +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Permission { /// Canonical object string, one of: @@ -115,6 +129,7 @@ pub struct Permission { #[prost(string, tag = "2")] pub action: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AuthorizationSnapshot { #[prost(string, tag = "1")] @@ -123,6 +138,7 @@ pub struct AuthorizationSnapshot { #[prost(message, repeated, tag = "2")] pub permissions: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Role { #[prost(string, tag = "1")] @@ -136,13 +152,16 @@ pub struct Role { #[prost(string, tag = "4")] pub parent: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListRolesRequest {} +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListRolesResponse { #[prost(message, repeated, tag = "1")] pub roles: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AddRoleRequest { #[prost(string, tag = "1")] @@ -151,11 +170,13 @@ pub struct AddRoleRequest { #[prost(string, tag = "2")] pub parent: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RemoveRoleRequest { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GrantPermissionRequest { #[prost(string, tag = "1")] @@ -165,6 +186,7 @@ pub struct GrantPermissionRequest { #[prost(string, tag = "3")] pub action: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RevokePermissionRequest { #[prost(string, tag = "1")] @@ -174,11 +196,13 @@ pub struct RevokePermissionRequest { #[prost(string, tag = "3")] pub action: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListRolePermissionsRequest { #[prost(string, tag = "1")] pub role: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct RolePermissions { #[prost(string, tag = "1")] @@ -190,6 +214,7 @@ pub struct RolePermissions { #[prost(message, repeated, tag = "3")] pub effective_permissions: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListGrantableObjectsRequest { /// The role being edited. The response contains only actions that may be @@ -197,6 +222,7 @@ pub struct ListGrantableObjectsRequest { #[prost(string, tag = "1")] pub target_role: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GrantableObject { /// Canonical value accepted by GrantPermission, for example @@ -216,11 +242,13 @@ pub struct GrantableObject { #[prost(string, repeated, tag = "5")] pub allowed_actions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListGrantableObjectsResponse { #[prost(message, repeated, tag = "1")] pub objects: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AssignUserRoleRequest { #[prost(string, tag = "1")] @@ -228,6 +256,7 @@ pub struct AssignUserRoleRequest { #[prost(string, tag = "2")] pub role: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ResetUserPasswordRequest { #[prost(string, tag = "1")] @@ -237,6 +266,7 @@ pub struct ResetUserPasswordRequest { #[prost(string, tag = "3")] pub new_password_confirmation: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct UserSummary { #[prost(string, tag = "1")] @@ -248,8 +278,10 @@ pub struct UserSummary { #[prost(string, tag = "4")] pub role: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListUsersRequest {} +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListUsersResponse { #[prost(message, repeated, tag = "1")] diff --git a/common/src/proto/komp_ac.document_data.rs b/common/src/proto/komp_ac.document_data.rs index ead2b734..9b39bdca 100644 --- a/common/src/proto/komp_ac.document_data.rs +++ b/common/src/proto/komp_ac.document_data.rs @@ -1,4 +1,5 @@ // This file is @generated by prost-build. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct SourceRecord { #[prost(string, tag = "1")] @@ -10,6 +11,7 @@ pub struct SourceRecord { #[prost(int64, tag = "4")] pub record_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SaveDocumentRequest { #[prost(message, repeated, tag = "1")] @@ -19,6 +21,7 @@ pub struct SaveDocumentRequest { #[prost(string, tag = "3")] pub data_json: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RegisterTypstTemplateRequest { #[prost(string, tag = "1")] @@ -30,11 +33,13 @@ pub struct RegisterTypstTemplateRequest { #[prost(string, tag = "4")] pub typst_compiler_version: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RegisterTypstTemplateResponse { #[prost(message, optional, tag = "1")] pub template_version: ::core::option::Option, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListTypstTemplatesRequest { #[prost(string, tag = "1")] @@ -42,16 +47,19 @@ pub struct ListTypstTemplatesRequest { #[prost(uint32, tag = "2")] pub limit: u32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListTypstTemplatesResponse { #[prost(message, repeated, tag = "1")] pub templates: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListEnabledTypstTemplatesRequest { #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct SaveDocumentResponse { #[prost(int64, tag = "1")] @@ -65,6 +73,7 @@ pub struct SaveDocumentResponse { #[prost(int32, tag = "5")] pub template_version: i32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct UpdateDocumentRequest { #[prost(int64, tag = "1")] @@ -72,6 +81,7 @@ pub struct UpdateDocumentRequest { #[prost(string, tag = "2")] pub data_json: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct UpdateDocumentResponse { #[prost(int64, tag = "1")] @@ -81,6 +91,7 @@ pub struct UpdateDocumentResponse { #[prost(int32, tag = "3")] pub version_number: i32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetDocumentsRequest { #[prost(int64, optional, tag = "1")] @@ -90,6 +101,7 @@ pub struct GetDocumentsRequest { #[prost(string, optional, tag = "3")] pub template_name: ::core::option::Option<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Document { #[prost(int64, tag = "1")] @@ -115,21 +127,25 @@ pub struct Document { #[prost(string, tag = "11")] pub data_sha256: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetDocumentsResponse { #[prost(message, repeated, tag = "1")] pub documents: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetDocumentRequest { #[prost(int64, tag = "1")] pub document_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetDocumentResponse { #[prost(message, optional, tag = "1")] pub document: ::core::option::Option, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetDocumentVersionRequest { #[prost(int64, tag = "1")] @@ -137,11 +153,13 @@ pub struct GetDocumentVersionRequest { #[prost(int32, tag = "2")] pub version_number: i32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetDocumentVersionResponse { #[prost(message, optional, tag = "1")] pub document: ::core::option::Option, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetTypstTemplateVersionRequest { #[prost(int64, tag = "1")] @@ -149,6 +167,7 @@ pub struct GetTypstTemplateVersionRequest { #[prost(int64, tag = "2")] pub document_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct TypstTemplateVersion { #[prost(int64, tag = "1")] @@ -170,6 +189,7 @@ pub struct TypstTemplateVersion { #[prost(string, tag = "9")] pub input_table_name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetTypstTemplateVersionResponse { #[prost(message, optional, tag = "1")] diff --git a/common/src/proto/komp_ac.ecb.rs b/common/src/proto/komp_ac.ecb.rs index 2d604b7b..d2c77cca 100644 --- a/common/src/proto/komp_ac.ecb.rs +++ b/common/src/proto/komp_ac.ecb.rs @@ -1,9 +1,11 @@ // This file is @generated by prost-build. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetEcbPipelineStatusRequest { #[prost(int32, tag = "1")] pub batch_limit: i32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct EcbImportBatch { #[prost(int64, tag = "1")] @@ -29,6 +31,7 @@ pub struct EcbImportBatch { #[prost(string, optional, tag = "11")] pub error_message: ::core::option::Option<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct EcbCurrencyCoverage { #[prost(string, tag = "1")] @@ -40,6 +43,7 @@ pub struct EcbCurrencyCoverage { #[prost(string, optional, tag = "4")] pub verified_through_date: ::core::option::Option<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetEcbPipelineStatusResponse { #[prost(string, optional, tag = "1")] diff --git a/common/src/proto/komp_ac.exchange_rates.rs b/common/src/proto/komp_ac.exchange_rates.rs index 64ed3319..a37a3aea 100644 --- a/common/src/proto/komp_ac.exchange_rates.rs +++ b/common/src/proto/komp_ac.exchange_rates.rs @@ -1,4 +1,5 @@ // This file is @generated by prost-build. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExchangeRateSelection { #[prost(enumeration = "ExchangeRateDateRule", tag = "1")] @@ -17,16 +18,19 @@ pub struct ExchangeRateSelection { #[prost(string, optional, tag = "6")] pub rate_source_id: ::core::option::Option<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RateSourceProvider { #[prost(string, tag = "1")] pub id: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListRateSourcesResponse { #[prost(message, repeated, tag = "1")] pub sources: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PreviewDirectConversionRequest { #[prost(string, tag = "1")] @@ -40,6 +44,7 @@ pub struct PreviewDirectConversionRequest { #[prost(message, optional, tag = "5")] pub exchange_rate_selection: ::core::option::Option, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PreviewDirectConversionResponse { #[prost(string, tag = "1")] @@ -93,6 +98,7 @@ pub struct PreviewDirectConversionResponse { #[prost(string, tag = "25")] pub selection_reason: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListConversionEvidenceRequest { #[prost(string, tag = "1")] @@ -108,6 +114,7 @@ pub struct ListConversionEvidenceRequest { #[prost(int64, optional, tag = "6")] pub before_evidence_id: ::core::option::Option, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ConversionEvidence { #[prost(int64, tag = "1")] @@ -167,6 +174,7 @@ pub struct ConversionEvidence { #[prost(string, optional, tag = "28")] pub selected_by_user_id: ::core::option::Option<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListConversionEvidenceResponse { #[prost(bool, tag = "1")] @@ -176,11 +184,13 @@ pub struct ListConversionEvidenceResponse { #[prost(bool, tag = "3")] pub has_more: bool, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetProfileExchangeRateSettingsRequest { #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ProfileForeignCurrency { #[prost(string, tag = "1")] @@ -190,6 +200,7 @@ pub struct ProfileForeignCurrency { #[prost(message, repeated, tag = "3")] pub sources: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ProfileCurrencySource { #[prost(string, tag = "1")] @@ -201,6 +212,7 @@ pub struct ProfileCurrencySource { #[prost(string, optional, tag = "4")] pub verified_through_date: ::core::option::Option<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ProfileExchangeRateSettings { #[prost(string, tag = "1")] @@ -212,6 +224,7 @@ pub struct ProfileExchangeRateSettings { #[prost(message, repeated, tag = "4")] pub foreign_currencies: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AddProfileCurrencySourceRequest { #[prost(string, tag = "1")] @@ -235,6 +248,7 @@ pub struct AddProfileCurrencySourceRequest { /// with source OFFICIAL, a specific_rate_date, and an audit reason. /// * A saved custom rate still uses one of these rules to determine which dated /// custom-rate record applies. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum ExchangeRateDateRule { @@ -291,6 +305,7 @@ impl ExchangeRateDateRule { /// * MANUAL is the current transaction-local, one-off quote path. If the product /// supports only persisted manual rates, this value and manual_foreign_units /// should be removed and SAVED_CUSTOM should be presented as "Manual rate". +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum ExchangeRateSource { diff --git a/common/src/proto/komp_ac.search.rs b/common/src/proto/komp_ac.search.rs index e0f5bcc9..f853e849 100644 --- a/common/src/proto/komp_ac.search.rs +++ b/common/src/proto/komp_ac.search.rs @@ -1,9 +1,11 @@ // This file is @generated by prost-build. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct SearchCountResponse { #[prost(uint64, tag = "1")] pub count: u64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ColumnConstraint { #[prost(string, tag = "1")] @@ -13,6 +15,7 @@ pub struct ColumnConstraint { #[prost(enumeration = "MatchMode", tag = "3")] pub mode: i32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SearchRequest { #[prost(string, tag = "1")] @@ -35,6 +38,7 @@ pub struct SearchRequest { #[prost(enumeration = "SearchVersionScope", tag = "8")] pub version_scope: i32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct SearchOrder { #[prost(string, tag = "1")] @@ -42,6 +46,7 @@ pub struct SearchOrder { #[prost(enumeration = "SearchOrderDirection", tag = "2")] pub direction: i32, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SearchResponse { #[prost(message, repeated, tag = "1")] @@ -49,6 +54,7 @@ pub struct SearchResponse { } /// Nested message and enum types in `SearchResponse`. pub mod search_response { + #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Hit { /// PostgreSQL row ID @@ -77,6 +83,7 @@ pub mod search_response { pub archived: bool, } } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum MatchMode { @@ -106,6 +113,7 @@ impl MatchMode { } } } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum SearchVersionScope { @@ -135,6 +143,7 @@ impl SearchVersionScope { } } } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum SearchOrderDirection { diff --git a/common/src/proto/komp_ac.search2.rs b/common/src/proto/komp_ac.search2.rs index e9e455ba..76c95b45 100644 --- a/common/src/proto/komp_ac.search2.rs +++ b/common/src/proto/komp_ac.search2.rs @@ -1,4 +1,5 @@ // This file is @generated by prost-build. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Search2Request { #[prost(string, tag = "1")] @@ -17,6 +18,7 @@ pub struct Search2Request { #[prost(bool, optional, tag = "7")] pub order_desc: ::core::option::Option, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ColumnFilter { #[prost(string, tag = "1")] @@ -29,6 +31,7 @@ pub struct ColumnFilter { #[prost(string, optional, tag = "4")] pub value2: ::core::option::Option<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Search2Response { #[prost(message, repeated, tag = "1")] @@ -39,6 +42,7 @@ pub struct Search2Response { } /// Nested message and enum types in `Search2Response`. pub mod search2_response { + #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Hit { #[prost(int64, tag = "1")] @@ -58,6 +62,7 @@ pub mod search2_response { >, } } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum FilterType { diff --git a/common/src/proto/komp_ac.table_definition.rs b/common/src/proto/komp_ac.table_definition.rs index 5559bcc7..3ab28d34 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -1,9 +1,11 @@ // This file is @generated by prost-build. +#[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, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct CreateCustomExchangeRatesTableResponse { #[prost(int64, tag = "1")] @@ -163,6 +165,7 @@ pub struct AddTableColumnsRequest { /// Removals use stable column IDs rather than aliases. Generated companion /// columns must be removed together with their generating column. New columns /// follow the same contract as AddTableColumns. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PutTableDefinitionRequest { #[prost(string, tag = "1")] @@ -186,6 +189,7 @@ pub struct PutTableDefinitionRequest { #[prost(string, tag = "8")] pub new_table_name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PutTableDefinitionResponse { #[prost(bool, tag = "1")] @@ -272,6 +276,7 @@ pub struct TableDefinitionResponse { pub sql: ::prost::alloc::string::String, } /// Describes the tree of all profiles and their tables. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ProfileTreeResponse { /// All profiles in the system. @@ -286,6 +291,7 @@ pub struct ProfileTreeResponse { /// Nested message and enum types in `ProfileTreeResponse`. pub mod profile_tree_response { /// One link: the table it points at, and the column carrying it. + #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Dependency { /// Table being referenced. @@ -298,6 +304,7 @@ pub mod profile_tree_response { pub column_name: ::prost::alloc::string::String, } /// Table entry in a profile. + #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Table { /// Internal ID from table_definitions.id (metadata record). @@ -329,6 +336,7 @@ pub mod profile_tree_response { pub profile_name: ::prost::alloc::string::String, } /// Profile (schema) entry. + #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Profile { /// Name of the schema/profile (as stored in `schemas.name`). @@ -339,18 +347,21 @@ pub mod profile_tree_response { pub tables: ::prost::alloc::vec::Vec, } } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetTableCatalogRequest { /// Selected profile. Omit this field to request the global-only scope. #[prost(string, optional, tag = "1")] pub profile_name: ::core::option::Option<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTableCatalogResponse { #[prost(message, repeated, tag = "1")] pub tables: ::prost::alloc::vec::Vec, } /// Request to fetch all tables, columns and scripts for a profile. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetProfileDetailsRequest { /// Profile (schema) name to fetch details for. @@ -358,6 +369,7 @@ pub struct GetProfileDetailsRequest { pub profile_name: ::prost::alloc::string::String, } /// Response with all tables, columns and scripts for a profile. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetProfileDetailsResponse { #[prost(string, tag = "1")] @@ -428,6 +440,7 @@ pub struct GetColumnAliasRenameHistoryResponse { pub entries: ::prost::alloc::vec::Vec, } /// Describes a table with its columns and associated scripts. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TableDetail { #[prost(string, tag = "1")] @@ -464,6 +477,7 @@ pub struct TableDetail { pub has_data: bool, } /// Server-owned behavior for one logical column returned in table details. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ColumnBehavior { /// True when the server created the column as a companion of another column. @@ -483,6 +497,7 @@ pub struct ColumnBehavior { pub column_id: i64, } /// A script that targets a specific column in a table. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ScriptInfo { #[prost(int64, tag = "1")] @@ -535,6 +550,7 @@ pub struct SetColumnPresentationResponse { pub row_version: i64, } /// Request to delete one table definition entirely. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct DeleteTableRequest { /// Profile (schema) name owning the table (must exist). @@ -546,6 +562,7 @@ pub struct DeleteTableRequest { pub table_name: ::prost::alloc::string::String, } /// Response after table deletion. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct DeleteTableResponse { /// True if table and metadata were successfully deleted in one transaction. @@ -562,6 +579,7 @@ pub struct DeleteTableResponse { /// list covers types clients may NOT declare as well (see `declarable`), so that /// the same call also explains the types GetProfileDetails reports back for /// server-generated companion columns. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListColumnTypesResponse { /// Every column type, declarable or not, ordered by name. @@ -571,6 +589,7 @@ pub struct ListColumnTypesResponse { /// Nested message and enum types in `ListColumnTypesResponse`. pub mod list_column_types_response { /// One column type and everything a client needs to know to offer it. + #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ColumnType { /// Logical column type (e.g. "money", "instant"). Passed to the server as @@ -630,6 +649,7 @@ pub mod list_column_types_response { /// Nested message and enum types in `ColumnType`. pub mod column_type { /// One column a compound type expands into. + #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GeneratedColumn { /// Name the server gives the column. Fixed, and reserved: a table @@ -647,6 +667,7 @@ pub mod list_column_types_response { } } } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum MoneyRounding { @@ -673,6 +694,7 @@ impl MoneyRounding { } } } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum BooleanLedgerOperator { @@ -703,6 +725,7 @@ impl BooleanLedgerOperator { } } /// How a column type is spelled in ColumnDefinition.field_type. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum ColumnTypeSpelling { diff --git a/common/src/proto/komp_ac.table_script.rs b/common/src/proto/komp_ac.table_script.rs index 4c9e7d86..8fb2c81f 100644 --- a/common/src/proto/komp_ac.table_script.rs +++ b/common/src/proto/komp_ac.table_script.rs @@ -72,6 +72,7 @@ pub struct TableScriptResponse { #[prost(string, tag = "2")] pub warnings: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetTableScriptsRequest { /// Required. Profile (schema) name. @@ -81,12 +82,14 @@ pub struct GetTableScriptsRequest { #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTableScriptsResponse { /// Scripts and dependency allowlists used to configure the client Steel runtime. #[prost(message, repeated, tag = "1")] pub scripts: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct StoredTableScript { /// Persistent script identifier. @@ -108,6 +111,7 @@ pub struct StoredTableScript { #[prost(message, repeated, tag = "6")] pub dependencies: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ScriptDependency { /// Logical table name referenced by the script. @@ -140,6 +144,7 @@ pub struct ScriptDependency { pub name_in_script: ::prost::alloc::string::String, } /// Identifies the active form row whose external Steel inputs must be hydrated. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct HydrateScriptDependenciesRequest { /// Required profile/database schema containing the scripted table. @@ -162,6 +167,7 @@ pub struct HydrateScriptDependenciesRequest { >, } /// One declared cross-table column input for the client Steel context. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct HydratedColumnValue { /// Logical related-table name used by steel_get_column. @@ -184,6 +190,7 @@ pub struct HydratedColumnValue { pub currency: ::prost::alloc::string::String, } /// One declared related-collection aggregate input for the client Steel context. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct HydratedAggregateValue { /// Normalized aggregate operation: sum, min, max, count, count_distinct, @@ -210,6 +217,7 @@ pub struct HydratedAggregateValue { pub currency: ::prost::alloc::string::String, } /// Complete external dependency snapshot for client-side Steel execution. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct HydrateScriptDependenciesResponse { /// Exact related-column inputs declared by stored scripts. diff --git a/common/src/proto/komp_ac.table_structure.rs b/common/src/proto/komp_ac.table_structure.rs index 0f0e52aa..3e567a7e 100644 --- a/common/src/proto/komp_ac.table_structure.rs +++ b/common/src/proto/komp_ac.table_structure.rs @@ -1,5 +1,6 @@ // This file is @generated by prost-build. /// Request identifying the profile (schema) and tables to inspect. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetTableStructureRequest { /// Required. Profile (PostgreSQL schema) name. Must exist in `schemas`. @@ -12,6 +13,7 @@ pub struct GetTableStructureRequest { pub table_names: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, } /// Batched response keyed by table name. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTableStructureResponse { /// Per-table physical column lists keyed by requested table name. @@ -22,6 +24,7 @@ pub struct GetTableStructureResponse { >, } /// Response with the ordered list of columns (by ordinal position) for one table. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TableStructureResponse { /// Columns of the physical table, including system columns (id, deleted, @@ -32,6 +35,7 @@ pub struct TableStructureResponse { pub columns: ::prost::alloc::vec::Vec, } /// One physical column entry as reported by information_schema. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct TableColumn { /// Public column name. Physical account_id columns are exposed as account. @@ -74,6 +78,7 @@ pub struct TableColumn { #[prost(int64, tag = "10")] pub column_id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetTableImportDescriptorRequest { #[prost(string, tag = "1")] @@ -81,6 +86,7 @@ pub struct GetTableImportDescriptorRequest { #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ImportLinkDescriptor { #[prost(int64, tag = "1")] @@ -94,6 +100,7 @@ pub struct ImportLinkDescriptor { ::prost::alloc::string::String, >, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ImportFieldDescriptor { /// Stable identity of a managed user column. System columns have zero here @@ -125,6 +132,7 @@ pub struct ImportFieldDescriptor { #[prost(enumeration = "ImportValueKind", repeated, tag = "13")] pub accepted_value_kinds: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTableImportDescriptorResponse { #[prost(string, tag = "1")] @@ -142,6 +150,7 @@ pub struct GetTableImportDescriptorResponse { #[prost(message, repeated, tag = "6")] pub fields: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum ImportValueKind { diff --git a/common/src/proto/komp_ac.table_validation.rs b/common/src/proto/komp_ac.table_validation.rs index e1f8cfa5..9069662f 100644 --- a/common/src/proto/komp_ac.table_validation.rs +++ b/common/src/proto/komp_ac.table_validation.rs @@ -1,5 +1,6 @@ // This file is @generated by prost-build. /// Request validation rules for a table +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetTableValidationRequest { #[prost(string, tag = "1")] @@ -402,6 +403,7 @@ impl CountMode { } } } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum MaskStorageMode { diff --git a/common/src/proto/komp_ac.tables_data.rs b/common/src/proto/komp_ac.tables_data.rs index f9c47dc1..70930ffd 100644 --- a/common/src/proto/komp_ac.tables_data.rs +++ b/common/src/proto/komp_ac.tables_data.rs @@ -59,6 +59,7 @@ pub struct PostAccountingTableDataRequest { >, } /// Insert response. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PostTableDataResponse { /// True if the insert succeeded. @@ -196,6 +197,7 @@ pub struct PutTableDataConfirmedRequest { >, } /// Update response. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PutTableDataResponse { /// True if the update succeeded (or no-op on empty data). @@ -223,6 +225,7 @@ pub struct TableUpdateImpactRequest { #[prost(int64, tag = "3")] pub id: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct TableUpdateImpactResponse { #[prost(string, repeated, tag = "1")] @@ -239,6 +242,7 @@ pub struct ArchiveTableDataRequest { #[prost(int64, tag = "4")] pub expected_revision: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ArchiveTableDataResponse { #[prost(bool, tag = "1")] @@ -271,6 +275,7 @@ pub struct DeleteTableDataRequest { pub expected_revision: i64, } /// Soft-delete response. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct DeleteTableDataResponse { /// True when the row was successfully marked deleted. @@ -301,6 +306,7 @@ pub struct GetTableDataVersionRequest { #[prost(int64, tag = "4")] pub version: i64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ResolvedTableLinkVersion { #[prost(int64, tag = "1")] @@ -315,6 +321,7 @@ pub struct ResolvedTableLinkVersion { #[prost(string, repeated, tag = "4")] pub row_display_columns: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ResolvedTableLinkUpdate { #[prost(int64, tag = "1")] @@ -329,6 +336,7 @@ pub struct ResolvedTableLinkUpdate { #[prost(string, repeated, tag = "4")] pub changed_fields: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ResolvedTableLink { #[prost(int64, tag = "1")] @@ -341,6 +349,7 @@ pub struct ResolvedTableLink { pub newer: ::core::option::Option, } /// Row payload: all columns returned as strings. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTableDataResponse { /// Map of column_name → stringified value for: @@ -406,6 +415,7 @@ pub struct GetLastTableDataRequest { pub navigation_mode: i32, } /// Complete numbering state returned with a navigated row. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct RowNavigation { #[prost(enumeration = "RowNavigationMode", tag = "1")] @@ -418,6 +428,7 @@ pub struct RowNavigation { #[prost(uint64, tag = "3")] pub total: u64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetLastTableDataResponse { /// Zero when the table has no visible rows. Retained as row identity; clients @@ -446,6 +457,7 @@ pub struct GetAdjacentTableDataRequest { #[prost(uint64, tag = "6")] pub anchor_position: u64, } +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetAdjacentTableDataResponse { /// Absent when no visible row exists in the requested direction. @@ -476,6 +488,7 @@ pub struct GetTableDataByPositionRequest { /// A link resolved exactly as the source row stored it, together with the /// target row's current state. The physical companion version column remains /// an internal implementation detail. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum ResolvedTableLinkStatus { @@ -514,6 +527,7 @@ impl ResolvedTableLinkStatus { } } /// Controls how an unfiltered form numbers rows. +#[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum RowNavigationMode { diff --git a/komp-app/Cargo.toml b/komp-app/Cargo.toml new file mode 100644 index 00000000..97b5d019 --- /dev/null +++ b/komp-app/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "komp-app" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Shared application services and rules for komp_ac clients" + +[dependencies] +anyhow.workspace = true +common = { path = "../common" } +jiff = { version = "0.2.15", default-features = false, features = ["std", "tzdb-bundle-always"] } +prost-types.workspace = true +sanitise-file-name = "1" +serde.workspace = true +tonic.workspace = true +uuid = { version = "1.23.3", features = ["v4"] } diff --git a/komp-app/src/auth.rs b/komp-app/src/auth.rs new file mode 100644 index 00000000..db6eaa44 --- /dev/null +++ b/komp-app/src/auth.rs @@ -0,0 +1,304 @@ +use anyhow::{Context, Result}; +use common::proto::komp_ac::auth::{ + AddRoleRequest, AssignUserRoleRequest, AuthResponse, AuthorizationSnapshot, + ChangePasswordRequest, GetAuthorizationRequest, GrantPermissionRequest, + GrantableObject, ListGrantableObjectsRequest, ListRolePermissionsRequest, ListRolesRequest, + ListUsersRequest, LoginRequest, LoginResponse, LogoutRequest, PasswordOperationResponse, + RegisterRequest, RemoveRoleRequest, ResetUserPasswordRequest, RevokePermissionRequest, + RevokeUserSessionsRequest, Role, RolePermissions, UserSummary, + auth_service_client::AuthServiceClient, +}; +use tonic::transport::Channel; +use tonic::Request; + +use crate::transport::{DEFAULT_GRPC_ENDPOINT, authenticated_request, connect_channel}; + +pub fn validate_password_change(password: &str, confirmation: &str) -> Result<()> { + if password.is_empty() { + anyhow::bail!("Password is required"); + } + if password != confirmation { + anyhow::bail!("Passwords do not match"); + } + if password.len() < 8 { + anyhow::bail!("Password must be at least 8 characters"); + } + Ok(()) +} + +pub fn validate_optional_password(password: Option<&str>, confirmation: Option<&str>) -> Result<()> { + let password = password.unwrap_or_default(); + let confirmation = confirmation.unwrap_or_default(); + if password.is_empty() && confirmation.is_empty() { + return Ok(()); + } + validate_password_change(password, confirmation) +} + +#[derive(Clone)] +pub struct AuthClient { + client: AuthServiceClient, +} + +impl AuthClient { + pub async fn new() -> Result { + let endpoint = std::env::var("GRPC_ENDPOINT") + .unwrap_or_else(|_| DEFAULT_GRPC_ENDPOINT.to_string()); + Self::connect(&endpoint).await + } + + pub async fn connect(endpoint: &str) -> Result { + Self::with_channel(connect_channel(endpoint).await?).await + } + + pub async fn with_channel(channel: Channel) -> Result { + Ok(Self { + client: AuthServiceClient::new(channel), + }) + } + + pub async fn login(&mut self, identifier: String, password: String) -> Result { + Ok(self + .client + .login(Request::new(LoginRequest { + identifier, + password, + })) + .await? + .into_inner()) + } + + pub async fn register( + &mut self, + username: String, + email: String, + password: Option, + password_confirmation: Option, + timezone: String, + phone_country: String, + ) -> Result { + validate_optional_password(password.as_deref(), password_confirmation.as_deref())?; + Ok(self + .client + .register(Request::new(RegisterRequest { + username, + email, + password: password.unwrap_or_default(), + password_confirmation: password_confirmation.unwrap_or_default(), + timezone, + phone_country, + })) + .await? + .into_inner()) + } + + pub async fn change_password( + &mut self, + token: &str, + current_password: String, + new_password: String, + new_password_confirmation: String, + ) -> Result { + validate_password_change(&new_password, &new_password_confirmation)?; + let request = authenticated_request( + Some(token), + ChangePasswordRequest { + current_password, + new_password, + new_password_confirmation, + }, + )?; + Ok(self.client.change_password(request).await?.into_inner()) + } + + pub async fn logout(&mut self, token: &str) -> Result<()> { + self.client + .logout(authenticated_request(Some(token), LogoutRequest {})?) + .await?; + Ok(()) + } + + pub async fn get_authorization(&mut self, token: &str) -> Result { + Ok(self + .client + .get_authorization(authenticated_request(Some(token), GetAuthorizationRequest {})?) + .await? + .into_inner()) + } + + pub async fn list_roles(&mut self, token: &str) -> Result> { + Ok(self + .client + .list_roles(authenticated_request(Some(token), ListRolesRequest {})?) + .await? + .into_inner() + .roles) + } + + pub async fn add_role(&mut self, token: &str, name: String, parent: String) -> Result<()> { + self.client + .add_role(authenticated_request( + Some(token), + AddRoleRequest { name, parent }, + )?) + .await?; + Ok(()) + } + + pub async fn remove_role(&mut self, token: &str, name: String) -> Result<()> { + self.client + .remove_role(authenticated_request(Some(token), RemoveRoleRequest { name })?) + .await?; + Ok(()) + } + + pub async fn grant_permission( + &mut self, + token: &str, + role: String, + object: String, + action: String, + ) -> Result<()> { + self.client + .grant_permission(authenticated_request( + Some(token), + GrantPermissionRequest { + role, + object, + action, + }, + )?) + .await?; + Ok(()) + } + + pub async fn revoke_permission( + &mut self, + token: &str, + role: String, + object: String, + action: String, + ) -> Result<()> { + self.client + .revoke_permission(authenticated_request( + Some(token), + RevokePermissionRequest { + role, + object, + action, + }, + )?) + .await?; + Ok(()) + } + + pub async fn try_revoke_permission( + &mut self, + token: &str, + role: String, + object: String, + action: String, + ) -> Result { + let request = authenticated_request( + Some(token), + RevokePermissionRequest { + role, + object, + action, + }, + )?; + match self.client.revoke_permission(request).await { + Ok(_) => Ok(true), + Err(status) if status.code() == tonic::Code::NotFound => Ok(false), + Err(status) => Err(status.into()), + } + } + + pub async fn list_role_permissions( + &mut self, + token: &str, + role: String, + ) -> Result { + Ok(self + .client + .list_role_permissions(authenticated_request( + Some(token), + ListRolePermissionsRequest { role }, + )?) + .await? + .into_inner()) + } + + pub async fn list_grantable_objects( + &mut self, + token: &str, + target_role: String, + ) -> Result> { + Ok(self + .client + .list_grantable_objects(authenticated_request( + Some(token), + ListGrantableObjectsRequest { target_role }, + )?) + .await? + .into_inner() + .objects) + } + + pub async fn assign_user_role( + &mut self, + token: &str, + username: String, + role: String, + ) -> Result<()> { + self.client + .assign_user_role(authenticated_request( + Some(token), + AssignUserRoleRequest { username, role }, + )?) + .await?; + Ok(()) + } + + pub async fn list_users(&mut self, token: &str) -> Result> { + Ok(self + .client + .list_users(authenticated_request(Some(token), ListUsersRequest {})?) + .await? + .into_inner() + .users) + } + + pub async fn revoke_user_sessions(&mut self, token: &str, username: String) -> Result<()> { + self.client + .revoke_user_sessions(authenticated_request( + Some(token), + RevokeUserSessionsRequest { username }, + )?) + .await?; + Ok(()) + } + + pub async fn reset_user_password( + &mut self, + token: &str, + username: String, + new_password: String, + new_password_confirmation: String, + ) -> Result { + validate_password_change(&new_password, &new_password_confirmation)?; + Ok(self + .client + .reset_user_password(authenticated_request( + Some(token), + ResetUserPasswordRequest { + username, + new_password, + new_password_confirmation, + }, + )?) + .await + .context("Failed to reset user password")? + .into_inner()) + } +} diff --git a/komp-app/src/csv.rs b/komp-app/src/csv.rs new file mode 100644 index 00000000..b1f8e0d4 --- /dev/null +++ b/komp-app/src/csv.rs @@ -0,0 +1,107 @@ +use anyhow::{Result, bail}; +use std::io::{BufRead, Write}; + +pub struct CsvRecordReader { + reader: R, + buffer: String, +} + +impl CsvRecordReader { + pub fn new(reader: R) -> Self { + Self { + reader, + buffer: String::new(), + } + } + + pub fn next_record(&mut self) -> Result>> { + self.buffer.clear(); + let mut line = String::new(); + loop { + line.clear(); + let bytes = self.reader.read_line(&mut line)?; + if bytes == 0 { + if self.buffer.is_empty() { + return Ok(None); + } + break; + } + self.buffer.push_str(&line); + if csv_record_complete(&self.buffer) { + break; + } + } + Ok(Some(parse_csv_record( + self.buffer.trim_end_matches(['\r', '\n']), + )?)) + } +} + +fn csv_record_complete(record: &str) -> bool { + let mut in_quotes = false; + let mut chars = record.chars().peekable(); + while let Some(character) = chars.next() { + if character == '"' { + if in_quotes && chars.peek() == Some(&'"') { + let _ = chars.next(); + } else { + in_quotes = !in_quotes; + } + } + } + !in_quotes +} + +pub fn parse_csv_record(record: &str) -> Result> { + let mut fields = Vec::new(); + let mut current = String::new(); + let mut chars = record.chars().peekable(); + let mut in_quotes = false; + while let Some(character) = chars.next() { + match character { + '"' if in_quotes && chars.peek() == Some(&'"') => { + current.push('"'); + let _ = chars.next(); + } + '"' => in_quotes = !in_quotes, + ',' if !in_quotes => { + fields.push(current); + current = String::new(); + } + _ => current.push(character), + } + } + if in_quotes { + bail!("CSV record has an unterminated quote"); + } + fields.push(current); + Ok(fields) +} + +pub fn write_csv_record(writer: &mut impl Write, fields: &[String]) -> Result<()> { + for (index, field) in fields.iter().enumerate() { + if index > 0 { + writer.write_all(b",")?; + } + if field.contains([',', '"', '\n', '\r']) { + writer.write_all(b"\"")?; + writer.write_all(field.replace('"', "\"\"").as_bytes())?; + writer.write_all(b"\"")?; + } else { + writer.write_all(field.as_bytes())?; + } + } + writer.write_all(b"\n")?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_quoted_fields() { + let row = parse_csv_record(r#"name,"hello, ""world""",42"#).unwrap(); + assert_eq!(row, vec!["name", "hello, \"world\"", "42"]); + } +} diff --git a/komp-app/src/grpc.rs b/komp-app/src/grpc.rs new file mode 100644 index 00000000..bbb30c37 --- /dev/null +++ b/komp-app/src/grpc.rs @@ -0,0 +1,1330 @@ +use crate::search::SearchGrpc; +use anyhow::{Context, Result, anyhow}; +use crate::transport::{ + DEFAULT_GRPC_ENDPOINT, authenticated_request as request_with_auth_token, connect_channel, + retryable_write_status, +}; +pub use crate::transport::PostOperation; +use common::proto::komp_ac::analytics::analytics_service_client::AnalyticsServiceClient; +use common::proto::komp_ac::ecb::ecb_service_client::EcbServiceClient; +use common::proto::komp_ac::search2::search2_client::Search2Client; +use common::proto::komp_ac::accounting::{ + Account, EnsureAccountRequest, GetJournalRequest, Journal, SearchJournalsRequest, + SearchJournalsResponse, accounting_client::AccountingClient, +}; +use common::proto::komp_ac::common::Empty; +use common::proto::komp_ac::document_data::{ + GetDocumentRequest, GetDocumentResponse, GetDocumentVersionRequest, GetDocumentVersionResponse, + GetDocumentsRequest, GetDocumentsResponse, SaveDocumentRequest, SaveDocumentResponse, + UpdateDocumentRequest, UpdateDocumentResponse, + document_data_service_client::DocumentDataServiceClient, +}; +use common::proto::komp_ac::exchange_rates::{ + AddProfileCurrencySourceRequest, GetProfileExchangeRateSettingsRequest, + ProfileExchangeRateSettings, ProfileForeignCurrency, + exchange_rate_service_client::ExchangeRateServiceClient, +}; +use common::proto::komp_ac::search::{ + ColumnConstraint, MatchMode, SearchOrder, SearchRequest, SearchResponse, +}; +use common::proto::komp_ac::table_definition::{ + CreateCustomExchangeRatesTableRequest, CreateCustomExchangeRatesTableResponse, + GetProfileDetailsRequest, GetProfileDetailsResponse, GetTableCatalogRequest, + GetTableCatalogResponse, ListColumnTypesResponse, PostTableDefinitionRequest, + ProfileTreeResponse, PutTableDefinitionRequest, PutTableDefinitionResponse, + SetColumnPresentationRequest, SetColumnPresentationResponse, TableDefinitionResponse, + table_definition_client::TableDefinitionClient, +}; +use common::proto::komp_ac::table_script::{ + GetTableScriptsRequest, GetTableScriptsResponse, HydrateScriptDependenciesRequest, + HydrateScriptDependenciesResponse, PostTableScriptRequest, TableScriptResponse, + table_script_client::TableScriptClient, +}; +use common::proto::komp_ac::table_structure::table_structure_service_client::TableStructureServiceClient; +use common::proto::komp_ac::table_structure::{ + GetTableImportDescriptorRequest, GetTableImportDescriptorResponse, GetTableStructureRequest, + GetTableStructureResponse, TableStructureResponse, +}; +use common::proto::komp_ac::table_validation::{ + ApplyValidationSetRequest, ApplyValidationSetResponse, DeleteValidationRuleRequest, + DeleteValidationRuleResponse, DeleteValidationSetRequest, DeleteValidationSetResponse, + FieldValidation, GetTableValidationRequest, ListValidationRulesRequest, + ListValidationRulesResponse, ListValidationSetsRequest, ListValidationSetsResponse, + ReplaceTableValidationRequest, ReplaceTableValidationResponse, TableValidationResponse, + UpdateFieldValidationRequest, UpdateFieldValidationResponse, UpsertValidationRuleRequest, + UpsertValidationRuleResponse, UpsertValidationSetRequest, UpsertValidationSetResponse, + table_validation_service_client::TableValidationServiceClient, +}; +use common::proto::komp_ac::tables_data::{ + AbortTableDataImportRequest, AbortTableDataImportResponse, ArchiveTableDataRequest, + ArchiveTableDataResponse, BeginTableDataImportRequest, BeginTableDataImportResponse, + CommitTableDataImportRequest, CommitTableDataImportResponse, DeleteTableDataRequest, + DeleteTableDataResponse, GetAdjacentTableDataRequest, GetAdjacentTableDataResponse, + GetLastTableDataRequest, GetLastTableDataResponse, GetTableDataByPositionRequest, + GetTableDataCountRequest, GetTableDataRequest, GetTableDataResponse, PostTableDataRequest, + PostTableDataResponse, PutTableDataRequest, PutTableDataResponse, RowIdDirection, + RowNavigationMode, + StageTableDataImportRequest, StageTableDataImportResponse, TableUpdateImpactRequest, + TableUpdateImpactResponse, tables_data_client::TablesDataClient, +}; +use prost_types::Value; +use std::collections::HashMap; +use tonic::Request; +use tonic::transport::Channel; + +#[derive(Clone)] +pub struct GrpcClient { + pub channel: Channel, + table_structure_client: TableStructureServiceClient, + table_definition_client: TableDefinitionClient, + table_script_client: TableScriptClient, + tables_data_client: TablesDataClient, + document_data_client: DocumentDataServiceClient, + search_client: SearchGrpc, + table_validation_client: TableValidationServiceClient, + accounting_client: AccountingClient, + exchange_rate_client: ExchangeRateServiceClient, + ecb_client: EcbServiceClient, + analytics_client: AnalyticsServiceClient, + search2_client: Search2Client, + pub auth_token: Option, +} + +impl GrpcClient { + pub async fn new() -> Result { + let endpoint = std::env::var("GRPC_ENDPOINT") + .unwrap_or_else(|_| DEFAULT_GRPC_ENDPOINT.to_string()); + Self::connect(&endpoint).await + } + + pub async fn connect(endpoint: &str) -> Result { + Self::from_channel(connect_channel(endpoint).await?) + } + + pub fn from_channel(channel: Channel) -> Result { + let table_structure_client = TableStructureServiceClient::new(channel.clone()); + let table_definition_client = TableDefinitionClient::new(channel.clone()); + let table_script_client = TableScriptClient::new(channel.clone()); + let tables_data_client = TablesDataClient::new(channel.clone()); + let document_data_client = DocumentDataServiceClient::new(channel.clone()); + let search_client = SearchGrpc::new(channel.clone()); + let table_validation_client = TableValidationServiceClient::new(channel.clone()); + let accounting_client = AccountingClient::new(channel.clone()); + let exchange_rate_client = ExchangeRateServiceClient::new(channel.clone()); + let ecb_client = EcbServiceClient::new(channel.clone()); + let analytics_client = AnalyticsServiceClient::new(channel.clone()); + let search2_client = Search2Client::new(channel.clone()); + + Ok(Self { + channel, + table_structure_client, + table_definition_client, + table_script_client, + tables_data_client, + document_data_client, + search_client, + table_validation_client, + accounting_client, + exchange_rate_client, + ecb_client, + analytics_client, + search2_client, + auth_token: None, + }) + } + + // Expose the shared channel so other typed clients can reuse it. + pub fn channel(&self) -> Channel { + self.channel.clone() + } + + pub fn set_auth_token(&mut self, auth_token: Option) { + self.auth_token = auth_token; + } + + pub async fn hydrate_script_dependencies( + &mut self, + profile_name: String, + table_name: String, + row_id: i64, + row_data: HashMap, + ) -> Result { + let request = self.authenticated_request(HydrateScriptDependenciesRequest { + profile_name, + table_name, + row_id, + row_data, + })?; + let response = self + .table_script_client + .hydrate_script_dependencies(request) + .await + .context("gRPC HydrateScriptDependencies call failed")?; + Ok(response.into_inner()) + } + + fn authenticated_request(&self, message: T) -> Result> { + request_with_auth_token(self.auth_token.as_deref(), message) + } + + pub async fn get_journal(&mut self, profile_name: String, journal_id: i64) -> Result { + let request = self.authenticated_request(GetJournalRequest { + profile_name, + journal_id, + include_deleted: false, + })?; + let response = self + .accounting_client + .get_journal(request) + .await + .context("gRPC GetJournal call failed")?; + Ok(response.into_inner()) + } + + pub async fn ensure_account( + &mut self, + profile_name: String, + account: String, + denomination_currency: String, + ) -> Result { + let request = self.authenticated_request(EnsureAccountRequest { + profile_name, + account, + denomination_currency, + })?; + let response = self + .accounting_client + .ensure_account(request) + .await + .context("gRPC EnsureAccount call failed")?; + Ok(response.into_inner()) + } + + pub async fn search_journals( + &mut self, + profile_name: String, + name_query: String, + page_size: i32, + page_token: String, + ) -> Result { + let request = self.authenticated_request(SearchJournalsRequest { + profile_name, + name_query, + page_size, + page_token, + })?; + let response = self + .accounting_client + .search_journals(request) + .await + .context("gRPC SearchJournals call failed")?; + Ok(response.into_inner()) + } + + // Fetch validation rules for a table. Absence of a field in response = no validation. + pub async fn get_table_validation( + &mut self, + profile_name: String, + table_name: String, + ) -> Result { + let req = GetTableValidationRequest { + profile_name, + table_name, + }; + let request = self.authenticated_request(req)?; + let resp = self + .table_validation_client + .get_table_validation(request) + .await + .context("gRPC GetTableValidation call failed")?; + Ok(resp.into_inner()) + } + + pub async fn update_field_validation( + &mut self, + profile_name: String, + table_name: String, + data_key: String, + validation: FieldValidation, + ) -> Result { + let req = UpdateFieldValidationRequest { + profile_name, + table_name, + data_key, + validation: Some(validation), + }; + let request = self.authenticated_request(req)?; + let resp = self + .table_validation_client + .update_field_validation(request) + .await + .context("gRPC UpdateFieldValidation call failed")?; + Ok(resp.into_inner()) + } + + pub async fn upsert_validation_rule( + &mut self, + request: UpsertValidationRuleRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let resp = self + .table_validation_client + .upsert_validation_rule(request) + .await + .map_err(|e| anyhow!("gRPC UpsertValidationRule call failed: {}", e))?; + Ok(resp.into_inner()) + } + + pub async fn upsert_validation_set( + &mut self, + request: UpsertValidationSetRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let resp = self + .table_validation_client + .upsert_validation_set(request) + .await + .map_err(|e| anyhow!("gRPC UpsertValidationSet call failed: {}", e))?; + Ok(resp.into_inner()) + } + + pub async fn delete_validation_rule( + &mut self, + request: DeleteValidationRuleRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let resp = self + .table_validation_client + .delete_validation_rule(request) + .await + .context("gRPC DeleteValidationRule call failed")?; + Ok(resp.into_inner()) + } + + pub async fn delete_validation_set( + &mut self, + request: DeleteValidationSetRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let resp = self + .table_validation_client + .delete_validation_set(request) + .await + .context("gRPC DeleteValidationSet call failed")?; + Ok(resp.into_inner()) + } + + pub async fn list_validation_rules( + &mut self, + profile_name: String, + ) -> Result { + let req = ListValidationRulesRequest { profile_name }; + let request = self.authenticated_request(req)?; + let resp = self + .table_validation_client + .list_validation_rules(request) + .await + .context("gRPC ListValidationRules call failed")?; + Ok(resp.into_inner()) + } + + pub async fn list_validation_sets( + &mut self, + profile_name: String, + ) -> Result { + let req = ListValidationSetsRequest { profile_name }; + let request = self.authenticated_request(req)?; + let resp = self + .table_validation_client + .list_validation_sets(request) + .await + .context("gRPC ListValidationSets call failed")?; + Ok(resp.into_inner()) + } + + pub async fn apply_validation_set( + &mut self, + request: ApplyValidationSetRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let resp = self + .table_validation_client + .apply_validation_set(request) + .await + .context("gRPC ApplyValidationSet call failed")?; + Ok(resp.into_inner()) + } + + pub async fn replace_table_validation( + &mut self, + request: ReplaceTableValidationRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let resp = self + .table_validation_client + .replace_table_validation(request) + .await + .context("gRPC ReplaceTableValidation call failed")?; + Ok(resp.into_inner()) + } + + pub async fn get_table_structures( + &mut self, + profile_name: String, + table_names: Vec, + ) -> Result { + let grpc_request = GetTableStructureRequest { + profile_name, + table_names, + }; + let request = self.authenticated_request(grpc_request)?; + let response = self + .table_structure_client + .get_table_structure(request) + .await + .context("gRPC GetTableStructure call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_table_structure( + &mut self, + profile_name: String, + table_name: String, + ) -> Result { + let response = self + .get_table_structures(profile_name.clone(), vec![table_name.clone()]) + .await?; + + response + .table_structures + .into_iter() + .find_map(|(response_table_name, structure)| { + (response_table_name == table_name).then_some(structure) + }) + .ok_or_else(|| { + anyhow!( + "GetTableStructure response for profile '{}' did not contain table '{}'", + profile_name, + table_name + ) + }) + } + + pub async fn get_table_import_descriptor( + &mut self, + profile_name: String, + table_name: String, + ) -> Result { + let request = self.authenticated_request(GetTableImportDescriptorRequest { + profile_name, + table_name, + })?; + let response = self + .table_structure_client + .get_table_import_descriptor(request) + .await + .context("gRPC GetTableImportDescriptor call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_profile_tree(&mut self) -> Result { + let request = self.authenticated_request(Empty::default())?; + let response = self + .table_definition_client + .get_profile_tree(request) + .await + .context("gRPC GetProfileTree call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_table_catalog( + &mut self, + profile_name: Option, + ) -> Result { + let request = self.authenticated_request(GetTableCatalogRequest { profile_name })?; + let response = self + .table_definition_client + .get_table_catalog(request) + .await + .context("gRPC GetTableCatalog call failed")?; + Ok(response.into_inner()) + } + + pub async fn list_column_types(&mut self) -> Result { + let request = self.authenticated_request(Empty::default())?; + let response = self + .table_definition_client + .list_column_types(request) + .await + .context("gRPC ListColumnTypes call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_profile_details( + &mut self, + profile_name: String, + ) -> Result { + let request = self.authenticated_request(GetProfileDetailsRequest { profile_name })?; + let response = self + .table_definition_client + .get_profile_details(request) + .await + .context("gRPC GetProfileDetails call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_profile_exchange_rate_settings( + &mut self, + profile_name: String, + ) -> Result { + let request = + self.authenticated_request(GetProfileExchangeRateSettingsRequest { profile_name })?; + Ok(self + .exchange_rate_client + .get_profile_exchange_rate_settings(request) + .await + .context("gRPC GetProfileExchangeRateSettings call failed")? + .into_inner()) + } + + pub async fn add_profile_currency_source( + &mut self, + profile_name: String, + currency: String, + source_id: String, + make_default: bool, + ) -> Result { + let request = self.authenticated_request(AddProfileCurrencySourceRequest { + profile_name, + currency, + source_id, + make_default, + })?; + Ok(self + .exchange_rate_client + .add_profile_currency_source(request) + .await + .context("gRPC AddProfileCurrencySource call failed")? + .into_inner()) + } + + pub async fn post_table_definition( + &mut self, + request: PostTableDefinitionRequest, + ) -> Result { + let tonic_request = self.authenticated_request(request)?; + let response = self + .table_definition_client + .post_table_definition(tonic_request) + .await + .context("gRPC PostTableDefinition call failed")?; + Ok(response.into_inner()) + } + + pub async fn put_table_definition( + &mut self, + request: PutTableDefinitionRequest, + ) -> Result { + let tonic_request = self.authenticated_request(request)?; + let response = self + .table_definition_client + .put_table_definition(tonic_request) + .await + .context("gRPC PutTableDefinition call failed")?; + Ok(response.into_inner()) + } + + pub async fn create_custom_exchange_rates_table( + &mut self, + profile_name: String, + ) -> Result { + let request = + self.authenticated_request(CreateCustomExchangeRatesTableRequest { profile_name })?; + let response = self + .table_definition_client + .create_custom_exchange_rates_table(request) + .await + .context("gRPC CreateCustomExchangeRatesTable call failed")?; + Ok(response.into_inner()) + } + + pub async fn set_column_presentation( + &mut self, + request: SetColumnPresentationRequest, + ) -> Result { + let tonic_request = self.authenticated_request(request)?; + let response = self + .table_definition_client + .set_column_presentation(tonic_request) + .await + .context("gRPC SetColumnPresentation call failed")?; + Ok(response.into_inner()) + } + + pub async fn post_table_script( + &mut self, + request: PostTableScriptRequest, + ) -> Result { + let tonic_request = self.authenticated_request(request)?; + let response = self + .table_script_client + .post_table_script(tonic_request) + .await + .context("gRPC PostTableScript call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_table_scripts( + &mut self, + profile_name: String, + table_name: String, + ) -> Result { + let request = self.authenticated_request(GetTableScriptsRequest { + profile_name, + table_name, + })?; + let response = self + .table_script_client + .get_table_scripts(request) + .await + .context("gRPC GetTableScripts call failed")?; + Ok(response.into_inner()) + } + + // Existing TablesData methods + pub async fn get_table_data_count( + &mut self, + profile_name: String, + table_name: String, + ) -> Result { + let grpc_request = GetTableDataCountRequest { + profile_name, + table_name, + }; + let request = self.authenticated_request(grpc_request)?; + let response = self + .tables_data_client + .get_table_data_count(request) + .await + .context("gRPC GetTableDataCount call failed")?; + let count = response.into_inner().count; + u64::try_from(count) + .with_context(|| format!("gRPC GetTableDataCount returned negative count {count}")) + } + + pub async fn get_last_table_data( + &mut self, + profile_name: String, + table_name: String, + navigation_mode: RowNavigationMode, + ) -> Result { + let request = self.authenticated_request(GetLastTableDataRequest { + profile_name, + table_name, + navigation_mode: navigation_mode as i32, + })?; + let response = self + .tables_data_client + .get_last_table_data(request) + .await + .context("gRPC GetLastTableData call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_adjacent_table_data( + &mut self, + profile_name: String, + table_name: String, + anchor_id: i64, + direction: RowIdDirection, + navigation_mode: RowNavigationMode, + anchor_position: u64, + ) -> Result { + let request = self.authenticated_request(GetAdjacentTableDataRequest { + profile_name, + table_name, + anchor_id, + direction: direction as i32, + navigation_mode: navigation_mode as i32, + anchor_position, + })?; + let response = self + .tables_data_client + .get_adjacent_table_data(request) + .await + .context("gRPC GetAdjacentTableData call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_table_data_by_position( + &mut self, + profile_name: String, + table_name: String, + position: i32, + ) -> Result { + let grpc_request = GetTableDataByPositionRequest { + profile_name, + table_name, + position, + record_id: 0, + navigation_mode: RowNavigationMode::Position as i32, + }; + let request = self.authenticated_request(grpc_request)?; + let response = self + .tables_data_client + .get_table_data_by_position(request) + .await + .context("gRPC GetTableDataByPosition call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_table_data_with_navigation( + &mut self, + profile_name: String, + table_name: String, + record_id: i64, + navigation_mode: RowNavigationMode, + ) -> Result { + let request = self.authenticated_request(GetTableDataByPositionRequest { + profile_name, + table_name, + position: 0, + record_id, + navigation_mode: navigation_mode as i32, + })?; + let response = self + .tables_data_client + .get_table_data_by_position(request) + .await + .context("gRPC navigated GetTableData call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_table_data( + &mut self, + profile_name: String, + table_name: String, + id: i64, + ) -> Result { + let grpc_request = GetTableDataRequest { + profile_name, + table_name, + id, + }; + let request = self.authenticated_request(grpc_request)?; + let response = self + .tables_data_client + .get_table_data(request) + .await + .context("gRPC GetTableData call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_table_data_version( + &mut self, + profile_name: String, + table_name: String, + id: i64, + version: i64, + ) -> Result { + let request = self.authenticated_request( + common::proto::komp_ac::tables_data::GetTableDataVersionRequest { + profile_name, + table_name, + id, + version, + }, + )?; + Ok(self + .tables_data_client + .get_table_data_version(request) + .await + .context("gRPC GetTableDataVersion call failed")? + .into_inner()) + } + + pub async fn delete_table_data( + &mut self, + profile_name: String, + table_name: String, + record_id: i64, + expected_revision: i64, + ) -> Result { + let grpc_request = DeleteTableDataRequest { + profile_name, + table_name, + record_id, + expected_revision, + }; + let request = self.authenticated_request(grpc_request)?; + let response = self + .tables_data_client + .delete_table_data(request) + .await + .context("gRPC DeleteTableData call failed")?; + Ok(response.into_inner()) + } + + pub async fn post_table_data( + &mut self, + profile_name: String, + table_name: String, + data: HashMap, + operation: PostOperation, + ) -> Result { + let grpc_request = PostTableDataRequest { + profile_name, + table_name, + data, + }; + let mut request = self.authenticated_request(grpc_request)?; + operation.add_metadata(&mut request)?; + let response = self + .tables_data_client + .post_table_data(request) + .await + .context("gRPC PostTableData call failed")?; + Ok(response.into_inner()) + } + + pub async fn begin_table_data_import( + &mut self, + profile_name: String, + ) -> Result { + let request = self.authenticated_request(BeginTableDataImportRequest { profile_name })?; + Ok(self + .tables_data_client + .begin_table_data_import(request) + .await + .context("gRPC BeginTableDataImport call failed")? + .into_inner()) + } + + pub async fn stage_table_data_import( + &mut self, + request: StageTableDataImportRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + Ok(self + .tables_data_client + .stage_table_data_import(request) + .await + .context("gRPC StageTableDataImport call failed")? + .into_inner()) + } + + pub async fn commit_table_data_import( + &mut self, + import_id: String, + ) -> Result { + for attempt in 0..2 { + let request = self.authenticated_request(CommitTableDataImportRequest { + import_id: import_id.clone(), + })?; + match self + .tables_data_client + .commit_table_data_import(request) + .await + { + Ok(response) => return Ok(response.into_inner()), + Err(status) if attempt == 0 && retryable_write_status(status.code()) => {} + Err(status) => { + return Err(status).context("gRPC CommitTableDataImport call failed"); + } + } + } + unreachable!("the commit retry loop returns on its second attempt") + } + + pub async fn abort_table_data_import( + &mut self, + import_id: String, + ) -> Result { + let request = self.authenticated_request(AbortTableDataImportRequest { import_id })?; + Ok(self + .tables_data_client + .abort_table_data_import(request) + .await + .context("gRPC AbortTableDataImport call failed")? + .into_inner()) + } + + pub async fn put_table_data( + &mut self, + profile_name: String, + table_name: String, + id: i64, + data: HashMap, + expected_revision: i64, + ) -> Result { + let grpc_request = PutTableDataRequest { + profile_name, + table_name, + id, + data, + expected_revision, + }; + let request = self.authenticated_request(grpc_request)?; + let response = self + .tables_data_client + .put_table_data(request) + .await + .context("gRPC PutTableData call failed")?; + Ok(response.into_inner()) + } + + pub async fn put_table_data_confirmed( + &mut self, + profile_name: String, + table_name: String, + id: i64, + data: HashMap, + expected_revision: i64, + expected_affected_profiles: Vec, + ) -> Result { + let request = self.authenticated_request( + common::proto::komp_ac::tables_data::PutTableDataConfirmedRequest { + update: Some(PutTableDataRequest { + profile_name, + table_name, + id, + data, + expected_revision, + }), + expected_affected_profiles, + }, + )?; + Ok(self + .tables_data_client + .put_table_data_confirmed(request) + .await + .context("gRPC confirmed PutTableData call failed")? + .into_inner()) + } + + pub async fn get_table_update_impact( + &mut self, + request: TableUpdateImpactRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + Ok(self + .tables_data_client + .get_table_update_impact(request) + .await + .context("gRPC table update impact call failed")? + .into_inner()) + } + + pub async fn archive_table_data( + &mut self, + request: ArchiveTableDataRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + Ok(self + .tables_data_client + .archive_table_data(request) + .await + .context("gRPC row archive call failed")? + .into_inner()) + } + + pub async fn save_document( + &mut self, + request: SaveDocumentRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = self + .document_data_client + .save_document(request) + .await + .context("gRPC SaveDocument call failed")?; + Ok(response.into_inner()) + } + + pub async fn update_document( + &mut self, + request: UpdateDocumentRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = self + .document_data_client + .update_document(request) + .await + .context("gRPC UpdateDocument call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_documents( + &mut self, + request: GetDocumentsRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = self + .document_data_client + .get_documents(request) + .await + .context("gRPC GetDocuments call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_document( + &mut self, + request: GetDocumentRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = self + .document_data_client + .get_document(request) + .await + .context("gRPC GetDocument call failed")?; + Ok(response.into_inner()) + } + + pub async fn get_document_version( + &mut self, + request: GetDocumentVersionRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + let response = self + .document_data_client + .get_document_version(request) + .await + .context("gRPC GetDocumentVersion call failed")?; + Ok(response.into_inner()) + } + + pub async fn search( + &mut self, + profile_name: String, + table_name: Option, + free_query: String, + must: Vec, + limit: Option, + offset: Option, + order: Option, + ) -> Result { + let request = self.authenticated_request(SearchRequest { + profile_name, + table_name, + free_query, + must, + limit, + offset, + order, + version_scope: 0, + })?; + self.search_client.search(request).await + } + + pub async fn count_table_rows( + &mut self, + profile_name: String, + table_name: String, + column: String, + value: String, + ) -> Result { + self.count_search_rows( + profile_name, + table_name, + String::new(), + vec![ColumnConstraint { + column, + query: value, + mode: MatchMode::Exact as i32, + }], + ) + .await + } + + pub async fn count_search_rows( + &mut self, + profile_name: String, + table_name: String, + free_query: String, + constraints: Vec, + ) -> Result { + let request = self.authenticated_request(SearchRequest { + profile_name, + table_name: Some(table_name), + free_query, + must: constraints, + limit: None, + offset: None, + order: None, + version_scope: 0, + })?; + let response = self + .search_client + .count(request) + .await + .context("gRPC child relationship count search failed")?; + Ok(response.count) + } + + pub async fn get_table_data_for_navigation( + &mut self, + profile_name: String, + table_name: String, + record_id: i64, + navigation_mode: RowNavigationMode, + ) -> Result { + self.get_table_data_with_navigation( + profile_name, + table_name, + record_id, + navigation_mode, + ) + .await + } + + pub async fn add_table_columns( + &mut self, + request: common::proto::komp_ac::table_definition::AddTableColumnsRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + Ok(self + .table_definition_client + .add_table_columns(request) + .await + .context("gRPC AddTableColumns call failed")? + .into_inner()) + } + + pub async fn delete_table( + &mut self, + request: common::proto::komp_ac::table_definition::DeleteTableRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + Ok(self + .table_definition_client + .delete_table(request) + .await + .context("gRPC DeleteTable call failed")? + .into_inner()) + } + + pub async fn copy_profile( + &mut self, + request: common::proto::komp_ac::table_definition::CopyProfileRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + Ok(self + .table_definition_client + .copy_profile(request) + .await + .context("gRPC CopyProfile call failed")? + .into_inner()) + } + + pub async fn get_column_alias_rename_history( + &mut self, + request: common::proto::komp_ac::table_definition::GetColumnAliasRenameHistoryRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + Ok(self + .table_definition_client + .get_column_alias_rename_history(request) + .await + .context("gRPC GetColumnAliasRenameHistory call failed")? + .into_inner()) + } + + pub async fn create_invoice_template_table( + &mut self, + request: common::proto::komp_ac::table_definition::CreateInvoiceTemplateTableRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + Ok(self + .table_definition_client + .create_invoice_template_table(request) + .await + .context("gRPC CreateInvoiceTemplateTable call failed")? + .into_inner()) + } + + pub async fn list_rate_sources( + &mut self, + ) -> Result { + let request = self.authenticated_request(Empty::default())?; + Ok(self + .exchange_rate_client + .list_rate_sources(request) + .await + .context("gRPC ListRateSources call failed")? + .into_inner()) + } + + pub async fn preview_direct_conversion( + &mut self, + request: common::proto::komp_ac::exchange_rates::PreviewDirectConversionRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + Ok(self + .exchange_rate_client + .preview_direct_conversion(request) + .await + .context("gRPC PreviewDirectConversion call failed")? + .into_inner()) + } + + pub async fn list_conversion_evidence( + &mut self, + request: common::proto::komp_ac::exchange_rates::ListConversionEvidenceRequest, + ) -> Result { + let request = self.authenticated_request(request)?; + Ok(self + .exchange_rate_client + .list_conversion_evidence(request) + .await + .context("gRPC ListConversionEvidence call failed")? + .into_inner()) + } + + pub async fn get_ecb_pipeline_status( + &mut self, + batch_limit: i32, + ) -> Result { + let request = self.authenticated_request( + common::proto::komp_ac::ecb::GetEcbPipelineStatusRequest { batch_limit }, + )?; + Ok(self + .ecb_client + .get_ecb_pipeline_status(request) + .await + .context("gRPC GetEcbPipelineStatus call failed")? + .into_inner()) + } + + pub async fn get_analytics_catalog( + &mut self, + profile_name: String, + ) -> Result { + let request = self.authenticated_request( + common::proto::komp_ac::analytics::GetAnalyticsCatalogRequest { profile_name }, + )?; + Ok(self + .analytics_client + .get_analytics_catalog(request) + .await + .context("gRPC GetAnalyticsCatalog call failed")? + .into_inner()) + } + + pub async fn execute_analytics_query( + &mut self, + profile_name: String, + sql: String, + max_rows: u32, + ) -> Result> { + let request = self.authenticated_request( + common::proto::komp_ac::analytics::ExecuteAnalyticsQueryRequest { + profile_name, + sql, + max_rows, + }, + )?; + Ok(self + .analytics_client + .execute_analytics_query(request) + .await + .context("gRPC ExecuteAnalyticsQuery call failed")? + .into_inner()) + } + + pub async fn search_versions( + &mut self, + profile_name: String, + table_name: Option, + free_query: String, + limit: Option, + offset: Option, + version_scope: common::proto::komp_ac::search::SearchVersionScope, + ) -> Result { + let request = self.authenticated_request(SearchRequest { + profile_name, + table_name, + free_query, + must: Vec::new(), + limit, + offset, + order: None, + version_scope: version_scope as i32, + })?; + self.search_client.search(request).await + } + + pub async fn search_table( + &mut self, + request: common::proto::komp_ac::search2::Search2Request, + ) -> Result { + let request = self.authenticated_request(request)?; + Ok(self + .search2_client + .search_table(request) + .await + .context("gRPC SearchTable call failed")? + .into_inner()) + } +} + +#[cfg(test)] +mod tests { + use super::{PostOperation, request_with_auth_token}; + use tonic::Request; + use uuid::Uuid; + + #[test] + fn post_operation_adds_required_grpc_metadata() { + let operation = PostOperation { + idempotency_key: Uuid::parse_str("1c32db5c-b7af-4427-bf6e-8f48f75d26bd").unwrap(), + created_at: "2026-07-30T12:00:00Z".parse().unwrap(), + }; + let mut request = Request::new(()); + + operation.add_metadata(&mut request).unwrap(); + + assert_eq!( + request + .metadata() + .get("idempotency-key") + .unwrap() + .to_str() + .unwrap(), + "1c32db5c-b7af-4427-bf6e-8f48f75d26bd" + ); + assert_eq!( + request + .metadata() + .get("operation-created-at") + .unwrap() + .to_str() + .unwrap(), + "2026-07-30T12:00:00Z" + ); + } + + #[test] + fn authenticated_request_adds_bearer_token() { + let request = request_with_auth_token(Some("access-token"), ()).unwrap(); + + assert_eq!( + request + .metadata() + .get("authorization") + .unwrap() + .to_str() + .unwrap(), + "Bearer access-token" + ); + } + + #[test] + fn authenticated_request_requires_token() { + let error = request_with_auth_token(None, ()).unwrap_err(); + + assert_eq!( + error.to_string(), + "Authentication is required for this request" + ); + } +} diff --git a/komp-app/src/import_export.rs b/komp-app/src/import_export.rs new file mode 100644 index 00000000..0e5fff17 --- /dev/null +++ b/komp-app/src/import_export.rs @@ -0,0 +1,444 @@ +use anyhow::{Result, anyhow, bail}; +use common::proto::komp_ac::table_definition::{ + ProfileTreeResponse, profile_tree_response::Profile, +}; +use common::proto::komp_ac::table_structure::{ + GetTableImportDescriptorResponse, TableStructureResponse, +}; +use common::proto::komp_ac::tables_data::TableDataImportRow; +use prost_types::{Value, value::Kind}; +use sanitise_file_name::{Options, sanitise_with_options}; +use std::collections::{HashMap, HashSet}; +use std::io::BufRead; +use std::path::Path; + +use crate::csv::CsvRecordReader; +use crate::grpc::GrpcClient; +use crate::value::convert_input_value; + +pub const CSV_IMPORT_PREVIEW_ROW_LIMIT: usize = 10; +pub const CSV_EXPORT_PREVIEW_ROW_LIMIT: u64 = 20; +pub const MAX_EXPORT_POSITION: u64 = i32::MAX as u64; + +#[derive(Debug, Clone)] +pub struct ImportTable { + pub name: String, + pub columns: Vec, + pub type_map: HashMap, + pub link_columns: HashSet, + pub revision: i64, +} + +impl ImportTable { + pub fn from_descriptor(descriptor: GetTableImportDescriptorResponse) -> Self { + let writable = descriptor + .fields + .into_iter() + .filter(|field| field.writable) + .collect::>(); + Self { + name: descriptor.table_name, + columns: writable.iter().map(|field| field.name.clone()).collect(), + type_map: writable + .iter() + .map(|field| (field.name.clone(), field.storage_type.clone())) + .collect(), + link_columns: writable + .iter() + .filter(|field| field.link.is_some()) + .map(|field| field.name.clone()) + .collect(), + revision: descriptor.table_revision, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImportHeader { + pub table_headers: Vec, + pub columns: Vec, +} + +#[derive(Debug, Clone)] +pub struct ResolvedImportTarget { + pub profile_name: String, + pub tables: Vec, + pub header: ImportHeader, +} + +#[derive(Debug, Clone)] +pub struct ExportTable { + pub name: String, + pub columns: Vec, + pub count: u64, +} + +pub fn export_table_header_row(tables: &[ExportTable]) -> Vec { + tables + .iter() + .flat_map(|table| std::iter::repeat_n(table.name.clone(), table.columns.len())) + .collect() +} + +pub fn export_column_header_row(tables: &[ExportTable]) -> Vec { + tables + .iter() + .flat_map(|table| table.columns.clone()) + .collect() +} + +pub fn maximum_export_row_count(tables: &[ExportTable]) -> u64 { + tables.iter().map(|table| table.count).max().unwrap_or(0) +} + +pub fn validate_output_filename(filename: &str) -> Result<()> { + if filename.is_empty() || filename.trim() != filename || filename.ends_with('.') { + bail!("Output filename must not be empty or start/end with whitespace or a dot"); + } + if sanitise_generated_filename(filename) != filename { + bail!("Output filename '{filename}' is not portable across Windows, macOS, and Linux"); + } + if Path::new(filename).components().count() != 1 { + bail!("Output filename must not contain a directory path"); + } + Ok(()) +} + +pub fn sanitise_generated_filename(filename: &str) -> String { + sanitise_with_options(filename, &portable_filename_options()) +} + +fn portable_filename_options() -> Options> { + Options { + normalise_whitespace: false, + trim_spaces_and_full_stops: false, + trim_more_punctuation: false, + six_measures_of_barley: "output", + ..Options::DEFAULT + } +} + +pub fn parse_target_tables(value: &str) -> Vec { + value + .split(',') + .map(str::trim) + .filter(|table| !table.is_empty()) + .map(ToString::to_string) + .collect() +} + +pub fn profile_header_row(profile_name: &str) -> Vec { + vec![profile_name.to_string()] +} + +pub fn table_header_row(tables: &[ImportTable]) -> Vec { + tables + .iter() + .flat_map(|table| std::iter::repeat_n(table.name.clone(), table.columns.len())) + .collect() +} + +pub fn column_header_row(tables: &[ImportTable]) -> Vec { + tables + .iter() + .flat_map(|table| table.columns.clone()) + .collect() +} + +pub fn parse_profile_header(row: &[String]) -> Option { + if row.len() == 1 && !row[0].trim().is_empty() { + Some(row[0].trim().to_string()) + } else { + None + } +} + +pub fn profile_has_tables(profile: &Profile, table_names: &[String]) -> bool { + table_names + .iter() + .all(|table_name| profile.tables.iter().any(|table| table.name == *table_name)) +} + +pub fn consecutive_table_names(values: &[String]) -> Vec { + let mut names = Vec::new(); + for value in values { + if value.is_empty() { + return Vec::new(); + } + if names.last() != Some(value) { + names.push(value.clone()); + } + } + names +} + +pub async fn load_import_tables_for_profile( + profile_name: &str, + client: &mut GrpcClient, + table_names: &[String], +) -> Result> { + let mut tables = Vec::new(); + for table_name in table_names { + let descriptor = client + .get_table_import_descriptor(profile_name.to_string(), table_name.clone()) + .await?; + tables.push(ImportTable::from_descriptor(descriptor)); + } + Ok(tables) +} + +pub async fn resolve_import_target_from_reader( + profile_tree: &ProfileTreeResponse, + client: &mut GrpcClient, + selected_profile: &str, + selected_tables: &[String], + reader: &mut CsvRecordReader, +) -> Result { + let mut profile_name = selected_profile.trim().to_string(); + let mut first = reader + .next_record()? + .ok_or_else(|| anyhow!("CSV file is empty"))?; + if let Some(header_profile) = parse_profile_header(&first) { + if !profile_name.is_empty() && profile_name != header_profile { + bail!( + "CSV profile '{}' does not match selected profile '{}'", + header_profile, + profile_name + ); + } + profile_name = header_profile; + first = reader + .next_record()? + .ok_or_else(|| anyhow!("CSV is missing a table or column header"))?; + } + + let profiles = candidate_profiles(profile_tree, &profile_name)?; + let mut second = None; + let mut candidates = Vec::new(); + if selected_tables.is_empty() { + for profile in profiles { + if let Some((tables, header)) = infer_multi_table_header( + client, + profile, + &first, + &mut second, + reader, + ) + .await? + { + candidates.push((profile.name.clone(), tables, header)); + } + for table in &profile.tables { + let tables = load_import_tables_for_profile( + &profile.name, + client, + &[table.name.clone()], + ) + .await?; + if first == column_header_row(&tables) { + candidates.push(( + profile.name.clone(), + tables, + ImportHeader { + table_headers: Vec::new(), + columns: first.clone(), + }, + )); + } + } + } + } else { + for profile in profiles { + if !profile_has_tables(profile, selected_tables) { + continue; + } + let tables = load_import_tables_for_profile( + &profile.name, + client, + selected_tables, + ) + .await?; + if let Some(header) = match_selected_header( + &first, + &mut second, + &tables, + reader, + )? { + candidates.push((profile.name.clone(), tables, header)); + } + } + } + + let (profile_name, tables, header) = match candidates.len() { + 0 => bail!("CSV header does not match any available profile and table"), + 1 => candidates.remove(0), + _ => { + let labels = candidates + .iter() + .map(|(profile_name, tables, _)| { + format!( + "{} / {}", + profile_name, + tables + .iter() + .map(|table| table.name.as_str()) + .collect::>() + .join(", ") + ) + }) + .collect::>(); + bail!( + "CSV header is ambiguous. Select a profile and table. Matches: {}", + labels.join("; ") + ) + } + }; + + Ok(ResolvedImportTarget { + profile_name, + tables, + header, + }) +} + +fn candidate_profiles<'a>( + profile_tree: &'a ProfileTreeResponse, + selected_profile: &str, +) -> Result> { + if selected_profile.is_empty() { + return Ok(profile_tree.profiles.iter().collect()); + } + let profiles = profile_tree + .profiles + .iter() + .filter(|profile| profile.name == selected_profile) + .collect::>(); + if profiles.is_empty() { + bail!("Selected profile '{}' is not available", selected_profile); + } + Ok(profiles) +} + +fn read_second_header<'a, R: BufRead>( + second: &'a mut Option>, + reader: &mut CsvRecordReader, +) -> Result<&'a Vec> { + if second.is_none() { + *second = Some( + reader + .next_record()? + .ok_or_else(|| anyhow!("CSV is missing the column header row"))?, + ); + } + Ok(second.as_ref().expect("second header was inserted")) +} + +fn match_selected_header( + first: &[String], + second: &mut Option>, + tables: &[ImportTable], + reader: &mut CsvRecordReader, +) -> Result> { + if tables.len() == 1 { + if first == table_header_row(tables) { + let second = read_second_header(second, reader)?; + return Ok( + (second == &column_header_row(tables)).then(|| ImportHeader { + table_headers: first.to_vec(), + columns: second.clone(), + }), + ); + } + return Ok((first == column_header_row(tables)).then(|| ImportHeader { + table_headers: Vec::new(), + columns: first.to_vec(), + })); + } + if first != table_header_row(tables) { + return Ok(None); + } + let second = read_second_header(second, reader)?; + Ok( + (second == &column_header_row(tables)).then(|| ImportHeader { + table_headers: first.to_vec(), + columns: second.clone(), + }), + ) +} + +async fn infer_multi_table_header( + client: &mut GrpcClient, + profile: &Profile, + first: &[String], + second: &mut Option>, + reader: &mut CsvRecordReader, +) -> Result, ImportHeader)>> { + let table_names = consecutive_table_names(first); + if table_names.is_empty() || !profile_has_tables(profile, &table_names) { + return Ok(None); + } + let tables = load_import_tables_for_profile(&profile.name, client, &table_names).await?; + if first != table_header_row(&tables) { + return Ok(None); + } + let second = read_second_header(second, reader)?; + if second != &column_header_row(&tables) { + return Ok(None); + } + Ok(Some(( + tables, + ImportHeader { + table_headers: first.to_vec(), + columns: second.clone(), + }, + ))) +} + +pub fn row_to_table_data( + table: &ImportTable, + header: &ImportHeader, + row: &[String], +) -> Result { + let mut data = HashMap::new(); + for (index, target_column) in header.columns.iter().enumerate() { + if header + .table_headers + .get(index) + .filter(|name| !name.is_empty()) + .is_some_and(|name| name != &table.name) + { + continue; + } + if !table.columns.contains(target_column) { + continue; + } + let Some(data_type) = table.type_map.get(target_column) else { + continue; + }; + let raw = row.get(index).map(String::as_str).unwrap_or(""); + let value = if table.link_columns.contains(target_column) && !raw.is_empty() { + Value { + kind: Some(Kind::StringValue(raw.to_string())), + } + } else { + convert_input_value(raw, data_type, target_column).map_err(anyhow::Error::msg)? + }; + data.insert(target_column.clone(), value); + } + Ok(TableDataImportRow { data }) +} + +pub fn exportable_columns(schema: &TableStructureResponse) -> Vec { + schema + .columns + .iter() + .filter(|column| { + !column.is_primary_key + && column.name != "id" + && column.name != "deleted" + && column.name != "created_at" + && column.name != "row_revision" + }) + .map(|column| column.name.clone()) + .collect() +} diff --git a/komp-app/src/lib.rs b/komp-app/src/lib.rs new file mode 100644 index 00000000..7f0558e6 --- /dev/null +++ b/komp-app/src/lib.rs @@ -0,0 +1,15 @@ +//! Product-specific application code shared by the komp_ac frontends. +//! +//! This crate deliberately has no dependency on Ratatui, Crossterm, Tauri, or +//! a browser UI. Frontends own presentation and platform storage; this crate +//! owns client behavior that must not drift between them. + +pub mod auth; +pub mod csv; +pub mod grpc; +pub mod import_export; +pub mod navigation; +mod search; +pub mod session; +pub mod transport; +pub mod value; diff --git a/komp-app/src/navigation.rs b/komp-app/src/navigation.rs new file mode 100644 index 00000000..269caa86 --- /dev/null +++ b/komp-app/src/navigation.rs @@ -0,0 +1,36 @@ +use common::proto::komp_ac::tables_data::RowNavigationMode; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum UnfilteredNavigationMode { + Position, + #[default] + Id, +} + +impl UnfilteredNavigationMode { + pub fn parse(value: &str) -> Option { + match value { + "id" => Some(Self::Id), + "position" => Some(Self::Position), + _ => None, + } + } + + pub fn as_str(self) -> &'static str { + match self { + Self::Id => "id", + Self::Position => "position", + } + } +} + +impl From for RowNavigationMode { + fn from(value: UnfilteredNavigationMode) -> Self { + match value { + UnfilteredNavigationMode::Id => Self::Id, + UnfilteredNavigationMode::Position => Self::Position, + } + } +} diff --git a/komp-app/src/search.rs b/komp-app/src/search.rs new file mode 100644 index 00000000..e987be98 --- /dev/null +++ b/komp-app/src/search.rs @@ -0,0 +1,27 @@ +use anyhow::Result; +use common::proto::komp_ac::search::{ + SearchCountResponse, SearchRequest, SearchResponse, searcher_client::SearcherClient, +}; +use tonic::transport::Channel; +use tonic::Request; + +#[derive(Clone)] +pub struct SearchGrpc { + client: SearcherClient, +} + +impl SearchGrpc { + pub fn new(channel: Channel) -> Self { + Self { + client: SearcherClient::new(channel), + } + } + + pub async fn search(&mut self, request: Request) -> Result { + Ok(self.client.search(request).await?.into_inner()) + } + + pub async fn count(&mut self, request: Request) -> Result { + Ok(self.client.count(request).await?.into_inner()) + } +} diff --git a/komp-app/src/session.rs b/komp-app/src/session.rs new file mode 100644 index 00000000..0878c078 --- /dev/null +++ b/komp-app/src/session.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct StoredAuthData { + pub access_token: String, + pub user_id: String, + pub role: String, + pub username: String, + pub timezone: String, + pub phone_country: String, + #[serde(default)] + pub selected_profile: Option, +} diff --git a/komp-app/src/transport.rs b/komp-app/src/transport.rs new file mode 100644 index 00000000..1426e64a --- /dev/null +++ b/komp-app/src/transport.rs @@ -0,0 +1,125 @@ +use anyhow::{bail, Context, Result}; +use jiff::Timestamp; +use std::time::Duration; +use tonic::metadata::{Ascii, MetadataValue}; +use tonic::transport::{Channel, Endpoint}; +use tonic::Request; +use uuid::Uuid; + +pub const DEFAULT_GRPC_ENDPOINT: &str = "http://[::1]:50051"; + +pub fn normalize_endpoint(endpoint_url: &str) -> Result { + let endpoint = endpoint_url.trim(); + if endpoint.is_empty() { + bail!("Endpoint cannot be empty"); + } + let scheme = endpoint + .split_once("://") + .map(|(scheme, _)| scheme.to_ascii_lowercase()); + if !matches!(scheme.as_deref(), Some("http") | Some("https")) { + bail!("Expected an HTTP or HTTPS endpoint"); + } + Endpoint::from_shared(endpoint.to_string()).context("Endpoint is not a valid URI")?; + Ok(endpoint.to_string()) +} + +pub async fn connect_channel(endpoint_url: &str) -> Result { + let endpoint_url = normalize_endpoint(endpoint_url)?; + let endpoint = Endpoint::from_shared(endpoint_url) + .context("Endpoint is not a valid URI")? + .connect_timeout(Duration::from_secs(5)) + .tcp_keepalive(Some(Duration::from_secs(30))) + .keep_alive_while_idle(true) + .http2_keep_alive_interval(Duration::from_secs(15)) + .keep_alive_timeout(Duration::from_secs(5)); + endpoint + .connect() + .await + .context("Failed to create gRPC channel") +} + +pub fn bearer(token: &str) -> Result> { + MetadataValue::try_from(format!("Bearer {token}")) + .context("Failed to encode authorization header") +} + +pub fn authenticated_request(auth_token: Option<&str>, message: T) -> Result> { + let token = auth_token.context("Authentication is required for this request")?; + let mut request = Request::new(message); + request.metadata_mut().insert("authorization", bearer(token)?); + Ok(request) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PostOperation { + pub idempotency_key: Uuid, + pub created_at: Timestamp, +} + +impl PostOperation { + pub fn new() -> Self { + Self { + idempotency_key: Uuid::new_v4(), + created_at: Timestamp::now(), + } + } + + pub fn add_metadata(&self, request: &mut Request) -> Result<()> { + request.metadata_mut().insert( + "idempotency-key", + self.idempotency_key + .to_string() + .parse() + .context("Failed to encode idempotency key")?, + ); + request.metadata_mut().insert( + "operation-created-at", + self.created_at + .to_string() + .parse() + .context("Failed to encode operation creation time")?, + ); + Ok(()) + } +} + +impl Default for PostOperation { + fn default() -> Self { + Self::new() + } +} + +pub fn retryable_write_status(code: tonic::Code) -> bool { + matches!( + code, + tonic::Code::Cancelled + | tonic::Code::Unknown + | tonic::Code::DeadlineExceeded + | tonic::Code::Internal + | tonic::Code::Unavailable + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn authenticated_requests_have_bearer_metadata() { + let request = authenticated_request(Some("access-token"), ()).unwrap(); + assert_eq!( + request.metadata().get("authorization").unwrap(), + "Bearer access-token" + ); + } + + #[test] + fn post_operations_have_stable_metadata() { + let operation = PostOperation::new(); + let mut first = Request::new(()); + let mut second = Request::new(()); + operation.add_metadata(&mut first).unwrap(); + operation.add_metadata(&mut second).unwrap(); + assert_eq!(first.metadata(), second.metadata()); + } +} diff --git a/komp-app/src/value.rs b/komp-app/src/value.rs new file mode 100644 index 00000000..18cf358c --- /dev/null +++ b/komp-app/src/value.rs @@ -0,0 +1,81 @@ +use common::decimal::{is_decimal_data_type, parse_decimal_exact}; +use common::proto::komp_ac::table_structure::TableStructureResponse; +use prost_types::{NullValue, Value, value::Kind}; +use std::collections::HashMap; + +pub const BOOLEAN_INPUT_VOCABULARY: &str = + "must be one of true/t/yes/y/1 or false/f/no/n/0, in any case"; + +pub fn parse_boolean_input(raw: &str) -> Option { + match raw.to_ascii_lowercase().as_str() { + "true" | "t" | "yes" | "y" | "1" => Some(true), + "false" | "f" | "no" | "n" | "0" => Some(false), + _ => None, + } +} + +pub fn convert_input_value(raw: &str, data_type: &str, field: &str) -> Result { + let normalized = data_type.to_ascii_uppercase(); + let kind = if raw.is_empty() { + Kind::NullValue(NullValue::NullValue.into()) + } else if matches!(normalized.as_str(), "BOOL" | "BOOLEAN") { + parse_boolean_input(raw) + .map(Kind::BoolValue) + .ok_or_else(|| format!("Invalid boolean for '{field}': {BOOLEAN_INPUT_VOCABULARY}"))? + } else if matches!(normalized.as_str(), "INT8" | "BIGINT" | "BIGSERIAL") { + let value = raw + .parse::() + .map_err(|_| format!("Invalid big integer for '{field}': value must fit in 64 bits"))?; + Kind::StringValue(value.to_string()) + } else if matches!(normalized.as_str(), "INT" | "INT4" | "INTEGER" | "SERIAL") { + let value = raw + .parse::() + .map_err(|_| format!("Invalid integer for '{field}': value must fit in 32 bits"))?; + Kind::NumberValue(f64::from(value)) + } else if is_decimal_data_type(&normalized) { + let decimal = parse_decimal_exact(raw) + .map_err(|error| format!("Invalid decimal for '{field}': {error}"))?; + Kind::StringValue(decimal.to_string()) + } else { + Kind::StringValue(raw.to_string()) + }; + Ok(Value { kind: Some(kind) }) +} + +pub fn convert_and_validate_data( + data: &HashMap, + schema: &TableStructureResponse, +) -> Result, String> { + let type_map: HashMap<_, _> = schema + .columns + .iter() + .map(|column| (column.name.as_str(), column.data_type.as_str())) + .collect(); + data.iter() + .map(|(field, raw)| { + let data_type = type_map.get(field.as_str()).copied().unwrap_or("TEXT"); + convert_input_value(raw, data_type, field).map(|value| (field.clone(), value)) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn preserves_large_integers_and_decimals_as_strings() { + assert_eq!( + convert_input_value("9223372036854775807", "BIGINT", "amount") + .unwrap() + .kind, + Some(Kind::StringValue("9223372036854775807".to_string())) + ); + assert_eq!( + convert_input_value("123456789012345678901.25", "NUMERIC", "amount") + .unwrap() + .kind, + Some(Kind::StringValue("123456789012345678901.25".to_string())) + ); + } +} diff --git a/server b/server index a4c398e0..2e9522fe 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit a4c398e0ff6c3462d348221163946b81f86fc1ed +Subproject commit 2e9522fe257344df11a9968d572e57258a2a355f