use std::{future::Future, pin::Pin, sync::Arc};
use arc_swap::ArcSwap;
use fraiseql_core::{runtime::Executor, security::SecurityContext};
use fraiseql_error::Result;
use fraiseql_functions::host::live::QueryExecutor;
use serde_json::Value;
pub const SOURCE_TENANT_VAR: &str = "__source_tenant";
#[derive(Clone)]
pub struct ExecutorSource(Source);
#[derive(Clone)]
enum Source {
Single(Arc<ArcSwap<Executor>>),
Tenants(Arc<TenantExecutorRegistry>),
}
use crate::routes::graphql::tenant_registry::TenantExecutorRegistry;
impl ExecutorSource {
#[must_use]
pub fn of(state: &crate::routes::graphql::AppState) -> Self {
Self(match state.tenant_registry() {
Some(registry) => Source::Tenants(Arc::clone(registry)),
None => Source::Single(state.executor.clone()),
})
}
pub fn executor_for(&self, tenant: Option<&str>) -> Result<Arc<Executor>> {
match &self.0 {
Source::Single(executor) => Ok(executor.load_full()),
Source::Tenants(registry) => Ok(Arc::clone(&*registry.executor_for(tenant)?)),
}
}
#[must_use]
pub const fn registry(&self) -> Option<&Arc<TenantExecutorRegistry>> {
match &self.0 {
Source::Tenants(registry) => Some(registry),
Source::Single(_) => None,
}
}
#[must_use]
pub const fn serving(&self) -> Option<&Arc<ArcSwap<Executor>>> {
match &self.0 {
Source::Single(executor) => Some(executor),
Source::Tenants(_) => None,
}
}
#[cfg(test)]
pub(crate) const fn single(executor: Arc<ArcSwap<Executor>>) -> Self {
Self(Source::Single(executor))
}
#[must_use]
pub fn downgrade(&self) -> WeakExecutorSource {
WeakExecutorSource(match &self.0 {
Source::Single(executor) => WeakSource::Single(Arc::downgrade(executor)),
Source::Tenants(registry) => WeakSource::Tenants(Arc::downgrade(registry)),
})
}
}
#[derive(Clone)]
pub struct WeakExecutorSource(WeakSource);
#[derive(Clone)]
enum WeakSource {
Single(std::sync::Weak<ArcSwap<Executor>>),
Tenants(std::sync::Weak<TenantExecutorRegistry>),
}
impl WeakExecutorSource {
#[must_use]
pub fn upgrade(&self) -> Option<ExecutorSource> {
Some(ExecutorSource(match &self.0 {
WeakSource::Single(executor) => Source::Single(executor.upgrade()?),
WeakSource::Tenants(registry) => Source::Tenants(registry.upgrade()?),
}))
}
}
pub struct RunAsQueryExecutor {
source: ExecutorSource,
identity: SecurityContext,
per_message_tenant: bool,
}
impl RunAsQueryExecutor {
#[must_use]
pub const fn new(source: ExecutorSource, identity: SecurityContext) -> Self {
Self {
source,
identity,
per_message_tenant: false,
}
}
#[must_use]
pub const fn for_source(source: ExecutorSource, identity: SecurityContext) -> Self {
Self {
source,
identity,
per_message_tenant: true,
}
}
}
fn resolve_identity(base: &SecurityContext, tenant: Option<&str>) -> SecurityContext {
match (base.tenant_id.is_none(), tenant) {
(true, Some(tenant)) => base.clone().with_tenant(tenant),
_ => base.clone(),
}
}
fn split_tenant_override(variables: Option<&Value>) -> (Option<Value>, Option<String>) {
let Some(Value::Object(map)) = variables else {
return (variables.cloned(), None);
};
if !map.contains_key(SOURCE_TENANT_VAR) {
return (Some(Value::Object(map.clone())), None);
}
let mut map = map.clone();
let tenant = match map.remove(SOURCE_TENANT_VAR) {
Some(Value::String(tenant)) if !tenant.trim().is_empty() => Some(tenant),
_ => None,
};
(Some(Value::Object(map)), tenant)
}
impl QueryExecutor for RunAsQueryExecutor {
fn execute_query(
&self,
query: &str,
variables: Option<&Value>,
) -> Pin<Box<dyn Future<Output = Result<Value>> + Send + '_>> {
let (variables, tenant) = split_tenant_override(variables);
let tenant = tenant.filter(|_| self.per_message_tenant);
let identity = resolve_identity(&self.identity, tenant.as_deref());
let executor = self.source.executor_for(identity.tenant_id.as_ref().map(|t| t.0.as_str()));
let query = query.to_owned();
Box::pin(fraiseql_core::runtime::dispatched_at(1, async move {
executor?.execute_with_security(&query, variables.as_ref(), &identity).await
}))
}
}
#[cfg(test)]
mod tests;