Skip to main content

fraiseql_functions/host/
mod.rs

1//! Host context trait for function runtime access to FraiseQL services.
2
3use std::future::Future;
4
5use fraiseql_error::Result;
6
7use crate::types::{EventPayload, LogEntry, LogLevel};
8
9#[cfg(feature = "host-live")]
10pub mod live;
11
12#[cfg(feature = "host-live")]
13pub mod factory;
14
15/// Response from an HTTP request.
16#[derive(Debug, Clone)]
17pub struct HttpResponse {
18    /// HTTP status code.
19    pub status:  u16,
20    /// Response headers.
21    pub headers: Vec<(String, String)>,
22    /// Response body.
23    pub body:    Vec<u8>,
24}
25
26/// Trait for providing host services to functions (queries, storage, HTTP, etc.).
27///
28/// This trait is implemented by the FraiseQL server to allow functions to call
29/// back into the server's services during execution.
30///
31/// The `#[trait_variant::make]` macro generates `SendHostContext` which is
32/// object-safe for `Box<dyn SendHostContext>` dynamic dispatch.
33#[allow(clippy::trait_duplication_in_bounds)] // Reason: trait_variant::make expands to bounds that look duplicated to clippy
34#[trait_variant::make(SendHostContext: Send)]
35pub trait HostContext: Send + Sync {
36    /// Execute a GraphQL query.
37    ///
38    /// # Errors
39    ///
40    /// Returns `Err` if the query fails to execute.
41    fn query(
42        &self,
43        graphql: &str,
44        variables: serde_json::Value,
45    ) -> impl Future<Output = Result<serde_json::Value>> + Send;
46
47    /// Execute a raw SQL query.
48    ///
49    /// # Errors
50    ///
51    /// Returns `Err` if the query fails to execute or is classified as insecure.
52    fn sql_query(
53        &self,
54        sql: &str,
55        params: &[serde_json::Value],
56    ) -> impl Future<Output = Result<Vec<serde_json::Value>>> + Send;
57
58    /// Make an HTTP request.
59    ///
60    /// # Errors
61    ///
62    /// Returns `Err` if the request fails or is blocked (e.g., SSRF check).
63    fn http_request(
64        &self,
65        method: &str,
66        url: &str,
67        headers: &[(String, String)],
68        body: Option<&[u8]>,
69    ) -> impl Future<Output = Result<HttpResponse>> + Send;
70
71    /// Retrieve an object from storage.
72    ///
73    /// # Errors
74    ///
75    /// Returns `Err` if the object does not exist or access is denied.
76    fn storage_get(&self, bucket: &str, key: &str) -> impl Future<Output = Result<Vec<u8>>> + Send;
77
78    /// Store an object to storage.
79    ///
80    /// # Errors
81    ///
82    /// Returns `Err` if the write fails or access is denied.
83    fn storage_put(
84        &self,
85        bucket: &str,
86        key: &str,
87        body: &[u8],
88        content_type: &str,
89    ) -> impl Future<Output = Result<()>> + Send;
90
91    /// Get the current authenticated user's context.
92    ///
93    /// # Errors
94    ///
95    /// Returns `Err` if authentication information is unavailable.
96    fn auth_context(&self) -> Result<serde_json::Value>;
97
98    /// Get an environment variable.
99    ///
100    /// Returns `Ok(None)` if the variable is not set.
101    ///
102    /// # Errors
103    ///
104    /// Returns `Err` if the variable is blocked from access.
105    fn env_var(&self, name: &str) -> Result<Option<String>>;
106
107    /// Get the current event payload (for reference).
108    fn event_payload(&self) -> &EventPayload;
109
110    /// Log a message to the tracing subscriber.
111    fn log(&self, level: LogLevel, message: &str);
112}
113
114/// A no-op host context for testing WASM execution without real backends.
115///
116/// All I/O methods return `Unsupported` errors. Logs are captured in-memory for test verification.
117pub struct NoopHostContext {
118    event_payload: EventPayload,
119    logs:          std::sync::Arc<std::sync::Mutex<Vec<LogEntry>>>,
120}
121
122impl NoopHostContext {
123    /// Create a new no-op host context for testing.
124    #[must_use]
125    pub fn new(event_payload: EventPayload) -> Self {
126        Self {
127            event_payload,
128            logs: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
129        }
130    }
131
132    /// Get a copy of all captured logs (for test verification).
133    ///
134    /// # Panics
135    ///
136    /// Panics if the Mutex is poisoned (should never happen in normal operation).
137    #[must_use]
138    pub fn captured_logs(&self) -> Vec<LogEntry> {
139        self.logs.lock().expect("log mutex poisoned").clone()
140    }
141}
142
143impl HostContext for NoopHostContext {
144    async fn query(
145        &self,
146        _graphql: &str,
147        _variables: serde_json::Value,
148    ) -> Result<serde_json::Value> {
149        Err(fraiseql_error::FraiseQLError::Unsupported {
150            message: "HostContext::query not implemented".to_string(),
151        })
152    }
153
154    async fn sql_query(
155        &self,
156        _sql: &str,
157        _params: &[serde_json::Value],
158    ) -> Result<Vec<serde_json::Value>> {
159        Err(fraiseql_error::FraiseQLError::Unsupported {
160            message: "HostContext::sql_query not implemented".to_string(),
161        })
162    }
163
164    async fn http_request(
165        &self,
166        _method: &str,
167        _url: &str,
168        _headers: &[(String, String)],
169        _body: Option<&[u8]>,
170    ) -> Result<HttpResponse> {
171        Err(fraiseql_error::FraiseQLError::Unsupported {
172            message: "HostContext::http_request not implemented".to_string(),
173        })
174    }
175
176    async fn storage_get(&self, _bucket: &str, _key: &str) -> Result<Vec<u8>> {
177        Err(fraiseql_error::FraiseQLError::Unsupported {
178            message: "HostContext::storage_get not implemented".to_string(),
179        })
180    }
181
182    async fn storage_put(
183        &self,
184        _bucket: &str,
185        _key: &str,
186        _body: &[u8],
187        _content_type: &str,
188    ) -> Result<()> {
189        Err(fraiseql_error::FraiseQLError::Unsupported {
190            message: "HostContext::storage_put not implemented".to_string(),
191        })
192    }
193
194    fn auth_context(&self) -> Result<serde_json::Value> {
195        Err(fraiseql_error::FraiseQLError::Unsupported {
196            message: "HostContext::auth_context not implemented".to_string(),
197        })
198    }
199
200    fn env_var(&self, _name: &str) -> Result<Option<String>> {
201        Ok(None)
202    }
203
204    fn event_payload(&self) -> &EventPayload {
205        &self.event_payload
206    }
207
208    fn log(&self, level: LogLevel, message: &str) {
209        let entry = LogEntry {
210            level,
211            message: message.to_string(),
212            timestamp: chrono::Utc::now(),
213        };
214        self.logs.lock().expect("log mutex poisoned").push(entry);
215    }
216}
217
218#[cfg(test)]
219mod tests;