Skip to main content

haystack_server/ops/
federation.rs

1//! Federation HTTP endpoints — status and sync for remote connectors.
2
3use actix_web::{HttpRequest, HttpResponse, web};
4
5use haystack_core::data::{HCol, HDict, HGrid};
6use haystack_core::kinds::{Kind, Number};
7
8use crate::content;
9use crate::error::HaystackError;
10use crate::state::AppState;
11
12/// GET /api/federation/status
13///
14/// Returns a grid with one row per connector: `name` and `entityCount`.
15pub async fn handle_status(
16    req: HttpRequest,
17    state: web::Data<AppState>,
18) -> Result<HttpResponse, HaystackError> {
19    let accept = req
20        .headers()
21        .get("Accept")
22        .and_then(|v| v.to_str().ok())
23        .unwrap_or("");
24
25    let status = state.federation.status();
26
27    let grid = if status.is_empty() {
28        HGrid::from_parts(
29            HDict::new(),
30            vec![HCol::new("name"), HCol::new("entityCount")],
31            vec![],
32        )
33    } else {
34        let rows: Vec<HDict> = status
35            .into_iter()
36            .map(|(name, count)| {
37                let mut row = HDict::new();
38                row.set("name", Kind::Str(name));
39                row.set("entityCount", Kind::Number(Number::unitless(count as f64)));
40                row
41            })
42            .collect();
43        HGrid::from_parts(
44            HDict::new(),
45            vec![HCol::new("name"), HCol::new("entityCount")],
46            rows,
47        )
48    };
49
50    let (encoded, ct) = content::encode_response_grid(&grid, accept)
51        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
52
53    Ok(HttpResponse::Ok().content_type(ct).body(encoded))
54}
55
56/// POST /api/federation/sync
57///
58/// Triggers `sync_all()` on all connectors. Returns a grid with `name`,
59/// `result` (entity count or error string), and `ok` (Bool).
60pub async fn handle_sync(
61    req: HttpRequest,
62    state: web::Data<AppState>,
63) -> Result<HttpResponse, HaystackError> {
64    let accept = req
65        .headers()
66        .get("Accept")
67        .and_then(|v| v.to_str().ok())
68        .unwrap_or("");
69
70    let results = state.federation.sync_all().await;
71
72    let rows: Vec<HDict> = results
73        .into_iter()
74        .map(|(name, result)| {
75            let mut row = HDict::new();
76            row.set("name", Kind::Str(name));
77            match result {
78                Ok(count) => {
79                    row.set("result", Kind::Str(format!("{count} entities")));
80                    row.set("ok", Kind::Bool(true));
81                }
82                Err(err) => {
83                    row.set("result", Kind::Str(err));
84                    row.set("ok", Kind::Bool(false));
85                }
86            }
87            row
88        })
89        .collect();
90
91    let grid = HGrid::from_parts(
92        HDict::new(),
93        vec![HCol::new("name"), HCol::new("result"), HCol::new("ok")],
94        rows,
95    );
96
97    let (encoded, ct) = content::encode_response_grid(&grid, accept)
98        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
99
100    Ok(HttpResponse::Ok().content_type(ct).body(encoded))
101}