fixing formatting bug for accounts to the client

This commit is contained in:
Priec
2026-08-14 00:27:33 +02:00
parent 05f29b82f6
commit b2f141ccba
2 changed files with 191 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
mod query_builder;
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::path::Path;
use std::sync::{Arc, Mutex};
@@ -32,6 +32,118 @@ const DEFAULT_LIST_LIMIT: usize = 5;
const SEARCH_SCORE_RELATIVE_FLOOR: f32 = 0.25;
const SEARCH_SCORE_GROUP_WIDTH: f32 = 0.25;
pub struct AccountPathProjection {
paths: HashMap<i64, String>,
account_column: Option<String>,
accounts_table: bool,
}
impl AccountPathProjection {
pub async fn load(
pool: &PgPool,
profile_name: &str,
table_name: &str,
physical_to_display: &HashMap<String, String>,
) -> Result<Self, Status> {
let account_column = physical_to_display
.get(common::system_column::ACCOUNT_REFERENCE_COLUMN)
.cloned();
let accounts_table = table_name == "accounts";
if account_column.is_none() && !accounts_table {
return Ok(Self {
paths: HashMap::new(),
account_column,
accounts_table,
});
}
let qualified_accounts = format!(
"\"{}\".\"accounts\"",
profile_name.replace('"', "\"\""),
);
let rows = sqlx::query(AssertSqlSafe(format!(
"SELECT id, segment, parent_account_id FROM {qualified_accounts} WHERE deleted = FALSE"
)))
.fetch_all(pool)
.await
.map_err(|error| Status::internal(format!("Account path lookup failed: {error}")))?;
let mut nodes = BTreeMap::new();
for row in rows {
let id: i64 = row.try_get("id").map_err(|error| {
Status::internal(format!("Account id read failed: {error}"))
})?;
let segment: String = row.try_get("segment").map_err(|error| {
Status::internal(format!("Account segment read failed: {error}"))
})?;
let parent: Option<i64> = row.try_get("parent_account_id").map_err(|error| {
Status::internal(format!("Account parent read failed: {error}"))
})?;
nodes.insert(id, (segment, parent));
}
let mut paths = HashMap::new();
for &id in nodes.keys() {
let mut segments = Vec::new();
let mut visited = BTreeSet::new();
let mut current = Some(id);
while let Some(node_id) = current {
if !visited.insert(node_id) {
return Err(Status::internal("Stored account hierarchy contains a cycle"));
}
let (segment, parent) = nodes.get(&node_id).ok_or_else(|| {
Status::internal(format!("Account {node_id} was not found"))
})?;
segments.push(segment.clone());
current = *parent;
}
segments.reverse();
paths.insert(id, segments.join("/"));
}
Ok(Self {
paths,
account_column,
accounts_table,
})
}
pub fn apply(&self, row_id: i64, value: &mut serde_json::Value) -> Result<(), Status> {
let Some(object) = value.as_object_mut() else {
return Ok(());
};
if let Some(column) = &self.account_column {
if let Some(stored) = object.get(column).filter(|value| !value.is_null()) {
let account_id = match stored {
serde_json::Value::Number(value) => value.as_i64(),
serde_json::Value::String(value) => value.parse().ok(),
_ => None,
}
.ok_or_else(|| Status::internal("Stored account reference is invalid"))?;
let path = self.paths.get(&account_id).ok_or_else(|| {
Status::internal(format!("Account {account_id} was not found"))
})?;
object.insert(column.clone(), serde_json::Value::String(path.clone()));
}
}
if self.accounts_table {
let path = self.paths.get(&row_id).ok_or_else(|| {
Status::internal(format!("Account {row_id} was not found"))
})?;
object.insert(
common::system_column::ACCOUNT_API_COLUMN.to_string(),
serde_json::Value::String(path.clone()),
);
}
Ok(())
}
pub fn row_display_columns(&self, columns: &[String]) -> Vec<String> {
if self.accounts_table {
vec![common::system_column::ACCOUNT_API_COLUMN.to_string()]
} else {
columns.to_vec()
}
}
}
pub struct SearcherService {
pub pool: PgPool,
profiles: Mutex<HashMap<String, Arc<ProfileIndex>>>,
@@ -1243,7 +1355,16 @@ async fn fetch_ordered_rows(
) -> Result<Vec<Hit>, Status> {
let physical_to_display = table_physical_to_display_map(pool, profile_name, table_name).await?;
let internal_columns = table_internal_column_names(pool, profile_name, table_name).await?;
let display_columns = table_row_display_columns(pool, profile_name, table_name).await?;
let account_projection = AccountPathProjection::load(
pool,
profile_name,
table_name,
&physical_to_display,
)
.await?;
let display_columns = account_projection.row_display_columns(
&table_row_display_columns(pool, profile_name, table_name).await?,
);
let (resolved_order, direction) = match order {
Some(order) => (
resolve_order_column(pool, profile_name, table_name, &order.column).await?,
@@ -1286,11 +1407,12 @@ async fn fetch_ordered_rows(
.get("version")
.and_then(|value| value.as_i64())
.unwrap_or(0);
let json_data = remap_json_to_display_names(
let mut json_data = remap_json_to_display_names(
json_data,
&physical_to_display,
&internal_columns,
)?;
account_projection.apply(id, &mut json_data)?;
let row_display_values = row_display_values(&json_data, &display_columns);
Ok(Hit {
id,
@@ -1531,7 +1653,16 @@ async fn run_search(
table_physical_to_display_map(pool, profile_name, &table_name).await?;
let internal_columns =
table_internal_column_names(pool, profile_name, &table_name).await?;
let display_columns = table_row_display_columns(pool, profile_name, &table_name).await?;
let account_projection = AccountPathProjection::load(
pool,
profile_name,
&table_name,
&physical_to_display,
)
.await?;
let display_columns = account_projection.row_display_columns(
&table_row_display_columns(pool, profile_name, &table_name).await?,
);
let qualified_table = qualified_visible_table(pool, profile_name, &table_name).await?;
let pg_ids = candidate_rows.iter().map(|item| item.0).collect::<Vec<_>>();
let revisions_by_id = candidate_rows.into_iter().collect::<HashMap<_, _>>();
@@ -1555,11 +1686,12 @@ async fn run_search(
continue;
}
let version = json_data.get("version").and_then(|value| value.as_i64()).unwrap_or(0);
let json_data = remap_json_to_display_names(
let mut json_data = remap_json_to_display_names(
json_data,
&physical_to_display,
&internal_columns,
)?;
account_projection.apply(id, &mut json_data)?;
let display_values = row_display_values(&json_data, &display_columns);
content_map.insert(search_row_key(&table_name, id), (
json_data.to_string(),
@@ -1587,7 +1719,16 @@ async fn run_search(
table_physical_to_display_map(pool, profile_name, &table_name).await?;
let internal_columns =
table_internal_column_names(pool, profile_name, &table_name).await?;
let display_columns = table_row_display_columns(pool, profile_name, &table_name).await?;
let account_projection = AccountPathProjection::load(
pool,
profile_name,
&table_name,
&physical_to_display,
)
.await?;
let display_columns = account_projection.row_display_columns(
&table_row_display_columns(pool, profile_name, &table_name).await?,
);
let table_definition_ids = archives.iter().map(|item| item.0).collect::<Vec<_>>();
let row_ids = archives.iter().map(|item| item.1).collect::<Vec<_>>();
let versions = archives.iter().map(|item| item.2).collect::<Vec<_>>();
@@ -1635,11 +1776,12 @@ async fn run_search(
row_id,
version,
);
let json_data = remap_json_to_display_names(
let mut json_data = remap_json_to_display_names(
snapshot,
&physical_to_display,
&internal_columns,
)?;
account_projection.apply(row_id, &mut json_data)?;
let display_values = row_display_values(&json_data, &display_columns);
content_map.insert(row_key, (
json_data.to_string(),
@@ -1683,7 +1825,16 @@ async fn fetch_ordered_candidate_rows(
) -> Result<Vec<Hit>, Status> {
let physical_to_display = table_physical_to_display_map(pool, profile_name, table_name).await?;
let internal_columns = table_internal_column_names(pool, profile_name, table_name).await?;
let display_columns = table_row_display_columns(pool, profile_name, table_name).await?;
let account_projection = AccountPathProjection::load(
pool,
profile_name,
table_name,
&physical_to_display,
)
.await?;
let display_columns = account_projection.row_display_columns(
&table_row_display_columns(pool, profile_name, table_name).await?,
);
let resolved_order =
resolve_order_column(pool, profile_name, table_name, &order.column).await?;
let qualified_table = qualified_visible_table(pool, profile_name, table_name).await?;
@@ -1754,11 +1905,12 @@ async fn fetch_ordered_candidate_rows(
.get("version")
.and_then(|value| value.as_i64())
.unwrap_or(0);
let json_data = remap_json_to_display_names(
let mut json_data = remap_json_to_display_names(
json_data,
&physical_to_display,
&internal_columns,
)?;
account_projection.apply(id, &mut json_data)?;
let display_values = row_display_values(&json_data, &display_columns);
Ok(Hit {
id,
@@ -1813,6 +1965,35 @@ mod tests {
assert_eq!(mapped, serde_json::json!({"customer": "Acme", "id": 4}));
}
#[test]
fn account_projection_replaces_internal_ids_with_public_paths() {
let projection = AccountPathProjection {
paths: HashMap::from([(3, "600/10".to_string())]),
account_column: Some("account".to_string()),
accounts_table: false,
};
let mut value = serde_json::json!({"account": 3, "name": "posting"});
projection.apply(9, &mut value).unwrap();
assert_eq!(value, serde_json::json!({"account": "600/10", "name": "posting"}));
}
#[test]
fn accounts_search_exposes_and_displays_the_complete_path() {
let projection = AccountPathProjection {
paths: HashMap::from([(3, "600/10".to_string())]),
account_column: None,
accounts_table: true,
};
let mut value = serde_json::json!({"segment": "10", "parent_account_id": 2});
projection.apply(3, &mut value).unwrap();
assert_eq!(value["account"], "600/10");
assert_eq!(projection.row_display_columns(&["segment".to_string()]), ["account"]);
}
#[test]
fn count_collector_returns_zero_documents_for_an_empty_index() {
let schema = create_search_schema();

2
server

Submodule server updated: d0504ad2a9...f83ebc802a