From e6051a3f6f47fececd11b13d6d7dc72b7084e9f7 Mon Sep 17 00:00:00 2001 From: Benjamin Rabier Date: Tue, 20 Feb 2024 00:34:49 +0100 Subject: [PATCH] chore: Cleaning up the request module in engine-v2 This PR only moves around code, no business logic is changed. The goal is to have a better separation between code that manipulates the Operation and the data model. It makes adding validation rules a lot clearer and avoids having a gigantic `OperationError`. Lastly, I'm also moving `request/flat.rs` to `plan/flat.rs`, it's only relevant for planning. Will certainly improve visibilty rules/split the file at some point. --- engine/crates/engine-v2/src/engine.rs | 16 +- engine/crates/engine-v2/src/plan/collected.rs | 7 +- .../engine-v2/src/{request => plan}/flat.rs | 143 +++++++++--------- engine/crates/engine-v2/src/plan/mod.rs | 4 +- .../engine-v2/src/plan/planning/boundary.rs | 7 +- .../engine-v2/src/plan/planning/collect.rs | 13 +- .../engine-v2/src/plan/planning/planner.rs | 40 ++--- engine/crates/engine-v2/src/plan/state.rs | 3 +- .../crates/engine-v2/src/plan/walkers/mod.rs | 9 +- .../src/request/bind/coercion/value.rs | 3 +- .../src/request/{bind.rs => bind/mod.rs} | 123 +++++++-------- engine/crates/engine-v2/src/request/build.rs | 82 ++++++++++ .../crates/engine-v2/src/request/location.rs | 8 +- engine/crates/engine-v2/src/request/mod.rs | 131 ++-------------- .../src/request/validation/introspection.rs | 47 ++++++ .../engine-v2/src/request/validation/mod.rs | 43 ++++++ .../request/validation/operation_limits.rs | 128 ++++++++++++++++ .../engine-v2/src/request/walkers/mod.rs | 1 - .../src/request/walkers/operation_limits.rs | 102 ------------- .../src/request/walkers/selection_set.rs | 26 +--- 20 files changed, 491 insertions(+), 445 deletions(-) rename engine/crates/engine-v2/src/{request => plan}/flat.rs (74%) rename engine/crates/engine-v2/src/request/{bind.rs => bind/mod.rs} (86%) create mode 100644 engine/crates/engine-v2/src/request/build.rs create mode 100644 engine/crates/engine-v2/src/request/validation/introspection.rs create mode 100644 engine/crates/engine-v2/src/request/validation/mod.rs create mode 100644 engine/crates/engine-v2/src/request/validation/operation_limits.rs delete mode 100644 engine/crates/engine-v2/src/request/walkers/operation_limits.rs diff --git a/engine/crates/engine-v2/src/engine.rs b/engine/crates/engine-v2/src/engine.rs index c76ce9a80..26ac78d8e 100644 --- a/engine/crates/engine-v2/src/engine.rs +++ b/engine/crates/engine-v2/src/engine.rs @@ -10,7 +10,7 @@ use schema::Schema; use crate::{ execution::{ExecutionCoordinator, PreparedExecution}, plan::OperationPlan, - request::{parse_operation, Operation}, + request::{bind_variables, Operation}, response::{ExecutionMetadata, GraphqlError, Response}, }; @@ -102,8 +102,7 @@ impl Engine { Err(error) => return Err(Response::from_error(error, ExecutionMetadata::default())), }; - let input_values = operation_plan - .bind_variables(self.schema.as_ref(), &mut request.variables) + let input_values = bind_variables(self.schema.as_ref(), &operation_plan, &mut request.variables) .map_err(|errors| Response::from_errors(errors, ExecutionMetadata::build(&operation_plan)))?; Ok(ExecutionCoordinator::new( @@ -121,15 +120,8 @@ impl Engine { return Ok(cached); } } - let parsed_operation = parse_operation(request)?; - let bound_operation = Operation::build( - &self.schema, - parsed_operation, - !request.operation_limits_disabled(), - request.introspection_state(), - request, - )?; - let prepared = Arc::new(OperationPlan::prepare(&self.schema, bound_operation)?); + let operation = Operation::build(&self.schema, request)?; + let prepared = Arc::new(OperationPlan::prepare(&self.schema, operation)?); #[cfg(feature = "plan_cache")] { self.plan_cache diff --git a/engine/crates/engine-v2/src/plan/collected.rs b/engine/crates/engine-v2/src/plan/collected.rs index acd5465ed..36e91bebe 100644 --- a/engine/crates/engine-v2/src/plan/collected.rs +++ b/engine/crates/engine-v2/src/plan/collected.rs @@ -1,12 +1,15 @@ use schema::{FieldId, ObjectId, ScalarType, Wrapping}; use crate::{ - request::{BoundFieldId, FlatTypeCondition, SelectionSetType}, + request::{BoundFieldId, SelectionSetType}, response::{ResponseEdge, SafeResponseKey}, utils::IdRange, }; -use super::{CollectedFieldId, CollectedSelectionSetId, ConditionalFieldId, ConditionalSelectionSetId, PlanBoundaryId}; +use super::{ + CollectedFieldId, CollectedSelectionSetId, ConditionalFieldId, ConditionalSelectionSetId, FlatTypeCondition, + PlanBoundaryId, +}; // TODO: The two AnyCollectedSelectionSet aren't great, need to split better the ones which are computed // during planning and the others. diff --git a/engine/crates/engine-v2/src/request/flat.rs b/engine/crates/engine-v2/src/plan/flat.rs similarity index 74% rename from engine/crates/engine-v2/src/request/flat.rs rename to engine/crates/engine-v2/src/plan/flat.rs index 65381296c..a564e5e04 100644 --- a/engine/crates/engine-v2/src/request/flat.rs +++ b/engine/crates/engine-v2/src/plan/flat.rs @@ -7,84 +7,83 @@ use std::{ use itertools::Itertools; use schema::{Definition, InterfaceId, ObjectId, Schema}; -use crate::request::{BoundFieldId, BoundSelectionSetId, SelectionSetType, TypeCondition}; - -use super::{BoundSelection, OperationWalker}; - -impl<'a> OperationWalker<'a> { - pub fn flatten_selection_sets(&self, root_selection_set_ids: Vec) -> FlatSelectionSet { - let ty = { - let selection_set_types = root_selection_set_ids +use crate::request::{BoundFieldId, BoundSelection, BoundSelectionSetId, Operation, SelectionSetType, TypeCondition}; + +pub fn flatten_selection_sets( + schema: &Schema, + operation: &Operation, + root_selection_set_ids: Vec, +) -> FlatSelectionSet { + let ty = { + let selection_set_types = root_selection_set_ids + .iter() + .map(|id| operation[*id].ty) + .collect::>(); + assert_eq!( + selection_set_types.len(), + 1, + "{}", + selection_set_types + .into_iter() + .map(|ty| schema.walk(Definition::from(ty)).name()) + .join(", ") + ); + selection_set_types.into_iter().next().unwrap() + }; + let mut flat_selection_set = FlatSelectionSet { + root_selection_set_ids, + ty, + fields: Vec::new(), + }; + let mut selections = VecDeque::from_iter(flat_selection_set.root_selection_set_ids.iter().flat_map( + |&selection_set_id| { + operation[selection_set_id] + .items .iter() - .map(|id| self.operation[*id].ty) - .collect::>(); - assert_eq!( - selection_set_types.len(), - 1, - "{}", - selection_set_types - .into_iter() - .map(|ty| self.schema_walker.walk(Definition::from(ty)).name()) - .join(", ") - ); - selection_set_types.into_iter().next().unwrap() - }; - let mut flat_selection_set = FlatSelectionSet { - root_selection_set_ids, - ty, - fields: Vec::new(), - }; - let mut selections = VecDeque::from_iter(flat_selection_set.root_selection_set_ids.iter().flat_map( - |&selection_set_id| { - self.operation[selection_set_id] - .items - .iter() - .map(move |selection| (Vec::::new(), vec![selection_set_id], selection)) - }, - )); - while let Some((mut type_condition_chain, mut selection_set_path, selection)) = selections.pop_front() { - match selection { - &BoundSelection::Field(bound_field_id) => { - let type_condition = - FlatTypeCondition::flatten(&self.schema_walker, flat_selection_set.ty, type_condition_chain); - if FlatTypeCondition::is_possible(&type_condition) { - flat_selection_set.fields.push(FlatField { - type_condition, - selection_set_path, - bound_field_id, - }); - } + .map(move |selection| (Vec::::new(), vec![selection_set_id], selection)) + }, + )); + while let Some((mut type_condition_chain, mut selection_set_path, selection)) = selections.pop_front() { + match selection { + &BoundSelection::Field(bound_field_id) => { + let type_condition = FlatTypeCondition::flatten(schema, flat_selection_set.ty, type_condition_chain); + if FlatTypeCondition::is_possible(&type_condition) { + flat_selection_set.fields.push(FlatField { + type_condition, + selection_set_path, + bound_field_id, + }); } - BoundSelection::FragmentSpread(spread_id) => { - let spread = &self.operation[*spread_id]; - let fragment = &self.operation[spread.fragment_id]; - type_condition_chain.push(fragment.type_condition); - selection_set_path.push(spread.selection_set_id); - selections.extend( - self.operation[spread.selection_set_id] - .items - .iter() - .map(|selection| (type_condition_chain.clone(), selection_set_path.clone(), selection)), - ); - } - BoundSelection::InlineFragment(inline_fragment_id) => { - let inline_fragment = &self.operation[*inline_fragment_id]; - if let Some(type_condition) = inline_fragment.type_condition { - type_condition_chain.push(type_condition); - } - selection_set_path.push(inline_fragment.selection_set_id); - selections.extend( - self.operation[inline_fragment.selection_set_id] - .items - .iter() - .map(|selection| (type_condition_chain.clone(), selection_set_path.clone(), selection)), - ); + } + BoundSelection::FragmentSpread(spread_id) => { + let spread = &operation[*spread_id]; + let fragment = &operation[spread.fragment_id]; + type_condition_chain.push(fragment.type_condition); + selection_set_path.push(spread.selection_set_id); + selections.extend( + operation[spread.selection_set_id] + .items + .iter() + .map(|selection| (type_condition_chain.clone(), selection_set_path.clone(), selection)), + ); + } + BoundSelection::InlineFragment(inline_fragment_id) => { + let inline_fragment = &operation[*inline_fragment_id]; + if let Some(type_condition) = inline_fragment.type_condition { + type_condition_chain.push(type_condition); } + selection_set_path.push(inline_fragment.selection_set_id); + selections.extend( + operation[inline_fragment.selection_set_id] + .items + .iter() + .map(|selection| (type_condition_chain.clone(), selection_set_path.clone(), selection)), + ); } } - - flat_selection_set } + + flat_selection_set } #[derive(Debug, Clone)] diff --git a/engine/crates/engine-v2/src/plan/mod.rs b/engine/crates/engine-v2/src/plan/mod.rs index 0a7ef2b3b..c7ca4bef4 100644 --- a/engine/crates/engine-v2/src/plan/mod.rs +++ b/engine/crates/engine-v2/src/plan/mod.rs @@ -1,18 +1,20 @@ use schema::{ResolverId, Schema}; use crate::{ - request::{EntityType, FlatTypeCondition, OpInputValues, Operation, QueryPath}, + request::{OpInputValues, Operation, QueryPath}, response::ReadSelectionSet, sources::Plan, utils::IdRange, }; mod collected; +mod flat; mod ids; mod planning; mod state; mod walkers; pub(crate) use collected::*; +pub(crate) use flat::*; pub(crate) use ids::*; pub(crate) use planning::*; pub(crate) use state::*; diff --git a/engine/crates/engine-v2/src/plan/planning/boundary.rs b/engine/crates/engine-v2/src/plan/planning/boundary.rs index 6084c4b57..d6dcf70c5 100644 --- a/engine/crates/engine-v2/src/plan/planning/boundary.rs +++ b/engine/crates/engine-v2/src/plan/planning/boundary.rs @@ -7,10 +7,9 @@ use schema::{FieldId, FieldResolverWalker, FieldSet, FieldSetItem, ResolverId, R use super::{logic::PlanningLogic, planner::Planner, PlanningError, PlanningResult}; use crate::{ - plan::{ParentToChildEdge, PlanId}, + plan::{flatten_selection_sets, EntityType, FlatField, FlatSelectionSet, ParentToChildEdge, PlanId}, request::{ - BoundField, BoundFieldId, BoundSelection, BoundSelectionSet, BoundSelectionSetId, EntityType, FlatField, - FlatSelectionSet, QueryPath, SelectionSetType, + BoundField, BoundFieldId, BoundSelection, BoundSelectionSet, BoundSelectionSetId, QueryPath, SelectionSetType, }, response::{ReadField, ReadSelectionSet, UnpackedResponseEdge}, }; @@ -375,7 +374,7 @@ impl<'schema, 'a> BoundaryPlanner<'schema, 'a> { .iter() .filter_map(|id| self.operation[*id].selection_set_id()) .collect(); - let flat_selection_set = self.walker().flatten_selection_sets(subselection_set_ids); + let flat_selection_set = flatten_selection_sets(self.schema, &self.operation, subselection_set_ids); let fields = self.create_boundary_fields(flat_selection_set)?; boundary_field.lazy_subselection = Some(fields) } diff --git a/engine/crates/engine-v2/src/plan/planning/collect.rs b/engine/crates/engine-v2/src/plan/planning/collect.rs index 24f9b6f68..6bb89a305 100644 --- a/engine/crates/engine-v2/src/plan/planning/collect.rs +++ b/engine/crates/engine-v2/src/plan/planning/collect.rs @@ -4,13 +4,12 @@ use std::collections::HashSet; use crate::{ plan::{ - AnyCollectedSelectionSet, AnyCollectedSelectionSetId, CollectedField, CollectedFieldId, CollectedSelectionSet, - CollectedSelectionSetId, ConditionalField, ConditionalFieldId, ConditionalSelectionSet, - ConditionalSelectionSetId, FieldType, OperationPlan, PlanBoundaryId, PlanId, - }, - request::{ - BoundFieldId, BoundSelectionSetId, EntityType, FlatField, FlatTypeCondition, OperationWalker, SelectionSetType, + flatten_selection_sets, AnyCollectedSelectionSet, AnyCollectedSelectionSetId, CollectedField, CollectedFieldId, + CollectedSelectionSet, CollectedSelectionSetId, ConditionalField, ConditionalFieldId, ConditionalSelectionSet, + ConditionalSelectionSetId, EntityType, FieldType, FlatField, FlatTypeCondition, OperationPlan, PlanBoundaryId, + PlanId, }, + request::{BoundFieldId, BoundSelectionSetId, OperationWalker, SelectionSetType}, utils::IdRange, }; @@ -78,7 +77,7 @@ impl<'schema, 'a> Collector<'schema, 'a> { selection_set_ids: Vec, concrete_parent: bool, ) -> PlanningResult { - let selection_set = self.walker().flatten_selection_sets(selection_set_ids); + let selection_set = flatten_selection_sets(self.schema, self.operation, selection_set_ids); let mut maybe_boundary_id = None; let mut plan_fields = Vec::new(); diff --git a/engine/crates/engine-v2/src/plan/planning/planner.rs b/engine/crates/engine-v2/src/plan/planning/planner.rs index d157108a2..7efe4fd35 100644 --- a/engine/crates/engine-v2/src/plan/planning/planner.rs +++ b/engine/crates/engine-v2/src/plan/planning/planner.rs @@ -13,10 +13,13 @@ use super::{ PlanningError, PlanningResult, }; use crate::{ - plan::{OperationPlan, ParentToChildEdge, PlanBoundaryId, PlanId, PlanInput, PlanOutput, PlannedResolver}, + plan::{ + flatten_selection_sets, EntityType, FlatField, FlatSelectionSet, FlatTypeCondition, OperationPlan, + ParentToChildEdge, PlanBoundaryId, PlanId, PlanInput, PlanOutput, PlannedResolver, + }, request::{ - BoundField, BoundFieldId, BoundSelection, BoundSelectionSet, BoundSelectionSetId, EntityType, FlatField, - FlatSelectionSet, FlatTypeCondition, Operation, OperationWalker, QueryPath, + BoundField, BoundFieldId, BoundSelection, BoundSelectionSet, BoundSelectionSetId, Operation, OperationWalker, + QueryPath, }, response::{ReadSelectionSet, ResponseKeys, SafeResponseKey}, sources::Plan, @@ -122,21 +125,20 @@ impl<'schema> Planner<'schema> { // The root plan is always introspection which also lets us handle operations like: // query { __typename } let introspection_resolver_id = self.schema.introspection_resolver_id(); - let (introspection_selection_set, selection_set) = self - .walker() - .flatten_selection_sets(vec![self.operation.root_selection_set_id]) - .partition_fields(|flat_field| { - let bound_field = &self.operation[flat_field.bound_field_id]; - if let Some(schema_field_id) = bound_field.schema_field_id() { - self.schema - .walker() - .walk(schema_field_id) - .resolvers() - .any(|FieldResolverWalker { resolver, .. }| resolver.id() == introspection_resolver_id) - } else { - true - } - }); + let (introspection_selection_set, selection_set) = + flatten_selection_sets(self.schema, &self.operation, vec![self.operation.root_selection_set_id]) + .partition_fields(|flat_field| { + let bound_field = &self.operation[flat_field.bound_field_id]; + if let Some(schema_field_id) = bound_field.schema_field_id() { + self.schema + .walker() + .walk(schema_field_id) + .resolvers() + .any(|FieldResolverWalker { resolver, .. }| resolver.id() == introspection_resolver_id) + } else { + true + } + }); if !introspection_selection_set.is_empty() { self.push_plan( @@ -252,7 +254,7 @@ impl<'schema> Planner<'schema> { let schema_field_id = self.operation[bound_field_ids[0]] .schema_field_id() .expect("wouldn't have a subselection"); - let flat_selection_set = self.walker().flatten_selection_sets(subselection_set_ids); + let flat_selection_set = flatten_selection_sets(self.schema, &self.operation, subselection_set_ids); self.attribute_selection_sets(&flat_selection_set.root_selection_set_ids, plan_id); self.recursive_plan_subselections(&path.child(key), &logic.child(schema_field_id), flat_selection_set)?; } diff --git a/engine/crates/engine-v2/src/plan/state.rs b/engine/crates/engine-v2/src/plan/state.rs index a4d070d0a..69818db8c 100644 --- a/engine/crates/engine-v2/src/plan/state.rs +++ b/engine/crates/engine-v2/src/plan/state.rs @@ -2,12 +2,11 @@ use std::sync::Arc; use schema::Schema; -use crate::request::FlatTypeCondition; use crate::response::{ResponseBoundaryItem, ResponseBuilder}; use crate::plan::{OperationPlan, PlanBoundaryId}; -use super::PlanId; +use super::{FlatTypeCondition, PlanId}; /// Holds the current state of the operation execution: /// - which plans have been executed diff --git a/engine/crates/engine-v2/src/plan/walkers/mod.rs b/engine/crates/engine-v2/src/plan/walkers/mod.rs index ce7b307f3..def2aac4f 100644 --- a/engine/crates/engine-v2/src/plan/walkers/mod.rs +++ b/engine/crates/engine-v2/src/plan/walkers/mod.rs @@ -4,16 +4,13 @@ use schema::{FieldId, ObjectId, Schema, SchemaWalker}; use crate::{ plan::{CollectedField, FieldType, RuntimeMergedConditionals}, - request::{ - BoundFieldId, FlatTypeCondition, OpInputValues, Operation, OperationWalker, SelectionSetType, - VariableDefinitionId, - }, + request::{BoundFieldId, OpInputValues, Operation, OperationWalker, SelectionSetType, VariableDefinitionId}, response::{ResponseEdge, ResponseKey, ResponseKeys, ResponsePart, ResponsePath, SafeResponseKey, SeedContext}, }; use super::{ - AnyCollectedSelectionSet, CollectedSelectionSetId, ConditionalSelectionSetId, OperationPlan, PlanId, PlanInput, - PlanOutput, RuntimeCollectedSelectionSet, + AnyCollectedSelectionSet, CollectedSelectionSetId, ConditionalSelectionSetId, FlatTypeCondition, OperationPlan, + PlanId, PlanInput, PlanOutput, RuntimeCollectedSelectionSet, }; mod argument; diff --git a/engine/crates/engine-v2/src/request/bind/coercion/value.rs b/engine/crates/engine-v2/src/request/bind/coercion/value.rs index b075670d4..dc7799d1d 100644 --- a/engine/crates/engine-v2/src/request/bind/coercion/value.rs +++ b/engine/crates/engine-v2/src/request/bind/coercion/value.rs @@ -4,8 +4,9 @@ use schema::{ Type, Wrapping, }; -use crate::request::{bind::Binder, BoundFieldId, Location, OpInputValue, OpInputValueId}; +use crate::request::{BoundFieldId, Location, OpInputValue, OpInputValueId}; +use super::super::Binder; use super::{ error::InputValueError, path::{value_path_to_string, ValuePathSegment}, diff --git a/engine/crates/engine-v2/src/request/bind.rs b/engine/crates/engine-v2/src/request/bind/mod.rs similarity index 86% rename from engine/crates/engine-v2/src/request/bind.rs rename to engine/crates/engine-v2/src/request/bind/mod.rs index 4eb08fc29..aa01fb8a7 100644 --- a/engine/crates/engine-v2/src/request/bind.rs +++ b/engine/crates/engine-v2/src/request/bind/mod.rs @@ -1,3 +1,6 @@ +mod coercion; +mod variable; + use std::collections::{HashMap, HashSet}; pub use engine_parser::types::OperationType; @@ -6,39 +9,23 @@ use engine_value::Name; use itertools::Itertools; use schema::{Definition, FieldWalker, IdRange, Schema}; -use crate::response::GraphqlError; - -use self::coercion::{const_value::coerce_graphql_const_value, value::coerce_value}; - -use super::{ - selection_set::BoundField, variable::VariableDefinition, BoundFieldArgument, BoundFieldArgumentId, BoundFieldId, - BoundFragment, BoundFragmentId, BoundFragmentSpread, BoundFragmentSpreadId, BoundInlineFragment, - BoundInlineFragmentId, BoundSelection, BoundSelectionSet, BoundSelectionSetId, Location, OpInputValue, - OpInputValues, Operation, ParsedOperation, ResponseKeys, SelectionSetType, TypeCondition, +use crate::{ + request::{ + BoundField, BoundFieldArgument, BoundFieldArgumentId, BoundFieldId, BoundFragment, BoundFragmentId, + BoundFragmentSpread, BoundFragmentSpreadId, BoundInlineFragment, BoundInlineFragmentId, BoundSelection, + BoundSelectionSet, BoundSelectionSetId, Location, OpInputValue, OpInputValues, Operation, SelectionSetType, + TypeCondition, VariableDefinition, + }, + response::{GraphqlError, ResponseKeys}, }; -mod coercion; -mod variable; +use self::coercion::{const_value::coerce_graphql_const_value, value::coerce_value}; +pub use variable::bind_variables; -pub use variable::{bind_variables, VariableError}; +use super::parse::ParsedOperation; -#[allow(clippy::enum_variant_names)] #[derive(thiserror::Error, Debug)] -pub enum OperationLimitExceededError { - #[error("Query is too complex.")] - QueryTooComplex, - #[error("Query is nested too deep.")] - QueryTooDeep, - #[error("Query is too high.")] - QueryTooHigh, - #[error("Query contains too many root fields.")] - QueryContainsTooManyRootFields, - #[error("Query contains too many aliases.")] - QueryContainsTooManyAliases, -} - -#[derive(thiserror::Error, Debug)] -pub enum OperationError { +pub enum BindError { #[error("Unknown type named '{name}'")] UnknownType { name: String, location: Location }, #[error("{container} does not have a field named '{name}'")] @@ -99,12 +86,8 @@ pub enum OperationError { }, #[error("Fragment cycle detected: {}", .cycle.iter().join(", "))] FragmentCycle { cycle: Vec, location: Location }, - #[error("{0}")] - OperationLimitExceeded(OperationLimitExceededError), #[error("Query is too big: {0}")] QueryTooBig(String), - #[error("GraphQL introspection is not allowed, but the query contained __schema or __type")] - IntrospectionWhenDisabled { location: Location }, #[error("{0}")] InvalidInputValue(#[from] coercion::InputValueError), #[error("Missing argument named '{name}' for field '{field}'")] @@ -115,29 +98,25 @@ pub enum OperationError { }, } -impl From for GraphqlError { - fn from(err: OperationError) -> Self { +impl From for GraphqlError { + fn from(err: BindError) -> Self { let locations = match err { - OperationError::UnknownField { location, .. } - | OperationError::UnknownType { location, .. } - | OperationError::UnknownFragment { location, .. } - | OperationError::UnionHaveNoFields { location, .. } - | OperationError::InvalidTypeConditionTargetType { location, .. } - | OperationError::CannotHaveSelectionSet { location, .. } - | OperationError::DisjointTypeCondition { location, .. } - | OperationError::InvalidVariableType { location, .. } - | OperationError::TooManyFields { location } - | OperationError::LeafMustBeAScalarOrEnum { location, .. } - | OperationError::DuplicateVariable { location, .. } - | OperationError::FragmentCycle { location, .. } - | OperationError::IntrospectionWhenDisabled { location, .. } - | OperationError::MissingArgument { location, .. } - | OperationError::UnusedVariable { location, .. } => vec![location], - OperationError::InvalidInputValue(ref err) => vec![err.location()], - OperationError::NoMutationDefined - | OperationError::NoSubscriptionDefined - | OperationError::QueryTooBig { .. } - | OperationError::OperationLimitExceeded { .. } => { + BindError::UnknownField { location, .. } + | BindError::UnknownType { location, .. } + | BindError::UnknownFragment { location, .. } + | BindError::UnionHaveNoFields { location, .. } + | BindError::InvalidTypeConditionTargetType { location, .. } + | BindError::CannotHaveSelectionSet { location, .. } + | BindError::DisjointTypeCondition { location, .. } + | BindError::InvalidVariableType { location, .. } + | BindError::TooManyFields { location } + | BindError::LeafMustBeAScalarOrEnum { location, .. } + | BindError::DuplicateVariable { location, .. } + | BindError::FragmentCycle { location, .. } + | BindError::MissingArgument { location, .. } + | BindError::UnusedVariable { location, .. } => vec![location], + BindError::InvalidInputValue(ref err) => vec![err.location()], + BindError::NoMutationDefined | BindError::NoSubscriptionDefined | BindError::QueryTooBig { .. } => { vec![] } }; @@ -149,7 +128,7 @@ impl From for GraphqlError { } } -pub type BindResult = Result; +pub type BindResult = Result; pub fn bind(schema: &Schema, mut unbound: ParsedOperation) -> BindResult { let root_object_id = match unbound.definition.ty { @@ -157,11 +136,11 @@ pub fn bind(schema: &Schema, mut unbound: ParsedOperation) -> BindResult schema .root_operation_types .mutation - .ok_or(OperationError::NoMutationDefined)?, + .ok_or(BindError::NoMutationDefined)?, OperationType::Subscription => schema .root_operation_types .subscription - .ok_or(OperationError::NoSubscriptionDefined)?, + .ok_or(BindError::NoSubscriptionDefined)?, }; let mut binder = Binder { @@ -255,7 +234,7 @@ impl<'a> Binder<'a> { let name_location = node.name.pos.try_into()?; if seen_names.contains(&name) { - return Err(OperationError::DuplicateVariable { + return Err(BindError::DuplicateVariable { name, location: name_location, }); @@ -302,7 +281,7 @@ impl<'a> Binder<'a> { let definition = self.schema .definition_by_name(type_name.as_str()) - .ok_or_else(|| OperationError::UnknownType { + .ok_or_else(|| BindError::UnknownType { name: type_name.to_string(), location, })?; @@ -310,7 +289,7 @@ impl<'a> Binder<'a> { definition, Definition::Enum(_) | Definition::Scalar(_) | Definition::InputObject(_) ) { - return Err(OperationError::InvalidVariableType { + return Err(BindError::InvalidVariableType { name: variable_name.to_string(), ty: self.schema.walker().walk(definition).name().to_string(), location, @@ -387,7 +366,7 @@ impl<'a> Binder<'a> { let bound_response_key = response_key .with_position(self.next_response_position) - .ok_or(OperationError::TooManyFields { + .ok_or(BindError::TooManyFields { location: name_location, })?; self.next_response_position += 1; @@ -407,7 +386,7 @@ impl<'a> Binder<'a> { self.schema.interface_field_by_name(interface_id, name) } SelectionSetType::Union(union_id) => { - return Err(OperationError::UnionHaveNoFields { + return Err(BindError::UnionHaveNoFields { name: name.to_string(), ty: walker.walk(union_id).name().to_string(), location: name_location, @@ -415,7 +394,7 @@ impl<'a> Binder<'a> { } } .map(|field_id| walker.walk(field_id)) - .ok_or_else(|| OperationError::UnknownField { + .ok_or_else(|| BindError::UnknownField { container: walker.walk(Definition::from(root)).name().to_string(), name: name.to_string(), location: name_location, @@ -440,7 +419,7 @@ impl<'a> Binder<'a> { schema_field.ty().inner().id(), Definition::Scalar(_) | Definition::Enum(_) ) { - return Err(OperationError::LeafMustBeAScalarOrEnum { + return Err(BindError::LeafMustBeAScalarOrEnum { name: name.to_string(), ty: schema_field.ty().inner().name().to_string(), location: name_location, @@ -450,7 +429,7 @@ impl<'a> Binder<'a> { } else { Some( SelectionSetType::maybe_from(schema_field.ty().inner().id()) - .ok_or_else(|| OperationError::CannotHaveSelectionSet { + .ok_or_else(|| BindError::CannotHaveSelectionSet { name: name.to_string(), ty: schema_field.ty().to_string(), location: name_location, @@ -521,7 +500,7 @@ impl<'a> Binder<'a> { input_value_id: self.input_values.push_value(OpInputValue::SchemaRef(id)), }); } else if argument_def.ty().wrapping().is_required() { - return Err(OperationError::MissingArgument { + return Err(BindError::MissingArgument { field: schema_field.name().to_string(), name: argument_def.name().to_string(), location: field_location, @@ -543,7 +522,7 @@ impl<'a> Binder<'a> { let name = spread.fragment_name.node.to_string(); if self.current_fragments_stack.contains(&name) { self.current_fragments_stack.push(name); - return Err(OperationError::FragmentCycle { + return Err(BindError::FragmentCycle { cycle: std::mem::take(&mut self.current_fragments_stack), location, }); @@ -554,7 +533,7 @@ impl<'a> Binder<'a> { let (fragment_name, mut fragment) = self.unbound_fragments .remove_entry(&name) - .ok_or_else(|| OperationError::UnknownFragment { + .ok_or_else(|| BindError::UnknownFragment { name: name.to_string(), location, })?; @@ -624,7 +603,7 @@ impl<'a> Binder<'a> { let definition = self .schema .definition_by_name(name) - .ok_or_else(|| OperationError::UnknownType { + .ok_or_else(|| BindError::UnknownType { name: name.to_string(), location, })?; @@ -633,7 +612,7 @@ impl<'a> Binder<'a> { Definition::Interface(interface_id) => TypeCondition::Interface(interface_id), Definition::Union(union_id) => TypeCondition::Union(union_id), _ => { - return Err(OperationError::InvalidTypeConditionTargetType { + return Err(BindError::InvalidTypeConditionTargetType { name: name.to_string(), location, }); @@ -651,7 +630,7 @@ impl<'a> Binder<'a> { .collect::>(); if possible_types.is_disjoint(&frament_possible_types) { let walker = self.schema.walker(); - return Err(OperationError::DisjointTypeCondition { + return Err(BindError::DisjointTypeCondition { parent: walker.walk(Definition::from(root)).name().to_string(), name: name.to_string(), location, @@ -663,7 +642,7 @@ impl<'a> Binder<'a> { fn validate_all_variables_used(&self) -> BindResult<()> { for variable in &self.variable_definitions { if variable.used_by.is_empty() { - return Err(OperationError::UnusedVariable { + return Err(BindError::UnusedVariable { name: variable.name.clone(), operation: self.operation_name.clone(), location: variable.name_location, diff --git a/engine/crates/engine-v2/src/request/build.rs b/engine/crates/engine-v2/src/request/build.rs new file mode 100644 index 000000000..0f04e5af5 --- /dev/null +++ b/engine/crates/engine-v2/src/request/build.rs @@ -0,0 +1,82 @@ +use schema::{CacheConfig, Merge, Schema}; + +use crate::response::GraphqlError; + +use super::{BoundSelectionSetWalker, Operation, OperationCacheControl}; + +#[derive(Debug, thiserror::Error)] +pub enum OperationError { + #[error(transparent)] + Bind(#[from] super::bind::BindError), + #[error(transparent)] + Validation(#[from] super::validation::ValidationError), + #[error(transparent)] + Parse(#[from] super::parse::ParseError), +} + +impl From for GraphqlError { + fn from(err: OperationError) -> Self { + match err { + OperationError::Bind(err) => err.into(), + OperationError::Validation(err) => err.into(), + OperationError::Parse(err) => err.into(), + } + } +} + +impl Operation { + /// Builds an `Operation` by binding unbound operation to a schema and configuring its non functional requirements + /// like caching, auth, .... + /// + /// All field names are mapped to their actual field id in the schema and respective configuration. + /// At this stage the operation might not be resolvable but it should make sense given the schema types. + pub fn build(schema: &Schema, request: &engine::Request) -> Result { + let parsed_operation = super::parse::parse_operation(request)?; + let mut operation = super::bind::bind(schema, parsed_operation)?; + + if operation.is_query() { + let root_cache_config = schema[operation.root_object_id] + .cache_config + .map(|cache_config_id| schema[cache_config_id]); + let selection_set = operation + .walker_with(schema.walker()) + .walk(operation.root_selection_set_id); + + let selection_set_cache_config = selection_set.cache_config(); + operation.cache_control = root_cache_config.merge(selection_set_cache_config).map( + |CacheConfig { + max_age, + stale_while_revalidate, + }| OperationCacheControl { + max_age, + key: request.cache_key(), + stale_while_revalidate, + }, + ); + } + + super::validation::validate_operation(schema, &operation, request)?; + + Ok(operation) + } +} + +impl BoundSelectionSetWalker<'_> { + // this merely traverses the selection set recursively and merge all cache_config present in the + // selected fields + fn cache_config(&self) -> Option { + self.fields() + .filter_map(|field| { + let cache_config = field.schema_field().and_then(|definition| { + definition + .cache_config() + .merge(definition.ty().inner().as_object().and_then(|obj| obj.cache_config())) + }); + let selection_set_cache_config = field + .selection_set() + .and_then(|selection_set| selection_set.cache_config()); + cache_config.merge(selection_set_cache_config) + }) + .reduce(|a, b| a.merge(b)) + } +} diff --git a/engine/crates/engine-v2/src/request/location.rs b/engine/crates/engine-v2/src/request/location.rs index f1d047d03..c9dd010f6 100644 --- a/engine/crates/engine-v2/src/request/location.rs +++ b/engine/crates/engine-v2/src/request/location.rs @@ -1,6 +1,6 @@ use std::fmt; -use super::bind::OperationError; +use super::bind::BindError; // 65 KB for query without any new lines is pretty huge. If a user ever has a QueryTooBig error // we'll increase it to u32. But for now it's just wasted memory. @@ -34,18 +34,18 @@ impl fmt::Display for Location { } impl TryFrom for Location { - type Error = OperationError; + type Error = BindError; fn try_from(value: engine_parser::Pos) -> Result { Ok(Self::new( value .line .try_into() - .map_err(|_| OperationError::QueryTooBig(format!("Too many lines ({})", value.line)))?, + .map_err(|_| BindError::QueryTooBig(format!("Too many lines ({})", value.line)))?, value .column .try_into() - .map_err(|_| OperationError::QueryTooBig(format!("Too many columns ({})", value.column)))?, + .map_err(|_| BindError::QueryTooBig(format!("Too many columns ({})", value.column)))?, )) } } diff --git a/engine/crates/engine-v2/src/request/mod.rs b/engine/crates/engine-v2/src/request/mod.rs index a2fbf1676..1337c1f86 100644 --- a/engine/crates/engine-v2/src/request/mod.rs +++ b/engine/crates/engine-v2/src/request/mod.rs @@ -1,30 +1,27 @@ -pub(crate) use bind::BindResult; +pub(crate) use bind::bind_variables; pub use cache_control::OperationCacheControl; pub(crate) use engine_parser::types::OperationType; -pub(crate) use flat::*; pub(crate) use ids::*; pub(crate) use input_value::*; pub(crate) use location::Location; -pub(crate) use parse::{parse_operation, ParsedOperation}; pub(crate) use path::QueryPath; -use schema::{CacheConfig, Merge, ObjectId, Schema, SchemaWalker}; +use schema::{ObjectId, SchemaWalker}; pub(crate) use selection_set::*; pub(crate) use variable::VariableDefinition; pub(crate) use walkers::*; use crate::response::ResponseKeys; -use self::bind::{OperationError, OperationLimitExceededError, VariableError}; - mod bind; +mod build; mod cache_control; -mod flat; pub mod ids; mod input_value; mod location; mod parse; mod path; mod selection_set; +mod validation; mod variable; mod walkers; @@ -47,103 +44,22 @@ pub(crate) struct Operation { } impl Operation { - pub fn parent_selection_set_id(&self, id: BoundFieldId) -> BoundSelectionSetId { - self.field_to_parent[usize::from(id)] + pub fn is_query(&self) -> bool { + matches!(self.ty, OperationType::Query) } - fn enforce_operation_limits(&self, schema: &Schema) -> Result<(), OperationLimitExceededError> { - let selection_set = self.walker_with(schema.walker()).walk(self.root_selection_set_id); - - if let Some(depth_limit) = schema.operation_limits.depth { - let max_depth = selection_set.max_depth(); - if max_depth > depth_limit { - return Err(OperationLimitExceededError::QueryTooDeep); - } - } - - if let Some(max_alias_count) = schema.operation_limits.aliases { - let alias_count = selection_set.alias_count(); - if alias_count > max_alias_count { - return Err(OperationLimitExceededError::QueryContainsTooManyAliases); - } - } - - if let Some(max_root_field_count) = schema.operation_limits.root_fields { - let root_field_count = selection_set.root_field_count(); - if root_field_count > max_root_field_count { - return Err(OperationLimitExceededError::QueryContainsTooManyRootFields); - } - } - - if let Some(max_height) = schema.operation_limits.height { - let height = selection_set.height(&mut Default::default()); - if height > max_height { - return Err(OperationLimitExceededError::QueryTooHigh); - } - } - - if let Some(max_complexity) = schema.operation_limits.complexity { - let complexity = selection_set.complexity(); - if complexity > max_complexity { - return Err(OperationLimitExceededError::QueryTooComplex); - } - } - - Ok(()) + pub fn parent_selection_set_id(&self, id: BoundFieldId) -> BoundSelectionSetId { + self.field_to_parent[usize::from(id)] } - /// Builds an `Operation` by binding unbound operation to a schema and configuring its non functional requirements - /// like caching, auth, .... - /// - /// All field names are mapped to their actual field id in the schema and respective configuration. - /// At this stage the operation might not be resolvable but it should make sense given the schema types. - pub fn build( - schema: &Schema, - unbound_operation: ParsedOperation, - operation_limits_enabled: bool, - introspection_state: engine::IntrospectionState, - request: &engine::Request, - ) -> BindResult { - let mut operation = bind::bind(schema, unbound_operation)?; - - if operation_limits_enabled { - operation - .enforce_operation_limits(schema) - .map_err(OperationError::OperationLimitExceeded)?; - } - - if operation.ty == OperationType::Query { - let root_cache_config = schema[operation.root_object_id] - .cache_config - .map(|cache_config_id| schema[cache_config_id]); - let selection_set = operation - .walker_with(schema.walker()) - .walk(operation.root_selection_set_id); - - match introspection_state { - engine::IntrospectionState::ForceEnabled => {} - engine::IntrospectionState::ForceDisabled => detect_introspection(&selection_set)?, - engine::IntrospectionState::UserPreference => { - if schema.disable_introspection { - detect_introspection(&selection_set)?; - } - } - }; - - let selection_set_cache_config = selection_set.cache_config(); - operation.cache_control = root_cache_config.merge(selection_set_cache_config).map( - |CacheConfig { - max_age, - stale_while_revalidate, - }| OperationCacheControl { - max_age, - key: request.cache_key(), - stale_while_revalidate, - }, - ); - } - - Ok(operation) + pub fn walk_selection_set<'op, 'schema>( + &'op self, + schema_walker: SchemaWalker<'schema, ()>, + ) -> BoundSelectionSetWalker<'op> + where + 'schema: 'op, + { + self.walker_with(schema_walker).walk(self.root_selection_set_id) } pub fn walker_with<'op, 'schema, SI>( @@ -159,19 +75,4 @@ impl Operation { item: (), } } - - pub fn bind_variables( - &self, - schema: &Schema, - variables: &mut engine_value::Variables, - ) -> Result> { - bind::bind_variables(schema, self, variables) - } -} - -fn detect_introspection(selection_set: &OperationWalker<'_, BoundSelectionSetId>) -> Result<(), OperationError> { - if let Some(location) = selection_set.find_introspection_field_location() { - return Err(OperationError::IntrospectionWhenDisabled { location }); - } - Ok(()) } diff --git a/engine/crates/engine-v2/src/request/validation/introspection.rs b/engine/crates/engine-v2/src/request/validation/introspection.rs new file mode 100644 index 000000000..47de02380 --- /dev/null +++ b/engine/crates/engine-v2/src/request/validation/introspection.rs @@ -0,0 +1,47 @@ +use schema::Schema; + +use crate::request::{BoundSelectionSetWalker, Location, Operation}; + +use super::ValidationError; + +pub(super) fn ensure_introspection_is_accepted( + schema: &Schema, + operation: &Operation, + request: &engine::Request, +) -> Result<(), ValidationError> { + if operation.is_query() { + let selection_set = operation.walk_selection_set(schema.walker()); + + match request.introspection_state() { + engine::IntrospectionState::ForceEnabled => {} + engine::IntrospectionState::ForceDisabled => detect_introspection(selection_set)?, + engine::IntrospectionState::UserPreference => { + if schema.disable_introspection { + detect_introspection(selection_set)?; + } + } + }; + } + + Ok(()) +} + +fn detect_introspection(selection_set: BoundSelectionSetWalker<'_>) -> Result<(), ValidationError> { + if let Some(location) = selection_set.find_introspection_field_location() { + return Err(ValidationError::IntrospectionWhenDisabled { location }); + } + Ok(()) +} + +impl BoundSelectionSetWalker<'_> { + fn find_introspection_field_location(self) -> Option { + self.fields().find_map(|field| { + let schema_field = field.schema_field(); + if schema_field.is_some_and(|field| field.name() == "__type" || field.name() == "__schema") { + field.name_location() + } else { + None + } + }) + } +} diff --git a/engine/crates/engine-v2/src/request/validation/mod.rs b/engine/crates/engine-v2/src/request/validation/mod.rs new file mode 100644 index 000000000..ba326d28c --- /dev/null +++ b/engine/crates/engine-v2/src/request/validation/mod.rs @@ -0,0 +1,43 @@ +mod introspection; +mod operation_limits; + +use crate::{ + request::{Location, Operation}, + response::GraphqlError, +}; +use introspection::*; +use operation_limits::*; +use schema::Schema; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum ValidationError { + #[error(transparent)] + OperationLimitExceeded(#[from] OperationLimitExceededError), + #[error("GraphQL introspection is not allowed, but the query contained __schema or __type")] + IntrospectionWhenDisabled { location: Location }, +} + +impl From for GraphqlError { + fn from(err: ValidationError) -> Self { + let locations = match err { + ValidationError::IntrospectionWhenDisabled { location } => vec![location], + ValidationError::OperationLimitExceeded { .. } => Vec::new(), + }; + GraphqlError { + message: err.to_string(), + locations, + ..Default::default() + } + } +} + +pub(crate) fn validate_operation( + schema: &Schema, + operation: &Operation, + request: &engine::Request, +) -> Result<(), ValidationError> { + enforce_operation_limits(schema, operation, request)?; + ensure_introspection_is_accepted(schema, operation, request)?; + + Ok(()) +} diff --git a/engine/crates/engine-v2/src/request/validation/operation_limits.rs b/engine/crates/engine-v2/src/request/validation/operation_limits.rs new file mode 100644 index 000000000..7d3206daf --- /dev/null +++ b/engine/crates/engine-v2/src/request/validation/operation_limits.rs @@ -0,0 +1,128 @@ +use std::collections::HashSet; + +use schema::Schema; + +use crate::request::{BoundSelectionSetWalker, Operation}; + +#[allow(clippy::enum_variant_names)] +#[derive(thiserror::Error, Debug)] +pub(crate) enum OperationLimitExceededError { + #[error("Query is too complex.")] + QueryTooComplex, + #[error("Query is nested too deep.")] + QueryTooDeep, + #[error("Query is too high.")] + QueryTooHigh, + #[error("Query contains too many root fields.")] + QueryContainsTooManyRootFields, + #[error("Query contains too many aliases.")] + QueryContainsTooManyAliases, +} + +pub(super) fn enforce_operation_limits( + schema: &Schema, + operation: &Operation, + request: &engine::Request, +) -> Result<(), OperationLimitExceededError> { + if request.operation_limits_disabled() { + return Ok(()); + } + + let selection_set = operation.walk_selection_set(schema.walker()); + + if let Some(depth_limit) = schema.operation_limits.depth { + let max_depth = selection_set.max_depth(); + if max_depth > depth_limit { + return Err(OperationLimitExceededError::QueryTooDeep); + } + } + + if let Some(max_alias_count) = schema.operation_limits.aliases { + let alias_count = selection_set.alias_count(); + if alias_count > max_alias_count { + return Err(OperationLimitExceededError::QueryContainsTooManyAliases); + } + } + + if let Some(max_root_field_count) = schema.operation_limits.root_fields { + let root_field_count = selection_set.root_field_count(); + if root_field_count > max_root_field_count { + return Err(OperationLimitExceededError::QueryContainsTooManyRootFields); + } + } + + if let Some(max_height) = schema.operation_limits.height { + let height = selection_set.height(&mut Default::default()); + if height > max_height { + return Err(OperationLimitExceededError::QueryTooHigh); + } + } + + if let Some(max_complexity) = schema.operation_limits.complexity { + let complexity = selection_set.complexity(); + if complexity > max_complexity { + return Err(OperationLimitExceededError::QueryTooComplex); + } + } + + Ok(()) +} + +impl BoundSelectionSetWalker<'_> { + fn max_depth(&self) -> u16 { + self.fields() + .map(|field| { + field + .selection_set() + .map(|selection_set| selection_set.max_depth()) + .unwrap_or_default() + + 1 + }) + .max() + .expect("must be defined") + } + + fn alias_count(&self) -> u16 { + self.fields() + .map(|field| { + field.alias().is_some() as u16 + + field + .selection_set() + .map(|selection_set| selection_set.alias_count()) + .unwrap_or_default() + }) + .sum() + } + + fn root_field_count(&self) -> u16 { + self.fields().count() as u16 + } + + fn complexity(&self) -> u16 { + self.fields() + .map(|field| { + field + .selection_set() + .map(|selection_set| selection_set.complexity()) + .unwrap_or_default() + + 1 + }) + .sum() + } + + // `None` stored in the set means `__typename`. + fn height(&self, fields_seen: &mut HashSet>) -> u16 { + self.fields() + .map(|field| { + (if fields_seen.insert(field.as_ref().schema_field_id()) { + 1 + } else { + 0 + }) + field + .selection_set() + .map(|selection_set| selection_set.height(&mut HashSet::new())) + .unwrap_or_default() + }) + .sum() + } +} diff --git a/engine/crates/engine-v2/src/request/walkers/mod.rs b/engine/crates/engine-v2/src/request/walkers/mod.rs index 70c23ef0a..14bd3a4c0 100644 --- a/engine/crates/engine-v2/src/request/walkers/mod.rs +++ b/engine/crates/engine-v2/src/request/walkers/mod.rs @@ -1,7 +1,6 @@ mod field; mod fragment; mod inline_fragment; -mod operation_limits; mod query_path; mod selection_set; diff --git a/engine/crates/engine-v2/src/request/walkers/operation_limits.rs b/engine/crates/engine-v2/src/request/walkers/operation_limits.rs deleted file mode 100644 index abf121636..000000000 --- a/engine/crates/engine-v2/src/request/walkers/operation_limits.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::collections::HashSet; - -use crate::request::Location; - -use super::{BoundSelectionSetWalker, BoundSelectionWalker}; - -impl<'a> BoundSelectionSetWalker<'a> { - pub(crate) fn max_depth(&self) -> u16 { - (*self) - .into_iter() - .map(|selection| match selection { - BoundSelectionWalker::Field(field) => { - field - .selection_set() - .map(|selection_set| selection_set.max_depth()) - .unwrap_or_default() - + 1 - } - BoundSelectionWalker::InlineFragment(inline) => inline.selection_set().max_depth(), - BoundSelectionWalker::FragmentSpread(spread) => spread.selection_set().max_depth(), - }) - .max() - .expect("must be defined") - } - - pub(crate) fn alias_count(&self) -> u16 { - (*self) - .into_iter() - .map(|selection| match selection { - BoundSelectionWalker::Field(field) => { - field.alias().is_some() as u16 - + field - .selection_set() - .map(|selection_set| selection_set.alias_count()) - .unwrap_or_default() - } - BoundSelectionWalker::InlineFragment(inline) => inline.selection_set().alias_count(), - BoundSelectionWalker::FragmentSpread(spread) => spread.selection_set().alias_count(), - }) - .sum() - } - - pub(crate) fn root_field_count(&self) -> u16 { - (*self) - .into_iter() - .map(|selection| match selection { - BoundSelectionWalker::Field(_) => 1, - BoundSelectionWalker::InlineFragment(inline) => inline.selection_set().root_field_count(), - BoundSelectionWalker::FragmentSpread(spread) => spread.selection_set().root_field_count(), - }) - .sum() - } - - pub(crate) fn complexity(&self) -> u16 { - (*self) - .into_iter() - .map(|selection| match selection { - BoundSelectionWalker::Field(field) => { - field - .selection_set() - .map(|selection_set| selection_set.complexity()) - .unwrap_or_default() - + 1 - } - BoundSelectionWalker::InlineFragment(inline) => inline.selection_set().complexity(), - BoundSelectionWalker::FragmentSpread(spread) => spread.selection_set().complexity(), - }) - .sum() - } - - // `None` stored in the set means `__typename`. - pub(crate) fn height(&self, fields_seen: &mut HashSet>) -> u16 { - (*self) - .into_iter() - .map(|selection| match selection { - BoundSelectionWalker::Field(field) => { - (if fields_seen.insert(field.schema_field().map(|field| field.id())) { - 1 - } else { - 0 - }) + field - .selection_set() - .map(|selection_set| selection_set.height(&mut HashSet::new())) - .unwrap_or_default() - } - BoundSelectionWalker::InlineFragment(inline) => inline.selection_set().height(fields_seen), - BoundSelectionWalker::FragmentSpread(spread) => spread.selection_set().height(fields_seen), - }) - .sum() - } - - pub(crate) fn find_introspection_field_location(self) -> Option { - self.fields().find_map(|field| { - let schema_field = field.schema_field(); - if schema_field.is_some_and(|field| field.name() == "__type" || field.name() == "__schema") { - field.name_location() - } else { - None - } - }) - } -} diff --git a/engine/crates/engine-v2/src/request/walkers/selection_set.rs b/engine/crates/engine-v2/src/request/walkers/selection_set.rs index 5a47c1fdf..61b720bf4 100644 --- a/engine/crates/engine-v2/src/request/walkers/selection_set.rs +++ b/engine/crates/engine-v2/src/request/walkers/selection_set.rs @@ -1,4 +1,4 @@ -use schema::{CacheConfig, Definition, DefinitionWalker, Merge}; +use schema::{Definition, DefinitionWalker}; use super::{BoundFieldWalker, BoundFragmentSpreadWalker, BoundInlineFragmentWalker, OperationWalker}; use crate::request::{BoundSelection, BoundSelectionSetId, SelectionSetType}; @@ -19,30 +19,6 @@ impl<'a> BoundSelectionSetWalker<'a> { } } -impl<'a> BoundSelectionSetWalker<'a> { - // this merely traverses the selection set recursively and merge all cache_config present in the - // selected fields - pub fn cache_config(self) -> Option { - self.into_iter() - .filter_map(|selection| match selection { - BoundSelectionWalker::Field(field) => { - let cache_config = field.schema_field().and_then(|definition| { - definition - .cache_config() - .merge(definition.ty().inner().as_object().and_then(|obj| obj.cache_config())) - }); - let selection_set_cache_config = field - .selection_set() - .and_then(|selection_set| selection_set.cache_config()); - cache_config.merge(selection_set_cache_config) - } - BoundSelectionWalker::InlineFragment(inline) => inline.selection_set().cache_config(), - BoundSelectionWalker::FragmentSpread(spread) => spread.selection_set().cache_config(), - }) - .reduce(|a, b| a.merge(b)) - } -} - impl<'a> std::ops::Deref for SelectionSetTypeWalker<'a> { type Target = DefinitionWalker<'a>;