fraiseql_functions/runtime/wasm/store/
mod.rs1use std::sync::Arc;
8
9use super::{
10 bindings::fraiseql::host::{context, io, logging},
11 host_bridge::DynHostContext,
12};
13use crate::types::{EventPayload, LogEntry, LogLevel, ResourceLimits};
14
15pub struct StoreData {
20 pub event_payload: EventPayload,
22
23 pub host_context: Option<Arc<dyn DynHostContext>>,
25
26 pub logs: Vec<LogEntry>,
28
29 pub limits: ResourceLimits,
31
32 pub memory_peak_bytes: u64,
34
35 pub memory_current_bytes: u64,
37
38 wasi_ctx: wasmtime_wasi::WasiCtx,
40
41 wasi_table: wasmtime::component::ResourceTable,
43}
44
45impl StoreData {
46 #[must_use]
48 pub fn new(event_payload: EventPayload, limits: ResourceLimits) -> Self {
49 Self {
50 event_payload,
51 host_context: None,
52 logs: Vec::new(),
53 limits,
54 memory_peak_bytes: 0,
55 memory_current_bytes: 0,
56 wasi_ctx: wasmtime_wasi::WasiCtxBuilder::new().build(),
57 wasi_table: wasmtime::component::ResourceTable::new(),
58 }
59 }
60
61 pub fn set_host_context(&mut self, context: Arc<dyn DynHostContext>) {
63 self.host_context = Some(context);
64 }
65
66 fn require_host_context(&self) -> Result<&dyn DynHostContext, String> {
68 self.host_context
69 .as_deref()
70 .ok_or_else(|| "host context not available".to_string())
71 }
72
73 pub fn log_message(&mut self, level: LogLevel, message: &str) {
77 if self.logs.len() < self.limits.max_log_entries {
78 let entry = LogEntry {
79 level,
80 message: message.to_string(),
81 timestamp: chrono::Utc::now(),
82 };
83 self.logs.push(entry);
84
85 match level {
86 LogLevel::Debug => tracing::debug!("{}", message),
87 LogLevel::Info => tracing::info!("{}", message),
88 LogLevel::Warn => tracing::warn!("{}", message),
89 LogLevel::Error => tracing::error!("{}", message),
90 }
91 }
92 }
93
94 #[must_use]
96 pub const fn event_payload_ref(&self) -> &EventPayload {
97 &self.event_payload
98 }
99
100 pub fn get_event_payload_json(&self) -> wasmtime::Result<String> {
106 serde_json::to_string(&self.event_payload).map_err(|e| wasmtime::Error::msg(e.to_string()))
107 }
108}
109
110impl wasmtime_wasi::WasiView for StoreData {
111 fn ctx(&mut self) -> wasmtime_wasi::WasiCtxView<'_> {
112 wasmtime_wasi::WasiCtxView {
113 ctx: &mut self.wasi_ctx,
114 table: &mut self.wasi_table,
115 }
116 }
117}
118
119const fn map_log_level(level: logging::LogLevel) -> LogLevel {
121 match level {
122 logging::LogLevel::Debug => LogLevel::Debug,
123 logging::LogLevel::Info => LogLevel::Info,
124 logging::LogLevel::Warn => LogLevel::Warn,
125 logging::LogLevel::Error => LogLevel::Error,
126 }
127}
128
129impl logging::Host for StoreData {
130 async fn log(&mut self, level: logging::LogLevel, message: String) {
131 self.log_message(map_log_level(level), &message);
132 }
133}
134
135#[allow(unknown_lints, clippy::unused_async_trait_impl)]
139impl context::Host for StoreData {
140 async fn get_event_payload(&mut self) -> String {
141 self.get_event_payload_json()
142 .unwrap_or_else(|e| format!("{{\"error\": \"{e}\"}}"))
143 }
144
145 async fn get_auth_context(&mut self) -> Result<String, String> {
146 let host = self.require_host_context()?;
147 let value = host.auth_context().map_err(|e| e.to_string())?;
148 serde_json::to_string(&value).map_err(|e| e.to_string())
149 }
150
151 async fn get_env_var(&mut self, name: String) -> Result<Option<String>, String> {
152 let host = self.require_host_context()?;
156 host.env_var(&name).map_err(host_err_string)
157 }
158
159 async fn get_idempotency_token(&mut self) -> Option<String> {
160 let Ok(host) = self.require_host_context() else {
161 return None;
162 };
163 host.idempotency_token()
164 }
165}
166
167#[allow(clippy::needless_pass_by_value)] fn host_err_string(error: fraiseql_error::FraiseQLError) -> String {
172 if error.is_client_error() {
173 format!("{} {error}", crate::types::PERMANENT_ERROR_MARKER)
174 } else {
175 error.to_string()
176 }
177}
178
179impl io::Host for StoreData {
180 async fn query(&mut self, graphql: String, variables: String) -> Result<String, String> {
181 let host = self.require_host_context()?;
182 let vars: serde_json::Value =
183 serde_json::from_str(&variables).map_err(|e| e.to_string())?;
184 let result = host.query(&graphql, vars).await.map_err(host_err_string)?;
185 serde_json::to_string(&result).map_err(|e| e.to_string())
186 }
187
188 async fn sql_query(&mut self, sql: String, params: String) -> Result<String, String> {
189 let host = self.require_host_context()?;
190 let params_vec: Vec<serde_json::Value> =
191 serde_json::from_str(¶ms).map_err(|e| e.to_string())?;
192 let result = host.sql_query(&sql, ¶ms_vec).await.map_err(host_err_string)?;
193 serde_json::to_string(&result).map_err(|e| e.to_string())
194 }
195
196 async fn http_request(
197 &mut self,
198 method: String,
199 url: String,
200 headers: Vec<(String, String)>,
201 body: Option<Vec<u8>>,
202 ) -> Result<io::HttpResponse, String> {
203 let host = self.require_host_context()?;
204 let response = host
205 .http_request(&method, &url, &headers, body.as_deref())
206 .await
207 .map_err(host_err_string)?;
208 Ok(io::HttpResponse {
209 status: response.status,
210 headers: response.headers,
211 body: response.body,
212 })
213 }
214
215 async fn storage_get(&mut self, bucket: String, key: String) -> Result<Vec<u8>, String> {
216 let host = self.require_host_context()?;
217 host.storage_get(&bucket, &key).await.map_err(host_err_string)
218 }
219
220 async fn storage_put(
221 &mut self,
222 bucket: String,
223 key: String,
224 body: Vec<u8>,
225 content_type: String,
226 ) -> Result<(), String> {
227 let host = self.require_host_context()?;
228 host.storage_put(&bucket, &key, &body, &content_type)
229 .await
230 .map_err(host_err_string)
231 }
232
233 async fn send_email(&mut self, request: String) -> Result<String, String> {
234 let host = self.require_host_context()?;
235 let req: crate::outbound::SendEmailRequest =
236 serde_json::from_str(&request).map_err(|e| e.to_string())?;
237 let response = host.send_email(&req).await.map_err(host_err_string)?;
238 serde_json::to_string(&response).map_err(|e| e.to_string())
239 }
240}
241
242impl wasmtime::ResourceLimiter for StoreData {
243 fn memory_growing(
244 &mut self,
245 _current: usize,
246 desired: usize,
247 _maximum: Option<usize>,
248 ) -> wasmtime::Result<bool> {
249 self.memory_current_bytes = desired as u64;
250 if self.memory_current_bytes > self.memory_peak_bytes {
251 self.memory_peak_bytes = self.memory_current_bytes;
252 }
253 let max = self.limits.max_memory_bytes;
254 if self.memory_current_bytes > max {
255 return Err(wasmtime::Error::msg(format!(
256 "Memory limit exceeded: {} > {max}",
257 self.memory_current_bytes
258 )));
259 }
260 Ok(true)
261 }
262
263 fn table_growing(
264 &mut self,
265 _current: usize,
266 _desired: usize,
267 _maximum: Option<usize>,
268 ) -> wasmtime::Result<bool> {
269 Ok(true)
270 }
271}
272
273#[cfg(test)]
274mod tests;