use actix_web::HttpResponse;
use serde_json::{Number, Value, json};
use super::conditions::RequestCondition;
use super::room_id;
use super::types::SortOptions;
use crate::utils::format::normalize_column_name;
pub fn parse_sort_options_from_body(
body: Option<&Value>,
force_snake: bool,
) -> Option<SortOptions> {
let obj = body?
.get("sort_by")
.or_else(|| body?.get("sortBy"))
.and_then(Value::as_object)?;
let field = obj
.get("field")
.or_else(|| obj.get("column"))
.and_then(Value::as_str)?;
let normalized = normalize_column_name(field, force_snake);
if normalized.is_empty() {
return None;
}
let ascending = obj
.get("direction")
.and_then(Value::as_str)
.map(|s| matches!(s.to_lowercase().as_str(), "asc" | "ascending"))
.unwrap_or(true);
Some(SortOptions {
column: normalized,
ascending,
})
}
pub fn build_fetch_hashed_cache_key(
table_name: &str,
conditions: &[RequestCondition],
columns_vec: &[String],
limit: i64,
strip_nulls: bool,
client_name: &str,
sort_options: Option<&SortOptions>,
) -> String {
build_fetch_hashed_cache_key_with_hash_len(
table_name,
conditions,
columns_vec,
limit,
strip_nulls,
client_name,
sort_options,
16,
)
}
pub(super) fn build_fetch_hashed_cache_key_legacy8(
table_name: &str,
conditions: &[RequestCondition],
columns_vec: &[String],
limit: i64,
strip_nulls: bool,
client_name: &str,
sort_options: Option<&SortOptions>,
) -> String {
build_fetch_hashed_cache_key_with_hash_len(
table_name,
conditions,
columns_vec,
limit,
strip_nulls,
client_name,
sort_options,
8,
)
}
fn build_fetch_hashed_cache_key_with_hash_len(
table_name: &str,
conditions: &[RequestCondition],
columns_vec: &[String],
limit: i64,
strip_nulls: bool,
client_name: &str,
sort_options: Option<&SortOptions>,
hash_len: usize,
) -> String {
let mut normalized_conditions: Vec<(String, Value, String)> = conditions
.iter()
.map(|c| {
let serialized_value = serde_json::to_string(&c.eq_value).unwrap_or_default();
(c.eq_column.clone(), c.eq_value.clone(), serialized_value)
})
.collect();
normalized_conditions.sort_by(|a, b| a.0.cmp(&b.0).then(a.2.cmp(&b.2)));
let first_eq_column: &str = normalized_conditions
.first()
.map_or("_", |(column, _, _)| column.as_str());
let hash_input: Value = json!({
"columns": columns_vec,
"conditions": normalized_conditions.iter().map(|(eq_column, eq_value, _)| json!({
"eq_column": eq_column,
"eq_value": eq_value.clone()
})).collect::<Vec<_>>(),
"limit": limit,
"strip_nulls": strip_nulls,
"client": client_name,
"sort": sort_options.map(|s| json!({"column": s.column, "ascending": s.ascending})),
});
let hash_str: String = sha256::digest(serde_json::to_string(&hash_input).unwrap_or_default());
let short_hash: &str = &hash_str[..hash_len.min(hash_str.len())];
format!(
"{}:{}:{}:{}:{}:{}",
table_name,
first_eq_column,
columns_vec.join(","),
limit,
strip_nulls,
short_hash
)
}
pub fn parse_gateway_fetch_conditions(
json_body: &Value,
force_camel_case_to_snake_case: bool,
) -> Result<Vec<RequestCondition>, HttpResponse> {
let mut conditions = Vec::new();
let Some(additional_conditions) = json_body.get("conditions").and_then(|c| c.as_array()) else {
return Ok(conditions);
};
for condition in additional_conditions {
if let Some(eq_column) = condition.get("eq_column").and_then(Value::as_str) {
let eq_column_str = eq_column.to_string();
let normalized_for_validation =
normalize_column_name(eq_column, force_camel_case_to_snake_case);
let eq_value_raw = match condition.get("eq_value") {
Some(value) => value.clone(),
None => {
if normalized_for_validation == "room_id" || eq_column_str == "roomId" {
return Err(HttpResponse::BadRequest().json(json!({
"error": "room_id is required and must be numeric"
})));
}
continue;
}
};
let eq_value: Value =
if normalized_for_validation == "room_id" || eq_column_str == "roomId" {
match room_id::parse_room_id_value(&eq_value_raw) {
Ok(room_id) => Value::Number(Number::from(room_id)),
Err(err_msg) => {
return Err(HttpResponse::BadRequest().json(json!({
"error": err_msg
})));
}
}
} else {
eq_value_raw
};
conditions.push(RequestCondition::new(eq_column_str, eq_value));
}
}
Ok(conditions)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn hashed_cache_key_stable_for_same_inputs() {
let conditions = vec![RequestCondition::new("workspace_id".into(), json!("abc"))];
let k1 = build_fetch_hashed_cache_key(
"users",
&conditions,
&["id".into(), "email".into()],
10,
false,
"supabase",
None,
);
let k2 = build_fetch_hashed_cache_key(
"users",
&conditions,
&["id".into(), "email".into()],
10,
false,
"supabase",
None,
);
assert_eq!(k1, k2);
}
#[test]
fn hashed_cache_key_stable_for_reordered_conditions() {
let conditions_a = vec![
RequestCondition::new("workspace_id".into(), json!("abc")),
RequestCondition::new("room_id".into(), json!(123)),
];
let conditions_b = vec![
RequestCondition::new("room_id".into(), json!(123)),
RequestCondition::new("workspace_id".into(), json!("abc")),
];
let k1 = build_fetch_hashed_cache_key(
"users",
&conditions_a,
&["id".into(), "email".into()],
10,
false,
"supabase",
None,
);
let k2 = build_fetch_hashed_cache_key(
"users",
&conditions_b,
&["id".into(), "email".into()],
10,
false,
"supabase",
None,
);
assert_eq!(k1, k2);
}
}