diff --git a/src/core/blueprint/operators/graphql.rs b/src/core/blueprint/operators/graphql.rs index ee25eb5fd1..15349f3aa6 100644 --- a/src/core/blueprint/operators/graphql.rs +++ b/src/core/blueprint/operators/graphql.rs @@ -12,48 +12,67 @@ use crate::core::ir::model::{IO, IR}; use crate::core::ir::RelatedFields; use crate::core::try_fold::TryFold; +#[allow(clippy::too_many_arguments)] fn create_related_fields( config: &Config, type_name: &str, visited: &mut HashSet, -) -> RelatedFields { + paths: &mut HashMap>, + path: Vec, + root: &str, +) -> Option { let mut map = HashMap::new(); if visited.contains(type_name) { - return RelatedFields(map); + return Some(RelatedFields(map)); } visited.insert(type_name.to_string()); - if let Some(type_) = config.find_type(type_name) { for (name, field) in &type_.fields { if !field.has_resolver() { - if let Some(modify) = &field.modify { - if let Some(modified_name) = &modify.name { - map.insert( - modified_name.clone(), - ( - name.clone(), - create_related_fields(config, field.type_of.name(), visited), - ), - ); - } - } else { - map.insert( + let used_name = match &field.modify { + Some(modify) => match &modify.name { + Some(modified_name) => Some(modified_name), + _ => None, + }, + _ => Some(name), + }; + let mut next_path = path.clone(); + next_path.push(used_name?.to_string()); + if !(paths.contains_key(field.type_of.name())) { + paths.insert(field.type_of.name().to_string(), next_path.clone()); + }; + map.insert( + used_name?.to_string(), + ( name.clone(), - ( - name.clone(), - create_related_fields(config, field.type_of.name(), visited), - ), - ); - } + create_related_fields( + config, + field.type_of.name(), + visited, + paths, + next_path.clone(), + root, + )?, + paths.get(field.type_of.name())?.to_vec(), + root == field.type_of.name(), + ), + ); } } } else if let Some(union_) = config.find_union(type_name) { for type_name in &union_.types { - map.extend(create_related_fields(config, type_name, visited).0); + let mut next_path = path.clone(); + next_path.push(type_name.to_string()); + if !(paths.contains_key(type_name)) { + paths.insert(type_name.to_string(), next_path.clone()); + }; + map.extend( + create_related_fields(config, type_name, visited, paths, next_path, root)?.0, + ); } }; - RelatedFields(map) + Some(RelatedFields(map)) } pub fn compile_graphql( @@ -66,17 +85,30 @@ pub fn compile_graphql( Valid::succeed(graphql.url.as_str()) .zip(helpers::headers::to_mustache_headers(&graphql.headers)) .and_then(|(base_url, headers)| { - Valid::from( - RequestTemplate::new( - base_url.to_owned(), - operation_type, - &graphql.name, - args, - headers, - create_related_fields(config, type_name, &mut HashSet::new()), - ) - .map_err(|e| ValidationError::new(e.to_string())), + Valid::from_option( + create_related_fields( + config, + type_name, + &mut HashSet::new(), + &mut HashMap::new(), + vec![], + type_name, + ), + "Logical error occurred while creating Related Fields".to_string(), ) + .and_then(|related_fields| { + Valid::from( + RequestTemplate::new( + base_url.to_owned(), + operation_type, + &graphql.name, + args, + headers, + related_fields, + ) + .map_err(|e| ValidationError::new(e.to_string())), + ) + }) }) .map(|req_template| { let field_name = graphql.name.clone(); diff --git a/src/core/ir/eval_context.rs b/src/core/ir/eval_context.rs index 89c03d80c6..c3859766f9 100644 --- a/src/core/ir/eval_context.rs +++ b/src/core/ir/eval_context.rs @@ -119,19 +119,43 @@ impl GraphQLOperationContext for EvalContext<'_, Ctx> fn selection_set(&self, related_fields: &RelatedFields) -> Option { let selection_field = self.graphql_ctx.field()?; - format_selection_set(selection_field.selection_set(), related_fields) + format_selection_set( + selection_field.selection_set(), + Some(related_fields), + related_fields, + ) } } fn format_selection_set<'a>( selection_set: impl Iterator, - related_fields: &RelatedFields, + related_fields: Option<&RelatedFields>, + global_related_fields: &RelatedFields, ) -> Option { let set = selection_set .filter_map(|field| { // add to set only related fields that should be resolved with current resolver - related_fields.get(field.name()).map(|related_fields| { - format_selection_field(field, &related_fields.0, &related_fields.1) + related_fields?.get(field.name()).map(|new_related_fields| { + let next_related_fields = if new_related_fields.3 { + Some(global_related_fields) + } else { + new_related_fields.2.iter().try_fold( + global_related_fields, + |parent_related_fields, node| { + parent_related_fields.get(node).map( + |(_nickname, next_related_fields, _path, _root)| { + next_related_fields + }, + ) + }, + ) + }; + format_selection_field( + field, + &new_related_fields.0, + next_related_fields, + global_related_fields, + ) }) }) .collect::>(); @@ -146,10 +170,12 @@ fn format_selection_set<'a>( fn format_selection_field( field: &SelectionField, name: &str, - related_fields: &RelatedFields, + related_fields: Option<&RelatedFields>, + global_related_fields: &RelatedFields, ) -> String { let arguments = format_selection_field_arguments(field); - let selection_set = format_selection_set(field.selection_set(), related_fields); + let selection_set = + format_selection_set(field.selection_set(), related_fields, global_related_fields); let mut output = format!("{}{}", name, arguments); diff --git a/src/core/ir/mod.rs b/src/core/ir/mod.rs index 4540424848..076d4c0187 100644 --- a/src/core/ir/mod.rs +++ b/src/core/ir/mod.rs @@ -21,10 +21,10 @@ pub use resolver_context_like::{ /// resolver i.e. fields that don't have their own resolver and are resolved by /// the ancestor #[derive(Debug, Default, Clone)] -pub struct RelatedFields(pub HashMap); +pub struct RelatedFields(pub HashMap, bool)>); impl Deref for RelatedFields { - type Target = HashMap; + type Target = HashMap, bool)>; fn deref(&self) -> &Self::Target { &self.0 diff --git a/tests/core/snapshots/graphql-conformance-019.md_0.snap b/tests/core/snapshots/graphql-conformance-019.md_0.snap new file mode 100644 index 0000000000..8e83a92a48 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-019.md_0.snap @@ -0,0 +1,27 @@ +--- +source: tests/core/spec.rs +expression: response +snapshot_kind: text +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "queryNodeA": { + "name": "nodeA", + "nodeB": { + "name": "nodeB" + }, + "nodeC": { + "name": "nodeC" + }, + "child": { + "name": "nodeA" + } + } + } + } +} diff --git a/tests/core/snapshots/graphql-conformance-019.md_client.snap b/tests/core/snapshots/graphql-conformance-019.md_client.snap new file mode 100644 index 0000000000..eeb5a90041 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-019.md_client.snap @@ -0,0 +1,31 @@ +--- +source: tests/core/spec.rs +expression: formatted +snapshot_kind: text +--- +type NodeA { + child: NodeA + name: String + nodeB: NodeB + nodeC: NodeC +} + +type NodeB { + name: String + nodeA: NodeA + nodeC: NodeC +} + +type NodeC { + name: String + nodeA: NodeA + nodeB: NodeB +} + +type Query { + queryNodeA: NodeA +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/graphql-conformance-019.md_merged.snap b/tests/core/snapshots/graphql-conformance-019.md_merged.snap new file mode 100644 index 0000000000..bd3b791805 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-019.md_merged.snap @@ -0,0 +1,31 @@ +--- +source: tests/core/spec.rs +expression: formatter +snapshot_kind: text +--- +schema @server(hostname: "0.0.0.0", port: 8000) @upstream { + query: Query +} + +type NodeA { + name: String + nodeA: NodeA @modify(name: "child") + nodeB: NodeB + nodeC: NodeC +} + +type NodeB { + name: String + nodeA: NodeA + nodeC: NodeC +} + +type NodeC { + name: String + nodeA: NodeA + nodeB: NodeB +} + +type Query { + queryNodeA: NodeA @graphQL(url: "http://upstream/graphql", name: "nodeA") +} diff --git a/tests/execution/graphql-conformance-019.md b/tests/execution/graphql-conformance-019.md new file mode 100644 index 0000000000..949ef7e02f --- /dev/null +++ b/tests/execution/graphql-conformance-019.md @@ -0,0 +1,71 @@ +# Complicated queries + +```graphql @config +schema @server(port: 8000, hostname: "0.0.0.0") { + query: Query +} + +type Query { + queryNodeA: NodeA @graphQL(url: "http://upstream/graphql", name: "nodeA") +} + +type NodeA { + name: String + nodeB: NodeB + nodeC: NodeC + nodeA: NodeA @modify(name: "child") +} + +type NodeB { + name: String + nodeA: NodeA + nodeC: NodeC +} + +type NodeC { + name: String + nodeA: NodeA + nodeB: NodeB +} +``` + +```yml @mock +- request: + method: POST + url: http://upstream/graphql + textBody: '{ "query": "query { nodeA { name nodeB { name } nodeC { name } nodeA { name } } }" }' + expectedHits: 1 + response: + status: 200 + body: + data: + nodeA: + name: nodeA + nodeB: + name: nodeB + nodeC: + name: nodeC + nodeA: + name: nodeA +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + body: + query: | + query queryNodeA { + queryNodeA { + name + nodeB { + name + } + nodeC { + name + } + child { + name + } + } + } +```