alias is surface, real column name is internal
This commit is contained in:
@@ -438,11 +438,13 @@ async fn resolve_constraint_targets(
|
|||||||
.map_err(|e| Status::internal(format!("Column mapping lookup failed: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Column mapping lookup failed: {}", e)))?;
|
||||||
|
|
||||||
if rows.is_empty() {
|
if rows.is_empty() {
|
||||||
let target = SearchConstraintTarget {
|
return Err(Status::invalid_argument(format!(
|
||||||
table_name: table_filter.map(str::to_string),
|
"Column alias '{}' was not found{}",
|
||||||
column: column.to_string(),
|
column,
|
||||||
};
|
table_filter
|
||||||
return Ok(vec![target]);
|
.map(|table_name| format!(" in table '{}'", table_name))
|
||||||
|
.unwrap_or_default()
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
@@ -527,20 +529,35 @@ async fn table_row_display_columns(
|
|||||||
fn remap_json_to_display_names(
|
fn remap_json_to_display_names(
|
||||||
value: serde_json::Value,
|
value: serde_json::Value,
|
||||||
physical_to_display: &HashMap<String, String>,
|
physical_to_display: &HashMap<String, String>,
|
||||||
) -> serde_json::Value {
|
) -> Result<serde_json::Value, Status> {
|
||||||
match value {
|
match value {
|
||||||
serde_json::Value::Object(object) => {
|
serde_json::Value::Object(object) => {
|
||||||
let mut remapped = serde_json::Map::with_capacity(object.len());
|
let mut remapped = serde_json::Map::with_capacity(object.len());
|
||||||
for (key, value) in object {
|
for (key, value) in object {
|
||||||
let final_key = physical_to_display.get(&key).cloned().unwrap_or(key);
|
let final_key = match physical_to_display.get(&key) {
|
||||||
|
Some(display_name) => display_name.clone(),
|
||||||
|
None if is_public_system_column(&key) => key,
|
||||||
|
None => {
|
||||||
|
return Err(Status::failed_precondition(
|
||||||
|
"A table column has no public alias mapping",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
remapped.insert(final_key, value);
|
remapped.insert(final_key, value);
|
||||||
}
|
}
|
||||||
serde_json::Value::Object(remapped)
|
Ok(serde_json::Value::Object(remapped))
|
||||||
}
|
}
|
||||||
other => other,
|
other => Ok(other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_public_system_column(name: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
name,
|
||||||
|
"id" | "deleted" | "created_at" | "row_revision" | "account_id"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// One value per display column, positionally aligned with them, so a column
|
/// One value per display column, positionally aligned with them, so a column
|
||||||
/// that is NULL for this row stays visible as an empty slot.
|
/// that is NULL for this row stays visible as an empty slot.
|
||||||
fn row_display_values(value: &serde_json::Value, columns: &[String]) -> Vec<String> {
|
fn row_display_values(value: &serde_json::Value, columns: &[String]) -> Vec<String> {
|
||||||
@@ -593,12 +610,17 @@ async fn resolve_order_column(
|
|||||||
table_physical_to_display_map(pool, profile_name, table_name).await?;
|
table_physical_to_display_map(pool, profile_name, table_name).await?;
|
||||||
let physical_column = physical_to_display
|
let physical_column = physical_to_display
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(physical, display)| {
|
.find(|(_, display)| display.eq_ignore_ascii_case(&requested_column))
|
||||||
physical.eq_ignore_ascii_case(&requested_column)
|
|
||||||
|| display.eq_ignore_ascii_case(&requested_column)
|
|
||||||
})
|
|
||||||
.map(|(physical, _)| physical.clone())
|
.map(|(physical, _)| physical.clone())
|
||||||
.unwrap_or_else(|| requested_column.clone());
|
.or_else(|| {
|
||||||
|
is_public_system_column(&requested_column).then(|| requested_column.clone())
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Status::invalid_argument(format!(
|
||||||
|
"Column alias '{}' was not found in table '{}.{}'",
|
||||||
|
requested_column, profile_name, table_name
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
let physical_column = sqlx::query_scalar::<_, String>(
|
let physical_column = sqlx::query_scalar::<_, String>(
|
||||||
r#"
|
r#"
|
||||||
@@ -698,15 +720,21 @@ async fn fetch_ordered_rows(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| Status::internal(format!("DB query for default results failed: {}", e)))?;
|
.map_err(|e| Status::internal(format!("DB query for default results failed: {}", e)))?;
|
||||||
|
|
||||||
Ok(rows
|
rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| {
|
.map(|row| -> Result<Hit, Status> {
|
||||||
let id: i64 = row.try_get("id").unwrap_or_default();
|
let id: i64 = row
|
||||||
let json_data: serde_json::Value = row.try_get("data").unwrap_or_default();
|
.try_get("id")
|
||||||
let position: i64 = row.try_get("picker_position").unwrap_or_default();
|
.map_err(|error| Status::internal(format!("Search id read failed: {}", error)))?;
|
||||||
let json_data = remap_json_to_display_names(json_data, &physical_to_display);
|
let json_data: serde_json::Value = row.try_get("data").map_err(|error| {
|
||||||
|
Status::internal(format!("Search row read failed: {}", error))
|
||||||
|
})?;
|
||||||
|
let position: i64 = row.try_get("picker_position").map_err(|error| {
|
||||||
|
Status::internal(format!("Search position read failed: {}", error))
|
||||||
|
})?;
|
||||||
|
let json_data = remap_json_to_display_names(json_data, &physical_to_display)?;
|
||||||
let row_display_values = row_display_values(&json_data, &display_columns);
|
let row_display_values = row_display_values(&json_data, &display_columns);
|
||||||
Hit {
|
Ok(Hit {
|
||||||
id,
|
id,
|
||||||
score: 0.0,
|
score: 0.0,
|
||||||
content_json: json_data.to_string(),
|
content_json: json_data.to_string(),
|
||||||
@@ -714,9 +742,9 @@ async fn fetch_ordered_rows(
|
|||||||
row_display_values,
|
row_display_values,
|
||||||
row_display_columns: display_columns.clone(),
|
row_display_columns: display_columns.clone(),
|
||||||
position: u64::try_from(position).ok(),
|
position: u64::try_from(position).ok(),
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
.collect())
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_search(
|
async fn run_search(
|
||||||
@@ -841,7 +869,7 @@ async fn run_search(
|
|||||||
for row in rows {
|
for row in rows {
|
||||||
let id: i64 = row.try_get("id").unwrap_or_default();
|
let id: i64 = row.try_get("id").unwrap_or_default();
|
||||||
let json_data: serde_json::Value = row.try_get("data").unwrap_or_default();
|
let json_data: serde_json::Value = row.try_get("data").unwrap_or_default();
|
||||||
let json_data = remap_json_to_display_names(json_data, &physical_to_display);
|
let json_data = remap_json_to_display_names(json_data, &physical_to_display)?;
|
||||||
let display_values = row_display_values(&json_data, &display_columns);
|
let display_values = row_display_values(&json_data, &display_columns);
|
||||||
content_map.insert(
|
content_map.insert(
|
||||||
(table_name.clone(), id),
|
(table_name.clone(), id),
|
||||||
@@ -923,16 +951,24 @@ async fn fetch_ordered_candidate_rows(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| Status::internal(format!("Ordered search query failed: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Ordered search query failed: {}", e)))?;
|
||||||
|
|
||||||
Ok(rows
|
rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| {
|
.map(|row| -> Result<Hit, Status> {
|
||||||
let id: i64 = row.try_get("id").unwrap_or_default();
|
let id: i64 = row
|
||||||
let json_data: serde_json::Value = row.try_get("data").unwrap_or_default();
|
.try_get("id")
|
||||||
let position: i64 = row.try_get("picker_position").unwrap_or_default();
|
.map_err(|error| Status::internal(format!("Search id read failed: {}", error)))?;
|
||||||
let score: f32 = row.try_get("candidate_score").unwrap_or_default();
|
let json_data: serde_json::Value = row.try_get("data").map_err(|error| {
|
||||||
let json_data = remap_json_to_display_names(json_data, &physical_to_display);
|
Status::internal(format!("Search row read failed: {}", error))
|
||||||
|
})?;
|
||||||
|
let position: i64 = row.try_get("picker_position").map_err(|error| {
|
||||||
|
Status::internal(format!("Search position read failed: {}", error))
|
||||||
|
})?;
|
||||||
|
let score: f32 = row.try_get("candidate_score").map_err(|error| {
|
||||||
|
Status::internal(format!("Search score read failed: {}", error))
|
||||||
|
})?;
|
||||||
|
let json_data = remap_json_to_display_names(json_data, &physical_to_display)?;
|
||||||
let display_values = row_display_values(&json_data, &display_columns);
|
let display_values = row_display_values(&json_data, &display_columns);
|
||||||
Hit {
|
Ok(Hit {
|
||||||
id,
|
id,
|
||||||
score,
|
score,
|
||||||
content_json: json_data.to_string(),
|
content_json: json_data.to_string(),
|
||||||
@@ -940,9 +976,9 @@ async fn fetch_ordered_candidate_rows(
|
|||||||
row_display_values: display_values,
|
row_display_values: display_values,
|
||||||
row_display_columns: display_columns.clone(),
|
row_display_columns: display_columns.clone(),
|
||||||
position: u64::try_from(position).ok(),
|
position: u64::try_from(position).ok(),
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
.collect())
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tonic::async_trait]
|
#[tonic::async_trait]
|
||||||
@@ -960,6 +996,30 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use common::proto::komp_ac::search::{SearchOrder, SearchOrderDirection};
|
use common::proto::komp_ac::search::{SearchOrder, SearchOrderDirection};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_response_mapping_exposes_aliases_only() {
|
||||||
|
let mappings = HashMap::from([("1".to_string(), "customer".to_string())]);
|
||||||
|
let mapped = remap_json_to_display_names(
|
||||||
|
serde_json::json!({"1": "Acme", "id": 4}),
|
||||||
|
&mappings,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(mapped, serde_json::json!({"customer": "Acme", "id": 4}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_response_mapping_fails_instead_of_leaking_a_real_name() {
|
||||||
|
let error = remap_json_to_display_names(
|
||||||
|
serde_json::json!({"1": "Acme", "2": "hidden"}),
|
||||||
|
&HashMap::from([("1".to_string(), "customer".to_string())]),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error.code(), tonic::Code::FailedPrecondition);
|
||||||
|
assert!(!error.message().contains("'2'"));
|
||||||
|
}
|
||||||
|
|
||||||
fn request(order: Option<SearchOrder>, table_name: Option<&str>) -> SearchRequest {
|
fn request(order: Option<SearchOrder>, table_name: Option<&str>) -> SearchRequest {
|
||||||
SearchRequest {
|
SearchRequest {
|
||||||
profile_name: "finance".to_string(),
|
profile_name: "finance".to_string(),
|
||||||
|
|||||||
2
server
2
server
Submodule server updated: 598521ccd4...585ae28e4f
Reference in New Issue
Block a user