diff --git a/CHANGELOG.md b/CHANGELOG.md index 509bbcd04f1..a8aa97136f2 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 @@ -57,7 +57,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) diff --git a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts b/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts index d09f250d900..091fa27a632 100644 --- a/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts +++ b/packages/apollo-federation/src/service/__tests__/buildFederatedSchema.test.ts @@ -1,6 +1,14 @@ import gql from 'graphql-tag'; -import { Kind, graphql } from 'graphql'; +import { + Kind, + graphql, + GraphQLSchema, + GraphQLObjectType, + GraphQLString, + GraphQLInt, +} from 'graphql'; import { buildFederatedSchema } from '../buildFederatedSchema'; +import { ResolvableGraphQLObjectType } from '../../types'; import { typeSerializer } from '../../snapshotSerializers'; expect.addSnapshotSerializer(typeSerializer); @@ -10,17 +18,32 @@ const EMPTY_DOCUMENT = { definitions: [], }; +const createBuildFederatedSchemaTests = ( + name: string, + schema: GraphQLSchema, + testSchema: (schema: GraphQLSchema) => Promise, +) => { + it(name, async () => { + await testSchema(schema); + }); + + it(`${name} (using "schema" argument)`, async () => { + await testSchema(buildFederatedSchema(schema)); + }); +}; + describe('buildFederatedSchema', () => { - it(`should mark a type with a key field as an entity`, () => { - const schema = buildFederatedSchema(gql` + 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 } - `); - - expect(schema.getType('Product')).toMatchInlineSnapshot(` + `), + async (schema: GraphQLSchema) => { + expect(schema.getType('Product')).toMatchInlineSnapshot(` type Product { upc: String! name: String @@ -28,22 +51,24 @@ type Product { } `); - expect(schema.getType('_Entity')).toMatchInlineSnapshot( - `union _Entity = 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` + createBuildFederatedSchemaTests( + `should mark a type with multiple key fields as an entity`, + buildFederatedSchema(gql` type Product @key(fields: "upc") @key(fields: "sku") { upc: String! sku: String! name: String price: Int } - `); - - expect(schema.getType('Product')).toMatchInlineSnapshot(` + `), + async (schema: GraphQLSchema) => { + expect(schema.getType('Product')).toMatchInlineSnapshot(` type Product { upc: String! sku: String! @@ -52,34 +77,33 @@ type Product { } `); - expect(schema.getType('_Entity')).toMatchInlineSnapshot( - `union _Entity = 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` + createBuildFederatedSchemaTests( + `should not mark a type without a key field as an entity`, + buildFederatedSchema(gql` type Money { amount: Int! currencyCode: String! } - `); - - expect(schema.getType('Money')).toMatchInlineSnapshot(` + `), + 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` + createBuildFederatedSchemaTests( + 'should preserve description text in generated SDL', + 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") { """ @@ -98,11 +122,17 @@ type Money { 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,30 +156,34 @@ 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` + createBuildFederatedSchemaTests( + `when a query root type with the default name has been defined`, + buildFederatedSchema(gql` type Query { rootField: String } type Product @key(fields: "upc") { upc: ID! } - `); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` + `), + 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` + createBuildFederatedSchemaTests( + `when a query root type with a non-default name has been defined`, + buildFederatedSchema(gql` schema { query: QueryRoot } @@ -160,80 +194,70 @@ type Query { type Product @key(fields: "upc") { upc: ID! } - `); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` + `), + 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(` + createBuildFederatedSchemaTests( + `when no query root type has been defined`, + 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` + }, + ); + createBuildFederatedSchemaTests( + `when no types with keys are found`, + buildFederatedSchema(gql` type Query { rootField: String } - `); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` + `), + 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` + }, + ); + createBuildFederatedSchemaTests( + `when only an interface with keys are found`, + buildFederatedSchema(gql` type Query { rootField: String } interface Product @key(fields: "upc") { upc: ID! } - `); - - expect(schema.getQueryType()).toMatchInlineSnapshot(` + `), + 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([ + createBuildFederatedSchemaTests( + 'executes resolveReference for a type if found', + buildFederatedSchema([ { typeDefs: gql` type Product @key(fields: "upc") { @@ -259,20 +283,48 @@ type Query { }, }, }, - ]); - 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'); + }, + ); + createBuildFederatedSchemaTests( + 'executes resolveReference with default representation values', + 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,37 +333,28 @@ 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 { - _service { - sdl - } - }`; - const schema = buildFederatedSchema(gql` + createBuildFederatedSchemaTests( + 'keeps extension types when owner type is not present', + buildFederatedSchema(gql` type Review { id: ID } @@ -324,12 +367,17 @@ type Query { 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") { + `), + async (schema: GraphQLSchema) => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + 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,14 +387,11 @@ type Review { title: String } `); - }); - it('keeps extension interface when owner interface is not present', async () => { - const query = `query GetServiceDetails { - _service { - sdl - } - }`; - const schema = buildFederatedSchema(gql` + }, + ); + createBuildFederatedSchemaTests( + 'keeps extension interface when owner interface is not present', + buildFederatedSchema(gql` type Review { id: ID } @@ -363,11 +408,16 @@ type Review { 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") { + `), + async (schema: GraphQLSchema) => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl).toEqual(`interface Node @key(fields: "id") { id: ID! } @@ -381,62 +431,62 @@ type Review { title: String } `); - }); - it('returns valid sdl for @key directives', async () => { - const query = `query GetServiceDetails { - _service { - sdl - } - }`; - const schema = buildFederatedSchema(gql` + }, + ); + createBuildFederatedSchemaTests( + 'returns valid sdl for @key directives', + 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") { + `), + async (schema: GraphQLSchema) => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + 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 { - _service { - sdl - } - }`; - const schema = buildFederatedSchema(gql` + }, + ); + createBuildFederatedSchemaTests( + 'returns valid sdl for multiple @key directives', + 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") { + `), + async (schema: GraphQLSchema) => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + 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 { - _service { - sdl - } - }`; - - const schema = buildFederatedSchema(gql` + }, + ); + createBuildFederatedSchemaTests( + 'supports all federation directives', + buildFederatedSchema(gql` type Review @key(fields: "id") { id: ID! body: String @@ -453,12 +503,17 @@ type Review { 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") { + `), + async (schema: GraphQLSchema) => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + 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 +530,134 @@ extend type User @key(fields: "email") { reviews: [Review] } `); + }, + ); + }); + + 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 { + name + } + ... on User { + firstName + } + } + }`; + + const variables = { + representations: [ + { __typename: 'Product', upc: 1 }, + { __typename: 'User', id: 1 }, + ], + }; + + const product: ResolvableGraphQLObjectType = new GraphQLObjectType({ + name: 'Product', + fields: () => ({ + upc: { type: GraphQLInt }, + name: { type: GraphQLString }, + }), + 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, + }, + }, + ], + }, + ], + }, }); + + product.resolveReference = (object: any) => { + expect(object.upc).toEqual(1); + return { name: 'Apollo Gateway' }; + }; + + const user: ResolvableGraphQLObjectType = new GraphQLObjectType({ + name: 'User', + fields: () => ({ + firstName: { type: GraphQLString }, + }), + 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, + }, + }, + ], + }, + ], + }, + }); + + user.resolveReference = (object: any) => { + expect(object.id).toEqual(1); + return Promise.resolve({ firstName: 'James' }); + }; + + 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..7a333363b46 100644 --- a/packages/apollo-federation/src/service/buildFederatedSchema.ts +++ b/packages/apollo-federation/src/service/buildFederatedSchema.ts @@ -1,100 +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 } from '../types'; - -import { printSchema } from './printFederatedSchema'; +import federationDirectives from '../directives'; import 'apollo-server-env'; +import { transformFederatedSchema } from './transformFederatedSchema'; +import { extractFederationResolvers } from './extractFederationResolvers'; export function buildFederatedSchema( - modulesOrSDL: (GraphQLSchemaModule | DocumentNode)[] | DocumentNode, + modulesOrSDLOrSchema: + | (GraphQLSchemaModule | DocumentNode)[] + | DocumentNode + | GraphQLSchema, ): GraphQLSchema { - const modules = modulesFromSDL(modulesOrSDL); + // Extract federation specific resolvers from already constructed + // GraphQLSchema and transform it to a federated schema. + if (modulesOrSDLOrSchema instanceof GraphQLSchema) { + return transformFederatedSchema(modulesOrSDLOrSchema, [ + extractFederationResolvers(modulesOrSDLOrSchema), + ]); + } - let schema = buildSchemaFromSDL( - modules, - new GraphQLSchema({ - query: undefined, - directives: [...specifiedDirectives, ...federationDirectives], - }), - ); + // Transform *modules* or *sdl* into a federated schema. + const modules = modulesFromSDL(modulesOrSDLOrSchema); - // 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); + const resolvers = modules + .filter(module => !!module.resolvers) + .map(module => module.resolvers as GraphQLResolverMap); - // 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: {}, + return transformFederatedSchema( + buildSchemaFromSDL( + modules, + new GraphQLSchema({ + query: undefined, + directives: [...specifiedDirectives, ...federationDirectives], }), - }); - } - - const entityTypes = Object.values(schema.getTypeMap()).filter( - type => isObjectType(type) && typeIncludesDirective(type, 'key'), + ), + resolvers, ); - 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; - }); - - for (const module of modules) { - if (!module.resolvers) continue; - addResolversToSchema(schema, module.resolvers); - } - - 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..31a96a58572 --- /dev/null +++ b/packages/apollo-federation/src/service/extractFederationResolvers.ts @@ -0,0 +1,32 @@ +import { GraphQLSchema } from 'graphql'; +import { GraphQLResolverMap } from 'apollo-graphql'; +import { + GraphQLReferenceResolver, + ResolvableGraphQLObjectType, +} from '../types'; + +function extractFederationResolverForType( + type: ResolvableGraphQLObjectType, +): { __resolveReference: GraphQLReferenceResolver } | void { + if (type.resolveReference) { + return { __resolveReference: type.resolveReference }; + } +} + +export function extractFederationResolvers( + schema: GraphQLSchema, +): GraphQLResolverMap { + const map: GraphQLResolverMap = {}; + + for (const [typeName, type] of Object.entries(schema.getTypeMap())) { + const resolvers = extractFederationResolverForType( + 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..b1a53dedc92 --- /dev/null +++ b/packages/apollo-federation/src/service/transformFederatedSchema.ts @@ -0,0 +1,80 @@ +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[] = [], +): 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; + }); + + resolvers.forEach(resolver => addResolversToSchema(schema, resolver)); + + return schema; +} diff --git a/packages/apollo-federation/src/types.ts b/packages/apollo-federation/src/types.ts index 7126b32c611..12e7812e671 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; // FIXME somehow get this to show up special in Engine traces? const result = resolveReference(reference, context, info);