use std::{
collections::HashMap,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
use super::{
context::ExecutorContext,
core::Executor,
runners::{
query_nested::{RootRowFilter, own_read, type_read_refusal, whole_object},
read_plan::ReadPlan,
},
support::security::refuse_unreadable_where,
};
use crate::{
error::{FraiseQLError, Result},
graphql::{FieldSelection, ParsedQuery},
runtime::projection::effective_selections,
schema::{CompiledSchema, SubscriptionDefinition},
security::SecurityContext,
};
static SUPPRESSED: AtomicU64 = AtomicU64::new(0);
#[must_use]
pub fn suppressed_subscription_events() -> u64 {
SUPPRESSED.load(Ordering::Relaxed)
}
pub struct SubscriptionPlan {
ctx: Arc<ExecutorContext>,
plan: ReadPlan,
root_rows: Option<RootRowFilter>,
principal: Option<SecurityContext>,
subscription: String,
type_name: String,
selections: Vec<FieldSelection>,
variables: HashMap<String, serde_json::Value>,
}
impl std::fmt::Debug for SubscriptionPlan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubscriptionPlan")
.field("subscription", &self.subscription)
.field("type_name", &self.type_name)
.finish_non_exhaustive()
}
}
impl SubscriptionPlan {
#[must_use]
pub fn subscription_name(&self) -> &str {
&self.subscription
}
#[must_use]
pub(crate) fn subscriber_uuid(&self) -> Option<uuid::Uuid> {
self.principal
.as_ref()
.and_then(|who| uuid::Uuid::parse_str(who.user_id.as_str()).ok())
}
#[must_use]
pub fn deliver(&self, after_image: &serde_json::Value) -> Option<serde_json::Value> {
if self.root_rows.as_ref().is_some_and(|rows| !rows.admits(after_image)) {
SUPPRESSED.fetch_add(1, Ordering::Relaxed);
return None;
}
if let Ok(served) = self.plan.serve(
&self.ctx,
self.principal.as_ref(),
&self.type_name,
&self.selections,
after_image,
&self.variables,
) {
Some(served)
} else {
SUPPRESSED.fetch_add(1, Ordering::Relaxed);
None
}
}
}
impl Executor {
pub fn plan_subscription(
&self,
document: &ParsedQuery,
variables: Option<&serde_json::Value>,
principal: Option<&SecurityContext>,
) -> Result<SubscriptionPlan> {
let ctx = &self.ctx;
let schema = &ctx.schema;
let defaults = crate::graphql::value_json::variable_defaults(&document.variables)?;
let defaulted = crate::graphql::value_json::with_variable_defaults(&defaults, variables);
let variables_map =
crate::graphql::selection_set::variables_map(defaulted.as_ref().or(variables));
let resolved = crate::graphql::selection_set::resolve_and_filter(
&document.selections,
&document.fragments,
&variables_map,
self.max_query_depth(),
)?;
let mut roots = resolved.iter().filter(|s| s.name != "__typename");
let (Some(root), None) = (roots.next(), roots.next()) else {
return Err(FraiseQLError::Validation {
message: "a subscription operation selects exactly one root field".to_string(),
path: None,
});
};
let definition =
schema.find_subscription(&root.name).ok_or_else(|| FraiseQLError::Validation {
message: format!("Subscription '{}' not found in schema", root.name),
path: None,
})?;
let type_name = definition.return_type.as_str();
refuse_undeclared_selection(schema, type_name, &root.nested_fields)?;
refuse_undelivered_selection(definition, &root.nested_fields)?;
self.refuse_unless_readable(&definition.name, type_name, principal)?;
let arguments: HashMap<String, serde_json::Value> =
crate::runtime::subscription::subscription_arguments(
schema,
document,
variables,
self.max_query_depth(),
)?
.into_iter()
.collect();
for path in active_filter_paths(definition, &arguments) {
let condition = crate::db::WhereClause::Field {
path,
operator: crate::db::WhereOperator::Eq,
value: serde_json::Value::Null,
};
refuse_unreadable_where(schema, type_name, &condition, principal)?;
}
self.plan_read(
&definition.name,
type_name,
root.nested_fields.clone(),
variables_map,
principal,
)
}
pub fn plan_type_stream(
&self,
type_name: &str,
principal: Option<&SecurityContext>,
) -> Result<SubscriptionPlan> {
let selections = whole_object(&self.ctx.schema, type_name, 0)?;
if let Some(authorizer) = self.ctx.config.authorizer.as_ref() {
let name = own_read(&self.ctx.schema, type_name).map_or(type_name, |q| q.name.as_str());
let ops = [crate::security::AuthzOperation::root(
crate::security::OperationKind::Query,
name,
Some(type_name),
)];
crate::security::authorizer::enforce_authz(authorizer.as_ref(), principal, &ops, None)?;
}
self.refuse_unless_readable(type_name, type_name, principal)?;
self.plan_read(type_name, type_name, selections, HashMap::new(), principal)
}
fn refuse_unless_readable(
&self,
read: &str,
type_name: &str,
principal: Option<&SecurityContext>,
) -> Result<()> {
match type_read_refusal(&self.ctx.schema, type_name, principal) {
None => Ok(()),
Some(why) => Err(FraiseQLError::Authorization {
message: format!("'{read}' delivers '{type_name}', {why}"),
action: Some("read".to_string()),
resource: Some(type_name.to_string()),
}),
}
}
fn plan_read(
&self,
read: &str,
type_name: &str,
selections: Vec<FieldSelection>,
variables: HashMap<String, serde_json::Value>,
principal: Option<&SecurityContext>,
) -> Result<SubscriptionPlan> {
let ctx = &self.ctx;
let root_rows = RootRowFilter::plan(ctx, read, type_name, principal)?;
let bound = serde_json::Value::Object(variables.clone().into_iter().collect());
let plan = ReadPlan::classify(ctx, principal, Some(&bound), &[(type_name, &selections)])?;
Ok(SubscriptionPlan {
ctx: Arc::clone(ctx),
plan,
root_rows,
principal: principal.cloned(),
subscription: read.to_string(),
type_name: type_name.to_string(),
selections,
variables,
})
}
}
fn refuse_undeclared_selection(
schema: &CompiledSchema,
type_name: &str,
selections: &[FieldSelection],
) -> Result<()> {
let Some(type_def) = schema.find_type(type_name) else {
return Ok(());
};
for sel in effective_selections(selections, type_name, schema) {
if sel.name == "__typename" {
continue;
}
let Some(field) = type_def.fields.iter().find(|f| f.name == sel.name) else {
return Err(FraiseQLError::Validation {
message: format!("Cannot query field '{}' on type '{type_name}'", sel.name),
path: None,
});
};
if let Some(child) = field.field_type.inner_type().unwrap_or(&field.field_type).type_name()
{
refuse_undeclared_selection(schema, child, &sel.nested_fields)?;
}
}
Ok(())
}
fn refuse_undelivered_selection(
definition: &SubscriptionDefinition,
selections: &[FieldSelection],
) -> Result<()> {
if definition.fields.is_empty() {
return Ok(());
}
let delivered: Vec<&str> = definition
.fields
.iter()
.filter_map(|f| f.trim_start_matches('/').split(['/', '.']).next())
.collect();
match selections
.iter()
.find(|s| s.name != "__typename" && !delivered.contains(&s.name.as_str()))
{
Some(sel) => Err(FraiseQLError::Validation {
message: format!(
"Subscription '{}' does not deliver field '{}'",
definition.name, sel.name
),
path: None,
}),
None => Ok(()),
}
}
fn active_filter_paths(
definition: &SubscriptionDefinition,
variables: &HashMap<String, serde_json::Value>,
) -> Vec<Vec<String>> {
let bound = |argument: &str| variables.get(argument).is_some_and(|v| !v.is_null());
let segments = |path: &str| -> Vec<String> {
path.trim_start_matches('/')
.split(['/', '.'])
.filter(|s| !s.is_empty())
.map(crate::utils::to_snake_case)
.collect()
};
let mut paths: Vec<Vec<String>> = definition
.filter_fields
.iter()
.filter(|field| bound(field))
.map(|field| segments(field))
.collect();
if let Some(filter) = &definition.filter {
paths.extend(
filter
.argument_paths
.iter()
.filter(|(argument, _)| bound(argument))
.map(|(_, path)| segments(path)),
);
paths.extend(filter.static_filters.iter().map(|condition| segments(&condition.path)));
}
paths
}
#[cfg(test)]
#[path = "subscription_plan_tests.rs"]
mod tests;