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, GetTypstTemplateVersionRequest, GetTypstTemplateVersionResponse, ListEnabledTypstTemplatesRequest, ListTypstTemplatesRequest, ListTypstTemplatesResponse, RegisterTypstTemplateRequest, RegisterTypstTemplateResponse, 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::{ ApplyInvoiceTemplateTableRequest, CreateCustomExchangeRatesTableRequest, CreateCustomExchangeRatesTableResponse, CreateInvoiceTemplateTableResponse, GetProfileDetailsRequest, GetProfileDetailsResponse, GetTableCatalogRequest, GetTableCatalogResponse, ListColumnTypesResponse, PostTableDefinitionRequest, ProfileTreeResponse, PutTableDefinitionRequest, PutTableDefinitionResponse, SetColumnFormVisibilityRequest, SetColumnFormVisibilityResponse, 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 set_column_form_visibility( &mut self, request: SetColumnFormVisibilityRequest, ) -> Result { let tonic_request = self.authenticated_request(request)?; let response = self .table_definition_client .set_column_form_visibility(tonic_request) .await .context("gRPC SetColumnFormVisibility 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 list_enabled_typst_templates( &mut self, profile_name: String, ) -> Result { let request = self.authenticated_request(ListEnabledTypstTemplatesRequest { profile_name, })?; let response = self .document_data_client .list_enabled_typst_templates(request) .await .context("gRPC ListEnabledTypstTemplates call failed")?; Ok(response.into_inner()) } pub async fn list_typst_templates( &mut self, search: String, limit: u32, ) -> Result { let request = self.authenticated_request(ListTypstTemplatesRequest { search, limit })?; let response = self .document_data_client .list_typst_templates(request) .await .context("gRPC ListTypstTemplates call failed")?; Ok(response.into_inner()) } pub async fn register_typst_template( &mut self, request: RegisterTypstTemplateRequest, ) -> Result { let request = self.authenticated_request(request)?; let response = self .document_data_client .register_typst_template(request) .await .context("gRPC RegisterTypstTemplate 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 get_typst_template_version( &mut self, request: GetTypstTemplateVersionRequest, ) -> Result { let request = self.authenticated_request(request)?; let response = self .document_data_client .get_typst_template_version(request) .await .context("gRPC GetTypstTemplateVersion 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 apply_invoice_template_table( &mut self, request: ApplyInvoiceTemplateTableRequest, ) -> Result { let request = self.authenticated_request(request)?; Ok(self .table_definition_client .apply_invoice_template_table(request) .await .context("gRPC ApplyInvoiceTemplateTable 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" ); } }