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)))?;
|
||||
|
||||
if rows.is_empty() {
|
||||
let target = SearchConstraintTarget {
|
||||
table_name: table_filter.map(str::to_string),
|
||||
column: column.to_string(),
|
||||
};
|
||||
return Ok(vec![target]);
|
||||
return Err(Status::invalid_argument(format!(
|
||||
"Column alias '{}' was not found{}",
|
||||
column,
|
||||
table_filter
|
||||
.map(|table_name| format!(" in table '{}'", table_name))
|
||||
.unwrap_or_default()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
@@ -527,20 +529,35 @@ async fn table_row_display_columns(
|
||||
fn remap_json_to_display_names(
|
||||
value: serde_json::Value,
|
||||
physical_to_display: &HashMap<String, String>,
|
||||
) -> serde_json::Value {
|
||||
) -> Result<serde_json::Value, Status> {
|
||||
match value {
|
||||
serde_json::Value::Object(object) => {
|
||||
let mut remapped = serde_json::Map::with_capacity(object.len());
|
||||
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);
|
||||
}
|
||||
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
|
||||
/// that is NULL for this row stays visible as an empty slot.
|
||||
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?;
|
||||
let physical_column = physical_to_display
|
||||
.iter()
|
||||
.find(|(physical, display)| {
|
||||
physical.eq_ignore_ascii_case(&requested_column)
|
||||
|| display.eq_ignore_ascii_case(&requested_column)
|
||||
})
|
||||
.find(|(_, display)| display.eq_ignore_ascii_case(&requested_column))
|
||||
.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>(
|
||||
r#"
|
||||
@@ -698,15 +720,21 @@ async fn fetch_ordered_rows(
|
||||
.await
|
||||
.map_err(|e| Status::internal(format!("DB query for default results failed: {}", e)))?;
|
||||
|
||||
Ok(rows
|
||||
rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let id: i64 = row.try_get("id").unwrap_or_default();
|
||||
let json_data: serde_json::Value = row.try_get("data").unwrap_or_default();
|
||||
let position: i64 = row.try_get("picker_position").unwrap_or_default();
|
||||
let json_data = remap_json_to_display_names(json_data, &physical_to_display);
|
||||
.map(|row| -> Result<Hit, Status> {
|
||||
let id: i64 = row
|
||||
.try_get("id")
|
||||
.map_err(|error| Status::internal(format!("Search id read failed: {}", error)))?;
|
||||
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);
|
||||
Hit {
|
||||
Ok(Hit {
|
||||
id,
|
||||
score: 0.0,
|
||||
content_json: json_data.to_string(),
|
||||
@@ -714,9 +742,9 @@ async fn fetch_ordered_rows(
|
||||
row_display_values,
|
||||
row_display_columns: display_columns.clone(),
|
||||
position: u64::try_from(position).ok(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn run_search(
|
||||
@@ -841,7 +869,7 @@ async fn run_search(
|
||||
for row in rows {
|
||||
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 = 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);
|
||||
content_map.insert(
|
||||
(table_name.clone(), id),
|
||||
@@ -923,16 +951,24 @@ async fn fetch_ordered_candidate_rows(
|
||||
.await
|
||||
.map_err(|e| Status::internal(format!("Ordered search query failed: {}", e)))?;
|
||||
|
||||
Ok(rows
|
||||
rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let id: i64 = row.try_get("id").unwrap_or_default();
|
||||
let json_data: serde_json::Value = row.try_get("data").unwrap_or_default();
|
||||
let position: i64 = row.try_get("picker_position").unwrap_or_default();
|
||||
let score: f32 = row.try_get("candidate_score").unwrap_or_default();
|
||||
let json_data = remap_json_to_display_names(json_data, &physical_to_display);
|
||||
.map(|row| -> Result<Hit, Status> {
|
||||
let id: i64 = row
|
||||
.try_get("id")
|
||||
.map_err(|error| Status::internal(format!("Search id read failed: {}", error)))?;
|
||||
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 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);
|
||||
Hit {
|
||||
Ok(Hit {
|
||||
id,
|
||||
score,
|
||||
content_json: json_data.to_string(),
|
||||
@@ -940,9 +976,9 @@ async fn fetch_ordered_candidate_rows(
|
||||
row_display_values: display_values,
|
||||
row_display_columns: display_columns.clone(),
|
||||
position: u64::try_from(position).ok(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
@@ -960,6 +996,30 @@ mod tests {
|
||||
use super::*;
|
||||
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 {
|
||||
SearchRequest {
|
||||
profile_name: "finance".to_string(),
|
||||
|
||||
2
server
2
server
Submodule server updated: 598521ccd4...585ae28e4f
Reference in New Issue
Block a user