pub mod executor;
pub mod ops;
pub mod tests;
pub mod transpile;
pub mod watchdog;
#[cfg(test)]
mod follow_up_tests;
#[cfg(test)]
mod transpile_tests;
#[cfg(all(test, feature = "host-live"))]
mod idempotency_tests;
#[cfg(all(test, feature = "host-live"))]
mod cursor_tests;
#[cfg(test)]
mod qonto_tests;
#[cfg(test)]
mod reply_awareness_tests;
#[cfg(test)]
mod scoring_tests;
use std::sync::Arc;
use fraiseql_error::Result;
use crate::{
HostContext,
host::dyn_context::DynHostContext,
runtime::FunctionRuntime,
types::{EventPayload, FunctionModule, FunctionResult, ResourceLimits},
};
pub const FRAISEQL_HOST_TYPES: &str = r"
// FraiseQL native-functions host surface (Deno.core.ops.fraiseql_*).
interface FraiseqlHttpResponse {
status: number;
headers: Array<[string, string]>;
body: Uint8Array;
}
interface FraiseqlHostOps {
// Execute a GraphQL query/mutation. `variables` is a JSON string; returns a JSON string.
fraiseql_query(graphql: string, variables: string): Promise<string>;
// NOT IMPLEMENTED (#871): no execution backend is wired — every call throws
// (statements are classified read-only/rejected, never executed). Use
// `fraiseql_query` instead.
fraiseql_sql_query(sql: string, params: string): Promise<string>;
// Make an outbound HTTP request (SSRF-allowlisted by the host).
fraiseql_http_request(
method: string,
url: string,
headers: Array<[string, string]>,
body: Uint8Array | null,
): Promise<FraiseqlHttpResponse>;
// Object storage.
fraiseql_storage_get(bucket: string, key: string): Promise<Uint8Array>;
fraiseql_storage_put(
bucket: string,
key: string,
body: Uint8Array,
contentType: string,
): Promise<void>;
// Send an email. `from` is host-owned; the request JSON carries only
// { to, subject, text?, html?, reply_to? }.
fraiseql_send_email(request: string): Promise<string>;
// The authenticated caller's context, as a JSON string.
fraiseql_auth_context(): string;
// Read a host-allowlisted environment variable (granted via
// FRAISEQL_FUNCTIONS_ALLOWED_ENV_VARS / [sources] allowed_env_vars). A
// non-allowlisted name THROWS; null means allowlisted but unset (#840).
fraiseql_env_var(name: string): string | null;
// Per-dispatch idempotency token, or null on a non-durably-dispatched invocation.
fraiseql_idempotency_token(): string | null;
// Scheduled-source cursor (Model B). `fraiseql_cursor_get` returns the JSON string
// the source last advanced to, or the string 'null'; `fraiseql_cursor_advance`
// persists any JSON value as the new cursor. Both fail on a non-source invocation.
fraiseql_cursor_get(): Promise<string>;
fraiseql_cursor_advance(valueJson: string): Promise<void>;
// Structured log. Levels: 0=debug, 1=info, 2=warn, 3=error.
fraiseql_log(level: number, message: string): void;
}
declare namespace Deno {
namespace core {
const ops: FraiseqlHostOps;
function encode(text: string): Uint8Array;
function decode(bytes: Uint8Array): string;
}
}
";
#[derive(Debug, Clone)]
pub struct DenoConfig {
pub enable_typescript: bool,
pub v8_flags: Vec<String>,
}
impl Default for DenoConfig {
fn default() -> Self {
Self {
enable_typescript: true,
v8_flags: vec![],
}
}
}
pub struct DenoRuntime {
config: DenoConfig,
}
impl std::fmt::Debug for DenoRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DenoRuntime").field("config", &self.config).finish()
}
}
impl Clone for DenoRuntime {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
}
}
}
impl DenoRuntime {
pub fn new(config: &DenoConfig) -> Result<Self> {
Ok(Self {
config: config.clone(),
})
}
pub async fn invoke_with_context(
&self,
module: &FunctionModule,
event: EventPayload,
host_context: Arc<dyn DynHostContext>,
limits: ResourceLimits,
) -> Result<FunctionResult> {
run_guest(module, event, limits, Some(host_context), self.config.enable_typescript).await
}
}
fn run_guest(
module: &FunctionModule,
event: EventPayload,
limits: ResourceLimits,
host: Option<Arc<dyn DynHostContext>>,
enable_typescript: bool,
) -> impl std::future::Future<Output = Result<FunctionResult>> + Send {
let raw = String::from_utf8_lossy(&module.bytecode).to_string();
let source = if enable_typescript {
transpile::transpile_typescript(&raw)
} else {
Ok(raw)
};
let event_data = event.data;
async move {
let source = source.map_err(|message| fraiseql_error::FraiseQLError::Validation {
message,
path: None,
})?;
let host = match host {
Some(h) => Some(
tokio::runtime::Handle::try_current()
.map(|handle| {
Arc::new(crate::host::runtime_pinned::RuntimePinnedHost::new(h, handle))
as Arc<dyn DynHostContext>
})
.map_err(|_| fraiseql_error::FraiseQLError::Internal {
message: "Deno host ops require a Tokio runtime context (#970): \
invoke_with_context must be awaited on the server runtime"
.to_string(),
source: None,
})?,
),
None => None,
};
let start = std::time::Instant::now();
let (tx, rx) = tokio::sync::oneshot::channel::<
std::result::Result<executor::ExecutionResult, String>,
>();
std::thread::spawn(move || {
let result = executor::run_in_dedicated_thread(&source, &event_data, &limits, host);
let _ = tx.send(result);
});
let exec_result = rx.await.map_err(|_| fraiseql_error::FraiseQLError::Internal {
message: "Deno executor thread crashed".to_string(),
source: None,
})?;
let duration = start.elapsed();
match exec_result {
Ok(execution_result) => Ok(FunctionResult {
value: Some(execution_result.value),
logs: execution_result.logs,
duration,
memory_peak_bytes: 0,
}),
Err(e) if e.starts_with("SyntaxError") => {
Err(fraiseql_error::FraiseQLError::Validation {
message: e,
path: None,
})
},
Err(e) if e.contains(crate::types::PERMANENT_ERROR_MARKER) => {
Err(fraiseql_error::FraiseQLError::Validation {
message: e,
path: None,
})
},
Err(e) => Err(fraiseql_error::FraiseQLError::Unsupported { message: e }),
}
}
}
impl FunctionRuntime for DenoRuntime {
#[allow(clippy::manual_async_fn)] fn invoke<H>(
&self,
module: &FunctionModule,
event: EventPayload,
_host: &H,
limits: ResourceLimits,
) -> impl std::future::Future<Output = Result<FunctionResult>> + Send
where
H: HostContext + ?Sized,
{
run_guest(module, event, limits, None, self.config.enable_typescript)
}
fn supported_extensions(&self) -> &[&str] {
&[".js", ".ts", ".mjs", ".mts"]
}
fn supports_hot_reload(&self) -> bool {
false
}
fn name(&self) -> &'static str {
"deno"
}
}