added more support for supabase-js

This commit is contained in:
2026-03-12 10:18:52 +02:00
parent c0792f2e1d
commit 6708cf28a7
62 changed files with 6563 additions and 526 deletions

23
functions/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "functions"
version = "0.1.0"
edition = "2021"
[dependencies]
wasmtime = "18.0.1"
wasmtime-wasi = "18.0.1"
wasi-common = "18.0.1"
axum.workspace = true
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
tracing.workspace = true
common.workspace = true
sqlx.workspace = true
anyhow.workspace = true
thiserror.workspace = true
chrono.workspace = true
base64 = "0.22"
uuid.workspace = true
deno_core = "0.272.0"

View File

@@ -0,0 +1,189 @@
use anyhow::Result;
use deno_core::{JsRuntime, RuntimeOptions, v8};
use serde_json::Value;
use std::collections::HashMap;
pub struct DenoRuntime {
// We create a new runtime for each execution to ensure isolation
// In a production environment, we might want to pool runtimes or use isolates more efficiently
}
impl DenoRuntime {
pub fn new() -> Self {
Self {}
}
pub async fn execute(&self, code: String, payload: Option<Value>, headers: HashMap<String, String>) -> Result<(String, String, u16, HashMap<String, String>)> {
let (tx, rx) = tokio::sync::oneshot::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let result = Self::execute_inner(code, payload, headers).await;
let _ = tx.send(result);
});
});
rx.await.map_err(|_| anyhow::anyhow!("Deno execution thread panicked"))?
}
async fn execute_inner(code: String, payload: Option<Value>, headers: HashMap<String, String>) -> Result<(String, String, u16, HashMap<String, String>)> {
// Initialize JS Runtime
let mut runtime = JsRuntime::new(RuntimeOptions::default());
// 1. Inject Preamble (Polyfills for Deno.serve, Request, Response, Headers)
let preamble = r#"
globalThis.console = {
log: (...args) => {
Deno.core.print(args.map(a => String(a)).join(" ") + "\n");
},
error: (...args) => {
Deno.core.print("[ERROR] " + args.map(a => String(a)).join(" ") + "\n", true);
}
};
class Headers {
constructor(init) {
this.map = new Map();
if (init) {
if (init instanceof Headers) {
init.forEach((v, k) => this.map.set(k.toLowerCase(), v));
} else if (Array.isArray(init)) {
init.forEach(([k, v]) => this.map.set(k.toLowerCase(), v));
} else {
Object.entries(init).forEach(([k, v]) => this.map.set(k.toLowerCase(), v));
}
}
}
get(key) { return this.map.get(key.toLowerCase()) || null; }
set(key, value) { this.map.set(key.toLowerCase(), value); }
has(key) { return this.map.has(key.toLowerCase()); }
forEach(callback) { this.map.forEach(callback); }
entries() { return this.map.entries(); }
}
globalThis.Headers = Headers;
globalThis.Deno = {
serve: (handler) => {
globalThis._handler = handler;
},
core: Deno.core,
env: {
get: (key) => {
return globalThis._env ? globalThis._env[key] : null;
},
toObject: () => {
return globalThis._env || {};
}
}
};
class Response {
constructor(body, init) {
this.body = body;
this.status = init?.status || 200;
this.headers = new Headers(init?.headers);
}
async text() { return String(this.body); }
async json() { return JSON.parse(this.body); }
}
globalThis.Response = Response;
class Request {
constructor(url, init) {
this.url = url;
this.method = init?.method || "GET";
this._body = init?.body;
this.headers = new Headers(init?.headers);
}
async json() { return typeof this._body === 'string' ? JSON.parse(this._body) : this._body; }
async text() { return typeof this._body === 'string' ? this._body : JSON.stringify(this._body); }
}
globalThis.Request = Request;
"#;
runtime.execute_script("<preamble>", preamble.to_string())?;
// 2. Execute User Code
runtime.execute_script("<user_script>", code.to_string())?;
// 3. Invoke Handler
let payload_json = serde_json::to_string(&payload.unwrap_or(serde_json::json!({})))?;
let headers_json = serde_json::to_string(&headers)?;
let invoke_script = format!(r#"
(async () => {{
if (!globalThis._handler) {{
return {{ error: "No handler registered via Deno.serve" }};
}}
try {{
const headers = {1};
const req = new Request("http://localhost", {{
method: "POST",
body: {0},
headers: headers
}});
const res = await globalThis._handler(req);
const text = await res.text();
// Convert Headers to plain object for return
const resHeaders = {{}};
if (res.headers && typeof res.headers.forEach === 'function') {{
res.headers.forEach((v, k) => resHeaders[k] = v);
}}
return {{
result: text,
headers: resHeaders,
status: res.status
}};
}} catch (e) {{
return {{ error: String(e) }};
}}
}})()
"#, payload_json, headers_json);
let result_val = runtime.execute_script("<invocation>", invoke_script)?;
let result = runtime.resolve_value(result_val).await?;
let scope = &mut runtime.handle_scope();
let local = v8::Local::new(scope, result);
let deserialized_value: Value = deno_core::serde_v8::from_v8(scope, local)?;
let stdout = if let Some(res) = deserialized_value.get("result") {
res.as_str().unwrap_or("").to_string()
} else {
String::new()
};
let stderr = if let Some(err) = deserialized_value.get("error") {
err.as_str().unwrap_or("Unknown error").to_string()
} else {
String::new()
};
let status = if let Some(s) = deserialized_value.get("status") {
s.as_u64().unwrap_or(200) as u16
} else {
200
};
let mut headers = HashMap::new();
if let Some(h) = deserialized_value.get("headers") {
if let Some(obj) = h.as_object() {
for (k, v) in obj {
if let Some(s) = v.as_str() {
headers.insert(k.clone(), s.to_string());
}
}
}
}
Ok((stdout, stderr, status, headers))
}
}

122
functions/src/handlers.rs Normal file
View File

@@ -0,0 +1,122 @@
use axum::{
extract::{Path, State},
http::{StatusCode, HeaderMap},
response::{IntoResponse, Json},
Extension,
};
use std::collections::HashMap;
use sqlx::PgPool;
use base64::prelude::*;
use crate::{FunctionsState, models::{DeployRequest, InvokeRequest, InvokeResponse, Function}};
pub async fn invoke_function(
State(state): State<FunctionsState>,
db: Option<Extension<PgPool>>,
Path(name): Path<String>,
headers: HeaderMap,
Json(payload): Json<InvokeRequest>,
) -> impl IntoResponse {
tracing::info!("Invoking function: {}", name);
let db = db.map(|Extension(p)| p).unwrap_or_else(|| state.db.clone());
// Convert headers
let mut header_map = HashMap::new();
for (k, v) in headers.iter() {
if let Ok(val) = v.to_str() {
header_map.insert(k.as_str().to_string(), val.to_string());
}
}
// 1. Fetch function
let func = sqlx::query_as::<_, Function>("SELECT * FROM functions.functions WHERE name = $1")
.bind(&name)
.fetch_optional(&db)
.await;
let func = match func {
Ok(Some(f)) => f,
Ok(None) => {
tracing::warn!("Function not found: {}", name);
return (StatusCode::NOT_FOUND, "Function not found").into_response();
},
Err(e) => {
tracing::error!("DB error fetching function: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response();
}
};
// 2. Execute
let result = if func.runtime == "deno" || func.runtime == "typescript" || func.runtime == "javascript" {
let code = match String::from_utf8(func.code) {
Ok(c) => c,
Err(e) => {
tracing::error!("Invalid UTF-8 in Deno function code: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid function code".to_string()).into_response();
}
};
state.deno_runtime.execute(code, payload.payload, header_map).await
} else {
// Assume WASM
let payload_str = payload.payload.as_ref().map(|v| v.to_string());
state.runtime.execute(&func.code, payload_str).await.map(|(out, err)| (out, err, 200, HashMap::new()))
};
match result {
Ok((stdout, stderr, status, headers)) => {
tracing::info!("Function executed successfully. Stdout len: {}, Stderr len: {}", stdout.len(), stderr.len());
let resp = InvokeResponse {
result: Some(stdout),
error: if stderr.is_empty() { None } else { Some(stderr) },
logs: vec![],
status,
headers: Some(headers),
};
Json(resp).into_response()
},
Err(e) => {
tracing::error!("Runtime execution error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, format!("Runtime error: {:?}", e)).into_response()
},
}
}
pub async fn deploy_function(
State(state): State<FunctionsState>,
db: Option<Extension<PgPool>>,
Json(payload): Json<DeployRequest>,
) -> impl IntoResponse {
tracing::info!("Deploying function: {}", payload.name);
let db = db.map(|Extension(p)| p).unwrap_or_else(|| state.db.clone());
// Decode base64
let code = match BASE64_STANDARD.decode(&payload.code_base64) {
Ok(c) => c,
Err(e) => {
tracing::error!("Invalid base64: {}", e);
return (StatusCode::BAD_REQUEST, format!("Invalid base64: {}", e)).into_response();
}
};
// Store in DB
let runtime = payload.runtime.unwrap_or("wasm".to_string());
let res = sqlx::query(
"INSERT INTO functions.functions (name, code, runtime) VALUES ($1, $2, $3) ON CONFLICT (name) DO UPDATE SET code = $2, runtime = $3, updated_at = NOW() RETURNING id"
)
.bind(&payload.name)
.bind(&code)
.bind(&runtime)
.fetch_one(&db)
.await;
match res {
Ok(_) => {
tracing::info!("Function deployed successfully");
(StatusCode::OK, "Function deployed").into_response()
},
Err(e) => {
tracing::error!("DB error deploying function: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
},
}
}

29
functions/src/lib.rs Normal file
View File

@@ -0,0 +1,29 @@
use axum::{
routing::post,
Router,
};
use common::Config;
use sqlx::PgPool;
use std::sync::Arc;
use runtime::WasmRuntime;
use deno_runtime::DenoRuntime;
pub mod handlers;
pub mod runtime;
pub mod deno_runtime;
pub mod models;
#[derive(Clone)]
pub struct FunctionsState {
pub db: PgPool,
pub config: Config,
pub runtime: Arc<WasmRuntime>,
pub deno_runtime: Arc<DenoRuntime>,
}
pub fn router(state: FunctionsState) -> Router {
Router::new()
.route("/:name", post(handlers::invoke_function))
.route("/", post(handlers::deploy_function))
.with_state(state)
}

35
functions/src/models.rs Normal file
View File

@@ -0,0 +1,35 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct Function {
pub id: Uuid,
pub name: String,
pub code: Vec<u8>,
pub runtime: String, // "wasm" or "deno"
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Deserialize)]
pub struct InvokeRequest {
pub payload: Option<serde_json::Value>,
}
#[derive(Serialize)]
pub struct InvokeResponse {
pub result: Option<String>,
pub error: Option<String>,
pub logs: Vec<String>,
pub status: u16,
pub headers: Option<std::collections::HashMap<String, String>>,
}
#[derive(Deserialize)]
pub struct DeployRequest {
pub name: String,
pub code_base64: String,
pub runtime: Option<String>,
}

85
functions/src/runtime.rs Normal file
View File

@@ -0,0 +1,85 @@
use anyhow::Result;
use wasmtime::{Config, Engine, Linker, Module, Store};
use wasmtime_wasi::WasiCtxBuilder;
use wasi_common::WasiCtx;
#[derive(Clone)]
pub struct WasmRuntime {
engine: Engine,
}
struct WasiState {
ctx: WasiCtx,
}
impl WasmRuntime {
pub fn new() -> Result<Self> {
let mut config = Config::new();
config.async_support(true); // Enable async
config.epoch_interruption(true); // Allow timeouts
let engine = Engine::new(&config).map_err(|e| anyhow::anyhow!(e))?;
Ok(Self { engine })
}
pub async fn execute(&self, wasm: &[u8], payload: Option<String>) -> Result<(String, String)> {
let start = std::time::Instant::now();
let payload_size = payload.as_ref().map(|s| s.len()).unwrap_or(0);
let module = Module::new(&self.engine, wasm).map_err(|e| anyhow::anyhow!(e).context("Failed to compile WASM module"))?;
// Setup WASI
let stdout = wasi_common::pipe::WritePipe::new_in_memory();
let stderr = wasi_common::pipe::WritePipe::new_in_memory();
let mut builder = WasiCtxBuilder::new();
builder
.stdout(Box::new(stdout.clone()))
.stderr(Box::new(stderr.clone()));
if let Some(p) = payload {
builder.env("PAYLOAD", &p).map_err(|e| anyhow::anyhow!(e))?;
}
let wasi = builder.build();
let mut store = Store::new(&self.engine, WasiState {
ctx: wasi,
});
store.set_epoch_deadline(1);
let mut linker = Linker::new(&self.engine);
wasmtime_wasi::add_to_linker(&mut linker, |s: &mut WasiState| &mut s.ctx)
.map_err(|e| anyhow::anyhow!(e))?;
let instance = linker.instantiate_async(&mut store, &module).await
.map_err(|e| anyhow::anyhow!(e).context("Failed to instantiate module"))?;
let start_func = instance.get_typed_func::<(), ()>(&mut store, "_start")
.map_err(|e| anyhow::anyhow!(e).context("Failed to find _start function"))?;
start_func.call_async(&mut store, ()).await
.map_err(|e| anyhow::anyhow!(e).context("Failed to execute function"))?;
// Drop store to release references to pipes
drop(store);
// Capture output
let out = stdout.try_into_inner().map_err(|_| anyhow::anyhow!("Failed to get stdout")).unwrap().into_inner();
let err = stderr.try_into_inner().map_err(|_| anyhow::anyhow!("Failed to get stderr")).unwrap().into_inner();
let stdout_str = String::from_utf8_lossy(&out).to_string();
let stderr_str = String::from_utf8_lossy(&err).to_string();
let duration = start.elapsed();
tracing::info!(
target: "function_metrics",
execution_time_ms = duration.as_millis(),
payload_size_bytes = payload_size,
success = true,
"Function executed successfully"
);
Ok((stdout_str, stderr_str))
}
}