diff --git a/src/execution/__tests__/abstract-test.ts b/src/execution/__tests__/abstract-test.ts index 5253d0d9e0..8a6c95e043 100644 --- a/src/execution/__tests__/abstract-test.ts +++ b/src/execution/__tests__/abstract-test.ts @@ -271,7 +271,7 @@ describe('Execute: Handles execution of abstract types', () => { errors: [ { message: - 'Abstract type "Pet" must resolve to an Object type at runtime for field "Query.pet". Either the "Pet" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.', + 'Abstract type "Pet" must resolve to an Object type or an intermediate Interface type at runtime for field "Query.pet". Either the "Pet" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.', locations: [{ line: 3, column: 9 }], path: ['pet'], }, @@ -610,7 +610,7 @@ describe('Execute: Handles execution of abstract types', () => { } expectError({ forTypeName: undefined }).toEqual( - 'Abstract type "Pet" must resolve to an Object type at runtime for field "Query.pet". Either the "Pet" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.', + 'Abstract type "Pet" must resolve to an Object type or an intermediate Interface type at runtime for field "Query.pet". Either the "Pet" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.', ); expectError({ forTypeName: 'Human' }).toEqual( @@ -618,7 +618,7 @@ describe('Execute: Handles execution of abstract types', () => { ); expectError({ forTypeName: 'String' }).toEqual( - 'Abstract type "Pet" was resolved to a non-object type "String".', + 'Abstract type "Pet" was resolved to a non-object and non-interface type "String".', ); expectError({ forTypeName: '__Schema' }).toEqual( @@ -629,7 +629,7 @@ describe('Execute: Handles execution of abstract types', () => { // @ts-expect-error assertInterfaceType(schema.getType('Pet')).resolveType = () => []; expectError({ forTypeName: undefined }).toEqual( - 'Abstract type "Pet" must resolve to an Object type at runtime for field "Query.pet" with value { __typename: undefined }, received "[]".', + 'Abstract type "Pet" must resolve to an Object type or an intermediate Interface type at runtime for field "Query.pet" with value { __typename: undefined }, received "[]".', ); // FIXME: workaround since we can't inject resolveType into SDL @@ -640,4 +640,119 @@ describe('Execute: Handles execution of abstract types', () => { 'Support for returning GraphQLObjectType from resolveType was removed in graphql-js@16.0.0 please return type name instead.', ); }); + + it('hierarchical resolveType with Interfaces yields useful error', () => { + const schema = buildSchema(` + type Query { + named: Named + } + + interface Named { + name: String + } + + interface Animal { + isFriendly: Boolean + } + + interface Pet implements Named & Animal { + name: String + isFriendly: Boolean + } + + type Cat implements Pet & Named & Animal { + name: String + isFriendly: Boolean + } + + type Dog implements Pet & Named & Animal { + name: String + isFriendly: Boolean + } + + type Person implements Named { + name: String + } + `); + + const document = parse(` + { + named { + name + } + } + `); + + function expectError() { + const rootValue = { named: {} }; + const result = executeSync({ schema, document, rootValue }); + return { + toEqual(message: string) { + expectJSON(result).toDeepEqual({ + data: { named: null }, + errors: [ + { + message, + locations: [{ line: 3, column: 9 }], + path: ['named'], + }, + ], + }); + }, + }; + } + + const namedType = assertInterfaceType(schema.getType('Named')); + // FIXME: workaround since we can't inject resolveType into SDL + namedType.resolveType = () => 'Animal'; + expectError().toEqual( + 'Interface type "Animal" is not a possible type for "Named".', + ); + + const petType = assertInterfaceType(schema.getType('Pet')); + // FIXME: workaround since we can't inject resolveType into SDL + namedType.resolveType = () => 'Pet'; + petType.resolveType = () => 'Person'; + expectError().toEqual( + 'Runtime Object type "Person" is not a possible type for "Pet".', + ); + + // FIXME: workaround since we can't inject resolveType into SDL + namedType.resolveType = () => 'Pet'; + petType.resolveType = () => undefined; + expectError().toEqual( + 'Abstract type "Pet" must resolve to an Object type or an intermediate Interface type at runtime for field "Query.named". Either the "Pet" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.', + ); + + // FIXME: workaround since we can't inject resolveType into SDL + petType.resolveType = () => 'Human'; + expectError().toEqual( + 'Abstract type "Pet" was resolved to a type "Human" that does not exist inside the schema.', + ); + + // FIXME: workaround since we can't inject resolveType into SDL + petType.resolveType = () => 'String'; + expectError().toEqual( + 'Abstract type "Pet" was resolved to a non-object and non-interface type "String".', + ); + + // FIXME: workaround since we can't inject resolveType into SDL + petType.resolveType = () => '__Schema'; + expectError().toEqual( + 'Runtime Object type "__Schema" is not a possible type for "Pet".', + ); + + // FIXME: workaround since we can't inject resolveType into SDL + // @ts-expect-error + petType.resolveType = () => []; + expectError().toEqual( + 'Abstract type "Pet" must resolve to an Object type or an intermediate Interface type at runtime for field "Query.named" with value {}, received "[]".', + ); + + // FIXME: workaround since we can't inject resolveType into SDL + petType.resolveType = () => 'Pet'; + expectError().toEqual( + 'Interface type "Pet" is not a possible type for "Pet".', + ); + }); }); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 6dc4246178..8f6c3ae12c 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -39,6 +39,7 @@ import type { } from '../type/definition'; import { isAbstractType, + isInterfaceType, isLeafType, isListType, isNonNullType, @@ -790,46 +791,25 @@ function completeLeafValue( */ function completeAbstractValue( exeContext: ExecutionContext, - returnType: GraphQLAbstractType, + abstractType: GraphQLAbstractType, fieldNodes: ReadonlyArray, info: GraphQLResolveInfo, path: Path, result: unknown, ): PromiseOrValue> { - const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; + const resolveTypeFn = abstractType.resolveType ?? exeContext.typeResolver; const contextValue = exeContext.contextValue; - const runtimeType = resolveTypeFn(result, contextValue, info, returnType); - - if (isPromise(runtimeType)) { - return runtimeType.then((resolvedRuntimeType) => - completeObjectValue( - exeContext, - ensureValidRuntimeType( - resolvedRuntimeType, - exeContext, - returnType, - fieldNodes, - info, - result, - ), - fieldNodes, - info, - path, - result, - ), - ); - } + const possibleRuntimeTypeName = resolveTypeFn( + result, + contextValue, + info, + abstractType, + ); - return completeObjectValue( + return completeAbstractValueImpl( exeContext, - ensureValidRuntimeType( - runtimeType, - exeContext, - returnType, - fieldNodes, - info, - result, - ), + abstractType, + possibleRuntimeTypeName, fieldNodes, info, path, @@ -837,59 +817,102 @@ function completeAbstractValue( ); } -function ensureValidRuntimeType( - runtimeTypeName: unknown, +function completeAbstractValueImpl( exeContext: ExecutionContext, - returnType: GraphQLAbstractType, + abstractType: GraphQLAbstractType, + possibleRuntimeTypeName: PromiseOrValue, fieldNodes: ReadonlyArray, info: GraphQLResolveInfo, + path: Path, result: unknown, -): GraphQLObjectType { - if (runtimeTypeName == null) { +): PromiseOrValue> { + if (isPromise(possibleRuntimeTypeName)) { + return possibleRuntimeTypeName.then((resolved) => + completeAbstractValueImpl( + exeContext, + abstractType, + resolved, + fieldNodes, + info, + path, + result, + ), + ); + } + + if (possibleRuntimeTypeName == null) { throw new GraphQLError( - `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, + `Abstract type "${abstractType.name}" must resolve to an Object type or an intermediate Interface type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${abstractType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, { nodes: fieldNodes }, ); } // releases before 16.0.0 supported returning `GraphQLObjectType` from `resolveType` // TODO: remove in 17.0.0 release - if (isObjectType(runtimeTypeName)) { + if (isObjectType(possibleRuntimeTypeName)) { throw new GraphQLError( 'Support for returning GraphQLObjectType from resolveType was removed in graphql-js@16.0.0 please return type name instead.', ); } - if (typeof runtimeTypeName !== 'string') { + if (typeof possibleRuntimeTypeName !== 'string') { throw new GraphQLError( - `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` + - `value ${inspect(result)}, received "${inspect(runtimeTypeName)}".`, + `Abstract type "${abstractType.name}" must resolve to an Object type or an intermediate Interface type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` + + `value ${inspect(result)}, received "${inspect( + possibleRuntimeTypeName, + )}".`, ); } - const runtimeType = exeContext.schema.getType(runtimeTypeName); - if (runtimeType == null) { + const possibleRuntimeType = exeContext.schema.getType( + possibleRuntimeTypeName, + ); + if (possibleRuntimeType == null) { throw new GraphQLError( - `Abstract type "${returnType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, + `Abstract type "${abstractType.name}" was resolved to a type "${possibleRuntimeTypeName}" that does not exist inside the schema.`, { nodes: fieldNodes }, ); } - if (!isObjectType(runtimeType)) { + if (isInterfaceType(possibleRuntimeType)) { + if (!exeContext.schema.isSubType(abstractType, possibleRuntimeType)) { + throw new GraphQLError( + `Interface type "${possibleRuntimeType.name}" is not a possible type for "${abstractType.name}".`, + { nodes: fieldNodes }, + ); + } + + return completeAbstractValue( + exeContext, + possibleRuntimeType, + fieldNodes, + info, + path, + result, + ); + } + + if (!isObjectType(possibleRuntimeType)) { throw new GraphQLError( - `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, + `Abstract type "${abstractType.name}" was resolved to a non-object and non-interface type "${possibleRuntimeTypeName}".`, { nodes: fieldNodes }, ); } - if (!exeContext.schema.isSubType(returnType, runtimeType)) { + if (!exeContext.schema.isSubType(abstractType, possibleRuntimeType)) { throw new GraphQLError( - `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, + `Runtime Object type "${possibleRuntimeType.name}" is not a possible type for "${abstractType.name}".`, { nodes: fieldNodes }, ); } - - return runtimeType; + return completeObjectValue( + exeContext, + possibleRuntimeType, + fieldNodes, + info, + path, + result, + ); } /**