From 76139833a16567887ca153fd96e9a24f7807d3da Mon Sep 17 00:00:00 2001 From: vitramir Date: Fri, 6 Sep 2019 17:20:53 +0300 Subject: [PATCH 1/3] Transform interfaces into possible types only --- packages/apollo-gateway/src/FieldSet.ts | 1 + .../src/__tests__/buildQueryPlan.test.ts | 52 +++++++++++++++++++ packages/apollo-gateway/src/buildQueryPlan.ts | 35 ++++++++++++- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/packages/apollo-gateway/src/FieldSet.ts b/packages/apollo-gateway/src/FieldSet.ts index f1476307150..98d0fe376e1 100644 --- a/packages/apollo-gateway/src/FieldSet.ts +++ b/packages/apollo-gateway/src/FieldSet.ts @@ -14,6 +14,7 @@ export interface Field { parentType: TParent; fieldNode: FieldNode; fieldDef: GraphQLField; + parentTypesArray?: TParent[]; } export type FieldSet = Field[]; diff --git a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts b/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts index 1b3832069b0..77b7e98d75e 100644 --- a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts +++ b/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts @@ -790,4 +790,56 @@ describe('buildQueryPlan', () => { } `); }); + + it(`interface fragments should expand into possible types only`, () => { + const query = gql` + query { + books { + ... on Product { + name + ... on Furniture { + upc + } + } + } + } + `; + + const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); + console.log(queryPlan); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "books") { + { + books { + __typename + isbn + title + year + } + } + }, + Flatten(path: "books.@") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + title + year + } + } => + { + ... on Book { + name + } + } + }, + }, + }, + } + `); + }); }); diff --git a/packages/apollo-gateway/src/buildQueryPlan.ts b/packages/apollo-gateway/src/buildQueryPlan.ts index ba5c243705c..7b5ae19ad52 100644 --- a/packages/apollo-gateway/src/buildQueryPlan.ts +++ b/packages/apollo-gateway/src/buildQueryPlan.ts @@ -402,6 +402,17 @@ function splitFields( if (isObjectType(parentType)) { // If parent type is an object type, we can directly look for the right // group. + + // ParentTypes collected from fragments. Multiple values mean nested fragments + if (field.parentTypesArray) { + let possibleParentTypes = field.parentTypesArray.map(type => + context.getPossibleTypes(type), + ); + if (!possibleParentTypes.every(arr => arr.includes(parentType))) { + continue; + } + } + const group = groupForField(field as Field); group.fields.push( completeField( @@ -422,7 +433,19 @@ function splitFields( GraphQLObjectType >(); - for (const runtimeParentType of context.getPossibleTypes(parentType)) { + let possibleTypes = context.getPossibleTypes(parentType); + + // ParentTypes collected from fragments. Multiple values mean nested fragments + if (field.parentTypesArray) { + let possibleParentTypes = field.parentTypesArray.map(type => + context.getPossibleTypes(type), + ); + possibleTypes = possibleTypes.filter(type => + possibleParentTypes.every(arr => arr.includes(type)), + ); + } + + for (const runtimeParentType of possibleTypes) { const fieldDef = context.getFieldDef( runtimeParentType, field.fieldNode, @@ -516,12 +539,18 @@ function collectFields( visitedFragmentNames: { [fragmentName: string]: boolean } = Object.create( null, ), + parentTypesArray: GraphQLCompositeType[] = [], ): FieldSet { for (const selection of selectionSet.selections) { switch (selection.kind) { case Kind.FIELD: const fieldDef = context.getFieldDef(parentType, selection); - fields.push({ parentType, fieldNode: selection, fieldDef }); + fields.push({ + parentType, + fieldNode: selection, + fieldDef, + parentTypesArray, + }); break; case Kind.INLINE_FRAGMENT: collectFields( @@ -530,6 +559,7 @@ function collectFields( selection.selectionSet, fields, visitedFragmentNames, + [...parentTypesArray, parentType], ); break; case Kind.FRAGMENT_SPREAD: @@ -551,6 +581,7 @@ function collectFields( fragment.selectionSet, fields, visitedFragmentNames, + [...parentTypesArray, parentType], ); break; } From db99c8385478e4c15e0eb62a04ae5f752ccd43ea Mon Sep 17 00:00:00 2001 From: vitramir Date: Fri, 6 Sep 2019 17:26:56 +0300 Subject: [PATCH 2/3] remove console.log --- packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts b/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts index 77b7e98d75e..8a9e2a264e4 100644 --- a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts +++ b/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts @@ -806,7 +806,6 @@ describe('buildQueryPlan', () => { `; const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); - console.log(queryPlan); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { From 94b8238eda3997e42452021dc7f4e3a3f60fe7e3 Mon Sep 17 00:00:00 2001 From: vitramir Date: Tue, 10 Sep 2019 16:59:20 +0300 Subject: [PATCH 3/3] Fix interface inside interface --- .../__tests__/__fixtures__/schemas/product.ts | 17 +++++++++ .../src/__tests__/buildQueryPlan.test.ts | 36 +++++++++++++++++++ packages/apollo-gateway/src/buildQueryPlan.ts | 16 +++++++-- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/__fixtures__/schemas/product.ts b/packages/apollo-gateway/src/__tests__/__fixtures__/schemas/product.ts index 8373265d589..2103f1592d3 100644 --- a/packages/apollo-gateway/src/__tests__/__fixtures__/schemas/product.ts +++ b/packages/apollo-gateway/src/__tests__/__fixtures__/schemas/product.ts @@ -24,6 +24,21 @@ export const typeDefs = gql` sku: String! name: String price: String + details: ProductDetails + } + + interface ProductDetails { + country: String + } + + type ProductDetailsFurniture implements ProductDetails { + country: String + color: String + } + + type ProductDetailsBook implements ProductDetails { + country: String + pages: Int } type Furniture implements Product @key(fields: "upc") @key(fields: "sku") { @@ -33,6 +48,7 @@ export const typeDefs = gql` price: String brand: Brand metadata: [MetadataOrError] + details: ProductDetailsFurniture } extend type Book implements Product @key(fields: "isbn") { @@ -43,6 +59,7 @@ export const typeDefs = gql` sku: String! name(delimeter: String = " "): String @requires(fields: "title year") price: String + details: ProductDetailsBook } type Car @key(fields: "id") { diff --git a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts b/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts index 8a9e2a264e4..c171621b343 100644 --- a/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts +++ b/packages/apollo-gateway/src/__tests__/buildQueryPlan.test.ts @@ -841,4 +841,40 @@ describe('buildQueryPlan', () => { } `); }); + + it(`interface inside interface should expand into possible types only`, () => { + const query = gql` + query { + product(upc: "") { + details { + country + } + } + } + `; + + const queryPlan = buildQueryPlan(buildOperationContext(schema, query)); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "product") { + { + product(upc: "") { + __typename + ... on Book { + details { + country + } + } + ... on Furniture { + details { + country + } + } + } + } + }, + } + `); + }); }); diff --git a/packages/apollo-gateway/src/buildQueryPlan.ts b/packages/apollo-gateway/src/buildQueryPlan.ts index 7b5ae19ad52..e95f9031282 100644 --- a/packages/apollo-gateway/src/buildQueryPlan.ts +++ b/packages/apollo-gateway/src/buildQueryPlan.ts @@ -19,6 +19,7 @@ import { isListType, isNamedType, isObjectType, + isInterfaceType, Kind, OperationDefinitionNode, SelectionSetNode, @@ -487,7 +488,19 @@ function completeField( fields: FieldSet, ): Field { const { fieldNode, fieldDef } = fields[0]; - const returnType = getNamedType(fieldDef.type); + + let returnType = getNamedType(fieldDef.type); + + if (isObjectType(parentType) || isInterfaceType(parentType)) { + // We should find field type by name, because it may be changed after + // replacing parent type inside splitFields function. + // For union only __typename may be requested and it's type + // will be taken from fieldDef + const realField = parentType.getFields()[fieldDef.name]; + if (realField) { + returnType = getNamedType(realField.type); + } + } if (!isCompositeType(returnType)) { // FIXME: We should look at all field nodes to make sure we take directives @@ -495,7 +508,6 @@ function completeField( return { parentType, fieldNode, fieldDef }; } else { // For composite types, we need to recurse. - const fieldPath = addPath(path, getResponseName(fieldNode), fieldDef.type); const subGroup = new FetchGroup(parentGroup.serviceName);