athena_rs 2.9.1

Database gateway API
Documentation
//! Request parsing for gateway fetch POST: sort options and cache key material.

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;

/// Parses optional `sortBy` or `sort_by` from the request body.
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,
    })
}

/// Builds the hashed cache key used for POST gateway fetch.
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 {
    let first_eq_column: &str = conditions.first().map_or("_", |c| &c.eq_column);
    let hash_input: Value = json!({
        "columns": columns_vec,
        "conditions": conditions.iter().map(|c| json!({
            "eq_column": c.eq_column,
            "eq_value": c.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[..8];
    format!(
        "{}:{}:{}:{}:{}:{}",
        table_name,
        first_eq_column,
        columns_vec.join(","),
        limit,
        strip_nulls,
        short_hash
    )
}

/// Parses `conditions` from a gateway fetch JSON body; returns HTTP 400 on invalid `room_id` filters.
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);
    }
}