Skip to main content

haystack_server/ops/
invoke.rs

1//! The `invokeAction` op — invoke an action on an entity.
2//!
3//! Parses `id` and `action` columns from the request grid, resolves
4//! the entity from the graph, and dispatches to the ActionRegistry.
5
6use actix_web::{HttpRequest, HttpResponse, web};
7
8use haystack_core::kinds::Kind;
9
10use crate::content;
11use crate::error::HaystackError;
12use crate::state::AppState;
13
14/// POST /api/invokeAction
15///
16/// Request grid must have `id` (Ref) and `action` (Str) columns in the
17/// first row.  Additional columns are passed as arguments to the handler.
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    // Extract id and action from the first row
38    let row = request_grid
39        .row(0)
40        .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
41
42    let ref_val = match row.get("id") {
43        Some(Kind::Ref(r)) => &r.val,
44        _ => {
45            return Err(HaystackError::bad_request(
46                "request row must have an 'id' column with a Ref value",
47            ));
48        }
49    };
50
51    let action = match row.get("action") {
52        Some(Kind::Str(s)) => s.as_str(),
53        _ => {
54            return Err(HaystackError::bad_request(
55                "request row must have an 'action' column with a Str value",
56            ));
57        }
58    };
59
60    // Check federation: if entity is not in local graph, proxy to remote.
61    if !state.graph.contains(ref_val) {
62        if let Some(connector) = state.federation.owner_of(ref_val) {
63            let args = row.clone();
64            let grid = connector
65                .proxy_invoke_action(ref_val, action, args)
66                .await
67                .map_err(|e| HaystackError::internal(format!("federation proxy error: {e}")))?;
68            let (encoded, ct) = content::encode_response_grid(&grid, accept)
69                .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
70            return Ok(HttpResponse::Ok().content_type(ct).body(encoded));
71        }
72        return Err(HaystackError::not_found(format!(
73            "entity not found: {ref_val}"
74        )));
75    }
76
77    // Resolve entity from the graph
78    let entity = state
79        .graph
80        .get(ref_val)
81        .ok_or_else(|| HaystackError::not_found(format!("entity not found: {ref_val}")))?;
82
83    // The remaining tags in the row serve as arguments (clone row as args dict)
84    let args = row.clone();
85
86    log::info!("invokeAction: id={ref_val} action={action}");
87
88    // Dispatch to the action registry
89    let result_grid = state
90        .actions
91        .invoke(&entity, action, &args)
92        .map_err(HaystackError::bad_request)?;
93
94    let (encoded, ct) = content::encode_response_grid(&result_grid, accept)
95        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
96
97    Ok(HttpResponse::Ok().content_type(ct).body(encoded))
98}