use std::sync::Arc;
use tracing::debug;
use super::query::QueryRunner;
use crate::{
backend::types::JsonbValue,
error::{FraiseQLError, Result},
runtime::{QueryFunctionRequest, ResultProjector, matcher::QueryMatch},
security::SecurityContext,
};
impl QueryRunner {
pub(in super::super) async fn execute_function_backed_query(
&self,
query_match: &QueryMatch,
variables: Option<&serde_json::Value>,
security_context: Option<&SecurityContext>,
) -> Result<serde_json::Value> {
let query_def = &query_match.query_def;
let field = query_def.name.as_str();
let function = query_def.function.as_deref().ok_or_else(|| FraiseQLError::Internal {
message: format!("`{field}` reached the function runner with no function declared"),
source: None,
})?;
let resolver = self.ctx.config.query_function_resolver.as_ref().ok_or_else(|| {
FraiseQLError::Validation {
message: format!(
"`{field}` is backed by the function `{function}`, and this server has no \
function resolver wired, so the field cannot be answered. Build the \
server with the `functions-runtime` feature, or remove the binding and \
recompile the schema."
),
path: Some(field.to_string()),
}
})?;
let root_fields =
query_match.selections.first().map_or(&[][..], |r| r.nested_fields.as_slice());
let gated_present = crate::security::field_authorizer::selection_set_selects_gated_field(
&self.ctx.schema,
&query_def.return_type,
root_fields,
);
if security_context.is_none() {
crate::security::field_authorizer::deny_if_gated_field_selected(
&self.ctx.schema,
&query_def.return_type,
root_fields,
"unauthenticated function-backed query",
)?;
}
let cache_key = self
.ctx
.response_cache
.as_ref()
.filter(|rc| rc.is_enabled() && !gated_present)
.map(|_| {
(
Self::compute_response_cache_key(query_match),
crate::cache::response_cache::hash_security_context(security_context),
)
});
let cache_fence = self.ctx.response_cache.as_ref().map(|rc| rc.invalidation_generation());
if let (Some((query_key, sec_hash)), Some(rc)) =
(cache_key, self.ctx.response_cache.as_ref())
{
if let Some(cached) = rc.get(query_key, sec_hash)? {
debug!(
target: "fraiseql::cache::response",
event = "hit", query = %field, query_key, sec_hash,
"response cache hit (function-backed)"
);
return Ok(Arc::unwrap_or_clone(cached));
}
}
let plan = self.ctx.planner.plan(query_match)?;
let selection_access = super::query_nested::SelectionAccess::classify(
&self.ctx.schema,
&query_def.return_type,
root_fields,
plan.projection_fields,
security_context,
super::query_nested::LevelAuthz::from_config(&self.ctx.config, variables),
)?;
let access = &selection_access.root;
let reader = Arc::new(super::super::support::hook_reader::CallerScopedReader::new(
Arc::clone(&self.ctx),
security_context,
));
let arguments = serde_json::Value::Object(
query_match
.arguments
.iter()
.collect::<std::collections::BTreeMap<_, _>>()
.into_iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
);
let value = resolver
.resolve(QueryFunctionRequest {
function,
field,
arguments: &arguments,
principal: security_context,
reader,
})
.await?;
let declared: Vec<&str> = self
.ctx
.schema
.find_type(&query_def.return_type)
.map(|t| t.fields.iter().map(|f| f.name.as_str()).collect())
.unwrap_or_default();
let rows = Self::rows_from_function_result(
field,
function,
value,
query_def.returns_list,
&query_def.return_type,
&declared,
)?;
let projector = ResultProjector::new(access.projected.clone())
.with_declared_scalars(&self.ctx.schema, &query_def.return_type)
.configure_typename_from_selections(&query_match.selections, &query_def.return_type);
let mut projected = projector.project_results(&rows, query_def.returns_list)?;
crate::runtime::project_nested_lists(
&mut projected,
&query_def.return_type,
root_fields,
&self.ctx.schema,
);
crate::runtime::stamp_nested_typenames(
&mut projected,
&query_def.return_type,
root_fields,
&self.ctx.schema,
);
selection_access.null_masked(
&mut projected,
&query_def.return_type,
root_fields,
&self.ctx.schema,
);
if let (true, Some(ctx)) = (gated_present, security_context) {
self.apply_dynamic_field_authorizer(query_match, ctx, access, &rows, &mut projected)?;
}
let response =
ResultProjector::wrap_in_data_envelope(projected, query_match.response_key());
if let (Some((query_key, sec_hash)), Some(rc)) =
(cache_key, self.ctx.response_cache.as_ref())
{
let accessed = crate::cache::extract_accessed_views(query_def);
let cached = Arc::new(response);
let _ = rc.put(query_key, sec_hash, Arc::clone(&cached), accessed, cache_fence);
return Ok(Arc::unwrap_or_clone(cached));
}
Ok(response)
}
fn rows_from_function_result(
field: &str,
function: &str,
value: serde_json::Value,
returns_list: bool,
return_type: &str,
declared: &[&str],
) -> Result<Vec<JsonbValue>> {
let refuse = |found: &str| FraiseQLError::Validation {
message: format!(
"`{field}` is backed by the function `{function}`, which returned {found}. A \
function-backed field returns the field's data: {} Return that, not a GraphQL \
response envelope.",
if returns_list {
"this one is a list, so an array of objects."
} else {
"this one is a single item, so one object (or null)."
}
),
path: Some(field.to_string()),
};
let unrelated = |object: &serde_json::Map<String, serde_json::Value>| {
!declared.is_empty() && !object.keys().any(|k| declared.contains(&k.as_str()))
};
let refuse_unrelated = |object: &serde_json::Map<String, serde_json::Value>| {
let mut keys: Vec<&str> = object.keys().map(String::as_str).collect();
keys.sort_unstable();
FraiseQLError::Validation {
message: format!(
"`{field}` is backed by the function `{function}`, which returned an object \
with none of `{return_type}`'s fields: it has [{}] and the type declares \
[{}]. Every selected field would render as null. A function returns the \
entity document itself, not a `{{\"data\": …}}` envelope.",
keys.join(", "),
declared.join(", "),
),
path: Some(field.to_string()),
}
};
match (value, returns_list) {
(serde_json::Value::Array(items), true) => items
.into_iter()
.map(|item| match item {
serde_json::Value::Object(object) if unrelated(&object) => {
Err(refuse_unrelated(&object))
},
object @ serde_json::Value::Object(_) => Ok(JsonbValue::new(object)),
_ => Err(refuse("an array containing a non-object element")),
})
.collect(),
(serde_json::Value::Array(_), false) => Err(refuse("an array")),
(serde_json::Value::Null, false) => Ok(Vec::new()),
(serde_json::Value::Null, true) => Err(refuse("null")),
(serde_json::Value::Object(object), false) if unrelated(&object) => {
Err(refuse_unrelated(&object))
},
(object @ serde_json::Value::Object(_), false) => Ok(vec![JsonbValue::new(object)]),
(_, _) => Err(refuse("a scalar")),
}
}
}
#[cfg(test)]
#[path = "query_function_tests.rs"]
mod tests;