diff --git a/packages/apollo-federation-integration-testsuite/src/fixtures/product.ts b/packages/apollo-federation-integration-testsuite/src/fixtures/product.ts index 344222eb46f..ec953dc663e 100644 --- a/packages/apollo-federation-integration-testsuite/src/fixtures/product.ts +++ b/packages/apollo-federation-integration-testsuite/src/fixtures/product.ts @@ -9,6 +9,7 @@ export const typeDefs = gql` extend type Query { product(upc: String!): Product + products(upcs: [String!]!): [Product] vehicle(id: String!): Vehicle topProducts(first: Int = 5): [Product] topCars(first: Int = 5): [Car] @@ -151,7 +152,8 @@ const products = [ { __typename: 'Book', isbn: '0201633612', price: 49 }, { __typename: 'Book', isbn: '1234567890', price: 59 }, { __typename: 'Book', isbn: '404404404', price: 0 }, - { __typename: 'Book', isbn: '0987654321', price: 29 }, + { __typename: 'Book', isbn: '0987654321', price: 29, upc: '0987654321' }, + { __typename: 'Book', isbn: '9999999999', price: 31, upc: '9999999999' }, ]; const vehicles = [ @@ -232,6 +234,10 @@ export const resolvers: GraphQLResolverMap = { product(_, args) { return products.find(product => product.upc === args.upc); }, + products(_, args) { + const upcs: Array = args.upcs + return upcs.map(upc => products.find(product => product.upc === upc)); + }, vehicle(_, args) { return vehicles.find(vehicles => vehicles.id === args.id); }, diff --git a/packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts b/packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts index 82573cadbf4..bea4721a22e 100644 --- a/packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts +++ b/packages/apollo-federation/src/service/__tests__/printComposedSdl.test.ts @@ -186,6 +186,7 @@ describe('printComposedSdl', () => { library(id: ID!): Library @resolve(graph: \\"books\\") body: Body! @resolve(graph: \\"documents\\") product(upc: String!): Product @resolve(graph: \\"product\\") + products(upcs: [String!]!): [Product] @resolve(graph: \\"product\\") vehicle(id: String!): Vehicle @resolve(graph: \\"product\\") topProducts(first: Int = 5): [Product] @resolve(graph: \\"product\\") topCars(first: Int = 5): [Car] @resolve(graph: \\"product\\") diff --git a/packages/apollo-federation/src/service/__tests__/printFederatedSchema.test.ts b/packages/apollo-federation/src/service/__tests__/printFederatedSchema.test.ts index 4495899835c..4182652230b 100644 --- a/packages/apollo-federation/src/service/__tests__/printFederatedSchema.test.ts +++ b/packages/apollo-federation/src/service/__tests__/printFederatedSchema.test.ts @@ -136,6 +136,7 @@ describe('printFederatedSchema', () => { library(id: ID!): Library body: Body! product(upc: String!): Product + products(upcs: [String!]!): [Product] vehicle(id: String!): Vehicle topProducts(first: Int = 5): [Product] topCars(first: Int = 5): [Car] diff --git a/packages/apollo-gateway/CHANGELOG.md b/packages/apollo-gateway/CHANGELOG.md index 09ffc14cf34..ae5329defe4 100644 --- a/packages/apollo-gateway/CHANGELOG.md +++ b/packages/apollo-gateway/CHANGELOG.md @@ -4,7 +4,7 @@ > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. -- _Nothing yet! Stay tuned!_ +- __FIX__: Continue resolving when an `@external` reference cannot be resolved. [#3914](https://github.com/apollographql/apollo-server/pull/3914) ## v0.19.0 diff --git a/packages/apollo-gateway/src/__tests__/executeQueryPlan.test.ts b/packages/apollo-gateway/src/__tests__/executeQueryPlan.test.ts index f51899a969d..b22a6f110f2 100644 --- a/packages/apollo-gateway/src/__tests__/executeQueryPlan.test.ts +++ b/packages/apollo-gateway/src/__tests__/executeQueryPlan.test.ts @@ -641,4 +641,50 @@ describe('executeQueryPlan', () => { } `); }); + + it('can execute queries with selections on unresolved @requires fields', async () => { + // query a book not known to the book service + const query = gql` + query { + products(upcs: ["9999999999", "0987654321"]) { + upc + name + price + } + } + `; + + const operationContext = buildOperationContext(schema, query); + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response.errors?.map(e => e.message)).toEqual(expect.arrayContaining([ + "Cannot return null for non-nullable field Book.isbn.", + "Field \"title\" was not found in response.", + "Field \"year\" was not found in response.", + ])); + + expect(response.data).toMatchInlineSnapshot(` + Object { + "products": Array [ + Object { + "name": "undefined (undefined)", + "price": "31", + "upc": "9999999999", + }, + Object { + "name": "No Books Like This Book! (2019)", + "price": "29", + "upc": "0987654321", + }, + ], + } + `); + }); }); diff --git a/packages/apollo-gateway/src/executeQueryPlan.ts b/packages/apollo-gateway/src/executeQueryPlan.ts index 5c477c0b353..0d567275fa6 100644 --- a/packages/apollo-gateway/src/executeQueryPlan.ts +++ b/packages/apollo-gateway/src/executeQueryPlan.ts @@ -235,7 +235,7 @@ async function executeFetch( const representationToEntity: number[] = []; entities.forEach((entity, index) => { - const representation = executeSelectionSet(entity, requires); + const representation = executeSelectionSet(entity, requires, context); if (representation && representation[TypeNameMetaFieldDef.name]) { representations.push(representation); representationToEntity.push(index); @@ -386,9 +386,10 @@ async function executeFetch( * @param source Result of GraphQL execution. * @param selectionSet */ -function executeSelectionSet( +function executeSelectionSet( source: Record | null, selectionSet: SelectionSetNode, + context: ExecutionContext, ): Record | null { // If the underlying service has returned null for the parent (source) @@ -406,16 +407,19 @@ function executeSelectionSet( const selectionSet = selection.selectionSet; if (typeof source[responseName] === 'undefined') { - throw new Error(`Field "${responseName}" was not found in response.`); + // collect error but continue to resolve + context.errors.push(new GraphQLError(`Field "${responseName}" was not found in response.`)); + break; } if (Array.isArray(source[responseName])) { result[responseName] = source[responseName].map((value: any) => - selectionSet ? executeSelectionSet(value, selectionSet) : value, + selectionSet ? executeSelectionSet(value, selectionSet, context) : value, ); } else if (selectionSet) { result[responseName] = executeSelectionSet( source[responseName], selectionSet, + context ); } else { result[responseName] = source[responseName]; @@ -430,7 +434,7 @@ function executeSelectionSet( if (typename === selection.typeCondition.name.value) { deepMerge( result, - executeSelectionSet(source, selection.selectionSet), + executeSelectionSet(source, selection.selectionSet, context), ); } break;