fraiseql_core/runtime/executor/
subscription_plan.rs1use std::{
26 collections::HashMap,
27 sync::{
28 Arc,
29 atomic::{AtomicU64, Ordering},
30 },
31};
32
33use super::{
34 context::ExecutorContext,
35 core::Executor,
36 runners::{
37 query_nested::{RootRowFilter, own_read, type_read_refusal, whole_object},
38 read_plan::ReadPlan,
39 },
40 support::security::refuse_unreadable_where,
41};
42use crate::{
43 error::{FraiseQLError, Result},
44 graphql::{FieldSelection, ParsedQuery},
45 runtime::projection::effective_selections,
46 schema::{CompiledSchema, SubscriptionDefinition},
47 security::SecurityContext,
48};
49
50static SUPPRESSED: AtomicU64 = AtomicU64::new(0);
53
54#[must_use]
56pub fn suppressed_subscription_events() -> u64 {
57 SUPPRESSED.load(Ordering::Relaxed)
58}
59
60pub struct SubscriptionPlan {
62 ctx: Arc<ExecutorContext>,
63 plan: ReadPlan,
64 root_rows: Option<RootRowFilter>,
65 principal: Option<SecurityContext>,
66 subscription: String,
67 type_name: String,
68 selections: Vec<FieldSelection>,
69 variables: HashMap<String, serde_json::Value>,
70}
71
72impl std::fmt::Debug for SubscriptionPlan {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.debug_struct("SubscriptionPlan")
75 .field("subscription", &self.subscription)
76 .field("type_name", &self.type_name)
77 .finish_non_exhaustive()
78 }
79}
80
81impl SubscriptionPlan {
82 #[must_use]
84 pub fn subscription_name(&self) -> &str {
85 &self.subscription
86 }
87
88 #[must_use]
92 pub(crate) fn subscriber_uuid(&self) -> Option<uuid::Uuid> {
93 self.principal
94 .as_ref()
95 .and_then(|who| uuid::Uuid::parse_str(who.user_id.as_str()).ok())
96 }
97
98 #[must_use]
101 pub fn deliver(&self, after_image: &serde_json::Value) -> Option<serde_json::Value> {
102 if self.root_rows.as_ref().is_some_and(|rows| !rows.admits(after_image)) {
103 SUPPRESSED.fetch_add(1, Ordering::Relaxed);
104 return None;
105 }
106 if let Ok(served) = self.plan.serve(
107 &self.ctx,
108 self.principal.as_ref(),
109 &self.type_name,
110 &self.selections,
111 after_image,
112 &self.variables,
113 ) {
114 Some(served)
115 } else {
116 SUPPRESSED.fetch_add(1, Ordering::Relaxed);
117 None
118 }
119 }
120}
121
122impl Executor {
123 pub fn plan_subscription(
133 &self,
134 document: &ParsedQuery,
135 variables: Option<&serde_json::Value>,
136 principal: Option<&SecurityContext>,
137 ) -> Result<SubscriptionPlan> {
138 let ctx = &self.ctx;
139 let schema = &ctx.schema;
140 let defaults = crate::graphql::value_json::variable_defaults(&document.variables)?;
143 let defaulted = crate::graphql::value_json::with_variable_defaults(&defaults, variables);
144 let variables_map =
145 crate::graphql::selection_set::variables_map(defaulted.as_ref().or(variables));
146 let resolved = crate::graphql::selection_set::resolve_and_filter(
147 &document.selections,
148 &document.fragments,
149 &variables_map,
150 self.max_query_depth(),
151 )?;
152 let mut roots = resolved.iter().filter(|s| s.name != "__typename");
153 let (Some(root), None) = (roots.next(), roots.next()) else {
154 return Err(FraiseQLError::Validation {
155 message: "a subscription operation selects exactly one root field".to_string(),
156 path: None,
157 });
158 };
159 let definition =
160 schema.find_subscription(&root.name).ok_or_else(|| FraiseQLError::Validation {
161 message: format!("Subscription '{}' not found in schema", root.name),
162 path: None,
163 })?;
164 let type_name = definition.return_type.as_str();
165
166 refuse_undeclared_selection(schema, type_name, &root.nested_fields)?;
167 refuse_undelivered_selection(definition, &root.nested_fields)?;
168 self.refuse_unless_readable(&definition.name, type_name, principal)?;
169 let arguments: HashMap<String, serde_json::Value> =
173 crate::runtime::subscription::subscription_arguments(
174 schema,
175 document,
176 variables,
177 self.max_query_depth(),
178 )?
179 .into_iter()
180 .collect();
181 for path in active_filter_paths(definition, &arguments) {
182 let condition = crate::db::WhereClause::Field {
183 path,
184 operator: crate::db::WhereOperator::Eq,
185 value: serde_json::Value::Null,
186 };
187 refuse_unreadable_where(schema, type_name, &condition, principal)?;
188 }
189 self.plan_read(
190 &definition.name,
191 type_name,
192 root.nested_fields.clone(),
193 variables_map,
194 principal,
195 )
196 }
197
198 pub fn plan_type_stream(
209 &self,
210 type_name: &str,
211 principal: Option<&SecurityContext>,
212 ) -> Result<SubscriptionPlan> {
213 let selections = whole_object(&self.ctx.schema, type_name, 0)?;
214 if let Some(authorizer) = self.ctx.config.authorizer.as_ref() {
216 let name = own_read(&self.ctx.schema, type_name).map_or(type_name, |q| q.name.as_str());
217 let ops = [crate::security::AuthzOperation::root(
218 crate::security::OperationKind::Query,
219 name,
220 Some(type_name),
221 )];
222 crate::security::authorizer::enforce_authz(authorizer.as_ref(), principal, &ops, None)?;
223 }
224 self.refuse_unless_readable(type_name, type_name, principal)?;
225 self.plan_read(type_name, type_name, selections, HashMap::new(), principal)
226 }
227
228 fn refuse_unless_readable(
231 &self,
232 read: &str,
233 type_name: &str,
234 principal: Option<&SecurityContext>,
235 ) -> Result<()> {
236 match type_read_refusal(&self.ctx.schema, type_name, principal) {
237 None => Ok(()),
238 Some(why) => Err(FraiseQLError::Authorization {
239 message: format!("'{read}' delivers '{type_name}', {why}"),
240 action: Some("read".to_string()),
241 resource: Some(type_name.to_string()),
242 }),
243 }
244 }
245
246 fn plan_read(
248 &self,
249 read: &str,
250 type_name: &str,
251 selections: Vec<FieldSelection>,
252 variables: HashMap<String, serde_json::Value>,
253 principal: Option<&SecurityContext>,
254 ) -> Result<SubscriptionPlan> {
255 let ctx = &self.ctx;
256 let root_rows = RootRowFilter::plan(ctx, read, type_name, principal)?;
257 let bound = serde_json::Value::Object(variables.clone().into_iter().collect());
258 let plan = ReadPlan::classify(ctx, principal, Some(&bound), &[(type_name, &selections)])?;
259 Ok(SubscriptionPlan {
260 ctx: Arc::clone(ctx),
261 plan,
262 root_rows,
263 principal: principal.cloned(),
264 subscription: read.to_string(),
265 type_name: type_name.to_string(),
266 selections,
267 variables,
268 })
269 }
270}
271
272fn refuse_undeclared_selection(
276 schema: &CompiledSchema,
277 type_name: &str,
278 selections: &[FieldSelection],
279) -> Result<()> {
280 let Some(type_def) = schema.find_type(type_name) else {
281 return Ok(());
282 };
283 for sel in effective_selections(selections, type_name, schema) {
284 if sel.name == "__typename" {
285 continue;
286 }
287 let Some(field) = type_def.fields.iter().find(|f| f.name == sel.name) else {
288 return Err(FraiseQLError::Validation {
289 message: format!("Cannot query field '{}' on type '{type_name}'", sel.name),
290 path: None,
291 });
292 };
293 if let Some(child) = field.field_type.inner_type().unwrap_or(&field.field_type).type_name()
294 {
295 refuse_undeclared_selection(schema, child, &sel.nested_fields)?;
296 }
297 }
298 Ok(())
299}
300
301fn refuse_undelivered_selection(
304 definition: &SubscriptionDefinition,
305 selections: &[FieldSelection],
306) -> Result<()> {
307 if definition.fields.is_empty() {
308 return Ok(());
309 }
310 let delivered: Vec<&str> = definition
311 .fields
312 .iter()
313 .filter_map(|f| f.trim_start_matches('/').split(['/', '.']).next())
314 .collect();
315 match selections
316 .iter()
317 .find(|s| s.name != "__typename" && !delivered.contains(&s.name.as_str()))
318 {
319 Some(sel) => Err(FraiseQLError::Validation {
320 message: format!(
321 "Subscription '{}' does not deliver field '{}'",
322 definition.name, sel.name
323 ),
324 path: None,
325 }),
326 None => Ok(()),
327 }
328}
329
330fn active_filter_paths(
334 definition: &SubscriptionDefinition,
335 variables: &HashMap<String, serde_json::Value>,
336) -> Vec<Vec<String>> {
337 let bound = |argument: &str| variables.get(argument).is_some_and(|v| !v.is_null());
338 let segments = |path: &str| -> Vec<String> {
339 path.trim_start_matches('/')
340 .split(['/', '.'])
341 .filter(|s| !s.is_empty())
342 .map(crate::utils::to_snake_case)
343 .collect()
344 };
345 let mut paths: Vec<Vec<String>> = definition
346 .filter_fields
347 .iter()
348 .filter(|field| bound(field))
349 .map(|field| segments(field))
350 .collect();
351 if let Some(filter) = &definition.filter {
352 paths.extend(
353 filter
354 .argument_paths
355 .iter()
356 .filter(|(argument, _)| bound(argument))
357 .map(|(_, path)| segments(path)),
358 );
359 paths.extend(filter.static_filters.iter().map(|condition| segments(&condition.path)));
360 }
361 paths
362}
363
364#[cfg(test)]
365#[path = "subscription_plan_tests.rs"]
366mod tests;