diff --git a/.changeset/old-wasps-worry.md b/.changeset/old-wasps-worry.md new file mode 100644 index 00000000000..7728403714f --- /dev/null +++ b/.changeset/old-wasps-worry.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/delegate': patch +--- + +fix(delegate): WrapConcreteTypes should not process fragments that are not on a root type (#2173) diff --git a/packages/delegate/src/transforms/WrapConcreteTypes.ts b/packages/delegate/src/transforms/WrapConcreteTypes.ts index 02b5f3b2e93..069aa81e868 100644 --- a/packages/delegate/src/transforms/WrapConcreteTypes.ts +++ b/packages/delegate/src/transforms/WrapConcreteTypes.ts @@ -10,6 +10,7 @@ import { visitWithTypeInfo, isObjectType, FieldNode, + FragmentDefinitionNode, } from 'graphql'; import { Request } from '@graphql-tools/utils'; @@ -47,10 +48,20 @@ function wrapConcreteTypes( return document; } + const queryTypeName = targetSchema.getQueryType()?.name; + const mutationTypeName = targetSchema.getMutationType()?.name; + const subscriptionTypeName = targetSchema.getSubscriptionType()?.name; + const typeInfo = new TypeInfo(targetSchema); const newDocument = visit( document, visitWithTypeInfo(typeInfo, { + [Kind.FRAGMENT_DEFINITION]: (node: FragmentDefinitionNode) => { + const typeName = node.typeCondition.name.value; + if (typeName !== queryTypeName && typeName !== mutationTypeName && typeName !== subscriptionTypeName) { + return false; + } + }, [Kind.FIELD]: (node: FieldNode) => { if (isAbstractType(getNamedType(typeInfo.getType()))) { return { diff --git a/packages/stitch/tests/fragments.test.ts b/packages/stitch/tests/fragments.test.ts deleted file mode 100644 index e58e1e066cd..00000000000 --- a/packages/stitch/tests/fragments.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { GraphQLSchema, graphql } from 'graphql'; - -import { delegateToSchema } from '@graphql-tools/delegate'; -import { IResolvers } from '@graphql-tools/utils'; - -import { stitchSchemas } from '../src/stitchSchemas'; - -import { - propertySchema, - bookingSchema, - sampleData, - Property, -} from './fixtures/schemas'; - -function findPropertyByLocationName( - properties: Record, - name: string, -): Property | undefined { - for (const key of Object.keys(properties)) { - const property = properties[key]; - if (property.location.name === name) { - return property; - } - } -} - -const COORDINATES_QUERY = ` - query BookingCoordinates($bookingId: ID!) { - bookingById (id: $bookingId) { - property { - location { - coordinates - } - } - } - } -`; - -const proxyResolvers: IResolvers = { - Booking: { - property: { - selectionSet: '{ propertyId }', - resolve(booking, _args, context, info) { - return delegateToSchema({ - schema: propertySchema, - operation: 'query', - fieldName: 'propertyById', - args: { id: booking.propertyId }, - context, - info, - }); - }, - }, - }, - Location: { - coordinates: { - selectionSet: '{ name }', - resolve: (location) => { - const name = location.name; - return findPropertyByLocationName(sampleData.Property, name).location - .coordinates; - }, - }, - }, -}; - -const proxyTypeDefs = ` - extend type Booking { - property: Property! - } - extend type Location { - coordinates: String! - } -`; - -describe('stitching', () => { - describe('delegateToSchema', () => { - let schema: GraphQLSchema; - beforeAll(() => { - schema = stitchSchemas({ - subschemas: [bookingSchema, propertySchema], - typeDefs: proxyTypeDefs, - resolvers: proxyResolvers, - }); - }); - test('should add fragments for deep types', async () => { - const result = await graphql( - schema, - COORDINATES_QUERY, - {}, - {}, - { bookingId: 'b1' }, - ); - - expect(result).toEqual({ - data: { - bookingById: { - property: { - location: { - coordinates: sampleData.Property.p1.location.coordinates, - }, - }, - }, - }, - }); - }); - }); -}); diff --git a/packages/stitch/tests/selectionSets.test.ts b/packages/stitch/tests/selectionSets.test.ts new file mode 100644 index 00000000000..45962680a32 --- /dev/null +++ b/packages/stitch/tests/selectionSets.test.ts @@ -0,0 +1,299 @@ +import { graphql } from 'graphql'; + +import { delegateToSchema } from '@graphql-tools/delegate'; +import { batchDelegateToSchema } from '@graphql-tools/batch-delegate'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { IResolvers } from '@graphql-tools/utils'; + +import { stitchSchemas } from '../src/stitchSchemas'; + +import { + propertySchema, + bookingSchema, + sampleData, + Property, +} from './fixtures/schemas'; + +describe('delegateToSchema ', () => { + test('should add selection sets for deep types', async () => { + function findPropertyByLocationName( + properties: Record, + name: string, + ): Property | undefined { + for (const key of Object.keys(properties)) { + const property = properties[key]; + if (property.location.name === name) { + return property; + } + } + } + + const COORDINATES_QUERY = ` + query BookingCoordinates($bookingId: ID!) { + bookingById (id: $bookingId) { + property { + location { + coordinates + } + } + } + } + `; + + const proxyResolvers: IResolvers = { + Booking: { + property: { + selectionSet: '{ propertyId }', + resolve(booking, _args, context, info) { + return delegateToSchema({ + schema: propertySchema, + operation: 'query', + fieldName: 'propertyById', + args: { id: booking.propertyId }, + context, + info, + }); + }, + }, + }, + Location: { + coordinates: { + selectionSet: '{ name }', + resolve: (location) => { + const name = location.name; + return findPropertyByLocationName(sampleData.Property, name).location + .coordinates; + }, + }, + }, + }; + + const proxyTypeDefs = ` + extend type Booking { + property: Property! + } + extend type Location { + coordinates: String! + } + `; + + const schema = stitchSchemas({ + subschemas: [bookingSchema, propertySchema], + typeDefs: proxyTypeDefs, + resolvers: proxyResolvers, + }); + + const result = await graphql( + schema, + COORDINATES_QUERY, + {}, + {}, + { bookingId: 'b1' }, + ); + + expect(result).toEqual({ + data: { + bookingById: { + property: { + location: { + coordinates: sampleData.Property.p1.location.coordinates, + }, + }, + }, + }, + }); + }); + + describe('should add selection sets for fragments', () => { + const networkSchema = makeExecutableSchema({ + typeDefs: ` + interface Domain { + id: ID! + name: String! + } + type Domain1 implements Domain { + id: ID! + name: String! + } + type Domain2 implements Domain { + id: ID! + name: String! + extra: String! + } + type Network { + id: ID! + domains: [Domain!]! + } + type Query { + networks(ids: [ID!]!): [Network!]! + } + `, + resolvers: { + Domain: { + __resolveType() { + return 'Domain1' + }, + }, + Query: { + networks: (_root, { ids }) => + ids.map((id: any) => ({ id, domains: [{ id: Number(id) + 3, name: `network${id}.com` }] })), + }, + }, + }); + + const postsSchema = makeExecutableSchema({ + typeDefs: ` + type Post { + id: ID! + networkId: ID! + } + type Query { + posts(ids: [ID!]!): [Post]! + } + `, + resolvers: { + Query: { + posts: (_root, { ids }) => + ids.map((id: any) => ({ + id, + networkId: Number(id) + 2, + })), + }, + }, + }); + + const gatewaySchema = stitchSchemas({ + subschemas: [networkSchema, postsSchema], + typeDefs: ` + extend type Post { + network: Network! + } + `, + resolvers: { + Post: { + network: { + selectionSet: '{ networkId }', + resolve(parent, _args, context, info) { + return batchDelegateToSchema({ + key: parent.networkId, + argsFromKeys: (ids) => ({ ids }), + context, + fieldName: 'networks', + info, + operation: 'query', + schema: networkSchema, + }) + }, + }, + }, + }, + }); + + const expectedData = [ + { + network: { id: '57', domains: [{ id: '60', name: 'network57.com' }] }, + }, + ]; + + it('should resolve with no fragments', async () => { + const { data } = await graphql( + gatewaySchema, + ` + query { + posts(ids: [55]) { + network { + id + domains { + id + name + } + } + } + } + `, + ); + + expect(data.posts).toEqual(expectedData); + }); + + it('should resolve with a fragment', async () => { + const { data } = await graphql( + gatewaySchema, + ` + query { + posts(ids: [55]) { + ...F1 + } + } + + fragment F1 on Post { + network { + id + domains { + id + name + } + } + } + `, + ); + + expect(data.posts).toEqual(expectedData); + }); + + it('should resolve with deep fragment', async () => { + const { data } = await graphql( + gatewaySchema, + ` + query { + posts(ids: [55]) { + network { + ...F1 + } + } + } + + fragment F1 on Network { + id + domains { + id + name + } + } + `, + ); + + expect(data.posts).toEqual(expectedData); + }); + + it('should resolve with nested fragments', async () => { + const { data } = await graphql( + gatewaySchema, + ` + query { + posts(ids: [55]) { + ...F1 + } + } + + fragment F1 on Post { + network { + ...F2 + } + } + + fragment F2 on Network { + id + domains { + id + name + } + } + `, + ) + + expect(data.posts).toEqual(expectedData); + }); + }); +}); +