Files
komp_ac/komp-app/src/grpc.rs
2026-08-30 23:59:13 +02:00

1417 lines
47 KiB
Rust

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<Channel>,
table_definition_client: TableDefinitionClient<Channel>,
table_script_client: TableScriptClient<Channel>,
tables_data_client: TablesDataClient<Channel>,
document_data_client: DocumentDataServiceClient<Channel>,
search_client: SearchGrpc,
table_validation_client: TableValidationServiceClient<Channel>,
accounting_client: AccountingClient<Channel>,
exchange_rate_client: ExchangeRateServiceClient<Channel>,
ecb_client: EcbServiceClient<Channel>,
analytics_client: AnalyticsServiceClient<Channel>,
search2_client: Search2Client<Channel>,
pub auth_token: Option<String>,
}
impl GrpcClient {
pub async fn new() -> Result<Self> {
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> {
Self::from_channel(connect_channel(endpoint).await?)
}
pub fn from_channel(channel: Channel) -> Result<Self> {
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<String>) {
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<String, String>,
) -> Result<HydrateScriptDependenciesResponse> {
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<T>(&self, message: T) -> Result<Request<T>> {
request_with_auth_token(self.auth_token.as_deref(), message)
}
pub async fn get_journal(&mut self, profile_name: String, journal_id: i64) -> Result<Journal> {
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<Account> {
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<SearchJournalsResponse> {
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<TableValidationResponse> {
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<UpdateFieldValidationResponse> {
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<UpsertValidationRuleResponse> {
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<UpsertValidationSetResponse> {
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<DeleteValidationRuleResponse> {
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<DeleteValidationSetResponse> {
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<ListValidationRulesResponse> {
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<ListValidationSetsResponse> {
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<ApplyValidationSetResponse> {
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<ReplaceTableValidationResponse> {
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<String>,
) -> Result<GetTableStructureResponse> {
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<TableStructureResponse> {
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<GetTableImportDescriptorResponse> {
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<ProfileTreeResponse> {
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<String>,
) -> Result<GetTableCatalogResponse> {
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<ListColumnTypesResponse> {
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<GetProfileDetailsResponse> {
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<ProfileExchangeRateSettings> {
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<ProfileForeignCurrency> {
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<TableDefinitionResponse> {
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<PutTableDefinitionResponse> {
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<CreateCustomExchangeRatesTableResponse> {
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<SetColumnPresentationResponse> {
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<SetColumnFormVisibilityResponse> {
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<TableScriptResponse> {
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<GetTableScriptsResponse> {
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<u64> {
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<GetLastTableDataResponse> {
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<GetAdjacentTableDataResponse> {
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<GetTableDataResponse> {
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<GetTableDataResponse> {
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<GetTableDataResponse> {
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<GetTableDataResponse> {
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<DeleteTableDataResponse> {
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<String, Value>,
operation: PostOperation,
) -> Result<PostTableDataResponse> {
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<BeginTableDataImportResponse> {
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<StageTableDataImportResponse> {
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<CommitTableDataImportResponse> {
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<AbortTableDataImportResponse> {
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<String, Value>,
expected_revision: i64,
) -> Result<PutTableDataResponse> {
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<String, Value>,
expected_revision: i64,
expected_affected_profiles: Vec<String>,
) -> Result<PutTableDataResponse> {
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<TableUpdateImpactResponse> {
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<ArchiveTableDataResponse> {
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<SaveDocumentResponse> {
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<ListTypstTemplatesResponse> {
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<ListTypstTemplatesResponse> {
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<RegisterTypstTemplateResponse> {
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<UpdateDocumentResponse> {
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<GetDocumentsResponse> {
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<GetDocumentResponse> {
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<GetDocumentVersionResponse> {
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<GetTypstTemplateVersionResponse> {
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<String>,
free_query: String,
must: Vec<ColumnConstraint>,
limit: Option<u32>,
offset: Option<u32>,
order: Option<SearchOrder>,
) -> Result<SearchResponse> {
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<u64> {
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<ColumnConstraint>,
) -> Result<u64> {
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<GetTableDataResponse> {
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<TableDefinitionResponse> {
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<common::proto::komp_ac::table_definition::DeleteTableResponse> {
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<common::proto::komp_ac::table_definition::CopyProfileResponse> {
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<common::proto::komp_ac::table_definition::GetColumnAliasRenameHistoryResponse> {
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<CreateInvoiceTemplateTableResponse> {
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<CreateInvoiceTemplateTableResponse> {
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<common::proto::komp_ac::exchange_rates::ListRateSourcesResponse> {
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<common::proto::komp_ac::exchange_rates::PreviewDirectConversionResponse> {
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<common::proto::komp_ac::exchange_rates::ListConversionEvidenceResponse> {
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<common::proto::komp_ac::ecb::GetEcbPipelineStatusResponse> {
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<common::proto::komp_ac::analytics::GetAnalyticsCatalogResponse> {
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<tonic::codec::Streaming<common::proto::komp_ac::analytics::AnalyticsResultBatch>> {
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<String>,
free_query: String,
limit: Option<u32>,
offset: Option<u32>,
version_scope: common::proto::komp_ac::search::SearchVersionScope,
) -> Result<SearchResponse> {
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<common::proto::komp_ac::search2::Search2Response> {
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"
);
}
}