runique 3.0.2

A Django-inspired web framework for Rust with ORM, templates, and comprehensive security middleware
Documentation
//! Foreign-key label resolution for admin views.
//!
//! Centralises the `FK id -> related label` lookup used to display a
//! human-readable value instead of a raw id. Consumed by the generated
//! `list_fn`/`get_fn` (via [`resolve_fk_labels_in_rows`]) and reusable on
//! any other admin view (detail, history) that holds FK ids as JSON.
use std::collections::HashSet;

use crate::utils::aliases::StrMap;
use sea_orm::{
    ConnectionTrait,
    sea_query::{Alias, Expr, ExprTrait, Func, Query},
};
use serde_json::Value;

/// Normalises an FK value to a unified `String` key.
///
/// Accepts integer ids (i32/i64) and string ids (UUID) so the same lookup
/// map works regardless of the related table's primary-key type.
#[must_use]
pub fn fk_key(value: &Value) -> Option<String> {
    value
        .as_i64()
        .map(|n| n.to_string())
        .or_else(|| value.as_str().map(str::to_string))
}

/// Fetches an `id -> label` map for `ids` from `fk_table`.
///
/// `id` cast to text for the real backend (`TEXT`, or `CHAR` on MySQL/MariaDB)
/// keeps the lookup engine-agnostic (i32, i64, UUID).
///
/// SQL-injection safe: `ids` are bound through `is_in`; `fk_table` and
/// `fk_col` are static identifiers emitted by the `derive_form` macro, never
/// user input. Returns an empty map on query error — this is a display-only
/// path and must never break the request.
pub async fn fetch_fk_label_map<C: ConnectionTrait>(
    db: &C,
    fk_table: &str,
    fk_col: &str,
    ids: &[String],
) -> StrMap {
    if ids.is_empty() {
        return StrMap::new();
    }
    let id_text = || {
        Expr::col(Alias::new("id")).cast_as(Alias::new(super::text_cast_type_of(
            db.get_database_backend(),
        )))
    };
    let stmt = Query::select()
        .expr(id_text())
        .expr(Expr::col(Alias::new(fk_col)))
        .from(Alias::new(fk_table))
        .and_where(id_text().is_in(ids.to_vec()))
        .to_owned();
    db.query_all(&stmt)
        .await
        .unwrap_or_default()
        .iter()
        .filter_map(|row| {
            let id = row.try_get_by_index::<String>(0).ok()?;
            let label = row.try_get_by_index::<String>(1).ok()?;
            Some((id, label))
        })
        .collect()
}

/// Fetches the ids from `fk_table` whose `fk_col` matches `pattern` (case-insensitive
/// substring), so a free-text admin search can reach a FK-displayed column.
///
/// The list/count search only ever runs against the child entity's own columns
/// ([`fetch_fk_label_map`] relabels rows already fetched — it can't filter a
/// `WHERE` clause). Resolving the pattern against the related table first, then
/// folding the matching ids into the child's raw FK column via `is_in`, is the
/// only way to search a FK-displayed column without a real SQL join.
///
/// SQL-injection safe on the same grounds as `fetch_fk_label_map`: `fk_table`/
/// `fk_col` are static identifiers from the `admin!{}` DSL, never user input;
/// `pattern` is bound through `like`. Returns an empty `Vec` on query error —
/// callers treat that as "no match", never as "match everything".
pub async fn fetch_fk_matching_ids<C: ConnectionTrait>(
    db: &C,
    fk_table: &str,
    fk_col: &str,
    pattern: &str,
) -> Vec<String> {
    let needle = format!("%{}%", pattern.to_lowercase());
    let stmt = Query::select()
        .expr(
            Expr::col(Alias::new("id")).cast_as(Alias::new(super::text_cast_type_of(
                db.get_database_backend(),
            ))),
        )
        .from(Alias::new(fk_table))
        .and_where(Expr::expr(Func::lower(Expr::col(Alias::new(fk_col)))).like(needle))
        .to_owned();
    db.query_all(&stmt)
        .await
        .unwrap_or_default()
        .iter()
        .filter_map(|row| row.try_get_by_index::<String>(0).ok())
        .collect()
}

/// Display-layer entry point: resolves FK ids to labels across `rows` using
/// the owned specs carried on `AdminResource::fk_display`. Thin borrow-adapter
/// over [`resolve_fk_labels_in_rows`].
pub async fn resolve_fk_labels<C: ConnectionTrait>(
    db: &C,
    rows: &mut [Value],
    fk_display: &[(String, String, String)],
) {
    if fk_display.is_empty() {
        return;
    }
    let specs: Vec<(&str, &str, &str)> = fk_display
        .iter()
        .map(|(col, table, label_col)| (col.as_str(), table.as_str(), label_col.as_str()))
        .collect();
    resolve_fk_labels_in_rows(db, rows, &specs).await;
}

/// Replaces FK ids with their related labels, in place, across `rows`.
///
/// `fk_cols` is a slice of `(column, fk_table, fk_col)`. One query per FK
/// column (ids batched via `is_in`), so a single-row view is just a slice of
/// length 1. Rows whose FK value has no match are left untouched (dangling id).
pub async fn resolve_fk_labels_in_rows<C: ConnectionTrait>(
    db: &C,
    rows: &mut [Value],
    fk_cols: &[(&str, &str, &str)],
) {
    for (col, fk_table, fk_col) in fk_cols {
        let ids: Vec<String> = rows
            .iter()
            .filter_map(|r| r.get(*col).and_then(fk_key))
            .collect::<HashSet<String>>()
            .into_iter()
            .collect();
        let label_map = fetch_fk_label_map(db, fk_table, fk_col, &ids).await;
        if label_map.is_empty() {
            continue;
        }
        for row in rows.iter_mut() {
            if let Some(key) = row.get(*col).and_then(fk_key)
                && let Some(label) = label_map.get(&key)
            {
                row[*col] = Value::String(label.clone());
            }
        }
    }
}