From d2924dc7151f5db11de9b349e3e3057d668406fc Mon Sep 17 00:00:00 2001 From: Jordan Date: Mon, 8 Jul 2019 16:34:47 -0700 Subject: [PATCH 1/6] add support for building federated schemas from existing schemas --- .../__tests__/buildFederatedSchema.test.ts | 853 +++++++++++------- .../src/service/buildFederatedSchema.ts | 106 ++- packages/apollo-federation/src/types.ts | 25 +- 3 files changed, 653 insertions(+), 331 deletions(-) diff --git a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts b/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts index d09f250d900..28c85177d84 100644 --- a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts +++ b/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts @@ -1,5 +1,12 @@ import gql from 'graphql-tag'; -import { Kind, graphql } from 'graphql'; +import { + Kind, + graphql, + GraphQLSchema, + GraphQLObjectType, + GraphQLString, + GraphQLInt, +} from 'graphql'; import { buildFederatedSchema } from '../buildFederatedSchema'; import { typeSerializer } from '../../snapshotSerializers'; @@ -10,17 +17,36 @@ const EMPTY_DOCUMENT = { definitions: [], }; -describe('buildFederatedSchema', () => { - it(`should mark a type with a key field as an entity`, () => { - const schema = buildFederatedSchema(gql` - type Product @key(fields: "upc") { - upc: String! - name: String - price: Int - } - `); +const testFederatedSchema = ( + name: string, + createSchema: () => GraphQLSchema, + runTest: (schema: GraphQLSchema) => Promise, +) => { + const schema = createSchema(); + + it(name, async () => { + await runTest(schema); + }); + + it(`${name} (using "schema" argument)`, async () => { + await runTest(buildFederatedSchema(schema)); + }); +}; - expect(schema.getType('Product')).toMatchInlineSnapshot(` +describe('buildFederatedSchema', () => { + testFederatedSchema( + 'should mark a type with a key field as an entity', + () => { + return buildFederatedSchema(gql` + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + } + `); + }, + async (schema: GraphQLSchema) => { + expect(schema.getType('Product')).toMatchInlineSnapshot(` type Product { upc: String! name: String @@ -28,22 +54,51 @@ type Product { } `); - expect(schema.getType('_Entity')).toMatchInlineSnapshot( - `union _Entity = Product`, - ); - }); - - it(`should mark a type with multiple key fields as an entity`, () => { - const schema = buildFederatedSchema(gql` - type Product @key(fields: "upc") @key(fields: "sku") { - upc: String! - sku: String! - name: String - price: Int - } - `); + expect(schema.getType('_Entity')).toMatchInlineSnapshot( + `union _Entity = Product`, + ); + }, + ); + testFederatedSchema( + `should mark a type with a key field as an entity`, + () => { + return buildFederatedSchema(gql` + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + } + `); + }, + async (schema: GraphQLSchema) => { + expect(schema.getType('Product')).toMatchInlineSnapshot(` +type Product { + upc: String! + name: String + price: Int +} +`); - expect(schema.getType('Product')).toMatchInlineSnapshot(` + expect(schema.getType('_Entity')).toMatchInlineSnapshot( + `union _Entity = Product`, + ); + }, + ); + + testFederatedSchema( + `should mark a type with multiple key fields as an entity`, + () => { + return buildFederatedSchema(gql` + type Product @key(fields: "upc") @key(fields: "sku") { + upc: String! + sku: String! + name: String + price: Int + } + `); + }, + schema => { + expect(schema.getType('Product')).toMatchInlineSnapshot(` type Product { upc: String! sku: String! @@ -52,57 +107,66 @@ type Product { } `); - expect(schema.getType('_Entity')).toMatchInlineSnapshot( - `union _Entity = Product`, - ); - }); - - it(`should not mark a type without a key field as an entity`, () => { - const schema = buildFederatedSchema(gql` - type Money { - amount: Int! - currencyCode: String! - } - `); - - expect(schema.getType('Money')).toMatchInlineSnapshot(` + expect(schema.getType('_Entity')).toMatchInlineSnapshot( + `union _Entity = Product`, + ); + }, + ); + + testFederatedSchema( + `should not mark a type without a key field as an entity`, + () => { + return buildFederatedSchema(gql` + type Money { + amount: Int! + currencyCode: String! + } + `); + }, + async (schema: GraphQLSchema) => { + expect(schema.getType('Money')).toMatchInlineSnapshot(` type Money { amount: Int! currencyCode: String! } `); - }); - - it('should preserve description text in generated SDL', async () => { - const query = `query GetServiceDetails { - _service { - sdl - } - }`; - const schema = buildFederatedSchema(gql` - "A user. This user is very complicated and requires so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so much description text" - type User @key(fields: "id") { - """ - The unique ID of the user. - """ - id: ID! - "The user's name." - name: String - username: String - foo( - "Description 1" - arg1: String - "Description 2" - arg2: String - "Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3" - arg3: String - ): String - } - `); + }, + ); + + testFederatedSchema( + 'should preserve description text in generated SDL', + () => { + return buildFederatedSchema(gql` + "A user. This user is very complicated and requires so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so much description text" + type User @key(fields: "id") { + """ + The unique ID of the user. + """ + id: ID! + "The user's name." + name: String + username: String + foo( + "Description 1" + arg1: String + "Description 2" + arg2: String + "Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3" + arg3: String + ): String + } + `); + }, + async (schema: GraphQLSchema) => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl).toEqual(`""" + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl).toEqual(`""" A user. This user is very complicated and requires so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so much description text @@ -126,153 +190,189 @@ type User @key(fields: "id") { ): String } `); - }); + }, + ); describe(`should add an _entities query root field to the schema`, () => { - it(`when a query root type with the default name has been defined`, () => { - const schema = buildFederatedSchema(gql` - type Query { - rootField: String - } - type Product @key(fields: "upc") { - upc: ID! - } - `); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` + testFederatedSchema( + `when a query root type with the default name has been defined`, + () => { + return buildFederatedSchema(gql` + type Query { + rootField: String + } + type Product @key(fields: "upc") { + upc: ID! + } + `); + }, + async (schema: GraphQLSchema) => { + expect(schema.getQueryType()).toMatchInlineSnapshot(` type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! rootField: String } `); - }); - - it(`when a query root type with a non-default name has been defined`, () => { - const schema = buildFederatedSchema(gql` - schema { - query: QueryRoot - } - - type QueryRoot { - rootField: String - } - type Product @key(fields: "upc") { - upc: ID! - } - `); + }, + ); - expect(schema.getQueryType()).toMatchInlineSnapshot(` + testFederatedSchema( + `when a query root type with a non-default name has been defined`, + () => { + return buildFederatedSchema(gql` + schema { + query: QueryRoot + } + + type QueryRoot { + rootField: String + } + type Product @key(fields: "upc") { + upc: ID! + } + `); + }, + async (schema: GraphQLSchema) => { + expect(schema.getQueryType()).toMatchInlineSnapshot(` type QueryRoot { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! rootField: String } `); - }); + }, + ); }); describe(`should not add an _entities query root field to the schema`, () => { - it(`when no query root type has been defined`, () => { - const schema = buildFederatedSchema(EMPTY_DOCUMENT); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` + testFederatedSchema( + `when no query root type has been defined`, + () => { + return buildFederatedSchema(EMPTY_DOCUMENT); + }, + async (schema: GraphQLSchema) => { + expect(schema.getQueryType()).toMatchInlineSnapshot(` type Query { _service: _Service! } `); - }); - it(`when no types with keys are found`, () => { - const schema = buildFederatedSchema(gql` - type Query { - rootField: String - } - `); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` + }, + ); + testFederatedSchema( + `when no types with keys are found`, + () => { + return buildFederatedSchema(gql` + type Query { + rootField: String + } + `); + }, + async (schema: GraphQLSchema) => { + expect(schema.getQueryType()).toMatchInlineSnapshot(` type Query { _service: _Service! rootField: String } `); - }); - it(`when only an interface with keys are found`, () => { - const schema = buildFederatedSchema(gql` - type Query { - rootField: String - } - interface Product @key(fields: "upc") { - upc: ID! - } - `); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` + }, + ); + testFederatedSchema( + `when only an interface with keys are found`, + () => { + return buildFederatedSchema(gql` + type Query { + rootField: String + } + interface Product @key(fields: "upc") { + upc: ID! + } + `); + }, + async (schema: GraphQLSchema) => { + expect(schema.getQueryType()).toMatchInlineSnapshot(` type Query { _service: _Service! rootField: String } `); - }); + }, + ); }); describe('_entities root field', () => { - it('executes resolveReference for a type if found', async () => { - const query = `query GetEntities($representations: [_Any!]!) { - _entities(representations: $representations) { - ... on Product { - name - } - ... on User { - firstName - } - } - }`; - - const variables = { - representations: [ - { __typename: 'Product', upc: 1 }, - { __typename: 'User', id: 1 }, - ], - }; - - const schema = buildFederatedSchema([ - { - typeDefs: gql` - type Product @key(fields: "upc") { - upc: Int - name: String - } - type User @key(fields: "id") { - firstName: String - } - `, - resolvers: { - Product: { - __resolveReference(object) { - expect(object.upc).toEqual(1); - return { name: 'Apollo Gateway' }; + testFederatedSchema( + 'executes resolveReference for a type if found', + () => { + return buildFederatedSchema([ + { + typeDefs: gql` + type Product @key(fields: "upc") { + upc: Int + name: String + } + type User @key(fields: "id") { + firstName: String + } + `, + resolvers: { + Product: { + __resolveReference(object) { + expect(object.upc).toEqual(1); + return { name: 'Apollo Gateway' }; + }, }, - }, - User: { - __resolveReference(object) { - expect(object.id).toEqual(1); - return Promise.resolve({ firstName: 'James' }); + User: { + __resolveReference(object) { + expect(object.id).toEqual(1); + return Promise.resolve({ firstName: 'James' }); + }, }, }, }, - }, - ]); - const { data, errors } = await graphql( - schema, - query, - null, - null, - variables, - ); - expect(errors).toBeUndefined(); - expect(data._entities[0].name).toEqual('Apollo Gateway'); - expect(data._entities[1].firstName).toEqual('James'); - }); - it('executes resolveReference with default representation values', async () => { - const query = `query GetEntities($representations: [_Any!]!) { + ]); + }, + async (schema: GraphQLSchema) => { + const query = `query GetEntities($representations: [_Any!]!) { + _entities(representations: $representations) { + ... on Product { + name + } + ... on User { + firstName + } + } + }`; + + const variables = { + representations: [ + { __typename: 'Product', upc: 1 }, + { __typename: 'User', id: 1 }, + ], + }; + + const { data, errors } = await graphql( + schema, + query, + null, + null, + variables, + ); + expect(errors).toBeUndefined(); + expect(data._entities[0].name).toEqual('Apollo Gateway'); + expect(data._entities[1].firstName).toEqual('James'); + }, + ); + testFederatedSchema( + 'executes resolveReference with default representation values', + () => { + return buildFederatedSchema(gql` + type Product @key(fields: "upc") { + upc: Int + name: String + } + `); + }, + async (schema: GraphQLSchema) => { + const query = `query GetEntities($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { upc @@ -281,55 +381,53 @@ type Query { } }`; - const variables = { - representations: [ - { __typename: 'Product', upc: 1, name: 'Apollo Gateway' }, - ], - }; - - const schema = buildFederatedSchema(gql` - type Product @key(fields: "upc") { - upc: Int - name: String - } - `); - const { data, errors } = await graphql( - schema, - query, - null, - null, - variables, - ); - expect(errors).toBeUndefined(); - expect(data._entities[0].name).toEqual('Apollo Gateway'); - }); + const variables = { + representations: [ + { __typename: 'Product', upc: 1, name: 'Apollo Gateway' }, + ], + }; + + const { data, errors } = await graphql( + schema, + query, + null, + null, + variables, + ); + expect(errors).toBeUndefined(); + expect(data._entities[0].name).toEqual('Apollo Gateway'); + }, + ); }); describe('_service root field', () => { - it('keeps extension types when owner type is not present', async () => { - const query = `query GetServiceDetails { + testFederatedSchema( + 'keeps extension types when owner type is not present', + () => { + return buildFederatedSchema(gql` + type Review { + id: ID + } + + extend type Review { + title: String + } + + extend type Product @key(fields: "upc") { + upc: String @external + reviews: [Review] + } + `); + }, + async (schema: GraphQLSchema) => { + const query = `query GetServiceDetails { _service { sdl } }`; - const schema = buildFederatedSchema(gql` - type Review { - id: ID - } - - extend type Review { - title: String - } - - extend type Product @key(fields: "upc") { - upc: String @external - reviews: [Review] - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl) - .toEqual(`extend type Product @key(fields: "upc") { + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl) + .toEqual(`extend type Product @key(fields: "upc") { upc: String @external reviews: [Review] } @@ -339,35 +437,39 @@ type Review { title: String } `); - }); - it('keeps extension interface when owner interface is not present', async () => { - const query = `query GetServiceDetails { + }, + ); + testFederatedSchema( + 'keeps extension interface when owner interface is not present', + () => { + return buildFederatedSchema(gql` + type Review { + id: ID + } + + extend type Review { + title: String + } + + interface Node @key(fields: "id") { + id: ID! + } + + extend interface Product @key(fields: "upc") { + upc: String @external + reviews: [Review] + } + `); + }, + async (schema: GraphQLSchema) => { + const query = `query GetServiceDetails { _service { sdl } }`; - const schema = buildFederatedSchema(gql` - type Review { - id: ID - } - - extend type Review { - title: String - } - - interface Node @key(fields: "id") { - id: ID! - } - - extend interface Product @key(fields: "upc") { - upc: String @external - reviews: [Review] - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl).toEqual(`interface Node @key(fields: "id") { + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl).toEqual(`interface Node @key(fields: "id") { id: ID! } @@ -381,84 +483,95 @@ type Review { title: String } `); - }); - it('returns valid sdl for @key directives', async () => { - const query = `query GetServiceDetails { + }, + ); + testFederatedSchema( + 'returns valid sdl for @key directives', + () => { + return buildFederatedSchema(gql` + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + } + `); + }, + async (schema: GraphQLSchema) => { + const query = `query GetServiceDetails { _service { sdl } }`; - const schema = buildFederatedSchema(gql` - type Product @key(fields: "upc") { - upc: String! - name: String - price: Int - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl).toEqual(`type Product @key(fields: "upc") { + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl).toEqual(`type Product @key(fields: "upc") { upc: String! name: String price: Int } `); - }); - it('returns valid sdl for multiple @key directives', async () => { - const query = `query GetServiceDetails { + }, + ); + testFederatedSchema( + 'returns valid sdl for multiple @key directives', + () => { + return buildFederatedSchema(gql` + type Product @key(fields: "upc") @key(fields: "name") { + upc: String! + name: String + price: Int + } + `); + }, + async (schema: GraphQLSchema) => { + const query = `query GetServiceDetails { _service { sdl } }`; - const schema = buildFederatedSchema(gql` - type Product @key(fields: "upc") @key(fields: "name") { - upc: String! - name: String - price: Int - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl) - .toEqual(`type Product @key(fields: "upc") @key(fields: "name") { + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl) + .toEqual(`type Product @key(fields: "upc") @key(fields: "name") { upc: String! name: String price: Int } `); - }); - it('supports all federation directives', async () => { - const query = `query GetServiceDetails { + }, + ); + testFederatedSchema( + 'supports all federation directives', + () => { + return buildFederatedSchema(gql` + type Review @key(fields: "id") { + id: ID! + body: String + author: User @provides(fields: "email") + product: Product @provides(fields: "upc") + } + + extend type User @key(fields: "email") { + email: String @external + reviews: [Review] + } + + extend type Product @key(fields: "upc") { + upc: String @external + reviews: [Review] + } + `); + }, + async (schema: GraphQLSchema) => { + const query = `query GetServiceDetails { _service { sdl } }`; - - const schema = buildFederatedSchema(gql` - type Review @key(fields: "id") { - id: ID! - body: String - author: User @provides(fields: "email") - product: Product @provides(fields: "upc") - } - - extend type User @key(fields: "email") { - email: String @external - reviews: [Review] - } - - extend type Product @key(fields: "upc") { - upc: String @external - reviews: [Review] - } - `); - - const { data, errors } = await graphql(schema, query); - expect(errors).toBeUndefined(); - expect(data._service.sdl) - .toEqual(`extend type Product @key(fields: "upc") { + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl) + .toEqual(`extend type Product @key(fields: "upc") { upc: String @external reviews: [Review] } @@ -475,6 +588,136 @@ extend type User @key(fields: "email") { reviews: [Review] } `); + }, + ); + }); + + it('executes resolveReference for a type if found using manually created GraphQLSchema', async () => { + const query = `query GetEntities($representations: [_Any!]!) { + _entities(representations: $representations) { + ... on Product { + name + } + ... on User { + firstName + } + } + }`; + + const variables = { + representations: [ + { __typename: 'Product', upc: 1 }, + { __typename: 'User', id: 1 }, + ], + }; + + const product: GraphQLObjectType = new GraphQLObjectType({ + name: 'Product', + fields: () => ({ + upc: { type: GraphQLInt }, + name: { type: GraphQLString }, + __resolveReference: { + type: product, + resolve: (object: any) => { + expect(object.upc).toEqual(1); + return { name: 'Apollo Gateway' }; + }, + }, + }), + astNode: { + kind: 'ObjectTypeDefinition', + name: { + kind: 'Name', + value: 'Product', + }, + interfaces: [], + directives: [ + { + kind: 'Directive', + name: { + kind: 'Name', + value: 'key', + }, + arguments: [ + { + kind: 'Argument', + name: { + kind: 'Name', + value: 'fields', + }, + value: { + kind: 'StringValue', + value: 'upc', + block: false, + }, + }, + ], + }, + ], + }, + }); + + const user: GraphQLObjectType = new GraphQLObjectType({ + name: 'User', + fields: () => ({ + firstName: { type: GraphQLString }, + __resolveReference: { + type: user, + resolve: (object: any) => { + expect(object.id).toEqual(1); + return Promise.resolve({ firstName: 'James' }); + }, + }, + }), + astNode: { + kind: 'ObjectTypeDefinition', + name: { + kind: 'Name', + value: 'User', + }, + interfaces: [], + directives: [ + { + kind: 'Directive', + name: { + kind: 'Name', + value: 'key', + }, + arguments: [ + { + kind: 'Argument', + name: { + kind: 'Name', + value: 'fields', + }, + value: { + kind: 'StringValue', + value: 'id', + block: false, + }, + }, + ], + }, + ], + }, }); + + const schema = buildFederatedSchema( + new GraphQLSchema({ + query: null, + types: [product, user], + }), + ); + + const { data, errors } = await graphql( + schema, + query, + null, + null, + variables, + ); + expect(errors).toBeUndefined(); + expect(data._entities[0].name).toEqual('Apollo Gateway'); + expect(data._entities[1].firstName).toEqual('James'); }); }); diff --git a/packages/apollo-federation/src/service/buildFederatedSchema.ts b/packages/apollo-federation/src/service/buildFederatedSchema.ts index 40454297668..d5a7ccf778f 100644 --- a/packages/apollo-federation/src/service/buildFederatedSchema.ts +++ b/packages/apollo-federation/src/service/buildFederatedSchema.ts @@ -16,25 +16,112 @@ import { } from 'apollo-graphql'; import federationDirectives, { typeIncludesDirective } from '../directives'; -import { serviceField, entitiesField, EntityType } from '../types'; +import { + serviceField, + entitiesField, + EntityType, + ResolvableGraphQLObjectType, + GraphQLReferenceResolver, +} from '../types'; import { printSchema } from './printFederatedSchema'; import 'apollo-server-env'; +interface ReferenceResolverMap { + [key: string]: { __resolveReference: GraphQLReferenceResolver }; +} + export function buildFederatedSchema( + modulesSDLOrSchema: + | (GraphQLSchemaModule | DocumentNode)[] + | DocumentNode + | GraphQLSchema, +): GraphQLSchema { + if (modulesSDLOrSchema instanceof GraphQLSchema) { + return buildFederatedSchemaFromSchema(modulesSDLOrSchema); + } + + return buildSchemaFromModulesOrSDL(modulesSDLOrSchema); +} + +function resolveReferenceForType( + type: ResolvableGraphQLObjectType, +): GraphQLReferenceResolver | undefined { + if (type.resolveReference) { + return type.resolveReference; + } + + if (isObjectType(type)) { + const fields = type.getFields(); + + if (fields.__resolveReference) { + const __resolveReference = fields.__resolveReference + .resolve as GraphQLReferenceResolver; + + delete fields.__resolveReference; + + return __resolveReference; + } + } + + return; +} + +function referenceResolversForSchema( + schema: GraphQLSchema, +): ReferenceResolverMap { + const map: ReferenceResolverMap = {}; + + for (const [typeName, type] of Object.entries(schema.getTypeMap())) { + const __resolveReference = resolveReferenceForType( + type as ResolvableGraphQLObjectType, + ); + + if (__resolveReference) { + map[typeName] = { __resolveReference }; + } + } + + return map; +} + +function buildFederatedSchemaFromSchema( + baseSchema: GraphQLSchema, +): GraphQLSchema { + const referenceResolvers = referenceResolversForSchema(baseSchema); + + const schema = transformFederatedSchema(baseSchema); + + addResolversToSchema(schema, referenceResolvers); + + return schema; +} + +function buildSchemaFromModulesOrSDL( modulesOrSDL: (GraphQLSchemaModule | DocumentNode)[] | DocumentNode, ): GraphQLSchema { const modules = modulesFromSDL(modulesOrSDL); - let schema = buildSchemaFromSDL( - modules, - new GraphQLSchema({ - query: undefined, - directives: [...specifiedDirectives, ...federationDirectives], - }), + const schema = transformFederatedSchema( + buildSchemaFromSDL( + modules, + new GraphQLSchema({ + query: null, + directives: [...specifiedDirectives, ...federationDirectives], + }), + ), ); + for (const module of modules) { + if (!module.resolvers) continue; + addResolversToSchema(schema, module.resolvers); + } + + return schema; +} + +function transformFederatedSchema(schema: GraphQLSchema): GraphQLSchema { // At this point in time, we have a schema to be printed into SDL which is // representative of what the user defined for their schema. This is before // we process any of the federation directives and add custom federation types @@ -91,10 +178,5 @@ export function buildFederatedSchema( return undefined; }); - for (const module of modules) { - if (!module.resolvers) continue; - addResolversToSchema(schema, module.resolvers); - } - return schema; } diff --git a/packages/apollo-federation/src/types.ts b/packages/apollo-federation/src/types.ts index b78bf635382..030c01419e1 100644 --- a/packages/apollo-federation/src/types.ts +++ b/packages/apollo-federation/src/types.ts @@ -54,20 +54,15 @@ function addTypeNameToPossibleReturn( return maybeObject as null | T & { __typename: string }; } -export type GraphQLReferenceResolver = ( +export type GraphQLReferenceResolver = ( reference: object, context: TContext, info: GraphQLResolveInfo, ) => any; -declare module 'graphql/type/definition' { - interface GraphQLObjectType { - resolveReference?: GraphQLReferenceResolver; - } - - interface GraphQLObjectTypeConfig { - resolveReference?: GraphQLReferenceResolver; - } +export interface ResolvableGraphQLObjectType + extends GraphQLObjectType { + resolveReference?: GraphQLReferenceResolver; } export const entitiesField: GraphQLFieldConfig = { @@ -89,11 +84,13 @@ export const entitiesField: GraphQLFieldConfig = { ); } - const resolveReference = type.resolveReference - ? type.resolveReference - : function defaultResolveReference() { - return reference; - }; + function defaultResolveReference() { + return reference; + } + + const resolveReference = + (type as ResolvableGraphQLObjectType).resolveReference || + defaultResolveReference; const result = resolveReference(reference, context, info); From 8412e824284b5e76681bf676a69f2feb0ad0b915 Mon Sep 17 00:00:00 2001 From: Jordan Date: Sun, 14 Jul 2019 19:46:31 -0700 Subject: [PATCH 2/6] test: cleanup buildFederatedSchema tests --- .../__tests__/buildFederatedSchema.test.ts | 416 ++++++++---------- 1 file changed, 190 insertions(+), 226 deletions(-) diff --git a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts b/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts index 28c85177d84..c7cd7c3bace 100644 --- a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts +++ b/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts @@ -17,34 +17,30 @@ const EMPTY_DOCUMENT = { definitions: [], }; -const testFederatedSchema = ( +const createBuildFederatedSchemaTests = ( name: string, - createSchema: () => GraphQLSchema, - runTest: (schema: GraphQLSchema) => Promise, + schema: GraphQLSchema, + testSchema: (schema: GraphQLSchema) => Promise, ) => { - const schema = createSchema(); - it(name, async () => { - await runTest(schema); + await testSchema(schema); }); it(`${name} (using "schema" argument)`, async () => { - await runTest(buildFederatedSchema(schema)); + await testSchema(buildFederatedSchema(schema)); }); }; describe('buildFederatedSchema', () => { - testFederatedSchema( + createBuildFederatedSchemaTests( 'should mark a type with a key field as an entity', - () => { - return buildFederatedSchema(gql` - type Product @key(fields: "upc") { - upc: String! - name: String - price: Int - } - `); - }, + buildFederatedSchema(gql` + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + } + `), async (schema: GraphQLSchema) => { expect(schema.getType('Product')).toMatchInlineSnapshot(` type Product { @@ -59,17 +55,15 @@ type Product { ); }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( `should mark a type with a key field as an entity`, - () => { - return buildFederatedSchema(gql` - type Product @key(fields: "upc") { - upc: String! - name: String - price: Int - } - `); - }, + buildFederatedSchema(gql` + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + } + `), async (schema: GraphQLSchema) => { expect(schema.getType('Product')).toMatchInlineSnapshot(` type Product { @@ -85,19 +79,17 @@ type Product { }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( `should mark a type with multiple key fields as an entity`, - () => { - return buildFederatedSchema(gql` - type Product @key(fields: "upc") @key(fields: "sku") { - upc: String! - sku: String! - name: String - price: Int - } - `); - }, - schema => { + buildFederatedSchema(gql` + type Product @key(fields: "upc") @key(fields: "sku") { + upc: String! + sku: String! + name: String + price: Int + } + `), + async (schema: GraphQLSchema) => { expect(schema.getType('Product')).toMatchInlineSnapshot(` type Product { upc: String! @@ -113,16 +105,14 @@ type Product { }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( `should not mark a type without a key field as an entity`, - () => { - return buildFederatedSchema(gql` - type Money { - amount: Int! - currencyCode: String! - } - `); - }, + buildFederatedSchema(gql` + type Money { + amount: Int! + currencyCode: String! + } + `), async (schema: GraphQLSchema) => { expect(schema.getType('Money')).toMatchInlineSnapshot(` type Money { @@ -133,30 +123,28 @@ type Money { }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( 'should preserve description text in generated SDL', - () => { - return buildFederatedSchema(gql` - "A user. This user is very complicated and requires so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so much description text" - type User @key(fields: "id") { - """ - The unique ID of the user. - """ - id: ID! - "The user's name." - name: String - username: String - foo( - "Description 1" - arg1: String - "Description 2" - arg2: String - "Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3" - arg3: String - ): String - } - `); - }, + buildFederatedSchema(gql` + "A user. This user is very complicated and requires so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so much description text" + type User @key(fields: "id") { + """ + The unique ID of the user. + """ + id: ID! + "The user's name." + name: String + username: String + foo( + "Description 1" + arg1: String + "Description 2" + arg2: String + "Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3" + arg3: String + ): String + } + `), async (schema: GraphQLSchema) => { const query = `query GetServiceDetails { _service { @@ -194,18 +182,16 @@ type User @key(fields: "id") { ); describe(`should add an _entities query root field to the schema`, () => { - testFederatedSchema( + createBuildFederatedSchemaTests( `when a query root type with the default name has been defined`, - () => { - return buildFederatedSchema(gql` - type Query { - rootField: String - } - type Product @key(fields: "upc") { - upc: ID! - } - `); - }, + buildFederatedSchema(gql` + type Query { + rootField: String + } + type Product @key(fields: "upc") { + upc: ID! + } + `), async (schema: GraphQLSchema) => { expect(schema.getQueryType()).toMatchInlineSnapshot(` type Query { @@ -217,22 +203,20 @@ type Query { }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( `when a query root type with a non-default name has been defined`, - () => { - return buildFederatedSchema(gql` - schema { - query: QueryRoot - } + buildFederatedSchema(gql` + schema { + query: QueryRoot + } - type QueryRoot { - rootField: String - } - type Product @key(fields: "upc") { - upc: ID! - } - `); - }, + type QueryRoot { + rootField: String + } + type Product @key(fields: "upc") { + upc: ID! + } + `), async (schema: GraphQLSchema) => { expect(schema.getQueryType()).toMatchInlineSnapshot(` type QueryRoot { @@ -245,11 +229,9 @@ type QueryRoot { ); }); describe(`should not add an _entities query root field to the schema`, () => { - testFederatedSchema( + createBuildFederatedSchemaTests( `when no query root type has been defined`, - () => { - return buildFederatedSchema(EMPTY_DOCUMENT); - }, + buildFederatedSchema(EMPTY_DOCUMENT), async (schema: GraphQLSchema) => { expect(schema.getQueryType()).toMatchInlineSnapshot(` type Query { @@ -258,15 +240,13 @@ type Query { `); }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( `when no types with keys are found`, - () => { - return buildFederatedSchema(gql` - type Query { - rootField: String - } - `); - }, + buildFederatedSchema(gql` + type Query { + rootField: String + } + `), async (schema: GraphQLSchema) => { expect(schema.getQueryType()).toMatchInlineSnapshot(` type Query { @@ -276,18 +256,16 @@ type Query { `); }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( `when only an interface with keys are found`, - () => { - return buildFederatedSchema(gql` - type Query { - rootField: String - } - interface Product @key(fields: "upc") { - upc: ID! - } - `); - }, + buildFederatedSchema(gql` + type Query { + rootField: String + } + interface Product @key(fields: "upc") { + upc: ID! + } + `), async (schema: GraphQLSchema) => { expect(schema.getQueryType()).toMatchInlineSnapshot(` type Query { @@ -299,37 +277,35 @@ type Query { ); }); describe('_entities root field', () => { - testFederatedSchema( + createBuildFederatedSchemaTests( 'executes resolveReference for a type if found', - () => { - return buildFederatedSchema([ - { - typeDefs: gql` - type Product @key(fields: "upc") { - upc: Int - name: String - } - type User @key(fields: "id") { - firstName: String - } - `, - resolvers: { - Product: { - __resolveReference(object) { - expect(object.upc).toEqual(1); - return { name: 'Apollo Gateway' }; - }, + buildFederatedSchema([ + { + typeDefs: gql` + type Product @key(fields: "upc") { + upc: Int + name: String + } + type User @key(fields: "id") { + firstName: String + } + `, + resolvers: { + Product: { + __resolveReference(object) { + expect(object.upc).toEqual(1); + return { name: 'Apollo Gateway' }; }, - User: { - __resolveReference(object) { - expect(object.id).toEqual(1); - return Promise.resolve({ firstName: 'James' }); - }, + }, + User: { + __resolveReference(object) { + expect(object.id).toEqual(1); + return Promise.resolve({ firstName: 'James' }); }, }, }, - ]); - }, + }, + ]), async (schema: GraphQLSchema) => { const query = `query GetEntities($representations: [_Any!]!) { _entities(representations: $representations) { @@ -361,16 +337,14 @@ type Query { expect(data._entities[1].firstName).toEqual('James'); }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( 'executes resolveReference with default representation values', - () => { - return buildFederatedSchema(gql` - type Product @key(fields: "upc") { - upc: Int - name: String - } - `); - }, + buildFederatedSchema(gql` + type Product @key(fields: "upc") { + upc: Int + name: String + } + `), async (schema: GraphQLSchema) => { const query = `query GetEntities($representations: [_Any!]!) { _entities(representations: $representations) { @@ -400,24 +374,22 @@ type Query { ); }); describe('_service root field', () => { - testFederatedSchema( + createBuildFederatedSchemaTests( 'keeps extension types when owner type is not present', - () => { - return buildFederatedSchema(gql` - type Review { - id: ID - } + buildFederatedSchema(gql` + type Review { + id: ID + } - extend type Review { - title: String - } + extend type Review { + title: String + } - extend type Product @key(fields: "upc") { - upc: String @external - reviews: [Review] - } - `); - }, + extend type Product @key(fields: "upc") { + upc: String @external + reviews: [Review] + } + `), async (schema: GraphQLSchema) => { const query = `query GetServiceDetails { _service { @@ -439,28 +411,26 @@ type Review { `); }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( 'keeps extension interface when owner interface is not present', - () => { - return buildFederatedSchema(gql` - type Review { - id: ID - } + buildFederatedSchema(gql` + type Review { + id: ID + } - extend type Review { - title: String - } + extend type Review { + title: String + } - interface Node @key(fields: "id") { - id: ID! - } + interface Node @key(fields: "id") { + id: ID! + } - extend interface Product @key(fields: "upc") { - upc: String @external - reviews: [Review] - } - `); - }, + extend interface Product @key(fields: "upc") { + upc: String @external + reviews: [Review] + } + `), async (schema: GraphQLSchema) => { const query = `query GetServiceDetails { _service { @@ -485,17 +455,15 @@ type Review { `); }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( 'returns valid sdl for @key directives', - () => { - return buildFederatedSchema(gql` - type Product @key(fields: "upc") { - upc: String! - name: String - price: Int - } - `); - }, + buildFederatedSchema(gql` + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + } + `), async (schema: GraphQLSchema) => { const query = `query GetServiceDetails { _service { @@ -512,17 +480,15 @@ type Review { `); }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( 'returns valid sdl for multiple @key directives', - () => { - return buildFederatedSchema(gql` - type Product @key(fields: "upc") @key(fields: "name") { - upc: String! - name: String - price: Int - } - `); - }, + buildFederatedSchema(gql` + type Product @key(fields: "upc") @key(fields: "name") { + upc: String! + name: String + price: Int + } + `), async (schema: GraphQLSchema) => { const query = `query GetServiceDetails { _service { @@ -540,28 +506,26 @@ type Review { `); }, ); - testFederatedSchema( + createBuildFederatedSchemaTests( 'supports all federation directives', - () => { - return buildFederatedSchema(gql` - type Review @key(fields: "id") { - id: ID! - body: String - author: User @provides(fields: "email") - product: Product @provides(fields: "upc") - } + buildFederatedSchema(gql` + type Review @key(fields: "id") { + id: ID! + body: String + author: User @provides(fields: "email") + product: Product @provides(fields: "upc") + } - extend type User @key(fields: "email") { - email: String @external - reviews: [Review] - } + extend type User @key(fields: "email") { + email: String @external + reviews: [Review] + } - extend type Product @key(fields: "upc") { - upc: String @external - reviews: [Review] - } - `); - }, + extend type Product @key(fields: "upc") { + upc: String @external + reviews: [Review] + } + `), async (schema: GraphQLSchema) => { const query = `query GetServiceDetails { _service { From 61d336040bf543afe136333c2bf48a0def2abe4d Mon Sep 17 00:00:00 2001 From: Jordan Date: Tue, 16 Jul 2019 09:56:55 -0700 Subject: [PATCH 3/6] test: remove duplicate test --- .../__tests__/buildFederatedSchema.test.ts | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts b/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts index c7cd7c3bace..10d4c9ea705 100644 --- a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts +++ b/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts @@ -48,29 +48,6 @@ type Product { name: String price: Int } -`); - - expect(schema.getType('_Entity')).toMatchInlineSnapshot( - `union _Entity = Product`, - ); - }, - ); - createBuildFederatedSchemaTests( - `should mark a type with a key field as an entity`, - buildFederatedSchema(gql` - type Product @key(fields: "upc") { - upc: String! - name: String - price: Int - } - `), - async (schema: GraphQLSchema) => { - expect(schema.getType('Product')).toMatchInlineSnapshot(` -type Product { - upc: String! - name: String - price: Int -} `); expect(schema.getType('_Entity')).toMatchInlineSnapshot( From 831067ba86e81ed94127af08d51daba989bea304 Mon Sep 17 00:00:00 2001 From: Jordan Date: Tue, 16 Jul 2019 16:39:24 -0700 Subject: [PATCH 4/6] fix: clean up buildFederatedSchema --- .../src/service/buildFederatedSchema.ts | 177 ++---------------- .../src/service/extractFederationResolvers.ts | 47 +++++ .../src/service/transformFederatedSchema.ts | 82 ++++++++ 3 files changed, 149 insertions(+), 157 deletions(-) create mode 100644 packages/apollo-federation/src/service/extractFederationResolvers.ts create mode 100644 packages/apollo-federation/src/service/transformFederatedSchema.ts diff --git a/packages/apollo-federation/src/service/buildFederatedSchema.ts b/packages/apollo-federation/src/service/buildFederatedSchema.ts index d5a7ccf778f..7a333363b46 100644 --- a/packages/apollo-federation/src/service/buildFederatedSchema.ts +++ b/packages/apollo-federation/src/service/buildFederatedSchema.ts @@ -1,182 +1,45 @@ -import { - DocumentNode, - GraphQLSchema, - isObjectType, - isUnionType, - GraphQLUnionType, - GraphQLObjectType, - specifiedDirectives, -} from 'graphql'; +import { DocumentNode, GraphQLSchema, specifiedDirectives } from 'graphql'; import { buildSchemaFromSDL, - transformSchema, GraphQLSchemaModule, modulesFromSDL, - addResolversToSchema, + GraphQLResolverMap, } from 'apollo-graphql'; -import federationDirectives, { typeIncludesDirective } from '../directives'; - -import { - serviceField, - entitiesField, - EntityType, - ResolvableGraphQLObjectType, - GraphQLReferenceResolver, -} from '../types'; - -import { printSchema } from './printFederatedSchema'; +import federationDirectives from '../directives'; import 'apollo-server-env'; - -interface ReferenceResolverMap { - [key: string]: { __resolveReference: GraphQLReferenceResolver }; -} +import { transformFederatedSchema } from './transformFederatedSchema'; +import { extractFederationResolvers } from './extractFederationResolvers'; export function buildFederatedSchema( - modulesSDLOrSchema: + modulesOrSDLOrSchema: | (GraphQLSchemaModule | DocumentNode)[] | DocumentNode | GraphQLSchema, ): GraphQLSchema { - if (modulesSDLOrSchema instanceof GraphQLSchema) { - return buildFederatedSchemaFromSchema(modulesSDLOrSchema); - } - - return buildSchemaFromModulesOrSDL(modulesSDLOrSchema); -} - -function resolveReferenceForType( - type: ResolvableGraphQLObjectType, -): GraphQLReferenceResolver | undefined { - if (type.resolveReference) { - return type.resolveReference; - } - - if (isObjectType(type)) { - const fields = type.getFields(); - - if (fields.__resolveReference) { - const __resolveReference = fields.__resolveReference - .resolve as GraphQLReferenceResolver; - - delete fields.__resolveReference; - - return __resolveReference; - } + // Extract federation specific resolvers from already constructed + // GraphQLSchema and transform it to a federated schema. + if (modulesOrSDLOrSchema instanceof GraphQLSchema) { + return transformFederatedSchema(modulesOrSDLOrSchema, [ + extractFederationResolvers(modulesOrSDLOrSchema), + ]); } - return; -} - -function referenceResolversForSchema( - schema: GraphQLSchema, -): ReferenceResolverMap { - const map: ReferenceResolverMap = {}; + // Transform *modules* or *sdl* into a federated schema. + const modules = modulesFromSDL(modulesOrSDLOrSchema); - for (const [typeName, type] of Object.entries(schema.getTypeMap())) { - const __resolveReference = resolveReferenceForType( - type as ResolvableGraphQLObjectType, - ); + const resolvers = modules + .filter(module => !!module.resolvers) + .map(module => module.resolvers as GraphQLResolverMap); - if (__resolveReference) { - map[typeName] = { __resolveReference }; - } - } - - return map; -} - -function buildFederatedSchemaFromSchema( - baseSchema: GraphQLSchema, -): GraphQLSchema { - const referenceResolvers = referenceResolversForSchema(baseSchema); - - const schema = transformFederatedSchema(baseSchema); - - addResolversToSchema(schema, referenceResolvers); - - return schema; -} - -function buildSchemaFromModulesOrSDL( - modulesOrSDL: (GraphQLSchemaModule | DocumentNode)[] | DocumentNode, -): GraphQLSchema { - const modules = modulesFromSDL(modulesOrSDL); - - const schema = transformFederatedSchema( + return transformFederatedSchema( buildSchemaFromSDL( modules, new GraphQLSchema({ - query: null, + query: undefined, directives: [...specifiedDirectives, ...federationDirectives], }), ), + resolvers, ); - - for (const module of modules) { - if (!module.resolvers) continue; - addResolversToSchema(schema, module.resolvers); - } - - return schema; -} - -function transformFederatedSchema(schema: GraphQLSchema): GraphQLSchema { - // At this point in time, we have a schema to be printed into SDL which is - // representative of what the user defined for their schema. This is before - // we process any of the federation directives and add custom federation types - // so its the right place to create our service definition sdl. - // - // We have to use a modified printSchema from graphql-js which includes - // support for preserving the *uses* of federation directives while removing - // their *definitions* from the sdl. - const sdl = printSchema(schema); - - // Add an empty query root type if none has been defined - if (!schema.getQueryType()) { - schema = new GraphQLSchema({ - ...schema.toConfig(), - query: new GraphQLObjectType({ - name: 'Query', - fields: {}, - }), - }); - } - - const entityTypes = Object.values(schema.getTypeMap()).filter( - type => isObjectType(type) && typeIncludesDirective(type, 'key'), - ); - const hasEntities = entityTypes.length > 0; - - schema = transformSchema(schema, type => { - // Add `_entities` and `_service` fields to query root type - if (isObjectType(type) && type === schema.getQueryType()) { - const config = type.toConfig(); - return new GraphQLObjectType({ - ...config, - fields: { - ...(hasEntities && { _entities: entitiesField }), - _service: { - ...serviceField, - resolve: () => ({ sdl }), - }, - ...config.fields, - }, - }); - } - - return undefined; - }); - - schema = transformSchema(schema, type => { - if (hasEntities && isUnionType(type) && type.name === EntityType.name) { - return new GraphQLUnionType({ - ...EntityType.toConfig(), - types: entityTypes.filter(isObjectType), - }); - } - return undefined; - }); - - return schema; } diff --git a/packages/apollo-federation/src/service/extractFederationResolvers.ts b/packages/apollo-federation/src/service/extractFederationResolvers.ts new file mode 100644 index 00000000000..99285d0905d --- /dev/null +++ b/packages/apollo-federation/src/service/extractFederationResolvers.ts @@ -0,0 +1,47 @@ +import { GraphQLSchema, isObjectType } from 'graphql'; +import { GraphQLResolverMap } from 'apollo-graphql'; +import { + GraphQLReferenceResolver, + ResolvableGraphQLObjectType, +} from '../types'; + +function extractFederationResolversForType( + type: ResolvableGraphQLObjectType, +): { __resolveReference: GraphQLReferenceResolver } | undefined { + if (type.resolveReference) { + return { __resolveReference: type.resolveReference }; + } + + if (isObjectType(type)) { + const fields = type.getFields(); + + if (fields.__resolveReference) { + const __resolveReference = fields.__resolveReference + .resolve as GraphQLReferenceResolver; + + delete fields.__resolveReference; + + return { __resolveReference }; + } + } + + return; +} + +export function extractFederationResolvers( + schema: GraphQLSchema, +): GraphQLResolverMap { + const map: GraphQLResolverMap = {}; + + for (const [typeName, type] of Object.entries(schema.getTypeMap())) { + const resolvers = extractFederationResolversForType( + type as ResolvableGraphQLObjectType, + ); + + if (resolvers) { + map[typeName] = resolvers; + } + } + + return map; +} diff --git a/packages/apollo-federation/src/service/transformFederatedSchema.ts b/packages/apollo-federation/src/service/transformFederatedSchema.ts new file mode 100644 index 00000000000..f8a4f8e59b0 --- /dev/null +++ b/packages/apollo-federation/src/service/transformFederatedSchema.ts @@ -0,0 +1,82 @@ +import { + GraphQLSchema, + isObjectType, + isUnionType, + GraphQLUnionType, + GraphQLObjectType, +} from 'graphql'; +import { + transformSchema, + addResolversToSchema, + GraphQLResolverMap, +} from 'apollo-graphql'; +import { typeIncludesDirective } from '../directives'; +import { serviceField, entitiesField, EntityType } from '../types'; +import { printSchema } from './printFederatedSchema'; + +export function transformFederatedSchema( + schema: GraphQLSchema, + resolvers: GraphQLResolverMap[] | GraphQLResolverMap = [], +): GraphQLSchema { + // At this point in time, we have a schema to be printed into SDL which is + // representative of what the user defined for their schema. This is before + // we process any of the federation directives and add custom federation types + // so its the right place to create our service definition sdl. + // + // We have to use a modified printSchema from graphql-js which includes + // support for preserving the *uses* of federation directives while removing + // their *definitions* from the sdl. + const sdl = printSchema(schema); + + // Add an empty query root type if none has been defined + if (!schema.getQueryType()) { + schema = new GraphQLSchema({ + ...schema.toConfig(), + query: new GraphQLObjectType({ + name: 'Query', + fields: {}, + }), + }); + } + + const entityTypes = Object.values(schema.getTypeMap()).filter( + type => isObjectType(type) && typeIncludesDirective(type, 'key'), + ); + const hasEntities = entityTypes.length > 0; + + schema = transformSchema(schema, type => { + // Add `_entities` and `_service` fields to query root type + if (isObjectType(type) && type === schema.getQueryType()) { + const config = type.toConfig(); + return new GraphQLObjectType({ + ...config, + fields: { + ...(hasEntities && { _entities: entitiesField }), + _service: { + ...serviceField, + resolve: () => ({ sdl }), + }, + ...config.fields, + }, + }); + } + + return undefined; + }); + + schema = transformSchema(schema, type => { + if (hasEntities && isUnionType(type) && type.name === EntityType.name) { + return new GraphQLUnionType({ + ...EntityType.toConfig(), + types: entityTypes.filter(isObjectType), + }); + } + return undefined; + }); + + (Array.isArray(resolvers) ? resolvers : [resolvers]).forEach(map => + addResolversToSchema(schema, map), + ); + + return schema; +} From 53757b0dbacc473f01c8dc8235b734623c0a793f Mon Sep 17 00:00:00 2001 From: Jordan Date: Wed, 31 Jul 2019 13:05:13 -0700 Subject: [PATCH 5/6] fix(apollo-federation): cleanups --- .../__tests__/buildFederatedSchema.test.ts | 31 +++++++++---------- .../src/service/extractFederationResolvers.ts | 23 +++----------- .../src/service/transformFederatedSchema.ts | 6 ++-- 3 files changed, 21 insertions(+), 39 deletions(-) diff --git a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts b/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts index 10d4c9ea705..091fa27a632 100644 --- a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts +++ b/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts @@ -8,6 +8,7 @@ import { GraphQLInt, } from 'graphql'; import { buildFederatedSchema } from '../buildFederatedSchema'; +import { ResolvableGraphQLObjectType } from '../../types'; import { typeSerializer } from '../../snapshotSerializers'; expect.addSnapshotSerializer(typeSerializer); @@ -534,6 +535,8 @@ extend type User @key(fields: "email") { }); it('executes resolveReference for a type if found using manually created GraphQLSchema', async () => { + expect.assertions(5); + const query = `query GetEntities($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { @@ -552,18 +555,11 @@ extend type User @key(fields: "email") { ], }; - const product: GraphQLObjectType = new GraphQLObjectType({ + const product: ResolvableGraphQLObjectType = new GraphQLObjectType({ name: 'Product', fields: () => ({ upc: { type: GraphQLInt }, name: { type: GraphQLString }, - __resolveReference: { - type: product, - resolve: (object: any) => { - expect(object.upc).toEqual(1); - return { name: 'Apollo Gateway' }; - }, - }, }), astNode: { kind: 'ObjectTypeDefinition', @@ -598,17 +594,15 @@ extend type User @key(fields: "email") { }, }); - const user: GraphQLObjectType = new GraphQLObjectType({ + product.resolveReference = (object: any) => { + expect(object.upc).toEqual(1); + return { name: 'Apollo Gateway' }; + }; + + const user: ResolvableGraphQLObjectType = new GraphQLObjectType({ name: 'User', fields: () => ({ firstName: { type: GraphQLString }, - __resolveReference: { - type: user, - resolve: (object: any) => { - expect(object.id).toEqual(1); - return Promise.resolve({ firstName: 'James' }); - }, - }, }), astNode: { kind: 'ObjectTypeDefinition', @@ -643,6 +637,11 @@ extend type User @key(fields: "email") { }, }); + user.resolveReference = (object: any) => { + expect(object.id).toEqual(1); + return Promise.resolve({ firstName: 'James' }); + }; + const schema = buildFederatedSchema( new GraphQLSchema({ query: null, diff --git a/packages/apollo-federation/src/service/extractFederationResolvers.ts b/packages/apollo-federation/src/service/extractFederationResolvers.ts index 99285d0905d..31a96a58572 100644 --- a/packages/apollo-federation/src/service/extractFederationResolvers.ts +++ b/packages/apollo-federation/src/service/extractFederationResolvers.ts @@ -1,31 +1,16 @@ -import { GraphQLSchema, isObjectType } from 'graphql'; +import { GraphQLSchema } from 'graphql'; import { GraphQLResolverMap } from 'apollo-graphql'; import { GraphQLReferenceResolver, ResolvableGraphQLObjectType, } from '../types'; -function extractFederationResolversForType( +function extractFederationResolverForType( type: ResolvableGraphQLObjectType, -): { __resolveReference: GraphQLReferenceResolver } | undefined { +): { __resolveReference: GraphQLReferenceResolver } | void { if (type.resolveReference) { return { __resolveReference: type.resolveReference }; } - - if (isObjectType(type)) { - const fields = type.getFields(); - - if (fields.__resolveReference) { - const __resolveReference = fields.__resolveReference - .resolve as GraphQLReferenceResolver; - - delete fields.__resolveReference; - - return { __resolveReference }; - } - } - - return; } export function extractFederationResolvers( @@ -34,7 +19,7 @@ export function extractFederationResolvers( const map: GraphQLResolverMap = {}; for (const [typeName, type] of Object.entries(schema.getTypeMap())) { - const resolvers = extractFederationResolversForType( + const resolvers = extractFederationResolverForType( type as ResolvableGraphQLObjectType, ); diff --git a/packages/apollo-federation/src/service/transformFederatedSchema.ts b/packages/apollo-federation/src/service/transformFederatedSchema.ts index f8a4f8e59b0..b1a53dedc92 100644 --- a/packages/apollo-federation/src/service/transformFederatedSchema.ts +++ b/packages/apollo-federation/src/service/transformFederatedSchema.ts @@ -16,7 +16,7 @@ import { printSchema } from './printFederatedSchema'; export function transformFederatedSchema( schema: GraphQLSchema, - resolvers: GraphQLResolverMap[] | GraphQLResolverMap = [], + resolvers: GraphQLResolverMap[] = [], ): GraphQLSchema { // At this point in time, we have a schema to be printed into SDL which is // representative of what the user defined for their schema. This is before @@ -74,9 +74,7 @@ export function transformFederatedSchema( return undefined; }); - (Array.isArray(resolvers) ? resolvers : [resolvers]).forEach(map => - addResolversToSchema(schema, map), - ); + resolvers.forEach(resolver => addResolversToSchema(schema, resolver)); return schema; } From b40a79d372bb8e08d137e0f373a95fefbafcd2db Mon Sep 17 00:00:00 2001 From: Jordan Date: Wed, 31 Jul 2019 13:05:45 -0700 Subject: [PATCH 6/6] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- packages/apollo-federation/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 971fbecd1fc..0072cf38b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The version headers in this history reflect the versions of Apollo Server itself > The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the the appropriate changes within that release will be moved into the new section. -- _Nothing yet!_ +- `@apollo/federation`: Support for creating federated schemas from existing schemas [#3013](https://github.com/apollographql/apollo-server/pull/3013) ### v2.8.1 @@ -61,7 +61,7 @@ The version headers in this history reflect the versions of Apollo Server itself } ``` - `apollo-engine-reporting`: **Behavior change**: By default, send no GraphQL request headers and values to Apollo's servers instead of sending all. Adding the new EngineReportingOption `sendHeaders` to send some or all header values. This replaces the `privateHeaders` option, which is now deprecated. [PR #2931](https://github.com/apollographql/apollo-server/pull/2931) - + To maintain the previous behavior of transmitting **all** GraphQL request headers and values, configure `engine`.`sendHeaders` as following: ```js engine: { diff --git a/packages/apollo-federation/CHANGELOG.md b/packages/apollo-federation/CHANGELOG.md index b2133e2965a..2e5acd239e9 100644 --- a/packages/apollo-federation/CHANGELOG.md +++ b/packages/apollo-federation/CHANGELOG.md @@ -2,6 +2,8 @@ ### vNEXT +* Support for creating federated schemas from existing schemas [#3013](https://github.com/apollographql/apollo-server/pull/3013) + # v0.6.8 * Support __typenames if defined by an incoming operation [#2922](https://github.com/apollographql/apollo-server/pull/2922)