use std::sync::Arc;
use fraiseql_core::security::{GuestQueryBridge, SecurityContext};
use fraiseql_error::{FraiseQLError, Result};
use crate::{
HostContext,
host::{HttpResponse, live::HostContextConfig},
types::{EventPayload, LogLevel},
};
pub struct RequestQueryHost {
event_payload: EventPayload,
reader: Option<Arc<dyn GuestQueryBridge>>,
principal: Option<SecurityContext>,
http: HostContextConfig,
http_client: Option<Arc<reqwest::Client>>,
}
impl RequestQueryHost {
#[must_use]
pub fn new(
event_payload: EventPayload,
reader: Option<Arc<dyn GuestQueryBridge>>,
principal: Option<&SecurityContext>,
http: HostContextConfig,
) -> Self {
Self {
event_payload,
reader,
principal: principal.cloned(),
http,
http_client: None,
}
}
#[must_use]
pub fn with_http_client(mut self, client: Arc<reqwest::Client>) -> Self {
self.http_client = Some(client);
self
}
fn outside_the_surface(op: &str) -> FraiseQLError {
FraiseQLError::Authorization {
message: format!(
"`{op}` is not available to a request:query function: it answers a read, on a \
path that is cached and that any caller who can issue the query can reach, so \
it must not cause anything. Move the effect to an after:mutation function, \
which is durable, retried and dead-lettered."
),
action: Some(op.to_string()),
resource: Some("request:query".to_string()),
}
}
}
#[allow(unknown_lints, clippy::unused_async_trait_impl)]
impl HostContext for RequestQueryHost {
async fn query(
&self,
graphql: &str,
variables: serde_json::Value,
) -> Result<serde_json::Value> {
let reader = self.reader.as_ref().ok_or_else(|| FraiseQLError::Unsupported {
message: "no read bridge is wired on this request:query host — the engine supplies \
one for every invocation"
.to_string(),
})?;
let variables = (!variables.is_null()).then_some(variables);
reader.query(graphql, variables.as_ref()).await
}
async fn sql_query(
&self,
_sql: &str,
_params: &[serde_json::Value],
) -> Result<Vec<serde_json::Value>> {
Err(Self::outside_the_surface("sql_query"))
}
async fn http_request(
&self,
method: &str,
url: &str,
headers: &[(String, String)],
body: Option<&[u8]>,
) -> Result<HttpResponse> {
crate::host::outbound_http::perform(
&self.http,
self.http_client.as_ref(),
method,
url,
headers,
body,
)
.await
}
async fn storage_get(&self, _bucket: &str, _key: &str) -> Result<Vec<u8>> {
Err(Self::outside_the_surface("storage_get"))
}
async fn storage_put(
&self,
_bucket: &str,
_key: &str,
_body: &[u8],
_content_type: &str,
) -> Result<()> {
Err(Self::outside_the_surface("storage_put"))
}
async fn send_email(
&self,
_request: &crate::outbound::SendEmailRequest,
) -> Result<crate::outbound::SendEmailResponse> {
Err(Self::outside_the_surface("send_email"))
}
fn auth_context(&self) -> Result<serde_json::Value> {
let context = self.principal.as_ref().ok_or_else(|| FraiseQLError::Unsupported {
message: "this query was issued anonymously: a request:query function has no \
authenticated context to read"
.to_string(),
})?;
Ok(crate::host::auth_context_json(context))
}
fn env_var(&self, _name: &str) -> Result<Option<String>> {
Err(Self::outside_the_surface("env_var"))
}
fn event_payload(&self) -> &EventPayload {
&self.event_payload
}
fn log(&self, level: LogLevel, message: &str) {
match level {
LogLevel::Debug => tracing::debug!(target: "fraiseql::functions::guest", "{message}"),
LogLevel::Info => tracing::info!(target: "fraiseql::functions::guest", "{message}"),
LogLevel::Warn => tracing::warn!(target: "fraiseql::functions::guest", "{message}"),
LogLevel::Error => tracing::error!(target: "fraiseql::functions::guest", "{message}"),
}
}
}
#[must_use]
pub fn request_query_payload(field: &str, arguments: serde_json::Value) -> EventPayload {
EventPayload {
trigger_type: "request:query".to_string(),
entity: field.to_string(),
event_kind: "request".to_string(),
data: arguments,
timestamp: chrono::Utc::now(),
}
}
#[must_use]
pub fn interpret_query_answer(value: Option<serde_json::Value>) -> serde_json::Value {
value.unwrap_or(serde_json::Value::Null)
}
#[cfg(test)]
#[path = "request_query/tests.rs"]
mod tests;