Skip to main content

fraiseql_functions/host/
dyn_context.rs

1//! Object-safe host-context bridge shared by the async-I/O runtime backends.
2//!
3//! [`HostContext`](crate::HostContext) uses `impl Future` return types, which are
4//! not dyn-compatible. This module provides [`DynHostContext`], an object-safe
5//! wrapper that erases the future types via `Pin<Box<dyn Future>>`, so a live host
6//! can be stored as `Arc<dyn DynHostContext>` and reached from either the WASM
7//! store or the Deno op-state.
8//!
9//! The trait lives here (not in a runtime-specific module) so both the
10//! `runtime-wasm` and `runtime-deno` backends share one host contract and one
11//! validation/SSRF policy path — there is no per-runtime duplication.
12
13use std::{future::Future, pin::Pin};
14
15use fraiseql_error::Result;
16
17use crate::host::HttpResponse;
18
19/// Boxed future type alias for readability.
20pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
21
22/// Object-safe version of [`HostContext`](crate::HostContext) for dynamic dispatch.
23///
24/// Each async method returns a boxed future instead of `impl Future`, allowing
25/// storage as `Arc<dyn DynHostContext>` in a WASM `StoreData` or a Deno `OpState`.
26pub trait DynHostContext: Send + Sync {
27    /// Execute a GraphQL query.
28    fn query(
29        &self,
30        graphql: &str,
31        variables: serde_json::Value,
32    ) -> BoxFuture<'_, Result<serde_json::Value>>;
33
34    /// Execute a raw SQL query.
35    fn sql_query(
36        &self,
37        sql: &str,
38        params: &[serde_json::Value],
39    ) -> BoxFuture<'_, Result<Vec<serde_json::Value>>>;
40
41    /// Make an HTTP request.
42    fn http_request(
43        &self,
44        method: &str,
45        url: &str,
46        headers: &[(String, String)],
47        body: Option<&[u8]>,
48    ) -> BoxFuture<'_, Result<HttpResponse>>;
49
50    /// Retrieve an object from storage.
51    fn storage_get(&self, bucket: &str, key: &str) -> BoxFuture<'_, Result<Vec<u8>>>;
52
53    /// Store an object to storage.
54    fn storage_put(
55        &self,
56        bucket: &str,
57        key: &str,
58        body: &[u8],
59        content_type: &str,
60    ) -> BoxFuture<'_, Result<()>>;
61
62    /// Send an email (host-owned `from`).
63    fn send_email<'a>(
64        &'a self,
65        request: &'a crate::outbound::SendEmailRequest,
66    ) -> BoxFuture<'a, Result<crate::outbound::SendEmailResponse>>;
67
68    /// Get the current auth context.
69    ///
70    /// # Errors
71    ///
72    /// Returns `Err` if auth context is unavailable.
73    fn auth_context(&self) -> Result<serde_json::Value>;
74
75    /// Get an environment variable.
76    ///
77    /// # Errors
78    ///
79    /// Returns `Err` if the variable is blocked from access.
80    fn env_var(&self, name: &str) -> Result<Option<String>>;
81
82    /// Get the event payload.
83    fn event_payload(&self) -> &crate::types::EventPayload;
84
85    /// Log a message.
86    fn log(&self, level: crate::types::LogLevel, message: &str);
87
88    /// A per-dispatch idempotency token (see
89    /// [`HostContext::idempotency_token`](crate::HostContext::idempotency_token)).
90    ///
91    /// Defaults to `None` so direct implementors that do not carry a token (e.g.
92    /// a WASM snapshot) need no change; the blanket impl forwards the real value.
93    fn idempotency_token(&self) -> Option<String> {
94        None
95    }
96
97    /// Read this source's opaque cursor (see
98    /// [`HostContext::cursor`](crate::HostContext::cursor)). Defaults to `None`.
99    fn cursor(&self) -> BoxFuture<'_, Result<Option<serde_json::Value>>> {
100        Box::pin(async { Ok(None) })
101    }
102
103    /// Advance this source's opaque cursor (see
104    /// [`HostContext::advance_cursor`](crate::HostContext::advance_cursor)).
105    /// Defaults to failing on a host not bound to a source.
106    fn advance_cursor(&self, value: serde_json::Value) -> BoxFuture<'_, Result<()>> {
107        Box::pin(async move {
108            let _ = value;
109            Err(fraiseql_error::FraiseQLError::validation(
110                "advance_cursor: this function is not a scheduled source (no cursor binding)",
111            ))
112        })
113    }
114}
115
116/// Blanket implementation: any `T: HostContext + Send + Sync` can be used as `DynHostContext`.
117impl<T: crate::HostContext + Send + Sync> DynHostContext for T {
118    fn query(
119        &self,
120        graphql: &str,
121        variables: serde_json::Value,
122    ) -> BoxFuture<'_, Result<serde_json::Value>> {
123        // Own the graphql string so the future doesn't borrow a local reference
124        let graphql = graphql.to_string();
125        Box::pin(async move { crate::HostContext::query(self, &graphql, variables).await })
126    }
127
128    fn sql_query(
129        &self,
130        sql: &str,
131        params: &[serde_json::Value],
132    ) -> BoxFuture<'_, Result<Vec<serde_json::Value>>> {
133        let sql = sql.to_string();
134        let params = params.to_vec();
135        Box::pin(async move { crate::HostContext::sql_query(self, &sql, &params).await })
136    }
137
138    fn http_request(
139        &self,
140        method: &str,
141        url: &str,
142        headers: &[(String, String)],
143        body: Option<&[u8]>,
144    ) -> BoxFuture<'_, Result<HttpResponse>> {
145        let method = method.to_string();
146        let url = url.to_string();
147        let headers = headers.to_vec();
148        let body = body.map(<[u8]>::to_vec);
149        Box::pin(async move {
150            crate::HostContext::http_request(self, &method, &url, &headers, body.as_deref()).await
151        })
152    }
153
154    fn storage_get(&self, bucket: &str, key: &str) -> BoxFuture<'_, Result<Vec<u8>>> {
155        let bucket = bucket.to_string();
156        let key = key.to_string();
157        Box::pin(async move { crate::HostContext::storage_get(self, &bucket, &key).await })
158    }
159
160    fn storage_put(
161        &self,
162        bucket: &str,
163        key: &str,
164        body: &[u8],
165        content_type: &str,
166    ) -> BoxFuture<'_, Result<()>> {
167        let bucket = bucket.to_string();
168        let key = key.to_string();
169        let body = body.to_vec();
170        let content_type = content_type.to_string();
171        Box::pin(async move {
172            crate::HostContext::storage_put(self, &bucket, &key, &body, &content_type).await
173        })
174    }
175
176    fn send_email<'a>(
177        &'a self,
178        request: &'a crate::outbound::SendEmailRequest,
179    ) -> BoxFuture<'a, Result<crate::outbound::SendEmailResponse>> {
180        let request = request.clone();
181        Box::pin(async move { crate::HostContext::send_email(self, &request).await })
182    }
183
184    fn auth_context(&self) -> Result<serde_json::Value> {
185        crate::HostContext::auth_context(self)
186    }
187
188    fn env_var(&self, name: &str) -> Result<Option<String>> {
189        crate::HostContext::env_var(self, name)
190    }
191
192    fn event_payload(&self) -> &crate::types::EventPayload {
193        crate::HostContext::event_payload(self)
194    }
195
196    fn log(&self, level: crate::types::LogLevel, message: &str) {
197        crate::HostContext::log(self, level, message);
198    }
199
200    fn idempotency_token(&self) -> Option<String> {
201        crate::HostContext::idempotency_token(self)
202    }
203
204    fn cursor(&self) -> BoxFuture<'_, Result<Option<serde_json::Value>>> {
205        Box::pin(async move { crate::HostContext::cursor(self).await })
206    }
207
208    fn advance_cursor(&self, value: serde_json::Value) -> BoxFuture<'_, Result<()>> {
209        Box::pin(async move { crate::HostContext::advance_cursor(self, value).await })
210    }
211}