From 8fad3ae686340de890931f5f45e6f53844564756 Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 10 Aug 2020 11:19:34 -0400 Subject: [PATCH 01/19] add fragments tests --- .../__tests__/integration/fragments.feature | 321 ++++++++++++++++++ .../src/__tests__/queryPlanCucumber.test.ts | 8 +- 2 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 packages/apollo-gateway/src/__tests__/integration/fragments.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/fragments.feature b/packages/apollo-gateway/src/__tests__/integration/fragments.feature new file mode 100644 index 00000000000..72de4a64fc0 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/fragments.feature @@ -0,0 +1,321 @@ +Feature: Query Planning > Fragments + +# important thing here: fetches to accounts service +Scenario: supports inline fragments (one level) + Given query + """ + query GetUser { + me { + ... on User { + username + } + } + } + """ + Then query plan + """ + {"kind":"QueryPlan","node":{"kind":"Fetch","serviceName":"accounts","variableUsages":[],"operation":"{me{username}}"}} + """ + +# important things: calls [accounts, reviews, products, books] +Scenario: supports inline fragments (multi level) + Given query + """ + query GetUser { + me { + ... on User { + username + reviews { + ... on Review { + body + product { + ... on Product { + ... on Book { + title + } + ... on Furniture { + name + } + } + } + } + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{username __typename id}}" + }, + { + "kind": "Flatten", + "path": ["me"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{body product{__typename ...on Book{__typename isbn}...on Furniture{__typename upc}}}}}}" + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": ["me", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{title}}}" + } + }, + { + "kind": "Flatten", + "path": ["me", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Furniture{name}}}" + } + } + ] + } + ] + } + } + """ + +Scenario: supports named fragments (one level) + Given query + """ + query GetUser { + me { + ...userDetails + } + } + + fragment userDetails on User { + username + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{username}}" + } + } + """ + +# important: calls accounts service +Scenario: supports multiple named fragments (one level, mixed ordering) + Given query + """ + fragment userInfo on User { + name + } + query GetUser { + me { + ...userDetails + ...userInfo + } + } + + fragment userDetails on User { + username + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{username name}}" + } + } + """ + +Scenario: supports multiple named fragments (multi level, mixed ordering) + Given query + """ + fragment reviewDetails on Review { + body + } + query GetUser { + me { + ...userDetails + } + } + + fragment userDetails on User { + username + reviews { + ...reviewDetails + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{username __typename id}}" + }, + { + "kind": "Flatten", + "path": ["me"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{body}}}}" + } + } + ] + } + } + """ + +# important: calls accounts & reviews, uses `variableUsages` +Scenario: supports variables within fragments + Given query + """ + query GetUser($format: Boolean) { + me { + ...userDetails + } + } + + fragment userDetails on User { + username + reviews { + body(format: $format) + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{username __typename id}}" + }, + { + "kind": "Flatten", + "path": ["me"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": ["format"], + "operation": "query($representations:[_Any!]!$format:Boolean){_entities(representations:$representations){...on User{reviews{body(format:$format)}}}}" + } + } + ] + } + } + """ + +Scenario: supports root fragments + Given query + """ + query GetUser { + ... on Query { + me { + username + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{username}}" + } + } + """ diff --git a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts b/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts index 120e3a68179..e9ed4f617a0 100644 --- a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts +++ b/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts @@ -7,13 +7,15 @@ import { QueryPlan } from '../..'; import { buildQueryPlan, buildOperationContext, BuildQueryPlanOptions } from '../buildQueryPlan'; import { getFederatedTestingSchema } from './execution-utils'; +const testDir = './packages/apollo-gateway/src/__tests__/'; const buildQueryPlanFeature = loadFeature( - './packages/apollo-gateway/src/__tests__/build-query-plan.feature' + testDir + 'build-query-plan.feature' ); - +const fragmentsFeature = loadFeature(testDir + 'integration/fragments.feature'); const features = [ - buildQueryPlanFeature + buildQueryPlanFeature, + fragmentsFeature ]; features.forEach((feature) => { From b9ce69fb4c1b6e6457e6a8be218fb97eb4d5e88a Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 10 Aug 2020 12:37:14 -0400 Subject: [PATCH 02/19] add requires tests that we can for now --- .../__tests__/integration/requires.feature | 135 ++++++++++++++++++ .../src/__tests__/queryPlanCucumber.test.ts | 6 +- 2 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 packages/apollo-gateway/src/__tests__/integration/requires.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/requires.feature b/packages/apollo-gateway/src/__tests__/integration/requires.feature new file mode 100644 index 00000000000..982544b4373 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/requires.feature @@ -0,0 +1,135 @@ +Feature: Query Planning > requires + +# requires { isbn, title, year } from books service +Scenario: supports passing additional fields defined by a requires + Given query + """ + query GetReviwedBookNames { + me { + reviews { + product { + ... on Book { + name + } + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{__typename id}}" + }, + { + "kind": "Flatten", + "path": ["me"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}}}}}}" + } + }, + { + "kind": "Flatten", + "path": ["me", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["me", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" + } + } + ] + } + } + + """ + +# todo: can we do this without redefining schemas? +# Scenario: collapses nested requires +# Given query +# """ +# query UserFavorites { +# user { +# favoriteColor +# favoriteAnimal +# } +# } +# """ +# Then query plan +# """ +# {} +# """ + +# Scenario: collapses nested requires with user-defined fragments +# Given query +# """ + +# """ +# Then query plan +# """ + # {} +# """ + +# I don't think we need to port this one. There aren't any query plan tests here +# Scenario: passes null values correctly +# Given query +# """ + +# """ +# Then query plan +# """ + # {} +# """ diff --git a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts b/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts index e9ed4f617a0..0c50273ca61 100644 --- a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts +++ b/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts @@ -12,10 +12,12 @@ const buildQueryPlanFeature = loadFeature( testDir + 'build-query-plan.feature' ); const fragmentsFeature = loadFeature(testDir + 'integration/fragments.feature'); +const requiresFeature = loadFeature(testDir + 'integration/requires.feature'); const features = [ - buildQueryPlanFeature, - fragmentsFeature + // buildQueryPlanFeature, + // fragmentsFeature, + requiresFeature ]; features.forEach((feature) => { From ae2eea850c644c3985e0bd70a136e3c66aac4505 Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 10 Aug 2020 12:48:57 -0400 Subject: [PATCH 03/19] add tests for variables --- .../__tests__/integration/variables.feature | 254 ++++++++++++++++++ .../src/__tests__/queryPlanCucumber.test.ts | 4 +- 2 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 packages/apollo-gateway/src/__tests__/integration/variables.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/variables.feature b/packages/apollo-gateway/src/__tests__/integration/variables.feature new file mode 100644 index 00000000000..1c7d235ec58 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/variables.feature @@ -0,0 +1,254 @@ +Feature: Query Planning > Variables + +# calls product with variable +Scenario: passes variables to root fields + Given query + """ + query GetProduct($upc: String!) { + product(upc: $upc) { + name + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": ["upc"], + "operation": "query($upc:String!){product(upc:$upc){__typename ...on Book{__typename isbn}...on Furniture{name}}}" + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" + } + } + ] + } + } + """ + +# calls product with default variable +Scenario: supports default variables in a variable definition + Given query + """ + query GetProduct($upc: String = "1") { + product(upc: $upc) { + name + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": ["upc"], + "operation": "query($upc:String=\"1\"){product(upc:$upc){__typename ...on Book{__typename isbn}...on Furniture{name}}}" + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" + } + } + ] + } + } + """ + +# calls reviews service with variable; calls accounts +Scenario: passes variables to nested services + Given query + """ + query GetProductsForUser($format: Boolean) { + me { + reviews { + body(format: $format) + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{__typename id}}" + }, + { + "kind": "Flatten", + "path": ["me"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": ["format"], + "operation": "query($representations:[_Any!]!$format:Boolean){_entities(representations:$representations){...on User{reviews{body(format:$format)}}}}" + } + } + ] + } + } + """ + +# XXX I think this test relies on execution to use the default variable, not the query plan +# Scenario: works with default variables in the schema +# Given query +# """ +# query LibraryUser($libraryId: ID!, $userId: ID) { +# library(id: $libraryId) { +# userAccount(id: $userId) { +# id +# name +# } +# } +# } +# """ +# Then query plan +# """ +# { +# "kind": "QueryPlan", +# "node": { +# "kind": "Sequence", +# "nodes": [ +# { +# "kind": "Fetch", +# "serviceName": "books", +# "variableUsages": ["libraryId"], +# "operation": "query($libraryId:ID!){library(id:$libraryId){__typename id name}}" +# }, +# { +# "kind": "Flatten", +# "path": ["library"], +# "node": { +# "kind": "Fetch", +# "serviceName": "accounts", +# "requires": [ +# { +# "kind": "InlineFragment", +# "typeCondition": "Library", +# "selections": [ +# { "kind": "Field", "name": "__typename" }, +# { "kind": "Field", "name": "id" }, +# { "kind": "Field", "name": "name" } +# ] +# } +# ], +# "variableUsages": ["userId"], +# "operation": "query($representations:[_Any!]!$userId:ID){_entities(representations:$representations){...on Library{userAccount(id:$userId){id name}}}}" +# } +# } +# ] +# } +# } +# """ + +# Scenario: +# Given query +# """ + +# """ +# Then query plan +# """ +# {} +# """ + diff --git a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts b/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts index 0c50273ca61..ce3e67d0078 100644 --- a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts +++ b/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts @@ -13,11 +13,13 @@ const buildQueryPlanFeature = loadFeature( ); const fragmentsFeature = loadFeature(testDir + 'integration/fragments.feature'); const requiresFeature = loadFeature(testDir + 'integration/requires.feature'); +const variablesFeature = loadFeature(testDir + 'integration/variables.feature'); const features = [ // buildQueryPlanFeature, // fragmentsFeature, - requiresFeature + // requiresFeature, + variablesFeature ]; features.forEach((feature) => { From 713ba80c6b45284c37f070dca146f1ba9e6d76fe Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 13 Aug 2020 14:26:37 -0400 Subject: [PATCH 04/19] wip -- comments --- .../src/__tests__/buildQueryPlan.test.ts | 2 +- packages/apollo-gateway/src/buildQueryPlan.ts | 62 +++++++++++++++++-- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts b/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts index 8e32e2c7581..e98ba300f13 100644 --- a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts +++ b/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts @@ -63,7 +63,7 @@ describe('buildQueryPlan', () => { `); }); - it(`should use a single fetch when requesting a root field from one service`, () => { + fit(`should use a single fetch when requesting a root field from one service`, () => { const query = gql` query { me { diff --git a/packages/apollo-gateway/src/buildQueryPlan.ts b/packages/apollo-gateway/src/buildQueryPlan.ts index ac528f5a340..252e4a21592 100644 --- a/packages/apollo-gateway/src/buildQueryPlan.ts +++ b/packages/apollo-gateway/src/buildQueryPlan.ts @@ -70,6 +70,7 @@ export function buildQueryPlan( operationContext: OperationContext, options: BuildQueryPlanOptions = { autoFragmentization: false }, ): QueryPlan { + // this gets us the schema, operation, all fragment defs, etc const context = buildQueryPlanningContext(operationContext, options); if (context.operation.operation === 'subscription') { @@ -83,14 +84,26 @@ export function buildQueryPlan( const isMutation = context.operation.operation === 'mutation'; + /** + * TODO: what does collectFields do? + * -- looks like it just gathers fields at a certain level of a query + * pass the root selection set and scope based off root type + * => `fields` is the root level fieldNodes + */ const fields = collectFields( context, context.newScope(rootType), context.operation.selectionSet, ); + // Mutations are a bit more specific in how FetchGroups can be built, as some // calls to the same service may need to be executed serially. + /** + * TODO: what does splitRootFields do? + * -- looks like it creates fetch groups based off service for the top level fields + * + */ const groups = isMutation ? splitRootFieldsSerially(context, fields) : splitRootFields(context, fields); @@ -510,19 +523,37 @@ function splitSubfields( }); } +/** + * wtf does this function do?! + * split into what + */ function splitFields( context: QueryPlanningContext, path: ResponsePath, fields: FieldSet, - groupForField: (field: Field) => FetchGroup, + // this function is for finding a FetchGroup to add a field to. The reason we have + // to use an outside function for this is that this lookup process happens differently + // for queries and mutations. In queries, we can just batch all fields from the same + // service togethere, whereas in mutations, we have to look at the last group + // that was constructed. If it was of the same service as the field in question, + // we can use that group but otherwise we need to create a new group. + getGroupForField: (field: Field) => FetchGroup, ) { + // group by the name the client will see -- alias or name, whichever is there + // Each iteration of the loop contains a set of fields with the same response name + // [FieldDef, FieldDef] for (const fieldsForResponseName of groupByResponseName(fields).values()) { + // TODO: How can fields in a single selection have different parent types?? + // is `fields` used across multiple selectionSets? for (const [parentType, fieldsForParentType] of groupByParentType(fieldsForResponseName)) { // Field nodes that share the same response name and parent type are guaranteed // to have the same field name and arguments. We only need the other nodes when // merging selection sets, to take node-specific subfields and directives // into account. + // TODO: how is the above statement true, when aliases could be anything? + // Is this only after deduping? + const field = fieldsForParentType[0]; const { scope, fieldDef } = field; @@ -547,8 +578,9 @@ function splitFields( if (isObjectType(parentType) && scope.possibleTypes.includes(parentType)) { // If parent type is an object type, we can directly look for the right // group. - const group = groupForField(field as Field); + const group = getGroupForField(field as Field); group.fields.push( + // TODO: what is completeField? completeField( context, scope as Scope, @@ -580,7 +612,7 @@ function splitFields( // With no extending field definitions, we can engage the optimization if (hasNoExtendingFieldDefs) { - const group = groupForField(field as Field); + const group = getGroupForField(field as Field); group.fields.push( completeField(context, scope, group, path, fieldsForResponseName) ); @@ -600,7 +632,7 @@ function splitFields( field.fieldNode, ); groupsByRuntimeParentTypes.add( - groupForField({ + getGroupForField({ scope: context.newScope(runtimeParentType, scope), fieldNode: field.fieldNode, fieldDef, @@ -641,6 +673,9 @@ function splitFields( } } +/** + * returns the complete field info which includes scope, def, and ast node for a field + */ function completeField( context: QueryPlanningContext, scope: Scope, @@ -648,9 +683,11 @@ function completeField( path: ResponsePath, fields: FieldSet, ): Field { + // TODO: why even pass in a FieldSet? const { fieldNode, fieldDef } = fields[0]; const returnType = getNamedType(fieldDef.type); + // if scalar return type for field, just return the field if (!isCompositeType(returnType)) { // FIXME: We should look at all field nodes to make sure we take directives // into account (or remove directives for the time being). @@ -658,11 +695,15 @@ function completeField( } else { // For composite types, we need to recurse. + debugger; + // if the fieldtype is a listtype, it also appends @ const fieldPath = addPath(path, getResponseName(fieldNode), fieldDef.type); const subGroup = new FetchGroup(parentGroup.serviceName); subGroup.mergeAt = fieldPath; + // figure out what fields can be provided by the service responsible for the parent group + // and add them to the subGroup subGroup.providedFields = context.getProvidedFields( fieldDef, parentGroup.serviceName, @@ -759,10 +800,15 @@ function getInternalFragment( return context.internalFragments.get(key)!; } +/** + * Accepts a selection set of AST nodes, Gathers field definitions from the + * schema, and pushes those definitions back to `fields` + */ function collectFields( context: QueryPlanningContext, scope: Scope, selectionSet: SelectionSetNode, + // this is mutated and appended to fields: FieldSet = [], visitedFragmentNames: { [fragmentName: string]: boolean } = Object.create( null, @@ -792,16 +838,20 @@ function collectFields( case Kind.FRAGMENT_SPREAD: const fragmentName = selection.name.value; + // check to make sure the spread fragments exists in the parsed operation const fragment = context.fragments[fragmentName]; if (!fragment) { continue; } + // new scope based on the parent type of the fragment + //(like `...BookFields` where `fragment BookFields on Book` would be `Book`) const newScope = context.newScope(getFragmentCondition(fragment), scope); if (newScope.possibleTypes.length === 0) { continue; } + // this makes sure we don't collect the fragment twice in a single selection if (visitedFragmentNames[fragmentName]) { continue; } @@ -1009,6 +1059,10 @@ export class QueryPlanningContext { return fieldDef; } + /** + * possible types includes all types in unions or implementing types + * of interfaces + */ getPossibleTypes( type: GraphQLAbstractType | GraphQLObjectType, ): ReadonlyArray { From 32b6a9074c2d60226dfc47401c6fa01798689b3e Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 15 Aug 2020 22:16:21 -0400 Subject: [PATCH 05/19] mutation for ran --- .../__tests__/integration/mutations.feature | 19 +++++++++++++++++++ .../src/__tests__/queryPlanCucumber.test.ts | 4 +++- packages/apollo-gateway/src/buildQueryPlan.ts | 8 ++++---- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 packages/apollo-gateway/src/__tests__/integration/mutations.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/mutations.feature b/packages/apollo-gateway/src/__tests__/integration/mutations.feature new file mode 100644 index 00000000000..af5d8f6cea8 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/mutations.feature @@ -0,0 +1,19 @@ +Feature: Query Planning > Mutations + +Scenario: supports mutations +Given query +""" +mutation Login($username: String!, $password: String!) { + login(username: $username, password: $password) { + reviews { + product { + upc + } + } + } +} +""" +Then query plan +""" +{} +""" diff --git a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts b/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts index ce3e67d0078..98c5f61a015 100644 --- a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts +++ b/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts @@ -14,12 +14,14 @@ const buildQueryPlanFeature = loadFeature( const fragmentsFeature = loadFeature(testDir + 'integration/fragments.feature'); const requiresFeature = loadFeature(testDir + 'integration/requires.feature'); const variablesFeature = loadFeature(testDir + 'integration/variables.feature'); +const mutationsFeature = loadFeature(testDir + 'integration/mutations.feature'); const features = [ // buildQueryPlanFeature, // fragmentsFeature, // requiresFeature, - variablesFeature + // variablesFeature, + mutationsFeature ]; features.forEach((feature) => { diff --git a/packages/apollo-gateway/src/buildQueryPlan.ts b/packages/apollo-gateway/src/buildQueryPlan.ts index 252e4a21592..ee688f2891c 100644 --- a/packages/apollo-gateway/src/buildQueryPlan.ts +++ b/packages/apollo-gateway/src/buildQueryPlan.ts @@ -414,7 +414,7 @@ function splitSubfields( let baseService, owningService; const parentTypeFederationMetadata = getFederationMetadata(parentType); - if (parentTypeFederationMetadata?.isValueType) { + if (parentTypeFederationMetadata!.isValueType) { baseService = parentGroup.serviceName; owningService = parentGroup.serviceName; } else { @@ -1112,7 +1112,7 @@ export class QueryPlanningContext { } getBaseService(parentType: GraphQLObjectType): string | null { - return (getFederationMetadata(parentType)?.serviceName) || null; + return (getFederationMetadata(parentType)!.serviceName) || null; } getOwningService( @@ -1122,7 +1122,7 @@ export class QueryPlanningContext { const fieldFederationMetadata = getFederationMetadata(fieldDef); if ( fieldFederationMetadata?.serviceName && - !fieldFederationMetadata?.belongsToValueType + !fieldFederationMetadata.belongsToValueType ) { return fieldFederationMetadata.serviceName; } else { @@ -1151,7 +1151,7 @@ export class QueryPlanningContext { }); for (const possibleType of this.getPossibleTypes(parentType)) { - const keys = getFederationMetadata(possibleType)?.keys?.[serviceName]; + const keys = getFederationMetadata(possibleType)!.keys?.[serviceName]; if (!(keys && keys.length > 0)) continue; From 6ef9f497b45ac36ada3a89f730d789d49b7f9528 Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 17 Aug 2020 15:58:09 -0400 Subject: [PATCH 06/19] add mutation tests --- .../__tests__/integration/mutations.feature | 336 +++++++++++++++++- 1 file changed, 324 insertions(+), 12 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/integration/mutations.feature b/packages/apollo-gateway/src/__tests__/integration/mutations.feature index af5d8f6cea8..f6eaf15b55d 100644 --- a/packages/apollo-gateway/src/__tests__/integration/mutations.feature +++ b/packages/apollo-gateway/src/__tests__/integration/mutations.feature @@ -1,19 +1,331 @@ Feature: Query Planning > Mutations Scenario: supports mutations -Given query -""" -mutation Login($username: String!, $password: String!) { - login(username: $username, password: $password) { - reviews { - product { + Given query + """ + mutation Login($username: String!, $password: String!) { + login(username: $username, password: $password) { + reviews { + product { + upc + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": ["username", "password"], + "operation": "mutation($username:String!$password:String!){login(username:$username password:$password){__typename id}}" + }, + { + "kind": "Flatten", + "path": ["login"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{upc}}}}}}" + } + }, + { + "kind": "Flatten", + "path": ["login", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{upc}}}" + } + } + ] + } + } + """ + +Scenario: mutations across service boundaries + Given query + """ + mutation Review($upc: String!, $body: String!) { + reviewProduct(upc: $upc, body: $body) { + ... on Furniture { + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": ["upc", "body"], + "operation": "mutation($upc:String!$body:String!){reviewProduct(upc:$upc body:$body){__typename ...on Furniture{__typename upc}}}" + }, + { + "kind": "Flatten", + "path": ["reviewProduct"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Furniture{name}}}" + } + } + ] + } + } + + """ + +Scenario: multiple root mutations + Given query + """ + mutation LoginAndReview( + $username: String! + $password: String! + $upc: String! + $body: String! + ) { + login(username: $username, password: $password) { + reviews { + product { + upc + } + } + } + reviewProduct(upc: $upc, body: $body) { + ... on Furniture { + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": ["username", "password"], + "operation": "mutation($username:String!$password:String!){login(username:$username password:$password){__typename id}}" + }, + { + "kind": "Flatten", + "path": ["login"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{upc}}}}}}" + } + }, + { + "kind": "Flatten", + "path": ["login", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{upc}}}" + } + }, + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": ["upc", "body"], + "operation": "mutation($upc:String!$body:String!){reviewProduct(upc:$upc body:$body){__typename ...on Furniture{__typename upc}}}" + }, + { + "kind": "Flatten", + "path": ["reviewProduct"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Furniture{name}}}" + } + } + ] + } + } + """ + +# important: order: Review > Update > Login > Delete +Scenario: multiple root mutations with correct service order + Given query + """ + mutation LoginAndReview( + $upc: String! + $body: String! + $updatedReview: UpdateReviewInput! + $username: String! + $password: String! + $reviewId: ID! + ) { + reviewProduct(upc: $upc, body: $body) { + ... on Furniture { upc } } + updateReview(review: $updatedReview) { + id + body + } + login(username: $username, password: $password) { + reviews { + product { + upc + } + } + } + deleteReview(id: $reviewId) } -} -""" -Then query plan -""" -{} -""" + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": ["upc", "body", "updatedReview"], + "operation": "mutation($upc:String!$body:String!$updatedReview:UpdateReviewInput!){reviewProduct(upc:$upc body:$body){__typename ...on Furniture{upc}}updateReview(review:$updatedReview){id body}}" + }, + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": ["username", "password"], + "operation": "mutation($username:String!$password:String!){login(username:$username password:$password){__typename id}}" + }, + { + "kind": "Flatten", + "path": ["login"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{upc}}}}}}" + } + }, + { + "kind": "Flatten", + "path": ["login", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{upc}}}" + } + }, + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": ["reviewId"], + "operation": "mutation($reviewId:ID!){deleteReview(id:$reviewId)}" + } + ] + } + } + """ + From 579941bcf690a81d0c4c2734afae6df44d0d58cb Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 17 Aug 2020 16:48:33 -0400 Subject: [PATCH 07/19] added boolean directive tests --- .../src/__tests__/integration/boolean.feature | 339 ++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 packages/apollo-gateway/src/__tests__/integration/boolean.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/boolean.feature b/packages/apollo-gateway/src/__tests__/integration/boolean.feature new file mode 100644 index 00000000000..6ada7cd6724 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/boolean.feature @@ -0,0 +1,339 @@ +Feature: Query Planning > Boolean + +Scenario: supports @skip when a boolean condition is met + Given query + """ + query GetReviewers { + topReviews { + body + author @skip(if: true) { + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{body author@skip(if:true){__typename id}}}" + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "author"], + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}" + } + } + ] + } + } + """ + +Scenario: supports @skip when a boolean condition is met (variable driven) + Given query + """ + query GetReviewers($skip: Boolean! = true) { + topReviews { + body + author @skip(if: $skip) { + username + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": ["skip"], + "operation": "query($skip:Boolean!=true){topReviews{body author@skip(if:$skip){username}}}" + } + } + """ + +Scenario: supports @skip when a boolean condition is not met + Given query + """ + query GetReviewers { + topReviews { + body + author @skip(if: false) { + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{body author@skip(if:false){__typename id}}}" + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "author"], + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}" + } + } + ] + } + } + """ + +Scenario: supports @skip when a boolean condition is not met (variable driven) + Given query + """ + query GetReviewers($skip: Boolean!) { + topReviews { + body + author @skip(if: $skip) { + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": ["skip"], + "operation": "query($skip:Boolean!){topReviews{body author@skip(if:$skip){__typename id}}}" + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "author"], + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}" + } + } + ] + } + } + """ + +Scenario: supports @include when a boolean condition is not met + Given query + """ + query GetReviewers { + topReviews { + body + author @include(if: false) { + username + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{body author@include(if:false){username}}}" + } + } + """ + +Scenario: supports @include when a boolean condition is not met (variable driven) + Given query + """ + query GetReviewers($include: Boolean! = false) { + topReviews { + body + author @include(if: $include) { + username + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": ["include"], + "operation": "query($include:Boolean!=false){topReviews{body author@include(if:$include){username}}}" + } + } + """ + +Scenario: supports @include when a boolean condition is met + Given query + """ + query GetReviewers { + topReviews { + body + author @include(if: true) { + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{body author@include(if:true){__typename id}}}" + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "author"], + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}" + } + } + ] + } + } + """ + + + +Scenario: supports @include when a boolean condition is met (variable driven) + Given query + """ + query GetReviewers($include: Boolean!) { + topReviews { + body + author @include(if: $include) { + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": ["include"], + "operation": "query($include:Boolean!){topReviews{body author@include(if:$include){__typename id}}}" + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "author"], + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}" + } + } + ] + } + } + """ + + +# Scenario: +# Given query +# """ + +# """ +# Then query plan +# """ +# {} +# """ From 780f88605dd7031fdd880e007c5ea77aac89e972 Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 17 Aug 2020 16:56:51 -0400 Subject: [PATCH 08/19] add provides tests --- .../src/__tests__/integration/boolean.feature | 11 --- .../__tests__/integration/provides.feature | 76 +++++++++++++++++++ 2 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 packages/apollo-gateway/src/__tests__/integration/provides.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/boolean.feature b/packages/apollo-gateway/src/__tests__/integration/boolean.feature index 6ada7cd6724..b008863eee1 100644 --- a/packages/apollo-gateway/src/__tests__/integration/boolean.feature +++ b/packages/apollo-gateway/src/__tests__/integration/boolean.feature @@ -326,14 +326,3 @@ Scenario: supports @include when a boolean condition is met (variable driven) } } """ - - -# Scenario: -# Given query -# """ - -# """ -# Then query plan -# """ -# {} -# """ diff --git a/packages/apollo-gateway/src/__tests__/integration/provides.feature b/packages/apollo-gateway/src/__tests__/integration/provides.feature new file mode 100644 index 00000000000..f6d4fb54ca5 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/provides.feature @@ -0,0 +1,76 @@ +Feature: Query Planner > Provides + +Scenario: does not have to go to another service when field is given + Given query + """ + query GetReviewers { + topReviews { + author { + username + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{author{username}}}" + } + } + """ + +# make sure the accounts service doesn't have User.username in its query +Scenario: does not load fields provided even when going to other service + Given query + """ + query GetReviewers { + topReviews { + author { + username + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{author{username __typename id}}}" + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "author"], + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}" + } + } + ] + } + } + """ From 62b02aff8ef5b86afc2c871e70180eac1f882b2b Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 17 Aug 2020 17:04:02 -0400 Subject: [PATCH 09/19] added 1/2 of the value types tests --- .../__tests__/integration/value-types.feature | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 packages/apollo-gateway/src/__tests__/integration/value-types.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/value-types.feature b/packages/apollo-gateway/src/__tests__/integration/value-types.feature new file mode 100644 index 00000000000..f90a6f795a6 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/value-types.feature @@ -0,0 +1,118 @@ +Feature: Query Planner > Value Types + +Scenario: resolves value types within their respective services + Given query + """ + fragment Metadata on MetadataOrError { + ... on KeyValue { + key + value + } + ... on Error { + code + message + } + } + + query ProducsWithMetadata { + topProducts(first: 10) { + upc + ... on Book { + metadata { + ...Metadata + } + } + ... on Furniture { + metadata { + ...Metadata + } + } + reviews { + metadata { + ...Metadata + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": [], + "operation": "{topProducts(first:10){__typename ...on Book{upc __typename isbn}...on Furniture{upc metadata{__typename ...on KeyValue{key value}...on Error{code message}}__typename}}}" + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": ["topProducts", "@"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{metadata{__typename ...on KeyValue{key value}...on Error{code message}}}}}" + } + }, + { + "kind": "Flatten", + "path": ["topProducts", "@"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{reviews{metadata{__typename ...on KeyValue{key value}...on Error{code message}}}}...on Furniture{reviews{metadata{__typename ...on KeyValue{key value}...on Error{code message}}}}}}" + } + } + ] + } + ] + } + } + """ + +# can't test as-is without modifying resolvers +# Scenario: resolves @provides fields on value types correctly via contrived example +# Given query +# """ + +# """ +# Then query plan +# """ +# {} +# """ From e684a60ec192d4055be1dd151af302723a03fd98 Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 18 Aug 2020 10:39:10 -0400 Subject: [PATCH 10/19] add abstract types tests --- .../integration/abstract-types.feature | 648 ++++++++++++++++++ 1 file changed, 648 insertions(+) create mode 100644 packages/apollo-gateway/src/__tests__/integration/abstract-types.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/abstract-types.feature b/packages/apollo-gateway/src/__tests__/integration/abstract-types.feature new file mode 100644 index 00000000000..45715d01873 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/abstract-types.feature @@ -0,0 +1,648 @@ +Feature: Query Planner > Abstract Types + +Scenario: handles an abstract type from the base service + Given query + """ + query GetProduct($upc: String!) { + product(upc: $upc) { + upc + name + price + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": ["upc"], + "operation": "query($upc:String!){product(upc:$upc){__typename ...on Book{upc __typename isbn price}...on Furniture{upc name price}}}" + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" + } + } + ] + } + } + """ + +Scenario: can request fields on extended interfaces + Given query + """ + query GetProduct($upc: String!) { + product(upc: $upc) { + inStock + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": ["upc"], + "operation": "query($upc:String!){product(upc:$upc){__typename ...on Book{__typename isbn}...on Furniture{__typename sku}}}" + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "inventory", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "sku" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{inStock}...on Furniture{inStock}}}" + } + } + ] + } + } + """ + +Scenario: can request fields on extended types that implement an interface + Given query + """ + query GetProduct($upc: String!) { + product(upc: $upc) { + inStock + ... on Furniture { + isHeavy + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": ["upc"], + "operation": "query($upc:String!){product(upc:$upc){__typename ...on Book{__typename isbn}...on Furniture{__typename sku}}}" + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "inventory", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "sku" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{inStock}...on Furniture{inStock isHeavy}}}" + } + } + ] + } + } + """ + +Scenario: prunes unfilled type conditions + Given query + """ + query GetProduct($upc: String!) { + product(upc: $upc) { + inStock + ... on Furniture { + isHeavy + } + ... on Book { + isCheckedOut + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": ["upc"], + "operation": "query($upc:String!){product(upc:$upc){__typename ...on Book{__typename isbn}...on Furniture{__typename sku}}}" + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "inventory", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "sku" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{inStock isCheckedOut}...on Furniture{inStock isHeavy}}}" + } + } + ] + } + } + """ + +Scenario: fetches interfaces returned from other services + Given query + """ + query GetUserAndProducts { + me { + reviews { + product { + price + ... on Book { + title + } + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{__typename id}}" + }, + { + "kind": "Flatten", + "path": ["me"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{__typename upc}}}}}}" + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": ["me", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{price}...on Furniture{price}}}" + } + }, + { + "kind": "Flatten", + "path": ["me", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{title}}}" + } + } + ] + } + ] + } + } + """ + +Scenario: fetches composite fields from a foreign type casted to an interface [@provides field + Given query + """ + query GetUserAndProducts { + me { + reviews { + product { + price + ... on Book { + name + } + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{__typename id}}" + }, + { + "kind": "Flatten", + "path": ["me"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{__typename upc}}}}}}" + } + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Flatten", + "path": ["me", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{price}...on Furniture{price}}}" + } + }, + { + "kind": "Sequence", + "nodes": [ + { + "kind": "Flatten", + "path": ["me", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["me", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" + } + } + ] + } + ] + } + ] + } + } + """ + +Scenario: allows for extending an interface from another service with fields + Given query + """ + query GetProduct($upc: String!) { + product(upc: $upc) { + reviews { + body + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": ["upc"], + "operation": "query($upc:String!){product(upc:$upc){__typename ...on Book{__typename isbn}...on Furniture{__typename upc}}}" + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{reviews{body}}...on Furniture{reviews{body}}}}" + } + } + ] + } + } + """ + +Scenario: handles unions from the same service + Given query + """ + query GetUserAndProducts { + me { + reviews { + product { + price + ... on Furniture { + brand { + ... on Ikea { + asile + } + ... on Amazon { + referrer + } + } + } + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{__typename id}}" + }, + { + "kind": "Flatten", + "path": ["me"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{__typename upc}}}}}}" + } + }, + { + "kind": "Flatten", + "path": ["me", "reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{price}...on Furniture{price brand{__typename ...on Ikea{asile}...on Amazon{referrer}}}}}" + } + } + ] + } + } + """ + +# can't test this yet -- original test overwrites schema def, which we don't support yet +# Scenario: doesn't expand interfaces with inline type conditions if all possibilities are fufilled by one service +# Given query +# """ +# query GetProducts { +# topProducts { +# name +# } +# } +# """ +# Then query plan +# """ +# {} +# """ From db633d086fa0885099d440c64aebee397754bb83 Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 18 Aug 2020 10:49:09 -0400 Subject: [PATCH 11/19] Add aliases tests --- .../src/__tests__/integration/aliases.feature | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 packages/apollo-gateway/src/__tests__/integration/aliases.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/aliases.feature b/packages/apollo-gateway/src/__tests__/integration/aliases.feature new file mode 100644 index 00000000000..ec9894277c3 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/aliases.feature @@ -0,0 +1,314 @@ +Feature: Aliases + + +Scenario: supports simple aliases + Given query + """ + query GetProduct($upc: String!) { + product(upc: $upc) { + name + title: name + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": ["upc"], + "operation": "query($upc:String!){product(upc:$upc){__typename ...on Book{__typename isbn}...on Furniture{name title:name}}}" + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name title:name}}}" + } + } + ] + } + } + """ + +Scenario: supports aliases of root fields on subservices + Given query + """ + query GetProduct($upc: String!) { + product(upc: $upc) { + name + title: name + reviews { + body + } + productReviews: reviews { + body + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": ["upc"], + "operation": "query($upc:String!){product(upc:$upc){__typename ...on Book{__typename isbn}...on Furniture{name title:name __typename upc}}}" + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Sequence", + "nodes": [ + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name title:name}}}" + } + } + ] + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{reviews{body}productReviews:reviews{body}}...on Furniture{reviews{body}productReviews:reviews{body}}}}" + } + } + ] + } + ] + } + } + """ + +Scenario: supports aliases of nested fields on subservices + Given query + """ + query GetProduct($upc: String!) { + product(upc: $upc) { + name + title: name + reviews { + content: body + body + } + productReviews: reviews { + body + reviewer: author { + name: username + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": ["upc"], + "operation": "query($upc:String!){product(upc:$upc){__typename ...on Book{__typename isbn}...on Furniture{name title:name __typename upc}}}" + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Sequence", + "nodes": [ + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name title:name}}}" + } + } + ] + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{reviews{body content:body}productReviews:reviews{body reviewer:author{name:username}}}...on Furniture{reviews{body content:body}productReviews:reviews{body reviewer:author{name:username}}}}}" + } + } + ] + } + ] + } + } + """ + +# Scenario: +# Given query: +# """ + +# """ +# Then query plan: +# """ +# {} +# """ From 353e4ef62a5b8535b7f0460e7d49d2f21720666e Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 18 Aug 2020 11:10:07 -0400 Subject: [PATCH 12/19] add custom directives test --- .../src/__tests__/integration/aliases.feature | 2 +- .../integration/custom-directives.feature | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 packages/apollo-gateway/src/__tests__/integration/custom-directives.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/aliases.feature b/packages/apollo-gateway/src/__tests__/integration/aliases.feature index ec9894277c3..53930d31737 100644 --- a/packages/apollo-gateway/src/__tests__/integration/aliases.feature +++ b/packages/apollo-gateway/src/__tests__/integration/aliases.feature @@ -1,4 +1,4 @@ -Feature: Aliases +Feature: Query Planning > Aliases Scenario: supports simple aliases diff --git a/packages/apollo-gateway/src/__tests__/integration/custom-directives.feature b/packages/apollo-gateway/src/__tests__/integration/custom-directives.feature new file mode 100644 index 00000000000..a61bcd2c446 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/custom-directives.feature @@ -0,0 +1,83 @@ +Feature: Query Planning > Custom Directives + +Scenario: successfully passes directives along in requests to an underlying service + Given query + """ + query GetReviewers { + topReviews { + body @stream + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{body@stream}}" + } + } + """ + +Scenario: successfully passes directives and their variables along in requests to underlying services + Given query + """ + query GetReviewers { + topReviews { + body @stream + author @transform(from: "JSON") { + name @stream + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{body@stream author@transform(from:\"JSON\"){__typename id}}}" + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "author"], + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name@stream}}}" + } + } + ] + } + } + """ + +# Scenario: +# Given query +# """ + +# """ +# Then query plan +# """ +# {} +# """ From 20c971a6dffa093e6e0bc7c1d36dca5b2f58f61a Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 18 Aug 2020 11:44:15 -0400 Subject: [PATCH 13/19] Added execution style test --- .../integration/execution-style.feature | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 packages/apollo-gateway/src/__tests__/integration/execution-style.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/execution-style.feature b/packages/apollo-gateway/src/__tests__/integration/execution-style.feature new file mode 100644 index 00000000000..87f86deb2b8 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/execution-style.feature @@ -0,0 +1,37 @@ +Feature: Query Planning > Execution Style + +Scenario: supports parallel root fields + Given query + """ + query GetUserAndReviews { + me { + username + } + topReviews { + body + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Parallel", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{username}}" + }, + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{body}}" + } + ] + } + } + """ From cc9e4634e27d409d6477e308e40535c00ddb9788 Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 18 Aug 2020 11:44:28 -0400 Subject: [PATCH 14/19] added single-service tests --- .../integration/single-service.feature | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 packages/apollo-gateway/src/__tests__/integration/single-service.feature diff --git a/packages/apollo-gateway/src/__tests__/integration/single-service.feature b/packages/apollo-gateway/src/__tests__/integration/single-service.feature new file mode 100644 index 00000000000..f8d7df43a80 --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/single-service.feature @@ -0,0 +1,65 @@ +Feature: Query Planning > Single Service + +# I don't think we need to move this test -- looks way too simple, maybe an +# early-written test? +# Scenario: executes a query plan over concrete types + +# this test looks a bit deceiving -- this is the correct query plan, but when +# executed, __typename should be returned +Scenario: does not remove __typename on root types + Given query + """ + query GetUser { + __typename + } + """ + Then query plan + """ + {"kind":"QueryPlan"} + """ + +Scenario: does not remove __typename if that is all that is requested on an entity + Given query + """ + query GetUser { + me { + __typename + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{__typename}}" + } + } + """ + +Scenario: does not remove __typename if that is all that is requested on a value type + Given query + """ + query GetUser { + me { + account { + __typename + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{account{__typename}}}" + } + } + """ From 1666b7129183d6fbfa4aa8698e18b6ebc6675110 Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 18 Aug 2020 11:47:42 -0400 Subject: [PATCH 15/19] update plan runner to execute all tests --- .../src/__tests__/queryPlanCucumber.test.ts | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts b/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts index 98c5f61a015..aba258a91b4 100644 --- a/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts +++ b/packages/apollo-gateway/src/__tests__/queryPlanCucumber.test.ts @@ -1,28 +1,29 @@ import gql from 'graphql-tag'; import { GraphQLSchemaValidationError } from 'apollo-graphql'; import { defineFeature, loadFeature } from 'jest-cucumber'; -import { DocumentNode, GraphQLSchema, GraphQLError, Kind } from 'graphql'; +import { DocumentNode, Kind } from 'graphql'; import { QueryPlan } from '../..'; import { buildQueryPlan, buildOperationContext, BuildQueryPlanOptions } from '../buildQueryPlan'; import { getFederatedTestingSchema } from './execution-utils'; const testDir = './packages/apollo-gateway/src/__tests__/'; -const buildQueryPlanFeature = loadFeature( - testDir + 'build-query-plan.feature' -); -const fragmentsFeature = loadFeature(testDir + 'integration/fragments.feature'); -const requiresFeature = loadFeature(testDir + 'integration/requires.feature'); -const variablesFeature = loadFeature(testDir + 'integration/variables.feature'); -const mutationsFeature = loadFeature(testDir + 'integration/mutations.feature'); const features = [ - // buildQueryPlanFeature, - // fragmentsFeature, - // requiresFeature, - // variablesFeature, - mutationsFeature -]; + testDir + 'build-query-plan.feature', + testDir + 'integration/fragments.feature', + testDir + 'integration/requires.feature', + testDir + 'integration/variables.feature', + testDir + 'integration/mutations.feature', + testDir + 'integration/boolean.feature', + testDir + 'integration/provides.feature', + testDir + 'integration/value-types.feature', + testDir + 'integration/abstract-types.feature', + testDir + 'integration/aliases.feature', + testDir + 'integration/custom-directives.feature', + testDir + 'integration/execution-style.feature', + testDir + 'integration/single-service.feature', +].map(path => loadFeature(path)); features.forEach((feature) => { defineFeature(feature, (test) => { From eb70cb01556d2c3a05f349a70c0f68c67375ad48 Mon Sep 17 00:00:00 2001 From: Jake Dawkins Date: Tue, 8 Sep 2020 16:19:43 -0400 Subject: [PATCH 16/19] remove `fit` from test suite --- packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts b/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts index e98ba300f13..8e32e2c7581 100644 --- a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts +++ b/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts @@ -63,7 +63,7 @@ describe('buildQueryPlan', () => { `); }); - fit(`should use a single fetch when requesting a root field from one service`, () => { + it(`should use a single fetch when requesting a root field from one service`, () => { const query = gql` query { me { From 6510ba78b54876d82e80be4906f112f36111b0cc Mon Sep 17 00:00:00 2001 From: Jake Dawkins Date: Thu, 10 Sep 2020 09:47:49 -0400 Subject: [PATCH 17/19] update buildQueryPlan - revert changes added for debugging - clean up my old comments trying to make sense of the query planner --- packages/apollo-gateway/src/buildQueryPlan.ts | 42 +++++-------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/packages/apollo-gateway/src/buildQueryPlan.ts b/packages/apollo-gateway/src/buildQueryPlan.ts index ee688f2891c..50103bf0bba 100644 --- a/packages/apollo-gateway/src/buildQueryPlan.ts +++ b/packages/apollo-gateway/src/buildQueryPlan.ts @@ -84,12 +84,7 @@ export function buildQueryPlan( const isMutation = context.operation.operation === 'mutation'; - /** - * TODO: what does collectFields do? - * -- looks like it just gathers fields at a certain level of a query - * pass the root selection set and scope based off root type - * => `fields` is the root level fieldNodes - */ + // root field nodes const fields = collectFields( context, context.newScope(rootType), @@ -99,11 +94,8 @@ export function buildQueryPlan( // Mutations are a bit more specific in how FetchGroups can be built, as some // calls to the same service may need to be executed serially. - /** - * TODO: what does splitRootFields do? - * -- looks like it creates fetch groups based off service for the top level fields - * - */ + // `groups` here is a list of fetch groups, split up by service, either + // serially or in parallel const groups = isMutation ? splitRootFieldsSerially(context, fields) : splitRootFields(context, fields); @@ -414,7 +406,7 @@ function splitSubfields( let baseService, owningService; const parentTypeFederationMetadata = getFederationMetadata(parentType); - if (parentTypeFederationMetadata!.isValueType) { + if (parentTypeFederationMetadata?.isValueType) { baseService = parentGroup.serviceName; owningService = parentGroup.serviceName; } else { @@ -523,10 +515,6 @@ function splitSubfields( }); } -/** - * wtf does this function do?! - * split into what - */ function splitFields( context: QueryPlanningContext, path: ResponsePath, @@ -537,23 +525,18 @@ function splitFields( // service togethere, whereas in mutations, we have to look at the last group // that was constructed. If it was of the same service as the field in question, // we can use that group but otherwise we need to create a new group. - getGroupForField: (field: Field) => FetchGroup, + groupForField: (field: Field) => FetchGroup, ) { // group by the name the client will see -- alias or name, whichever is there // Each iteration of the loop contains a set of fields with the same response name // [FieldDef, FieldDef] for (const fieldsForResponseName of groupByResponseName(fields).values()) { - // TODO: How can fields in a single selection have different parent types?? - // is `fields` used across multiple selectionSets? for (const [parentType, fieldsForParentType] of groupByParentType(fieldsForResponseName)) { // Field nodes that share the same response name and parent type are guaranteed // to have the same field name and arguments. We only need the other nodes when // merging selection sets, to take node-specific subfields and directives // into account. - // TODO: how is the above statement true, when aliases could be anything? - // Is this only after deduping? - const field = fieldsForParentType[0]; const { scope, fieldDef } = field; @@ -578,9 +561,8 @@ function splitFields( if (isObjectType(parentType) && scope.possibleTypes.includes(parentType)) { // If parent type is an object type, we can directly look for the right // group. - const group = getGroupForField(field as Field); + const group = groupForField(field as Field); group.fields.push( - // TODO: what is completeField? completeField( context, scope as Scope, @@ -612,7 +594,7 @@ function splitFields( // With no extending field definitions, we can engage the optimization if (hasNoExtendingFieldDefs) { - const group = getGroupForField(field as Field); + const group = groupForField(field as Field); group.fields.push( completeField(context, scope, group, path, fieldsForResponseName) ); @@ -632,7 +614,7 @@ function splitFields( field.fieldNode, ); groupsByRuntimeParentTypes.add( - getGroupForField({ + groupForField({ scope: context.newScope(runtimeParentType, scope), fieldNode: field.fieldNode, fieldDef, @@ -683,7 +665,6 @@ function completeField( path: ResponsePath, fields: FieldSet, ): Field { - // TODO: why even pass in a FieldSet? const { fieldNode, fieldDef } = fields[0]; const returnType = getNamedType(fieldDef.type); @@ -695,7 +676,6 @@ function completeField( } else { // For composite types, we need to recurse. - debugger; // if the fieldtype is a listtype, it also appends @ const fieldPath = addPath(path, getResponseName(fieldNode), fieldDef.type); @@ -1112,7 +1092,7 @@ export class QueryPlanningContext { } getBaseService(parentType: GraphQLObjectType): string | null { - return (getFederationMetadata(parentType)!.serviceName) || null; + return (getFederationMetadata(parentType)?.serviceName) || null; } getOwningService( @@ -1122,7 +1102,7 @@ export class QueryPlanningContext { const fieldFederationMetadata = getFederationMetadata(fieldDef); if ( fieldFederationMetadata?.serviceName && - !fieldFederationMetadata.belongsToValueType + !fieldFederationMetadata?.belongsToValueType ) { return fieldFederationMetadata.serviceName; } else { @@ -1151,7 +1131,7 @@ export class QueryPlanningContext { }); for (const possibleType of this.getPossibleTypes(parentType)) { - const keys = getFederationMetadata(possibleType)!.keys?.[serviceName]; + const keys = getFederationMetadata(possibleType)?.keys?.[serviceName]; if (!(keys && keys.length > 0)) continue; From 0df52d998d2079a5349c0ae99886b6f4aca7fd11 Mon Sep 17 00:00:00 2001 From: Jake Dawkins Date: Thu, 10 Sep 2020 09:54:26 -0400 Subject: [PATCH 18/19] clean up stubbed tests --- .../src/__tests__/integration/aliases.feature | 10 ---------- .../__tests__/integration/custom-directives.feature | 10 ---------- .../src/__tests__/integration/requires.feature | 11 ----------- .../src/__tests__/integration/variables.feature | 9 --------- 4 files changed, 40 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/integration/aliases.feature b/packages/apollo-gateway/src/__tests__/integration/aliases.feature index 53930d31737..be2ed66a66e 100644 --- a/packages/apollo-gateway/src/__tests__/integration/aliases.feature +++ b/packages/apollo-gateway/src/__tests__/integration/aliases.feature @@ -302,13 +302,3 @@ Scenario: supports aliases of nested fields on subservices } } """ - -# Scenario: -# Given query: -# """ - -# """ -# Then query plan: -# """ -# {} -# """ diff --git a/packages/apollo-gateway/src/__tests__/integration/custom-directives.feature b/packages/apollo-gateway/src/__tests__/integration/custom-directives.feature index a61bcd2c446..864ca4b4f53 100644 --- a/packages/apollo-gateway/src/__tests__/integration/custom-directives.feature +++ b/packages/apollo-gateway/src/__tests__/integration/custom-directives.feature @@ -71,13 +71,3 @@ Scenario: successfully passes directives and their variables along in requests t } } """ - -# Scenario: -# Given query -# """ - -# """ -# Then query plan -# """ -# {} -# """ diff --git a/packages/apollo-gateway/src/__tests__/integration/requires.feature b/packages/apollo-gateway/src/__tests__/integration/requires.feature index 982544b4373..823746481d2 100644 --- a/packages/apollo-gateway/src/__tests__/integration/requires.feature +++ b/packages/apollo-gateway/src/__tests__/integration/requires.feature @@ -122,14 +122,3 @@ Scenario: supports passing additional fields defined by a requires # """ # {} # """ - -# I don't think we need to port this one. There aren't any query plan tests here -# Scenario: passes null values correctly -# Given query -# """ - -# """ -# Then query plan -# """ - # {} -# """ diff --git a/packages/apollo-gateway/src/__tests__/integration/variables.feature b/packages/apollo-gateway/src/__tests__/integration/variables.feature index 1c7d235ec58..2e5b649a660 100644 --- a/packages/apollo-gateway/src/__tests__/integration/variables.feature +++ b/packages/apollo-gateway/src/__tests__/integration/variables.feature @@ -242,13 +242,4 @@ Scenario: passes variables to nested services # } # """ -# Scenario: -# Given query -# """ - -# """ -# Then query plan -# """ -# {} -# """ From 4984b03b6a931515684217c648bd68ce0f3e9a26 Mon Sep 17 00:00:00 2001 From: Jake Dawkins Date: Thu, 10 Sep 2020 10:18:09 -0400 Subject: [PATCH 19/19] update the readme for cucumber tests --- packages/apollo-gateway/src/__tests__/CucumberREADME.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/CucumberREADME.md b/packages/apollo-gateway/src/__tests__/CucumberREADME.md index 25d4dc90438..b7a26146ad5 100644 --- a/packages/apollo-gateway/src/__tests__/CucumberREADME.md +++ b/packages/apollo-gateway/src/__tests__/CucumberREADME.md @@ -2,9 +2,9 @@ ## Introduction -There are two files used to test the query plan builder: +There two kinds of files used to test the query plan builder: -1. [build-query-plan.feature](./build-query-plan.feature): Programming-language agnostic files written in a format called [Gherkin](https://cucumber.io/docs/gherkin/reference/) for [Cucumber](https://cucumber.io/). +1. `.feature` files written in a programming-language agnostic format called [Gherkin](https://cucumber.io/docs/gherkin/reference/) for [Cucumber](https://cucumber.io/). 2. [queryPlanCucumber.test.ts](./queryPlanCucumber.test.ts): The implementation which provides coverage for the Gherkin-specified behavior. > If you're not familiar with Cucumber or BDD, check out [this video](https://youtu.be/lC0jzd8sGIA) for a great introduction to the concepts involved. Cucumber has test runners in multiple languages, allowing a test spec to be written in plain English and then individual implementations of the test suite can describe how they would like tests to be run for their specific implementation. For Java, Kotlin, Ruby, and JavaScript, Cucumber even has a [10-minute tutorial](https://cucumber.io/docs/guides/10-minute-tutorial/) to help get started. @@ -52,6 +52,9 @@ Scenario: should not confuse union types with overlapping field names """ ``` +Currently, this is the format of all of the tests contained in this test suite, +but future tests will add more complicated behavior. + There can be multiple of any kind of step using the `And` keyword. In the following example, there are 2 `Given` steps. One represented by the `Given` keyword itself, and another represented with the `And` keyword. ```