Skip to main content

haystack_server/ops/
data.rs

1//! The `export` and `import` ops — bulk data import/export.
2
3use axum::extract::State;
4use axum::http::HeaderMap;
5use axum::response::{IntoResponse, Response};
6
7use haystack_core::data::{HCol, HDict, HGrid};
8use haystack_core::kinds::{Kind, Number};
9
10use crate::content;
11use crate::error::HaystackError;
12use crate::state::SharedState;
13
14/// POST /api/export
15pub async fn handle_export(
16    State(state): State<SharedState>,
17    headers: HeaderMap,
18) -> Result<Response, HaystackError> {
19    let accept = headers
20        .get("Accept")
21        .and_then(|v| v.to_str().ok())
22        .unwrap_or("");
23
24    let grid = state
25        .graph
26        .read(|g| g.to_grid(""))
27        .map_err(|e| HaystackError::internal(format!("export failed: {e}")))?;
28
29    let (encoded, ct) = content::encode_response_grid(&grid, accept)
30        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
31
32    Ok(([(axum::http::header::CONTENT_TYPE, ct)], encoded).into_response())
33}
34
35/// POST /api/import
36pub async fn handle_import(
37    State(state): State<SharedState>,
38    headers: HeaderMap,
39    body: String,
40) -> Result<Response, HaystackError> {
41    let content_type = headers
42        .get("Content-Type")
43        .and_then(|v| v.to_str().ok())
44        .unwrap_or("");
45    let accept = headers
46        .get("Accept")
47        .and_then(|v| v.to_str().ok())
48        .unwrap_or("");
49
50    let request_grid = content::decode_request_grid(&body, content_type)
51        .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
52
53    let mut count: usize = 0;
54
55    for row in &request_grid.rows {
56        let ref_val = match row.id() {
57            Some(r) => r.val.clone(),
58            None => {
59                // Skip rows without a valid Ref id
60                continue;
61            }
62        };
63
64        if state.graph.contains(&ref_val) {
65            // Update existing entity
66            state.graph.update(&ref_val, row.clone()).map_err(|e| {
67                HaystackError::internal(format!("update failed for {ref_val}: {e}"))
68            })?;
69        } else {
70            // Add new entity
71            state
72                .graph
73                .add(row.clone())
74                .map_err(|e| HaystackError::internal(format!("add failed for {ref_val}: {e}")))?;
75        }
76
77        count += 1;
78    }
79
80    // Build response grid with count
81    let mut row = HDict::new();
82    row.set("count", Kind::Number(Number::new(count as f64, None)));
83
84    let cols = vec![HCol::new("count")];
85    let result_grid = HGrid::from_parts(HDict::new(), cols, vec![row]);
86
87    let (encoded, ct) = content::encode_response_grid(&result_grid, accept)
88        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
89
90    Ok(([(axum::http::header::CONTENT_TYPE, ct)], encoded).into_response())
91}