use std::sync::Arc;
use async_trait::async_trait;
use fraiseql_arrow::QueryExecutor;
use fraiseql_core::security::SecurityContext;
use http::HeaderMap;
use crate::routes::graphql::{app_state::AppState, tenant_dispatch};
pub struct PolicyGatedExecutor {
state: AppState,
}
impl PolicyGatedExecutor {
#[must_use]
pub const fn new(state: AppState) -> Self {
Self { state }
}
}
#[async_trait]
impl QueryExecutor for PolicyGatedExecutor {
async fn execute_with_security(
&self,
query: &str,
variables: Option<&serde_json::Value>,
security_context: &SecurityContext,
) -> Result<serde_json::Value, fraiseql_core::error::FraiseQLError> {
if let Some(ref store) = self.state.trusted_docs {
store.resolve(None, Some(query)).map_err(|e| match e {
crate::trusted_documents::TrustedDocumentError::ForbiddenRawQuery => {
crate::trusted_documents::record_rejected();
fraiseql_core::error::FraiseQLError::Authorization {
message: "Ad-hoc GraphQL documents are forbidden (persisted queries \
only). The Arrow Flight transport cannot supply a persisted \
document ID."
.to_string(),
resource: None,
action: None,
}
},
other => fraiseql_core::error::FraiseQLError::validation(format!(
"Trusted document rejection: {other}"
)),
})?;
}
let tenant_key = tenant_dispatch::resolve_tenant_key(
&self.state,
Some(security_context),
&HeaderMap::new(),
)?;
let dispatch = tenant_dispatch::dispatch_to_tenant(&self.state, tenant_key.as_deref())?;
let executor = &dispatch.executor;
let estimated_cost = tenant_dispatch::estimate_request_cost(query, variables, executor);
tenant_dispatch::charge_cost_budget(
&self.state,
tenant_key.as_deref(),
Some(security_context),
estimated_cost,
)?;
executor
.execute_with_security(query, variables, security_context)
.await
.map_err(|e| {
tracing::warn!(error = %e, "Flight GraphQL execution failed");
self.state.error_sanitizer.sanitize_error(e)
})
}
}
pub(crate) fn policy_gated_executor(state: AppState) -> Arc<dyn QueryExecutor> {
Arc::new(PolicyGatedExecutor::new(state))
}