diff --git a/.c8rc.json b/.c8rc.json index 22bb450ee4..fdd5bb2de9 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -8,7 +8,8 @@ "src/jsutils/ObjMap.ts", "src/jsutils/PromiseOrValue.ts", "src/utilities/assertValidName.ts", - "src/utilities/typedQueryDocumentNode.ts" + "src/utilities/typedQueryDocumentNode.ts", + "src/**/__tests__/**/*.ts" ], "clean": true, "temp-directory": "coverage", diff --git a/src/execution/__tests__/union-interface-test.ts b/src/execution/__tests__/union-interface-test.ts index 7ce9f8b3bc..7089f2ba39 100644 --- a/src/execution/__tests__/union-interface-test.ts +++ b/src/execution/__tests__/union-interface-test.ts @@ -12,7 +12,7 @@ import { import { GraphQLBoolean, GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; -import { executeSync } from '../execute'; +import { execute, executeSync } from '../execute'; class Dog { name: string; @@ -118,7 +118,6 @@ const PetType = new GraphQLUnionType({ if (value instanceof Cat) { return CatType.name; } - /* c8 ignore next 3 */ // Not reachable, all possible types have been considered. expect.fail('Not reachable'); }, @@ -154,6 +153,71 @@ odie.mother.progeny = [odie]; const liz = new Person('Liz'); const john = new Person('John', [garfield, odie], [liz, odie]); +const SearchableInterface = new GraphQLInterfaceType({ + name: 'Searchable', + fields: { + id: { type: GraphQLString }, + }, +}); + +const TypeA = new GraphQLObjectType({ + name: 'TypeA', + interfaces: [SearchableInterface], + fields: () => ({ + id: { type: GraphQLString }, + nameA: { type: GraphQLString }, + }), + isTypeOf: (_value, _context, _info) => + new Promise((_resolve, reject) => + // eslint-disable-next-line + setTimeout(() => reject(new Error('TypeA_isTypeOf_rejected')), 10), + ), +}); + +const TypeB = new GraphQLObjectType({ + name: 'TypeB', + interfaces: [SearchableInterface], + fields: () => ({ + id: { type: GraphQLString }, + nameB: { type: GraphQLString }, + }), + isTypeOf: (value: any, _context, _info) => value.id === 'b', +}); + +const queryTypeWithSearchable = new GraphQLObjectType({ + name: 'Query', + fields: { + person: { + type: PersonType, + resolve: () => john, + }, + search: { + type: SearchableInterface, + args: { id: { type: GraphQLString } }, + resolve: (_source, { id }) => { + if (id === 'a') { + return { id: 'a', nameA: 'Object A' }; + } else if (id === 'b') { + return { id: 'b', nameB: 'Object B' }; + } + }, + }, + }, +}); + +const schemaWithSearchable = new GraphQLSchema({ + query: queryTypeWithSearchable, + types: [ + PetType, + TypeA, + TypeB, + SearchableInterface, + PersonType, + DogType, + CatType, + ], +}); + describe('Execute: Union and intersection types', () => { it('can introspect on union and intersection types', () => { const document = parse(` @@ -545,4 +609,51 @@ describe('Execute: Union and intersection types', () => { expect(encounteredRootValue).to.equal(rootValue); expect(encounteredContext).to.equal(contextValue); }); + + it('handles promises from isTypeOf correctly when a later type matches synchronously', async () => { + const document = parse(` + query TestSearch { + search(id: "b") { + __typename + id + ... on TypeA { + nameA + } + ... on TypeB { + nameB + } + } + } + `); + + let unhandledRejection: any = null; + const unhandledRejectionListener = (reason: any) => { + unhandledRejection = reason; + }; + // eslint-disable-next-line + process.on('unhandledRejection', unhandledRejectionListener); + + const result = await execute({ + schema: schemaWithSearchable, + document, + }); + + expect(result.errors).to.equal(undefined); + expect(result.data).to.deep.equal({ + search: { + __typename: 'TypeB', + id: 'b', + nameB: 'Object B', + }, + }); + + // Give the TypeA promise a chance to reject and the listener to fire + // eslint-disable-next-line + await new Promise((resolve) => setTimeout(resolve, 20)); + + // eslint-disable-next-line + process.removeListener('unhandledRejection', unhandledRejectionListener); + + expect(unhandledRejection).to.equal(null); + }); }); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 5cd64d40f9..3021354e28 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1002,6 +1002,14 @@ export const defaultTypeResolver: GraphQLTypeResolver = if (isPromise(isTypeOfResult)) { promisedIsTypeOfResults[i] = isTypeOfResult; } else if (isTypeOfResult) { + if (promisedIsTypeOfResults.length) { + // Explicitly ignore any promise rejections + Promise.allSettled(promisedIsTypeOfResults) + /* c8 ignore next 3 */ + .catch(() => { + // Do nothing + }); + } return type.name; } }