diff --git a/client b/client index 4ef069e7..dc88add4 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 4ef069e7b1972dd4b4e7fdb2bb6db6512eda29c3 +Subproject commit dc88add49e3cdb51f832d6768465a90b94a8ad51 diff --git a/client-gui2 b/client-gui2 index 782877f6..965938b5 160000 --- a/client-gui2 +++ b/client-gui2 @@ -1 +1 @@ -Subproject commit 782877f62aa4523c6c1dee718b38b3c611c5dad4 +Subproject commit 965938b510f238bd848fcccf68686ad1b98a0ad3 diff --git a/common/proto/search.proto b/common/proto/search.proto index 2418b78a..508356b1 100644 --- a/common/proto/search.proto +++ b/common/proto/search.proto @@ -47,12 +47,18 @@ message ReindexResponse { uint64 affected_tables = 2; } +enum SearchIndexOperation { + SEARCH_INDEX_OPERATION_UNSPECIFIED = 0; + SEARCH_INDEX_OPERATION_UPSERT = 1; + SEARCH_INDEX_OPERATION_DELETE = 2; +} + message SearchIndexError { int64 job_id = 1; string profile_name = 2; string table_name = 3; int64 row_id = 4; - string operation = 5; + SearchIndexOperation operation = 5; int32 attempts = 6; string last_error = 7; } diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index 3e1281e2..8c37b5ee 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -437,7 +437,7 @@ message ProfileTreeResponse { repeated string row_display_columns = 4; // "dynamic" for user-defined tables, "system" for backend-managed tables. - string table_kind = 5; + ManagedTableKind table_kind = 5; // True when this table is shared by every profile. bool global = 6; @@ -590,6 +590,12 @@ message GetAliasChangeHistoryResponse { repeated AliasChangeHistoryEntry entries = 2; } +enum ManagedTableKind { + MANAGED_TABLE_KIND_UNSPECIFIED = 0; + MANAGED_TABLE_KIND_DYNAMIC = 1; + MANAGED_TABLE_KIND_SYSTEM = 2; +} + // Describes a table with its columns and associated scripts. message TableDetail { string name = 1; @@ -598,7 +604,7 @@ message TableDetail { repeated ScriptInfo scripts = 4; repeated string row_display_columns = 6; map column_behaviors = 7; - string table_kind = 8; + ManagedTableKind table_kind = 8; bool global = 9; // Revision of this table definition for optimistic concurrency control. int64 row_version = 10; diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 9f81056d..37f665a6 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/komp_ac.search.rs b/common/src/proto/komp_ac.search.rs index 8d8e8cae..df1fd04e 100644 --- a/common/src/proto/komp_ac.search.rs +++ b/common/src/proto/komp_ac.search.rs @@ -42,8 +42,8 @@ pub struct SearchIndexError { pub table_name: ::prost::alloc::string::String, #[prost(int64, tag = "4")] pub row_id: i64, - #[prost(string, tag = "5")] - pub operation: ::prost::alloc::string::String, + #[prost(enumeration = "SearchIndexOperation", tag = "5")] + pub operation: i32, #[prost(int32, tag = "6")] pub attempts: i32, #[prost(string, tag = "7")] @@ -181,6 +181,36 @@ pub struct BatchCountResponse { #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] +pub enum SearchIndexOperation { + Unspecified = 0, + Upsert = 1, + Delete = 2, +} +impl SearchIndexOperation { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "SEARCH_INDEX_OPERATION_UNSPECIFIED", + Self::Upsert => "SEARCH_INDEX_OPERATION_UPSERT", + Self::Delete => "SEARCH_INDEX_OPERATION_DELETE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "SEARCH_INDEX_OPERATION_UNSPECIFIED" => Some(Self::Unspecified), + "SEARCH_INDEX_OPERATION_UPSERT" => Some(Self::Upsert), + "SEARCH_INDEX_OPERATION_DELETE" => Some(Self::Delete), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] pub enum MatchMode { Unspecified = 0, Fuzzy = 1, diff --git a/common/src/proto/komp_ac.table_definition.rs b/common/src/proto/komp_ac.table_definition.rs index cde3e16f..e824406f 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -389,8 +389,8 @@ pub mod profile_tree_response { ::prost::alloc::string::String, >, /// "dynamic" for user-defined tables, "system" for backend-managed tables. - #[prost(string, tag = "5")] - pub table_kind: ::prost::alloc::string::String, + #[prost(enumeration = "super::ManagedTableKind", tag = "5")] + pub table_kind: i32, /// True when this table is shared by every profile. #[prost(bool, tag = "6")] pub global: bool, @@ -625,8 +625,8 @@ pub struct TableDetail { ::prost::alloc::string::String, ColumnBehavior, >, - #[prost(string, tag = "8")] - pub table_kind: ::prost::alloc::string::String, + #[prost(enumeration = "ManagedTableKind", tag = "8")] + pub table_kind: i32, #[prost(bool, tag = "9")] pub global: bool, /// Revision of this table definition for optimistic concurrency control. @@ -1156,6 +1156,36 @@ impl AliasChangeKind { } } } +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ManagedTableKind { + Unspecified = 0, + Dynamic = 1, + System = 2, +} +impl ManagedTableKind { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "MANAGED_TABLE_KIND_UNSPECIFIED", + Self::Dynamic => "MANAGED_TABLE_KIND_DYNAMIC", + Self::System => "MANAGED_TABLE_KIND_SYSTEM", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "MANAGED_TABLE_KIND_UNSPECIFIED" => Some(Self::Unspecified), + "MANAGED_TABLE_KIND_DYNAMIC" => Some(Self::Dynamic), + "MANAGED_TABLE_KIND_SYSTEM" => Some(Self::System), + _ => None, + } + } +} /// 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)] diff --git a/server b/server index c51ead9a..8dc36863 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit c51ead9a190add6150e10d96e34b1359c1d73306 +Subproject commit 8dc3686366c04119f37a896eddace84c8be97808 diff --git a/web/src/pages/add_table/loader.rs b/web/src/pages/add_table/loader.rs index 54e6f88e..01ccf4ae 100644 --- a/web/src/pages/add_table/loader.rs +++ b/web/src/pages/add_table/loader.rs @@ -112,12 +112,13 @@ pub(crate) async fn load_page( // The profile's ledger accounts are not a link target of their own: an // ACCOUNTING column is how a row is posted to one. .filter(|table| table.name != crate::schema::LEDGER_ACCOUNTS_TABLE) - .map(|table| RelationTableOption { + .map(|table| Ok(RelationTableOption { name: table.name, global: table.global, - system: table.table_kind == "system", - }) - .collect::>(); + system: table_scope::table_kind(table.table_kind).map_err(LoadError::Backend)? + == crate::definitions::table_definition::ManagedTableKind::System, + })) + .collect::, LoadError>>()?; // Every table the new one would sit beside holds its name — including the // shared ones, which exist in every profile, and including in a profile diff --git a/web/src/pages/admin/admin/loader.rs b/web/src/pages/admin/admin/loader.rs index 491bfd46..db1e29cf 100644 --- a/web/src/pages/admin/admin/loader.rs +++ b/web/src/pages/admin/admin/loader.rs @@ -101,7 +101,7 @@ pub(crate) async fn load_admin_page( .map(|tables| { tables .into_iter() - .map(|table| TableView { + .map(|table| Ok(TableView { name: table.name, // One entry per link, named by the column carrying it, so a // table pointing at one target twice reads as two links. @@ -113,10 +113,11 @@ pub(crate) async fn load_admin_page( }) .collect(), row_display_columns: table.row_display_columns, - table_kind: table.table_kind, - }) - .collect::>() + table_kind: table_scope::table_kind(table.table_kind).map_err(LoadError::Backend)?, + })) + .collect::, LoadError>>() }) + .transpose()? .unwrap_or_default(); let selected_table = (!selection.table.is_empty()).then_some(selection.table); diff --git a/web/src/pages/admin/admin/state.rs b/web/src/pages/admin/admin/state.rs index 4fe30b4d..665ae7ae 100644 --- a/web/src/pages/admin/admin/state.rs +++ b/web/src/pages/admin/admin/state.rs @@ -84,14 +84,14 @@ pub(crate) struct TableView { pub name: String, pub depends_on: Vec, pub row_display_columns: Vec, - pub table_kind: String, + pub table_kind: crate::definitions::table_definition::ManagedTableKind, } impl TableView { /// System tables are backend-managed: the server refuses every structural /// write on them, so the pane offers none of the action links for one. pub(crate) fn is_system(&self) -> bool { - self.table_kind == "system" + self.table_kind == crate::definitions::table_definition::ManagedTableKind::System } } diff --git a/web/src/pages/admin/admin/ui.rs b/web/src/pages/admin/admin/ui.rs index 02b4c9b4..f716b5e3 100644 --- a/web/src/pages/admin/admin/ui.rs +++ b/web/src/pages/admin/admin/ui.rs @@ -155,7 +155,7 @@ mod tests { name: "invoice".to_string(), depends_on: Vec::new(), row_display_columns: vec!["number".to_string()], - table_kind: "dynamic".to_string(), + table_kind: crate::definitions::table_definition::ManagedTableKind::Dynamic, }], selected_table: Some("invoice".to_string()), columns: Vec::new(), @@ -195,7 +195,7 @@ mod tests { name: "currencies".to_string(), depends_on: Vec::new(), row_display_columns: vec!["code".to_string()], - table_kind: "dynamic".to_string(), + table_kind: crate::definitions::table_definition::ManagedTableKind::Dynamic, }], selected_table: Some("currencies".to_string()), columns: Vec::new(), diff --git a/web/src/pages/admin/table_definition/loader.rs b/web/src/pages/admin/table_definition/loader.rs index 6e1ed01e..4f905a47 100644 --- a/web/src/pages/admin/table_definition/loader.rs +++ b/web/src/pages/admin/table_definition/loader.rs @@ -146,9 +146,9 @@ pub(crate) async fn load_page( } else { table_scope::linkable_tables(&tree.profiles, &catalog_tables, &inputs.selection.profile) }; - let summary = |table: crate::definitions::table_definition::profile_tree_response::Table| TableSummary { + let summary = |table: crate::definitions::table_definition::profile_tree_response::Table| -> Result { Ok(TableSummary { name: table.name, - table_kind: table.table_kind, + table_kind: table_scope::table_kind(table.table_kind).map_err(LoadError::Backend)?, global: table.global, // One entry per link, named by the column carrying it, so a table // pointing at one target twice reads as two links. @@ -157,9 +157,9 @@ pub(crate) async fn load_page( .into_iter() .map(|dependency| format!("{} ({})", dependency.table_name, dependency.column_name)) .collect(), - }; - let tables = selected_tables.into_iter().map(summary).collect::>(); - let link_targets = link_targets.into_iter().map(summary).collect::>(); + }) }; + let tables = selected_tables.into_iter().map(summary).collect::, _>>()?; + let link_targets = link_targets.into_iter().map(summary).collect::, _>>()?; // A table the scope does not hold is dropped rather than acted on. It is // said out loud, though: dropping it in silence is what left the panel diff --git a/web/src/pages/admin/table_definition/state.rs b/web/src/pages/admin/table_definition/state.rs index d6de9cca..1caf59e0 100644 --- a/web/src/pages/admin/table_definition/state.rs +++ b/web/src/pages/admin/table_definition/state.rs @@ -67,7 +67,7 @@ impl Selection { #[derive(Clone, Debug)] pub(crate) struct TableSummary { pub name: String, - pub table_kind: String, + pub table_kind: crate::definitions::table_definition::ManagedTableKind, pub global: bool, pub depends_on: Vec, } @@ -76,7 +76,7 @@ impl TableSummary { /// System tables are backend-managed: the server refuses every write below /// on them, so the workspace does not offer the panels either. pub(crate) fn is_system(&self) -> bool { - self.table_kind == "system" + self.table_kind == crate::definitions::table_definition::ManagedTableKind::System } } diff --git a/web/src/pages/admin/table_definition/ui.rs b/web/src/pages/admin/table_definition/ui.rs index 88d8eee4..74611a9d 100644 --- a/web/src/pages/admin/table_definition/ui.rs +++ b/web/src/pages/admin/table_definition/ui.rs @@ -247,10 +247,10 @@ mod tests { schema::ColumnDraft, }; - fn table(name: &str, kind: &str) -> TableSummary { + fn table(name: &str, kind: crate::definitions::table_definition::ManagedTableKind) -> TableSummary { TableSummary { name: name.to_string(), - table_kind: kind.to_string(), + table_kind: kind, global: false, depends_on: Vec::new(), } @@ -268,8 +268,8 @@ mod tests { profile: "billing".to_string(), table: "invoice".to_string(), }, - tables: vec![table("invoice", "dynamic"), table("ledger_accounts", "system")], - link_targets: vec![table("invoice", "dynamic"), table("ledger_accounts", "system")], + tables: vec![table("invoice", crate::definitions::table_definition::ManagedTableKind::Dynamic), table("ledger_accounts", crate::definitions::table_definition::ManagedTableKind::System)], + link_targets: vec![table("invoice", crate::definitions::table_definition::ManagedTableKind::Dynamic), table("ledger_accounts", crate::definitions::table_definition::ManagedTableKind::System)], detail: Some(TableDetailView { id: 7, row_version: 1, @@ -364,7 +364,7 @@ mod tests { }; state.tables = vec![TableSummary { name: "currencies".to_string(), - table_kind: "dynamic".to_string(), + table_kind: crate::definitions::table_definition::ManagedTableKind::Dynamic, global: true, depends_on: Vec::new(), }]; @@ -598,12 +598,12 @@ mod tests { state.columns.type_input = "link".to_string(); // The link targets are their own list: a profile's table may point at // a shared one, which is not among the profile's own tables. - state.link_targets.push(table("customer", "dynamic")); + state.link_targets.push(table("customer", crate::definitions::table_definition::ManagedTableKind::Dynamic)); state.link_targets.push(TableSummary { global: true, - ..table("currencies", "dynamic") + ..table("currencies", crate::definitions::table_definition::ManagedTableKind::Dynamic) }); - state.link_targets.push(table("audit_log", "system")); + state.link_targets.push(table("audit_log", crate::definitions::table_definition::ManagedTableKind::System)); let html = render_column_panel(&state); diff --git a/web/src/pages/table_scope.rs b/web/src/pages/table_scope.rs index 14f7fd7e..27c93fc4 100644 --- a/web/src/pages/table_scope.rs +++ b/web/src/pages/table_scope.rs @@ -13,6 +13,14 @@ //! profile's own tables are the tree's. Neither is derived from the other. use crate::definitions::table_definition::profile_tree_response::{Profile, Table}; +use crate::definitions::table_definition::ManagedTableKind; + +pub(crate) fn table_kind(value: i32) -> Result { + match ManagedTableKind::try_from(value) { + Ok(kind @ (ManagedTableKind::Dynamic | ManagedTableKind::System)) => Ok(kind), + _ => Err(format!("Invalid managed table kind: {value}")), + } +} /// The tables of the global scope: the catalog for `profile_name: None`, which /// is every shared table in the deployment, whether or not any profile exists @@ -74,7 +82,7 @@ pub(crate) mod tests { name: name.to_string(), depends_on: Vec::new(), row_display_columns: Vec::new(), - table_kind: "dynamic".to_string(), + table_kind: ManagedTableKind::Dynamic.into(), global, profile_name: if global { SHARED_PROFILE.to_string()