diff --git a/.changeset/giant-avocados-destroy.md b/.changeset/giant-avocados-destroy.md new file mode 100644 index 00000000000..e6f8726a7aa --- /dev/null +++ b/.changeset/giant-avocados-destroy.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/delegate': patch +--- + +Handle type merging with union types correctly -> See https://github.com/ardatan/graphql-tools/issues/4902 diff --git a/packages/delegate/src/prepareGatewayDocument.ts b/packages/delegate/src/prepareGatewayDocument.ts index 91a6040b136..c58809a3840 100644 --- a/packages/delegate/src/prepareGatewayDocument.ts +++ b/packages/delegate/src/prepareGatewayDocument.ts @@ -14,7 +14,7 @@ import { visitWithTypeInfo, InlineFragmentNode, GraphQLOutputType, - isObjectType, + isCompositeType, FieldNode, } from 'graphql'; @@ -382,7 +382,7 @@ function wrapConcreteTypes( ): DocumentNode { const namedType = getNamedType(returnType); - if (!isObjectType(namedType)) { + if (!isCompositeType(namedType)) { return document; } diff --git a/packages/stitch/tests/typeMerging.test.ts b/packages/stitch/tests/typeMerging.test.ts index b92ade7faa8..3719a605917 100644 --- a/packages/stitch/tests/typeMerging.test.ts +++ b/packages/stitch/tests/typeMerging.test.ts @@ -1,7 +1,7 @@ // The below is meant to be an alternative canonical schema stitching example // which relies on type merging. -import { graphql, OperationTypeNode } from 'graphql'; +import { graphql, OperationTypeNode, parse } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -13,6 +13,7 @@ import { RenameRootFields, RenameTypes } from '@graphql-tools/wrap'; import { assertSome } from '@graphql-tools/utils'; import { stitchSchemas } from '../src/stitchSchemas.js'; +import { normalizedExecutor } from '@graphql-tools/executor'; describe('merging using type merging', () => { test('works', async () => { @@ -552,6 +553,114 @@ describe('merging using type merging when renaming', () => { expect(userByIdData.chirps[1].text).not.toBe(null); expect(userByIdData.chirps[1].author.email).not.toBe(null); }); + it('union merge', async () => { + const carVehicle = { + id: '1', + brand: 'Tesla', + __typename: 'Car', + }; + + const vehiclesSchema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + getVehicle: Vehicle! + } + + union Vehicle = Car | Bike + + type Car { + id: ID! + brand: String! + } + + type Bike { + id: ID! + brand: String! + } + `, + resolvers: { + Query: { + getVehicle: () => carVehicle, + }, + Vehicle: { + __resolveType: () => 'Car', + }, + }, + }); + const licensePlateSchema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + scalar Any + type Query { + _entities(representations: [Any]!): [Entity]! + } + union Entity = Car + type Car { + id: ID! + licensePlate: String! + } + `, + resolvers: { + Query: { + _entities: (_, { representations }: { representations: any[] }) => representations, + }, + Entity: { + __resolveType: (root: any) => root.__typename, + }, + Car: { + licensePlate: () => 'ZH 1234', + }, + }, + }); + const stitchedSchema = stitchSchemas({ + subschemas: [ + { + schema: vehiclesSchema, + merge: { + Vehicle: { + fieldName: 'getVehicle', + selectionSet: '{ id }', + key: representation => representation, + argsFromKeys: representations => ({ representations }), + }, + }, + }, + { + schema: licensePlateSchema, + merge: { + Car: { + fieldName: '_entities', + selectionSet: '{ id }', + key: representation => representation, + argsFromKeys: representations => ({ representations }), + }, + }, + }, + ], + }); + const result = await normalizedExecutor({ + schema: stitchedSchema, + document: parse(/* GraphQL */ ` + { + getVehicle { + ... on Car { + id + brand + licensePlate ## 💥 + } + } + } + `), + }); + expect(result).toEqual({ + data: { + getVehicle: { + id: '1', + brand: 'Tesla', + licensePlate: 'ZH 1234', + }, + }, + }); + }); }); describe('external object annotation with batchDelegateToSchema', () => {