diff --git a/apollo-federation/src/sources/connect/json_selection/selection_set.rs b/apollo-federation/src/sources/connect/json_selection/selection_set.rs index 4c7c4d198f..414ba58cc8 100644 --- a/apollo-federation/src/sources/connect/json_selection/selection_set.rs +++ b/apollo-federation/src/sources/connect/json_selection/selection_set.rs @@ -22,6 +22,7 @@ use apollo_compiler::collections::IndexSet; use apollo_compiler::executable::Field; use apollo_compiler::executable::Selection; use apollo_compiler::executable::SelectionSet; +use apollo_compiler::ExecutableDocument; use apollo_compiler::Node; use multimap::MultiMap; @@ -40,21 +41,29 @@ use crate::sources::connect::SubSelection; impl JSONSelection { /// Apply a selection set to create a new [`JSONSelection`] - pub fn apply_selection_set(&self, selection_set: &SelectionSet) -> Self { + pub fn apply_selection_set( + &self, + document: &ExecutableDocument, + selection_set: &SelectionSet, + ) -> Self { match self { - Self::Named(sub) => Self::Named(sub.apply_selection_set(selection_set)), - Self::Path(path) => Self::Path(path.apply_selection_set(selection_set)), + Self::Named(sub) => Self::Named(sub.apply_selection_set(document, selection_set)), + Self::Path(path) => Self::Path(path.apply_selection_set(document, selection_set)), } } } impl SubSelection { /// Apply a selection set to create a new [`SubSelection`] - pub fn apply_selection_set(&self, selection_set: &SelectionSet) -> Self { + pub fn apply_selection_set( + &self, + document: &ExecutableDocument, + selection_set: &SelectionSet, + ) -> Self { let mut new_selections = Vec::new(); let mut dropped_fields = IndexSet::default(); let mut referenced_fields = HashSet::new(); - let field_map = map_fields_by_name(selection_set); + let field_map = map_fields_by_name(document, selection_set); // When the operation contains __typename, it might be used to complete // an entity reference (e.g. `__typename id`) for a subsequent fetch. @@ -112,8 +121,9 @@ impl SubSelection { Some(Alias::new(field_response_key)) }, name.clone(), - sub.as_ref() - .map(|sub| sub.apply_selection_set(&field.selection_set)), + sub.as_ref().map(|sub| { + sub.apply_selection_set(document, &field.selection_set) + }), )); } } else if self.star.is_some() { @@ -130,7 +140,8 @@ impl SubSelection { for field in fields { new_selections.push(NamedSelection::Path( Some(Alias::new(field.response_key().as_str())), - path_selection.apply_selection_set(&field.selection_set), + path_selection + .apply_selection_set(document, &field.selection_set), )); } } @@ -141,7 +152,7 @@ impl SubSelection { // the new PathWithSubSelection to the new_selections. new_selections.push(NamedSelection::Path( None, - path_selection.apply_selection_set(selection_set), + path_selection.apply_selection_set(document, selection_set), )); } @@ -159,7 +170,7 @@ impl SubSelection { for field in fields { new_selections.push(NamedSelection::Group( Alias::new(field.response_key().as_str()), - sub.apply_selection_set(&field.selection_set), + sub.apply_selection_set(document, &field.selection_set), )); } } @@ -194,10 +205,14 @@ impl SubSelection { impl PathSelection { /// Apply a selection set to create a new [`PathSelection`] - pub fn apply_selection_set(&self, selection_set: &SelectionSet) -> Self { + pub fn apply_selection_set( + &self, + document: &ExecutableDocument, + selection_set: &SelectionSet, + ) -> Self { Self { path: WithRange::new( - self.path.apply_selection_set(selection_set), + self.path.apply_selection_set(document, selection_set), self.path.range(), ), } @@ -205,26 +220,44 @@ impl PathSelection { } impl PathList { - pub(crate) fn apply_selection_set(&self, selection_set: &SelectionSet) -> Self { + pub(crate) fn apply_selection_set( + &self, + document: &ExecutableDocument, + selection_set: &SelectionSet, + ) -> Self { match self { Self::Var(name, path) => Self::Var( name.clone(), - WithRange::new(path.apply_selection_set(selection_set), path.range()), + WithRange::new( + path.apply_selection_set(document, selection_set), + path.range(), + ), ), Self::Key(key, path) => Self::Key( key.clone(), - WithRange::new(path.apply_selection_set(selection_set), path.range()), + WithRange::new( + path.apply_selection_set(document, selection_set), + path.range(), + ), ), Self::Expr(expr, path) => Self::Expr( expr.clone(), - WithRange::new(path.apply_selection_set(selection_set), path.range()), + WithRange::new( + path.apply_selection_set(document, selection_set), + path.range(), + ), ), Self::Method(method_name, args, path) => Self::Method( method_name.clone(), args.clone(), - WithRange::new(path.apply_selection_set(selection_set), path.range()), + WithRange::new( + path.apply_selection_set(document, selection_set), + path.range(), + ), ), - Self::Selection(sub) => Self::Selection(sub.apply_selection_set(selection_set)), + Self::Selection(sub) => { + Self::Selection(sub.apply_selection_set(document, selection_set)) + } Self::Empty => Self::Empty, } } @@ -238,23 +271,32 @@ fn key_name(path_selection: &PathSelection) -> Option<&str> { } } -fn map_fields_by_name(set: &SelectionSet) -> MultiMap> { +fn map_fields_by_name<'a>( + document: &'a ExecutableDocument, + set: &'a SelectionSet, +) -> MultiMap> { let mut map = MultiMap::new(); - map_fields_by_name_impl(set, &mut map); + map_fields_by_name_impl(document, set, &mut map); map } -fn map_fields_by_name_impl<'a>(set: &'a SelectionSet, map: &mut MultiMap>) { +fn map_fields_by_name_impl<'a>( + document: &'a ExecutableDocument, + set: &'a SelectionSet, + map: &mut MultiMap>, +) { for selection in &set.selections { match selection { Selection::Field(field) => { map.insert(field.name.to_string(), field); } - Selection::FragmentSpread(_) => { - // Ignore - these are always expanded into inline fragments + Selection::FragmentSpread(f) => { + if let Some(fragment) = f.fragment_def(document) { + map_fields_by_name_impl(document, &fragment.selection_set, map); + } } Selection::InlineFragment(fragment) => { - map_fields_by_name_impl(&fragment.selection_set, map); + map_fields_by_name_impl(document, &fragment.selection_set, map); } } } @@ -264,12 +306,14 @@ fn map_fields_by_name_impl<'a>(set: &'a SelectionSet, map: &mut MultiMap, s: &str) -> SelectionSet { - apollo_compiler::ExecutableDocument::parse_and_validate(schema, s, "./") - .unwrap() + fn selection_set(schema: &Valid, s: &str) -> (ExecutableDocument, SelectionSet) { + let document = + apollo_compiler::ExecutableDocument::parse_and_validate(schema, s, "./").unwrap(); + let selection_set = document .operations .anonymous .as_ref() @@ -279,7 +323,8 @@ mod tests { .next() .unwrap() .selection_set - .clone() + .clone(); + (document.into_inner(), selection_set) } #[test] @@ -323,12 +368,12 @@ mod tests { ) .unwrap(); - let selection_set = selection_set( + let (document, selection_set) = selection_set( &schema, "{ t { z: a, y: b, x: d, w: h v: k { u: l t: m } } }", ); - let transformed = json.apply_selection_set(&selection_set); + let transformed = json.apply_selection_set(&document, &selection_set); assert_eq!( transformed.to_string(), r###"$.result { @@ -400,12 +445,12 @@ mod tests { ) .unwrap(); - let selection_set = selection_set( + let (document, selection_set) = selection_set( &schema, "{ t { a b_alias c { e: e_alias h group { j } } path_to_f } }", ); - let transformed = json_selection.apply_selection_set(&selection_set); + let transformed = json_selection.apply_selection_set(&document, &selection_set); assert_eq!( transformed.to_string(), r###"$.result { @@ -511,9 +556,9 @@ mod tests { ) .unwrap(); - let selection_set = selection_set(&schema, "{ t { a { b { renamed } } } }"); + let (document, selection_set) = selection_set(&schema, "{ t { a { b { renamed } } } }"); - let transformed = json.apply_selection_set(&selection_set); + let transformed = json.apply_selection_set(&document, &selection_set); assert_eq!( transformed.to_string(), r###"$.result { @@ -579,10 +624,10 @@ mod tests { ) .unwrap(); - let selection_set = + let (document, selection_set) = selection_set(&schema, "{ t { id __typename author { __typename id } } }"); - let transformed = json.apply_selection_set(&selection_set); + let transformed = json.apply_selection_set(&document, &selection_set); assert_eq!( transformed.to_string(), r###"$.result { @@ -592,6 +637,88 @@ mod tests { __typename: $->echo("A") id: authorId } +}"### + ); + } + + #[test] + fn test_fragments() { + let json = super::JSONSelection::parse( + r###" + reviews: result { + id + product: { upc: product_upc } + author: { id: author_id } + } + "###, + ) + .unwrap() + .1; + + let schema = Schema::parse_and_validate( + r###" + type Query { + _entities(representations: [_Any!]!): [_Entity] + } + + scalar _Any + + union _Entity = Product + + type Product { + upc: String + reviews: [Review] + } + + type Review { + id: ID + product: Product + author: User + } + + type User { + id: ID + } + "###, + "./", + ) + .unwrap(); + + let (document, selection_set) = selection_set( + &schema, + "query ($representations: [_Any!]!) { + _entities(representations: $representations) { + ..._generated_onProduct1_0 + } + } + fragment _generated_onProduct1_0 on Product { + reviews { + id + product { + __typename + upc + } + author { + __typename + id + } + } + }", + ); + + let transformed = json.apply_selection_set(&document, &selection_set); + assert_eq!( + transformed.to_string(), + r###"reviews: result { + id + product: { + __typename: $->echo("Product") + upc: product_upc + } + author: { + __typename: $->echo("User") + id: author_id + } }"### ); } diff --git a/apollo-router/src/plugins/connectors/make_requests.rs b/apollo-router/src/plugins/connectors/make_requests.rs index 57f53a8306..7739dc7712 100644 --- a/apollo-router/src/plugins/connectors/make_requests.rs +++ b/apollo-router/src/plugins/connectors/make_requests.rs @@ -224,7 +224,7 @@ fn root_fields( selection: Arc::new( connector .selection - .apply_selection_set(&field.selection_set), + .apply_selection_set(&request.operation, &field.selection_set), ), inputs: request_inputs, }; @@ -278,12 +278,13 @@ fn entities_from_request( .get(None) .map_err(|_| InvalidOperation("no operation document".into()))?; - let (entities_field, typename_requested) = graphql_utils::get_entity_fields(op)?; + let (entities_field, typename_requested) = + graphql_utils::get_entity_fields(&request.operation, op)?; let selection = Arc::new( connector .selection - .apply_selection_set(&entities_field.selection_set), + .apply_selection_set(&request.operation, &entities_field.selection_set), ); representations @@ -365,18 +366,48 @@ fn entities_with_fields_from_request( .get(None) .map_err(|_| InvalidOperation("no operation document".into()))?; - let (entities_field, typename_requested) = graphql_utils::get_entity_fields(op)?; + let (entities_field, typename_requested) = + graphql_utils::get_entity_fields(&request.operation, op)?; let types_and_fields = entities_field .selection_set .selections .iter() .map(|selection| match selection { - Selection::Field(_) => Ok(vec![]), - - Selection::FragmentSpread(_) => Err(InvalidOperation( - "_entities selection can't be a named fragment".into(), - )), + Selection::Field(_) => Ok::<_, MakeRequestError>(vec![]), + + Selection::FragmentSpread(f) => { + let Some(frag) = f.fragment_def(&request.operation) else { + return Err(InvalidOperation(format!( + "invalid operation: fragment `{}` missing", + f.fragment_name + ))); + }; + let typename = frag.type_condition(); + Ok(frag + .selection_set + .selections + .iter() + .filter_map(|sel| { + let field = match sel { + Selection::Field(f) => { + if f.name == TYPENAME { + None + } else { + Some(f) + } + } + Selection::FragmentSpread(_) | Selection::InlineFragment(_) => { + return Some(Err(InvalidOperation( + "handling fragments inside entity selections not implemented" + .into(), + ))) + } + }; + field.map(|f| Ok((typename.to_string(), f))) + }) + .collect::, _>>()?) + } Selection::InlineFragment(frag) => { let typename = frag @@ -387,17 +418,23 @@ fn entities_with_fields_from_request( .selection_set .selections .iter() - .map(|sel| { + .filter_map(|sel| { let field = match sel { - Selection::Field(f) => f, + Selection::Field(f) => { + if f.name == TYPENAME { + None + } else { + Some(f) + } + } Selection::FragmentSpread(_) | Selection::InlineFragment(_) => { - return Err(InvalidOperation( + return Some(Err(InvalidOperation( "handling fragments inside entity selections not implemented" .into(), - )) + ))); } }; - Ok((typename.to_string(), field)) + field.map(|f| Ok((typename.to_string(), f))) }) .collect::, _>>()?) } @@ -424,7 +461,7 @@ fn entities_with_fields_from_request( let selection = Arc::new( connector .selection - .apply_selection_set(&field.selection_set), + .apply_selection_set(&request.operation, &field.selection_set), ); representations.iter().map(move |(i, representation)| { @@ -1298,34 +1335,51 @@ mod tests { } #[test] - fn entities_from_request_root_field() { + fn entities_from_request_entity_with_fragment() { let partial_sdl = r#" type Query { entity(id: ID!): Entity } type Entity { - field: T - } - - type T { field: String } "#; - let schema = Arc::new(Schema::parse_and_validate(partial_sdl, "./").unwrap()); + + let subgraph_schema = Arc::new( + Schema::parse_and_validate( + format!( + r#"{partial_sdl} + extend type Query {{ + _entities(representations: [_Any!]!): _Entity + }} + scalar _Any + union _Entity = Entity + "# + ), + "./", + ) + .unwrap(), + ); let req = crate::services::connect::Request::builder() .service_name("subgraph_Query_entity_0".into()) .context(Context::default()) .operation(Arc::new( ExecutableDocument::parse_and_validate( - &schema, + &subgraph_schema, r#" - query($a: ID!, $b: ID!) { - a: entity(id: $a) { field { field } } - b: entity(id: $b) { field { alias: field } } + query($representations: [_Any!]!) { + _entities(representations: $representations) { + ... _generated_Entity + } } - "# + fragment _generated_Entity on Entity { + __typename + field + alias: field + } + "# .to_string(), "./", ) @@ -1333,8 +1387,10 @@ mod tests { )) .variables(Variables { variables: serde_json_bytes::json!({ - "a": "1", - "b": "2" + "representations": [ + { "__typename": "Entity", "id": "1" }, + { "__typename": "Entity", "id": "2" }, + ] }) .as_object() .unwrap() @@ -1365,22 +1421,72 @@ mod tests { headers: Default::default(), body: Default::default(), }, - selection: JSONSelection::parse("field { field }").unwrap().1, - entity_resolver: None, + selection: JSONSelection::parse("field").unwrap().1, + entity_resolver: Some(super::EntityResolver::Explicit), config: Default::default(), max_requests: None, }; assert_debug_snapshot!(super::entities_from_request(&connector, &req).unwrap(), @r###" [ - RootField { - name: "a", + Entity { + index: 0, typename: Concrete( "Entity", ), selection: Named( SubSelection { selections: [ + Path( + Some( + Alias { + name: WithRange { + node: Field( + "__typename", + ), + range: None, + }, + range: None, + }, + ), + PathSelection { + path: WithRange { + node: Var( + WithRange { + node: $, + range: None, + }, + WithRange { + node: Method( + WithRange { + node: "echo", + range: None, + }, + Some( + MethodArgs { + args: [ + WithRange { + node: String( + "_Entity", + ), + range: None, + }, + ], + range: None, + }, + ), + WithRange { + node: Empty, + range: None, + }, + ), + range: None, + }, + ), + range: None, + }, + }, + ), Field( None, WithRange { @@ -1391,38 +1497,42 @@ mod tests { 0..5, ), }, + None, + ), + Field( Some( - SubSelection { - selections: [ - Field( - None, - WithRange { - node: Field( - "field", - ), - range: Some( - 8..13, - ), - }, - None, + Alias { + name: WithRange { + node: Field( + "alias", ), - ], - star: None, - range: Some( - 6..15, - ), + range: None, + }, + range: None, }, ), + WithRange { + node: Field( + "field", + ), + range: Some( + 0..5, + ), + }, + None, ), ], star: None, range: Some( - 0..15, + 0..5, ), }, ), inputs: RequestInputs { args: { + "__typename": String( + "Entity", + ), "id": String( "1", ), @@ -1430,14 +1540,64 @@ mod tests { this: {}, }, }, - RootField { - name: "b", + Entity { + index: 1, typename: Concrete( "Entity", ), selection: Named( SubSelection { selections: [ + Path( + Some( + Alias { + name: WithRange { + node: Field( + "__typename", + ), + range: None, + }, + range: None, + }, + ), + PathSelection { + path: WithRange { + node: Var( + WithRange { + node: $, + range: None, + }, + WithRange { + node: Method( + WithRange { + node: "echo", + range: None, + }, + Some( + MethodArgs { + args: [ + WithRange { + node: String( + "_Entity", + ), + range: None, + }, + ], + range: None, + }, + ), + WithRange { + node: Empty, + range: None, + }, + ), + range: None, + }, + ), + range: None, + }, + }, + ), Field( None, WithRange { @@ -1448,46 +1608,244 @@ mod tests { 0..5, ), }, + None, + ), + Field( Some( - SubSelection { - selections: [ - Field( - Some( - Alias { - name: WithRange { - node: Field( - "alias", - ), - range: None, - }, - range: None, - }, - ), - WithRange { - node: Field( - "field", - ), - range: Some( - 8..13, - ), - }, - None, + Alias { + name: WithRange { + node: Field( + "alias", ), - ], - star: None, - range: Some( - 6..15, - ), + range: None, + }, + range: None, }, ), - ), - ], - star: None, - range: Some( - 0..15, - ), - }, - ), + WithRange { + node: Field( + "field", + ), + range: Some( + 0..5, + ), + }, + None, + ), + ], + star: None, + range: Some( + 0..5, + ), + }, + ), + inputs: RequestInputs { + args: { + "__typename": String( + "Entity", + ), + "id": String( + "2", + ), + }, + this: {}, + }, + }, + ] + "###); + } + + #[test] + fn entities_from_request_root_field() { + let partial_sdl = r#" + type Query { + entity(id: ID!): Entity + } + + type Entity { + field: T + } + + type T { + field: String + } + "#; + let schema = Arc::new(Schema::parse_and_validate(partial_sdl, "./").unwrap()); + + let req = crate::services::connect::Request::builder() + .service_name("subgraph_Query_entity_0".into()) + .context(Context::default()) + .operation(Arc::new( + ExecutableDocument::parse_and_validate( + &schema, + r#" + query($a: ID!, $b: ID!) { + a: entity(id: $a) { field { field } } + b: entity(id: $b) { field { alias: field } } + } + "# + .to_string(), + "./", + ) + .unwrap(), + )) + .variables(Variables { + variables: serde_json_bytes::json!({ + "a": "1", + "b": "2" + }) + .as_object() + .unwrap() + .clone(), + inverted_paths: Default::default(), + contextual_arguments: Default::default(), + }) + .supergraph_request(Arc::new( + http::Request::builder() + .body(graphql::Request::builder().build()) + .unwrap(), + )) + .build(); + + let connector = Connector { + id: ConnectId::new( + "subgraph_name".into(), + None, + name!(Query), + name!(entity), + 0, + "test label", + ), + transport: HttpJsonTransport { + source_url: Some(Url::parse("http://localhost/api").unwrap()), + connect_template: "/path".parse().unwrap(), + method: HTTPMethod::Get, + headers: Default::default(), + body: Default::default(), + }, + selection: JSONSelection::parse("field { field }").unwrap().1, + entity_resolver: None, + config: Default::default(), + max_requests: None, + }; + + assert_debug_snapshot!(super::entities_from_request(&connector, &req).unwrap(), @r###" + [ + RootField { + name: "a", + typename: Concrete( + "Entity", + ), + selection: Named( + SubSelection { + selections: [ + Field( + None, + WithRange { + node: Field( + "field", + ), + range: Some( + 0..5, + ), + }, + Some( + SubSelection { + selections: [ + Field( + None, + WithRange { + node: Field( + "field", + ), + range: Some( + 8..13, + ), + }, + None, + ), + ], + star: None, + range: Some( + 6..15, + ), + }, + ), + ), + ], + star: None, + range: Some( + 0..15, + ), + }, + ), + inputs: RequestInputs { + args: { + "id": String( + "1", + ), + }, + this: {}, + }, + }, + RootField { + name: "b", + typename: Concrete( + "Entity", + ), + selection: Named( + SubSelection { + selections: [ + Field( + None, + WithRange { + node: Field( + "field", + ), + range: Some( + 0..5, + ), + }, + Some( + SubSelection { + selections: [ + Field( + Some( + Alias { + name: WithRange { + node: Field( + "alias", + ), + range: None, + }, + range: None, + }, + ), + WithRange { + node: Field( + "field", + ), + range: Some( + 8..13, + ), + }, + None, + ), + ], + star: None, + range: Some( + 6..15, + ), + }, + ), + ), + ], + star: None, + range: Some( + 0..15, + ), + }, + ), inputs: RequestInputs { args: { "id": String( @@ -1779,6 +2137,285 @@ mod tests { "###); } + #[test] + fn entities_with_fields_from_request_with_fragment() { + let partial_sdl = r#" + type Query { _: String } # just to make it valid + + type Entity { # @key(fields: "id") + id: ID! + field(foo: String): T + } + + type T { + selected: String + } + "#; + + let subgraph_schema = Arc::new( + Schema::parse_and_validate( + format!( + r#"{partial_sdl} + extend type Query {{ + _entities(representations: [_Any!]!): _Entity + }} + scalar _Any + union _Entity = Entity + "# + ), + "./", + ) + .unwrap(), + ); + + let req = crate::services::connect::Request::builder() + .service_name("subgraph_Entity_field_0".into()) + .context(Context::default()) + .operation(Arc::new( + ExecutableDocument::parse_and_validate( + &subgraph_schema, + r#" + query($representations: [_Any!]!, $bye: String) { + _entities(representations: $representations) { + ... _generated_Entity + } + } + fragment _generated_Entity on Entity { + __typename + field(foo: "hi") { selected } + alias: field(foo: $bye) { selected } + } + "# + .to_string(), + "./", + ) + .unwrap(), + )) + .variables(Variables { + variables: serde_json_bytes::json!({ + "representations": [ + { "__typename": "Entity", "id": "1" }, + { "__typename": "Entity", "id": "2" }, + ], + "bye": "bye" + }) + .as_object() + .unwrap() + .clone(), + inverted_paths: Default::default(), + contextual_arguments: Default::default(), + }) + .supergraph_request(Arc::new( + http::Request::builder() + .body(graphql::Request::builder().build()) + .unwrap(), + )) + .build(); + + let connector = Connector { + id: ConnectId::new( + "subgraph_name".into(), + None, + name!(Entity), + name!(field), + 0, + "test label", + ), + transport: HttpJsonTransport { + source_url: Some(Url::parse("http://localhost/api").unwrap()), + connect_template: "/path".parse().unwrap(), + method: HTTPMethod::Get, + headers: Default::default(), + body: Default::default(), + }, + selection: JSONSelection::parse("selected").unwrap().1, + entity_resolver: None, + config: Default::default(), + max_requests: None, + }; + + assert_debug_snapshot!(super::entities_with_fields_from_request(&connector, &req).unwrap(), @r###" + [ + EntityField { + index: 0, + field_name: "field", + typename: Concrete( + "Entity", + ), + selection: Named( + SubSelection { + selections: [ + Field( + None, + WithRange { + node: Field( + "selected", + ), + range: Some( + 0..8, + ), + }, + None, + ), + ], + star: None, + range: Some( + 0..8, + ), + }, + ), + inputs: RequestInputs { + args: { + "foo": String( + "hi", + ), + }, + this: { + "__typename": String( + "Entity", + ), + "id": String( + "1", + ), + }, + }, + }, + EntityField { + index: 1, + field_name: "field", + typename: Concrete( + "Entity", + ), + selection: Named( + SubSelection { + selections: [ + Field( + None, + WithRange { + node: Field( + "selected", + ), + range: Some( + 0..8, + ), + }, + None, + ), + ], + star: None, + range: Some( + 0..8, + ), + }, + ), + inputs: RequestInputs { + args: { + "foo": String( + "hi", + ), + }, + this: { + "__typename": String( + "Entity", + ), + "id": String( + "2", + ), + }, + }, + }, + EntityField { + index: 0, + field_name: "alias", + typename: Concrete( + "Entity", + ), + selection: Named( + SubSelection { + selections: [ + Field( + None, + WithRange { + node: Field( + "selected", + ), + range: Some( + 0..8, + ), + }, + None, + ), + ], + star: None, + range: Some( + 0..8, + ), + }, + ), + inputs: RequestInputs { + args: { + "foo": String( + "bye", + ), + }, + this: { + "__typename": String( + "Entity", + ), + "id": String( + "1", + ), + }, + }, + }, + EntityField { + index: 1, + field_name: "alias", + typename: Concrete( + "Entity", + ), + selection: Named( + SubSelection { + selections: [ + Field( + None, + WithRange { + node: Field( + "selected", + ), + range: Some( + 0..8, + ), + }, + None, + ), + ], + star: None, + range: Some( + 0..8, + ), + }, + ), + inputs: RequestInputs { + args: { + "foo": String( + "bye", + ), + }, + this: { + "__typename": String( + "Entity", + ), + "id": String( + "2", + ), + }, + }, + }, + ] + "###); + } + #[test] fn entities_with_fields_from_request_interface_object() { let partial_sdl = r#" diff --git a/apollo-router/src/plugins/connectors/make_requests/graphql_utils.rs b/apollo-router/src/plugins/connectors/make_requests/graphql_utils.rs index 6fc3766663..b955aad9a0 100644 --- a/apollo-router/src/plugins/connectors/make_requests/graphql_utils.rs +++ b/apollo-router/src/plugins/connectors/make_requests/graphql_utils.rs @@ -2,6 +2,7 @@ use apollo_compiler::executable::Field; use apollo_compiler::executable::Operation; use apollo_compiler::executable::Selection; use apollo_compiler::schema::Value; +use apollo_compiler::ExecutableDocument; use apollo_compiler::Node; use serde_json::Number; use serde_json_bytes::ByteString; @@ -85,9 +86,13 @@ pub(super) fn argument_value_to_json( } } -pub(super) fn get_entity_fields( - op: &Node, -) -> Result<(&Node, bool), MakeRequestError> { +/// Looks for _entities near the root of the operation. Also looks for +/// __typename within the _entities selection — if it was selected, then we +/// don't have a interfaceObject query. +pub(super) fn get_entity_fields<'a>( + document: &'a ExecutableDocument, + op: &'a Node, +) -> Result<(&'a Node, bool), MakeRequestError> { use MakeRequestError::*; let root_field = op @@ -109,8 +114,21 @@ pub(super) fn get_entity_fields( typename_requested = true; } } - Selection::FragmentSpread(_) => { - return Err(UnsupportedOperation("fragment spread not supported".into())) + Selection::FragmentSpread(f) => { + let fragment = document + .fragments + .get(f.fragment_name.as_str()) + .ok_or_else(|| InvalidOperation("missing fragment".into()))?; + for selection in fragment.selection_set.selections.iter() { + match selection { + Selection::Field(f) => { + if f.name == "__typename" { + typename_requested = true; + } + } + Selection::FragmentSpread(_) | Selection::InlineFragment(_) => {} + } + } } Selection::InlineFragment(f) => { for selection in f.selection_set.selections.iter() { @@ -120,11 +138,7 @@ pub(super) fn get_entity_fields( typename_requested = true; } } - Selection::FragmentSpread(_) | Selection::InlineFragment(_) => { - return Err(UnsupportedOperation( - "fragment spread not supported".into(), - )) - } + Selection::FragmentSpread(_) | Selection::InlineFragment(_) => {} } } }