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
5//! [`ActionRegistry`](crate::actions::ActionRegistry).
6//!
7//! # Request Grid Columns
8//!
9//! | Column   | Kind | Description                               |
10//! |----------|------|-------------------------------------------|
11//! | `id`     | Ref  | Target entity reference                   |
12//! | `action` | Str  | Action name to invoke                     |
13//! | *(other)* | *any* | Additional columns passed as arguments  |
14//!
15//! # Response Grid Columns
16//!
17//! Determined by the action handler. Typically action-specific result data.
18//!
19//! # Errors
20//!
21//! - **400 Bad Request** — missing `id` or `action` column, or action handler error.
22//! - **404 Not Found** — entity not in local graph and not owned by any
23//!   federation connector.
24//! - **500 Internal Server Error** — federation proxy or encoding error.
25
26use actix_web::{HttpRequest, HttpResponse, web};
27
28use haystack_core::kinds::Kind;
29
30use crate::content;
31use crate::error::HaystackError;
32use crate::state::AppState;
33
34/// POST /api/invokeAction
35///
36/// Request grid must have `id` (Ref) and `action` (Str) columns in the
37/// first row.  Additional columns are passed as arguments to the handler.
38pub async fn handle(
39    req: HttpRequest,
40    body: String,
41    state: web::Data<AppState>,
42) -> Result<HttpResponse, HaystackError> {
43    let content_type = req
44        .headers()
45        .get("Content-Type")
46        .and_then(|v| v.to_str().ok())
47        .unwrap_or("");
48    let accept = req
49        .headers()
50        .get("Accept")
51        .and_then(|v| v.to_str().ok())
52        .unwrap_or("");
53
54    let request_grid = content::decode_request_grid(&body, content_type)
55        .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
56
57    // Extract id and action from the first row
58    let row = request_grid
59        .row(0)
60        .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
61
62    let ref_val = match row.get("id") {
63        Some(Kind::Ref(r)) => &r.val,
64        _ => {
65            return Err(HaystackError::bad_request(
66                "request row must have an 'id' column with a Ref value",
67            ));
68        }
69    };
70
71    let action = match row.get("action") {
72        Some(Kind::Str(s)) => s.as_str(),
73        _ => {
74            return Err(HaystackError::bad_request(
75                "request row must have an 'action' column with a Str value",
76            ));
77        }
78    };
79
80    // Check federation: if entity is not in local graph, proxy to remote.
81    if !state.graph.contains(ref_val) {
82        if let Some(connector) = state.federation.owner_of(ref_val) {
83            let args = row.clone();
84            let grid = connector
85                .proxy_invoke_action(ref_val, action, args)
86                .await
87                .map_err(|e| HaystackError::internal(format!("federation proxy error: {e}")))?;
88            let (encoded, ct) = content::encode_response_grid(&grid, accept)
89                .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
90            return Ok(HttpResponse::Ok().content_type(ct).body(encoded));
91        }
92        return Err(HaystackError::not_found(format!(
93            "entity not found: {ref_val}"
94        )));
95    }
96
97    // Resolve entity from the graph
98    let entity = state
99        .graph
100        .get(ref_val)
101        .ok_or_else(|| HaystackError::not_found(format!("entity not found: {ref_val}")))?;
102
103    // The remaining tags in the row serve as arguments (clone row as args dict)
104    let args = row.clone();
105
106    log::info!("invokeAction: id={ref_val} action={action}");
107
108    // Dispatch to the action registry
109    let result_grid = state
110        .actions
111        .invoke(&entity, action, &args)
112        .map_err(HaystackError::bad_request)?;
113
114    let (encoded, ct) = content::encode_response_grid(&result_grid, accept)
115        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
116
117    Ok(HttpResponse::Ok().content_type(ct).body(encoded))
118}