fixing formatting bug for accounts to the client
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
mod query_builder;
|
mod query_builder;
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, Mutex};
|
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_RELATIVE_FLOOR: f32 = 0.25;
|
||||||
const SEARCH_SCORE_GROUP_WIDTH: 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 struct SearcherService {
|
||||||
pub pool: PgPool,
|
pub pool: PgPool,
|
||||||
profiles: Mutex<HashMap<String, Arc<ProfileIndex>>>,
|
profiles: Mutex<HashMap<String, Arc<ProfileIndex>>>,
|
||||||
@@ -1243,7 +1355,16 @@ async fn fetch_ordered_rows(
|
|||||||
) -> Result<Vec<Hit>, Status> {
|
) -> Result<Vec<Hit>, Status> {
|
||||||
let physical_to_display = table_physical_to_display_map(pool, profile_name, table_name).await?;
|
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 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 {
|
let (resolved_order, direction) = match order {
|
||||||
Some(order) => (
|
Some(order) => (
|
||||||
resolve_order_column(pool, profile_name, table_name, &order.column).await?,
|
resolve_order_column(pool, profile_name, table_name, &order.column).await?,
|
||||||
@@ -1286,11 +1407,12 @@ async fn fetch_ordered_rows(
|
|||||||
.get("version")
|
.get("version")
|
||||||
.and_then(|value| value.as_i64())
|
.and_then(|value| value.as_i64())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let json_data = remap_json_to_display_names(
|
let mut json_data = remap_json_to_display_names(
|
||||||
json_data,
|
json_data,
|
||||||
&physical_to_display,
|
&physical_to_display,
|
||||||
&internal_columns,
|
&internal_columns,
|
||||||
)?;
|
)?;
|
||||||
|
account_projection.apply(id, &mut json_data)?;
|
||||||
let row_display_values = row_display_values(&json_data, &display_columns);
|
let row_display_values = row_display_values(&json_data, &display_columns);
|
||||||
Ok(Hit {
|
Ok(Hit {
|
||||||
id,
|
id,
|
||||||
@@ -1531,7 +1653,16 @@ async fn run_search(
|
|||||||
table_physical_to_display_map(pool, profile_name, &table_name).await?;
|
table_physical_to_display_map(pool, profile_name, &table_name).await?;
|
||||||
let internal_columns =
|
let internal_columns =
|
||||||
table_internal_column_names(pool, profile_name, &table_name).await?;
|
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 qualified_table = qualified_visible_table(pool, profile_name, &table_name).await?;
|
||||||
let pg_ids = candidate_rows.iter().map(|item| item.0).collect::<Vec<_>>();
|
let pg_ids = candidate_rows.iter().map(|item| item.0).collect::<Vec<_>>();
|
||||||
let revisions_by_id = candidate_rows.into_iter().collect::<HashMap<_, _>>();
|
let revisions_by_id = candidate_rows.into_iter().collect::<HashMap<_, _>>();
|
||||||
@@ -1555,11 +1686,12 @@ async fn run_search(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let version = json_data.get("version").and_then(|value| value.as_i64()).unwrap_or(0);
|
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,
|
json_data,
|
||||||
&physical_to_display,
|
&physical_to_display,
|
||||||
&internal_columns,
|
&internal_columns,
|
||||||
)?;
|
)?;
|
||||||
|
account_projection.apply(id, &mut json_data)?;
|
||||||
let display_values = row_display_values(&json_data, &display_columns);
|
let display_values = row_display_values(&json_data, &display_columns);
|
||||||
content_map.insert(search_row_key(&table_name, id), (
|
content_map.insert(search_row_key(&table_name, id), (
|
||||||
json_data.to_string(),
|
json_data.to_string(),
|
||||||
@@ -1587,7 +1719,16 @@ async fn run_search(
|
|||||||
table_physical_to_display_map(pool, profile_name, &table_name).await?;
|
table_physical_to_display_map(pool, profile_name, &table_name).await?;
|
||||||
let internal_columns =
|
let internal_columns =
|
||||||
table_internal_column_names(pool, profile_name, &table_name).await?;
|
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 table_definition_ids = archives.iter().map(|item| item.0).collect::<Vec<_>>();
|
||||||
let row_ids = archives.iter().map(|item| item.1).collect::<Vec<_>>();
|
let row_ids = archives.iter().map(|item| item.1).collect::<Vec<_>>();
|
||||||
let versions = archives.iter().map(|item| item.2).collect::<Vec<_>>();
|
let versions = archives.iter().map(|item| item.2).collect::<Vec<_>>();
|
||||||
@@ -1635,11 +1776,12 @@ async fn run_search(
|
|||||||
row_id,
|
row_id,
|
||||||
version,
|
version,
|
||||||
);
|
);
|
||||||
let json_data = remap_json_to_display_names(
|
let mut json_data = remap_json_to_display_names(
|
||||||
snapshot,
|
snapshot,
|
||||||
&physical_to_display,
|
&physical_to_display,
|
||||||
&internal_columns,
|
&internal_columns,
|
||||||
)?;
|
)?;
|
||||||
|
account_projection.apply(row_id, &mut json_data)?;
|
||||||
let display_values = row_display_values(&json_data, &display_columns);
|
let display_values = row_display_values(&json_data, &display_columns);
|
||||||
content_map.insert(row_key, (
|
content_map.insert(row_key, (
|
||||||
json_data.to_string(),
|
json_data.to_string(),
|
||||||
@@ -1683,7 +1825,16 @@ async fn fetch_ordered_candidate_rows(
|
|||||||
) -> Result<Vec<Hit>, Status> {
|
) -> Result<Vec<Hit>, Status> {
|
||||||
let physical_to_display = table_physical_to_display_map(pool, profile_name, table_name).await?;
|
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 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 =
|
let resolved_order =
|
||||||
resolve_order_column(pool, profile_name, table_name, &order.column).await?;
|
resolve_order_column(pool, profile_name, table_name, &order.column).await?;
|
||||||
let qualified_table = qualified_visible_table(pool, profile_name, table_name).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")
|
.get("version")
|
||||||
.and_then(|value| value.as_i64())
|
.and_then(|value| value.as_i64())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let json_data = remap_json_to_display_names(
|
let mut json_data = remap_json_to_display_names(
|
||||||
json_data,
|
json_data,
|
||||||
&physical_to_display,
|
&physical_to_display,
|
||||||
&internal_columns,
|
&internal_columns,
|
||||||
)?;
|
)?;
|
||||||
|
account_projection.apply(id, &mut json_data)?;
|
||||||
let display_values = row_display_values(&json_data, &display_columns);
|
let display_values = row_display_values(&json_data, &display_columns);
|
||||||
Ok(Hit {
|
Ok(Hit {
|
||||||
id,
|
id,
|
||||||
@@ -1813,6 +1965,35 @@ mod tests {
|
|||||||
assert_eq!(mapped, serde_json::json!({"customer": "Acme", "id": 4}));
|
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]
|
#[test]
|
||||||
fn count_collector_returns_zero_documents_for_an_empty_index() {
|
fn count_collector_returns_zero_documents_for_an_empty_index() {
|
||||||
let schema = create_search_schema();
|
let schema = create_search_schema();
|
||||||
|
|||||||
2
server
2
server
Submodule server updated: d0504ad2a9...f83ebc802a
Reference in New Issue
Block a user