From d6e760cf7060217bd25fa934bd104ff2400aad96 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Sat, 5 Sep 2020 04:46:20 +0300 Subject: [PATCH] resolveType: add workaround for transformed schemas (#2793) closes #2779 --- src/__tests__/starWarsSchema.js | 11 +- src/execution/__tests__/abstract-test.js | 371 ++++-------------- .../__tests__/union-interface-test.js | 6 +- src/execution/execute.js | 41 +- src/type/definition.js | 3 + 5 files changed, 112 insertions(+), 320 deletions(-) diff --git a/src/__tests__/starWarsSchema.js b/src/__tests__/starWarsSchema.js index b4e5e2c549..e06c2cef71 100644 --- a/src/__tests__/starWarsSchema.js +++ b/src/__tests__/starWarsSchema.js @@ -125,12 +125,11 @@ const characterInterface = new GraphQLInterfaceType({ }, }), resolveType(character) { - if (character.type === 'Human') { - return humanType; - } - // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') - if (character.type === 'Droid') { - return droidType; + switch (character.type) { + case 'Human': + return humanType.name; + case 'Droid': + return droidType.name; } // istanbul ignore next (Not reachable. All possible types have been considered) diff --git a/src/execution/__tests__/abstract-test.js b/src/execution/__tests__/abstract-test.js index a4884fc7be..1d8bbeaab0 100644 --- a/src/execution/__tests__/abstract-test.js +++ b/src/execution/__tests__/abstract-test.js @@ -62,14 +62,6 @@ class Cat { } } -class Human { - name: string; - - constructor(name: string) { - this.name = name; - } -} - describe('Execute: Handles execution of abstract types', () => { it('isTypeOf used to resolve runtime type for Interface', async () => { const PetType = new GraphQLInterfaceType({ @@ -305,309 +297,17 @@ describe('Execute: Handles execution of abstract types', () => { }); }); - it('resolveType on Interface yields useful error', async () => { + it('deprecated(will be removed in v16.0.0): resolveType allows resolving with type object', async () => { const PetType = new GraphQLInterfaceType({ name: 'Pet', resolveType(obj, context) { if (obj instanceof Dog) { return context.async ? Promise.resolve(DogType) : DogType; } - if (obj instanceof Cat) { - return context.async ? Promise.resolve(CatType) : CatType; - } // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') - if (obj instanceof Human) { - return context.async ? Promise.resolve(HumanType) : HumanType; - } - - // istanbul ignore next (Not reachable. All possible types have been considered) - invariant(false); - }, - fields: { - name: { type: GraphQLString }, - }, - }); - - const HumanType = new GraphQLObjectType({ - name: 'Human', - fields: { - name: { type: GraphQLString }, - }, - }); - - const DogType = new GraphQLObjectType({ - name: 'Dog', - interfaces: [PetType], - fields: { - name: { type: GraphQLString }, - woofs: { type: GraphQLBoolean }, - }, - }); - - const CatType = new GraphQLObjectType({ - name: 'Cat', - interfaces: [PetType], - fields: { - name: { type: GraphQLString }, - meows: { type: GraphQLBoolean }, - }, - }); - - const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - pets: { - type: new GraphQLList(PetType), - resolve() { - return [ - new Dog('Odie', true), - new Cat('Garfield', false), - new Human('Jon'), - ]; - }, - }, - }, - }), - types: [CatType, DogType], - }); - - const query = ` - { - pets { - name - ... on Dog { - woofs - } - ... on Cat { - meows - } - } - } - `; - - expect(await executeQuery({ schema, query })).to.deep.equal({ - data: { - pets: [ - { - name: 'Odie', - woofs: true, - }, - { - name: 'Garfield', - meows: false, - }, - null, - ], - }, - errors: [ - { - message: - 'Runtime Object type "Human" is not a possible type for "Pet".', - locations: [{ line: 3, column: 9 }], - path: ['pets', 2], - }, - ], - }); - }); - - it('resolveType on Union yields useful error', async () => { - const HumanType = new GraphQLObjectType({ - name: 'Human', - fields: { - name: { type: GraphQLString }, - }, - }); - - const DogType = new GraphQLObjectType({ - name: 'Dog', - fields: { - name: { type: GraphQLString }, - woofs: { type: GraphQLBoolean }, - }, - }); - - const CatType = new GraphQLObjectType({ - name: 'Cat', - fields: { - name: { type: GraphQLString }, - meows: { type: GraphQLBoolean }, - }, - }); - - const PetType = new GraphQLUnionType({ - name: 'Pet', - resolveType(obj, context) { - if (obj instanceof Dog) { - return context.async ? Promise.resolve(DogType) : DogType; - } if (obj instanceof Cat) { return context.async ? Promise.resolve(CatType) : CatType; } - // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') - if (obj instanceof Human) { - return context.async ? Promise.resolve(HumanType) : HumanType; - } - - // istanbul ignore next (Not reachable. All possible types have been considered) - invariant(false); - }, - types: [DogType, CatType], - }); - - const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - pets: { - type: new GraphQLList(PetType), - resolve() { - return [ - new Dog('Odie', true), - new Cat('Garfield', false), - new Human('Jon'), - ]; - }, - }, - }, - }), - }); - - const query = ` - { - pets { - ... on Dog { - name - woofs - } - ... on Cat { - name - meows - } - } - } - `; - - expect(await executeQuery({ schema, query })).to.deep.equal({ - data: { - pets: [ - { - name: 'Odie', - woofs: true, - }, - { - name: 'Garfield', - meows: false, - }, - null, - ], - }, - errors: [ - { - message: - 'Runtime Object type "Human" is not a possible type for "Pet".', - locations: [{ line: 3, column: 9 }], - path: ['pets', 2], - }, - ], - }); - }); - - it('returning invalid value from resolveType yields useful error', async () => { - const fooInterface = new GraphQLInterfaceType({ - name: 'FooInterface', - fields: { bar: { type: GraphQLString } }, - resolveType(_source, context) { - // $FlowExpectedError[incompatible-call] - return context.async ? Promise.resolve([]) : []; - }, - }); - - const fooObject = new GraphQLObjectType({ - name: 'FooObject', - fields: { bar: { type: GraphQLString } }, - interfaces: [fooInterface], - }); - - const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - foo: { - type: fooInterface, - resolve: () => 'dummy', - }, - }, - }), - types: [fooObject], - }); - - expect( - await executeQuery({ schema, query: '{ foo { bar } }' }), - ).to.deep.equal({ - data: { foo: null }, - errors: [ - { - message: - 'Abstract type "FooInterface" must resolve to an Object type at runtime for field "Query.foo" with value "dummy", received "[]". Either the "FooInterface" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.', - locations: [{ line: 1, column: 3 }], - path: ['foo'], - }, - ], - }); - }); - - it('missing both resolveType and isTypeOf yields useful error', async () => { - const fooInterface = new GraphQLInterfaceType({ - name: 'FooInterface', - fields: { bar: { type: GraphQLString } }, - }); - - const fooObject = new GraphQLObjectType({ - name: 'FooObject', - fields: { bar: { type: GraphQLString } }, - interfaces: [fooInterface], - }); - - const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - foo: { - type: fooInterface, - resolve: () => 'dummy', - }, - }, - }), - types: [fooObject], - }); - - expect( - await executeQuery({ schema, query: '{ foo { bar } }' }), - ).to.deep.equal({ - data: { foo: null }, - errors: [ - { - message: - 'Abstract type "FooInterface" must resolve to an Object type at runtime for field "Query.foo" with value "dummy", received "undefined". Either the "FooInterface" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.', - locations: [{ line: 1, column: 3 }], - path: ['foo'], - }, - ], - }); - }); - - it('resolveType allows resolving with type name', async () => { - const PetType = new GraphQLInterfaceType({ - name: 'Pet', - resolveType(obj, context) { - if (obj instanceof Dog) { - return context.async ? Promise.resolve('Dog') : 'Dog'; - } - // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') - if (obj instanceof Cat) { - return context.async ? Promise.resolve('Cat') : 'Cat'; - } // istanbul ignore next (Not reachable. All possible types have been considered) invariant(false); @@ -890,4 +590,73 @@ describe('Execute: Handles execution of abstract types', () => { }, }); }); + + it('resolveType on Interface yields useful error', () => { + const schema = buildSchema(` + type Query { + pet: Pet + } + + interface Pet { + name: String + } + + type Cat implements Pet { + name: String + } + + type Dog implements Pet { + name: String + } + `); + + const document = parse(` + { + pet { + name + } + } + `); + + function expectError({ forTypeName }: {| forTypeName: mixed |}) { + const rootValue = { pet: { __typename: forTypeName } }; + const result = executeSync({ schema, document, rootValue }); + return { + toEqual(message: string) { + expect(result).to.deep.equal({ + data: { pet: null }, + errors: [ + { + message, + locations: [{ line: 3, column: 9 }], + path: ['pet'], + }, + ], + }); + }, + }; + } + + 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.', + ); + + expectError({ forTypeName: 'Human' }).toEqual( + 'Abstract type "Pet" was resolve to a type "Human" that does not exist inside schema.', + ); + + expectError({ forTypeName: 'String' }).toEqual( + 'Abstract type "Pet" was resolve to a non-object type "String".', + ); + + expectError({ forTypeName: '__Schema' }).toEqual( + 'Runtime Object type "__Schema" is not a possible type for "Pet".', + ); + + // FIXME: workaround since we can't inject resolveType into SDL + (schema.getType('Pet'): any).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 "[]".', + ); + }); }); diff --git a/src/execution/__tests__/union-interface-test.js b/src/execution/__tests__/union-interface-test.js index 212ec26a8b..1d750d273d 100644 --- a/src/execution/__tests__/union-interface-test.js +++ b/src/execution/__tests__/union-interface-test.js @@ -115,11 +115,11 @@ const PetType = new GraphQLUnionType({ types: [DogType, CatType], resolveType(value) { if (value instanceof Dog) { - return DogType; + return DogType.name; } // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') if (value instanceof Cat) { - return CatType; + return CatType.name; } // istanbul ignore next (Not reachable. All possible types have been considered) @@ -514,7 +514,7 @@ describe('Execute: Union and intersection types', () => { encounteredContext = context; encounteredSchema = info.schema; encounteredRootValue = info.rootValue; - return PersonType2; + return PersonType2.name; }, }); diff --git a/src/execution/execute.js b/src/execution/execute.js index cf1d2ddbda..f272b65aef 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -52,6 +52,7 @@ import { GraphQLSkipDirective, } from '../type/directives'; import { + isNamedType, isObjectType, isAbstractType, isLeafType, @@ -996,23 +997,43 @@ function completeAbstractValue( } function ensureValidRuntimeType( - runtimeTypeOrName: ?GraphQLObjectType | string, + runtimeTypeOrName: mixed, exeContext: ExecutionContext, returnType: GraphQLAbstractType, fieldNodes: $ReadOnlyArray, info: GraphQLResolveInfo, result: mixed, ): GraphQLObjectType { - const runtimeType = - typeof runtimeTypeOrName === 'string' - ? exeContext.schema.getType(runtimeTypeOrName) - : runtimeTypeOrName; + if (runtimeTypeOrName == 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.`, + fieldNodes, + ); + } - if (!isObjectType(runtimeType)) { + // FIXME: temporary workaround until support for passing object types would be removed in v16.0.0 + const runtimeTypeName = isNamedType(runtimeTypeOrName) + ? runtimeTypeOrName.name + : runtimeTypeOrName; + + if (typeof runtimeTypeName !== '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(runtimeType)}". ` + - `Either the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, + `value ${inspect(result)}, received "${inspect(runtimeTypeOrName)}".`, + ); + } + + const runtimeType = exeContext.schema.getType(runtimeTypeName); + if (runtimeType == null) { + throw new GraphQLError( + `Abstract type "${returnType.name}" was resolve to a type "${runtimeTypeName}" that does not exist inside schema.`, + fieldNodes, + ); + } + + if (!isObjectType(runtimeType)) { + throw new GraphQLError( + `Abstract type "${returnType.name}" was resolve to a non-object type "${runtimeTypeName}".`, fieldNodes, ); } @@ -1157,7 +1178,7 @@ export const defaultTypeResolver: GraphQLTypeResolver = function ( if (isPromise(isTypeOfResult)) { promisedIsTypeOfResults[i] = isTypeOfResult; } else if (isTypeOfResult) { - return type; + return type.name; } } } @@ -1166,7 +1187,7 @@ export const defaultTypeResolver: GraphQLTypeResolver = function ( return Promise.all(promisedIsTypeOfResults).then((isTypeOfResults) => { for (let i = 0; i < isTypeOfResults.length; i++) { if (isTypeOfResults[i]) { - return possibleTypes[i]; + return possibleTypes[i].name; } } }); diff --git a/src/type/definition.js b/src/type/definition.js index c8f92e21d3..ca0e1f5a38 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -924,6 +924,9 @@ export type GraphQLObjectTypeConfig = {| extensionASTNodes?: ?$ReadOnlyArray, |}; +/** + * Note: returning GraphQLObjectType is deprecated and will be removed in v16.0.0 + */ export type GraphQLTypeResolver = ( value: TSource, context: TContext,