use std::sync::Arc;
use axum::{
Router,
body::Bytes,
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::post,
};
use fraiseql_functions::{
FunctionStore,
runtime::SendFunctionRuntime,
types::{EventPayload, FunctionModule, ResourceLimits},
};
use serde_json::json;
use sha2::Digest as _;
#[derive(Clone)]
pub struct FunctionsRouteState {
pub store: Arc<dyn FunctionStore>,
pub runtime: Arc<dyn SendFunctionRuntime>,
}
pub async fn invoke_function_handler(
State(state): State<FunctionsRouteState>,
Path(name): Path<String>,
body: Bytes,
) -> Response {
let record = match state.store.get_function(&name).await {
Ok(Some(r)) => r,
Ok(None) => {
return (
StatusCode::NOT_FOUND,
json!({ "error": format!("function '{name}' not found") }).to_string(),
)
.into_response();
},
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
json!({ "error": e.to_string() }).to_string(),
)
.into_response();
},
};
let module = FunctionModule {
name: record.name.clone(),
source_hash: hex::encode(sha2::Sha256::digest(&record.bytecode)),
bytecode: record.bytecode,
runtime: record.runtime,
};
let data: serde_json::Value = if body.is_empty() {
serde_json::Value::Null
} else {
serde_json::from_slice(&body).unwrap_or(serde_json::Value::Null)
};
let event = EventPayload {
trigger_type: "http".to_string(),
entity: name.clone(),
event_kind: "invoke".to_string(),
data,
timestamp: chrono::Utc::now(),
};
match state.runtime.invoke_raw(&module, event, ResourceLimits::default()).await {
Ok(result) => {
let body = json!({
"value": result.value,
"logs": result.logs,
"duration_ms": result.duration.as_millis(),
"memory_peak_bytes": result.memory_peak_bytes,
});
(StatusCode::OK, body.to_string()).into_response()
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": e.to_string() }).to_string())
.into_response()
},
}
}
pub fn functions_router(state: FunctionsRouteState) -> Router {
Router::new()
.route("/functions/v1/{name}", post(invoke_function_handler))
.with_state(state)
}
#[cfg(test)]
mod tests;