Skip to main content

haystack_server/ops/
read.rs

1//! The `read` op — read entities by filter or by id.
2
3use actix_web::{HttpRequest, HttpResponse, web};
4
5use haystack_core::data::{HCol, HDict, HGrid};
6use haystack_core::filter::{matches, parse_filter};
7use haystack_core::kinds::Kind;
8
9use crate::content;
10use crate::error::HaystackError;
11use crate::state::AppState;
12
13/// POST /api/read
14///
15/// Request grid may have:
16/// - A `filter` column with a filter expression string, and optional `limit` column
17/// - An `id` column with Ref values for reading specific entities
18pub async fn handle(
19    req: HttpRequest,
20    body: String,
21    state: web::Data<AppState>,
22) -> Result<HttpResponse, HaystackError> {
23    let content_type = req
24        .headers()
25        .get("Content-Type")
26        .and_then(|v| v.to_str().ok())
27        .unwrap_or("");
28    let accept = req
29        .headers()
30        .get("Accept")
31        .and_then(|v| v.to_str().ok())
32        .unwrap_or("");
33
34    let request_grid = content::decode_request_grid(&body, content_type)
35        .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
36
37    let result_grid = if request_grid.col("id").is_some() {
38        // Read by ID
39        read_by_id(&request_grid, &state)
40    } else if request_grid.col("filter").is_some() {
41        // Read by filter
42        read_by_filter(&request_grid, &state)
43    } else {
44        Err(HaystackError::bad_request(
45            "request must have 'id' or 'filter' column",
46        ))
47    }?;
48
49    let (encoded, ct) = content::encode_response_grid(&result_grid, accept)
50        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
51
52    Ok(HttpResponse::Ok().content_type(ct).body(encoded))
53}
54
55/// Read entities by filter expression.
56///
57/// After reading from the local graph, also includes matching entities from
58/// any federated remote connectors.
59fn read_by_filter(request_grid: &HGrid, state: &AppState) -> Result<HGrid, HaystackError> {
60    let row = request_grid
61        .row(0)
62        .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
63
64    let filter = match row.get("filter") {
65        Some(Kind::Str(s)) => s.as_str(),
66        _ => return Err(HaystackError::bad_request("filter must be a Str value")),
67    };
68
69    let limit = match row.get("limit") {
70        Some(Kind::Number(n)) => n.val as usize,
71        _ => 0, // 0 means no limit
72    };
73
74    let effective_limit = if limit == 0 { usize::MAX } else { limit };
75
76    // Read from local graph.
77    let local_grid = state
78        .graph
79        .read_filter(filter, limit)
80        .map_err(|e| HaystackError::bad_request(format!("filter error: {e}")))?;
81
82    let mut results: Vec<HDict> = local_grid.rows;
83
84    // Merge federated entities if we have not yet hit the limit.
85    if results.len() < effective_limit {
86        let federated = state.federation.all_cached_entities();
87        if !federated.is_empty() {
88            let ast = parse_filter(filter)
89                .map_err(|e| HaystackError::bad_request(format!("filter error: {e}")))?;
90
91            for entity in federated {
92                if results.len() >= effective_limit {
93                    break;
94                }
95                if matches(&ast, &entity, None) {
96                    results.push(entity);
97                }
98            }
99        }
100    }
101
102    if results.is_empty() {
103        return Ok(HGrid::new());
104    }
105
106    // Build column set from all result entities.
107    let mut col_set: Vec<String> = Vec::new();
108    let mut seen = std::collections::HashSet::new();
109    for entity in &results {
110        for name in entity.tag_names() {
111            if seen.insert(name.to_string()) {
112                col_set.push(name.to_string());
113            }
114        }
115    }
116    col_set.sort();
117    let cols: Vec<HCol> = col_set.iter().map(|n| HCol::new(n.as_str())).collect();
118
119    Ok(HGrid::from_parts(HDict::new(), cols, results))
120}
121
122/// Read entities by ID references.
123fn read_by_id(request_grid: &HGrid, state: &AppState) -> Result<HGrid, HaystackError> {
124    let mut results: Vec<HDict> = Vec::new();
125    let mut col_set: Vec<String> = Vec::new();
126    let mut seen = std::collections::HashSet::new();
127
128    for row in request_grid.rows.iter() {
129        let ref_val = match row.get("id") {
130            Some(Kind::Ref(r)) => &r.val,
131            _ => continue,
132        };
133
134        if let Some(entity) = state.graph.get(ref_val) {
135            for name in entity.tag_names() {
136                if seen.insert(name.to_string()) {
137                    col_set.push(name.to_string());
138                }
139            }
140            results.push(entity);
141        } else {
142            // Entity not found: add empty row with just the id marker
143            // (per Haystack spec, missing entities are still returned)
144            let mut missing = HDict::new();
145            missing.set(
146                "id",
147                Kind::Ref(haystack_core::kinds::HRef::from_val(ref_val.as_str())),
148            );
149            if seen.insert("id".to_string()) {
150                col_set.push("id".to_string());
151            }
152            results.push(missing);
153        }
154    }
155
156    if results.is_empty() {
157        return Ok(HGrid::new());
158    }
159
160    col_set.sort();
161    let cols: Vec<HCol> = col_set.iter().map(|n| HCol::new(n.as_str())).collect();
162    Ok(HGrid::from_parts(HDict::new(), cols, results))
163}