Skip to main content

vantage_cmd/table_source/
detail.rs

1//! The detail read: one id hydrated by the table's DETAIL script.
2
3use ciborium::Value as CborValue;
4use indexmap::IndexMap;
5use serde_json::Value as JsonValue;
6use vantage_core::error;
7use vantage_dataset::traits::Result;
8use vantage_table::table::Table;
9use vantage_types::{Entity, Record};
10
11use super::list::rows_to_records;
12use crate::cmd::Cmd;
13use crate::rhai_engine::QueryContext;
14use crate::types::cbor_to_string;
15
16/// The DETAIL script's answer for `id`: the row keyed by `id`; else, when
17/// the script returned exactly one row and that row carries no id, that row
18/// stamped with `id` (detail scripts commonly return just the extra fields).
19/// A row carrying another id, or several id-less rows, answer `None`.
20fn detail_row(
21    mut records: IndexMap<String, Record<CborValue>>,
22    id_field: Option<&str>,
23    id: &str,
24) -> Option<Record<CborValue>> {
25    if let Some(row) = records.shift_remove(id) {
26        return Some(row);
27    }
28    if records.len() != 1 {
29        return None;
30    }
31    let (_, mut row) = records.pop()?;
32    let carried = id_field
33        .and_then(|f| row.get(f))
34        .is_some_and(|v| !cbor_to_string(v).is_empty());
35    if carried {
36        return None;
37    }
38    if let Some(field) = id_field {
39        row.insert(field.to_string(), CborValue::Text(id.to_string()));
40    }
41    Some(row)
42}
43
44impl Cmd {
45    /// Detail-script hydration for one id, with the existing list-pass `row`
46    /// injected into the script scope as `row`. Falls back to the normal
47    /// list-and-pick path when the table has no detail script. The detail
48    /// script runs without the table's conditions. Its answer is the row
49    /// keyed by `id`, or a single row carrying no id, stamped with `id`.
50    pub async fn get_table_value_with_row<E>(
51        &self,
52        table: &Table<Self, E>,
53        id: &str,
54        row: &Record<CborValue>,
55    ) -> Result<Option<Record<CborValue>>>
56    where
57        E: Entity<CborValue>,
58        Self: Sized,
59    {
60        let table_name = table.table_name().to_string();
61        if !self.has_detail_script(&table_name) {
62            let mut all = self.list_rows(table).await?;
63            return Ok(all.shift_remove(id));
64        }
65
66        let id_field = table.id_field().map(|c| c.name().to_string());
67        let columns: Vec<String> = table.columns().keys().cloned().collect();
68        let row_map = CborValue::Map(
69            row.iter()
70                .map(|(k, v)| (CborValue::Text(k.clone()), v.clone()))
71                .collect(),
72        );
73        let ctx = QueryContext {
74            conditions: Vec::new(),
75            columns,
76            limit: None,
77            offset: None,
78            id_column: id_field.clone(),
79            id: Some(id.to_string()),
80            row: row_map,
81        };
82        let cmd = self.clone();
83        let rows: Vec<JsonValue> = tokio::task::spawn_blocking(move || {
84            let compiled = cmd
85                .compiled_detail_script(&table_name)?
86                .ok_or_else(|| error!("detail script vanished"))?;
87            compiled.eval(ctx)
88        })
89        .await
90        .map_err(|e| error!("command task failed to join", detail = e.to_string()))??;
91
92        Ok(detail_row(
93            rows_to_records(rows, id_field.as_deref()),
94            id_field.as_deref(),
95            id,
96        ))
97    }
98}