diff --git a/packages/apollo-federation/src/composition/__tests__/normalize.test.ts b/packages/apollo-federation/src/composition/__tests__/normalize.test.ts index e7bb5c4732d..bc6c05af613 100644 --- a/packages/apollo-federation/src/composition/__tests__/normalize.test.ts +++ b/packages/apollo-federation/src/composition/__tests__/normalize.test.ts @@ -3,6 +3,7 @@ import { defaultRootOperationTypes, replaceExtendedDefinitionsWithExtensions, normalizeTypeDefs, + stripFederationPrimitives, } from '../normalize'; import { astSerializer } from '../../snapshotSerializers'; @@ -142,15 +143,160 @@ describe('SDL normalization and its respective parts', () => { }); }); + describe('stripFederationPrimitives', () => { + it(`removes all federation directive definitions`, () => { + const typeDefs = gql` + directive @key(fields: _FieldSet!) on OBJECT | INTERFACE + directive @external on FIELD_DEFINITION + directive @requires(fields: _FieldSet!) on FIELD_DEFINITION + directive @provides(fields: _FieldSet!) on FIELD_DEFINITION + directive @extends on OBJECT | INTERFACE + + type Query { + thing: String + } + `; + + expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(` + type Query { + thing: String + } + `); + }); + + it(`doesn't remove custom directive definitions`, () => { + const typeDefs = gql` + directive @custom on OBJECT + + type Query { + thing: String + } + `; + + expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(` + directive @custom on OBJECT + + type Query { + thing: String + } + `); + }); + + it(`removes all federation type definitions (scalars, unions, object types)`, () => { + const typeDefs = gql` + scalar _Any + scalar _FieldSet + + union _Entity + + type _Service { + sdl: String + } + + type Query { + thing: String + } + `; + + expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(` + type Query { + thing: String + } + `); + }); + + it(`doesn't remove custom scalar, union, or object type definitions`, () => { + const typeDefs = gql` + scalar CustomScalar + + type CustomType { + field: String! + } + + union CustomUnion + + type Query { + thing: String + } + `; + + expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(` + scalar CustomScalar + + type CustomType { + field: String! + } + + union CustomUnion + + type Query { + thing: String + } + `); + }); + + it(`removes all federation field definitions (_service, _entities)`, () => { + const typeDefs = gql` + type Query { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + thing: String + } + `; + + expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(` + type Query { + thing: String + } + `); + }); + + it(`removes the Query type altogether if it has no fields left after normalization`, () => { + const typeDefs = gql` + type Query { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + } + + type Custom { + field: String + } + `; + + expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(` + type Custom { + field: String + } + `); + }); + }); + describe('normalizeTypeDefs', () => { it('integration', () => { const typeDefsToNormalize = gql` + directive @key(fields: _FieldSet!) on OBJECT | INTERFACE + directive @external on FIELD_DEFINITION + directive @requires(fields: _FieldSet!) on FIELD_DEFINITION + directive @provides(fields: _FieldSet!) on FIELD_DEFINITION + directive @extends on OBJECT | INTERFACE + + scalar _Any + scalar _FieldSet + + union _Entity + + type _Service { + sdl: String + } + schema { query: RootQuery mutation: RootMutation } type RootQuery { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! product: Product } diff --git a/packages/apollo-federation/src/composition/compose.ts b/packages/apollo-federation/src/composition/compose.ts index d88d087be0e..ad1742f02f6 100644 --- a/packages/apollo-federation/src/composition/compose.ts +++ b/packages/apollo-federation/src/composition/compose.ts @@ -30,6 +30,7 @@ import { isFederationDirective, executableDirectiveLocations, stripTypeSystemDirectivesFromTypeDefs, + defaultRootOperationNameLookup, } from './utils'; import { ServiceDefinition, @@ -41,13 +42,13 @@ import { compositionRules } from './rules'; const EmptyQueryDefinition = { kind: Kind.OBJECT_TYPE_DEFINITION, - name: { kind: Kind.NAME, value: 'Query' }, + name: { kind: Kind.NAME, value: defaultRootOperationNameLookup.query }, fields: [], serviceName: null, }; const EmptyMutationDefinition = { kind: Kind.OBJECT_TYPE_DEFINITION, - name: { kind: Kind.NAME, value: 'Mutation' }, + name: { kind: Kind.NAME, value: defaultRootOperationNameLookup.mutation }, fields: [], serviceName: null, }; @@ -531,16 +532,9 @@ export function composeServices(services: ServiceDefinition[]) { // TODO: We should fix this to take non-default operation root types in // implementing services into account. - - const operationTypeMap = { - query: 'Query', - mutation: 'Mutation', - subscription: 'Subscription', - }; - schema = new GraphQLSchema({ ...schema.toConfig(), - ...mapValues(operationTypeMap, typeName => + ...mapValues(defaultRootOperationNameLookup, typeName => typeName ? (schema.getType(typeName) as GraphQLObjectType) : undefined, diff --git a/packages/apollo-federation/src/composition/index.ts b/packages/apollo-federation/src/composition/index.ts index 1d30fd138bb..58c910b8a25 100644 --- a/packages/apollo-federation/src/composition/index.ts +++ b/packages/apollo-federation/src/composition/index.ts @@ -2,4 +2,5 @@ export * from './compose'; export * from './composeAndValidate'; export * from './types'; export { compositionRules } from './rules'; -export { defaultRootOperationNameLookup, normalizeTypeDefs } from './normalize'; +export { normalizeTypeDefs } from './normalize'; +export { defaultRootOperationNameLookup } from './utils'; diff --git a/packages/apollo-federation/src/composition/normalize.ts b/packages/apollo-federation/src/composition/normalize.ts index 36fe58d51fe..6db552f0744 100644 --- a/packages/apollo-federation/src/composition/normalize.ts +++ b/packages/apollo-federation/src/composition/normalize.ts @@ -3,27 +3,30 @@ import { DocumentNode, visit, ObjectTypeDefinitionNode, + ObjectTypeExtensionNode, Kind, - OperationTypeNode, InterfaceTypeDefinitionNode, + VisitFn, } from 'graphql'; -import { findDirectivesOnTypeOrField, defKindToExtKind } from './utils'; +import { + findDirectivesOnTypeOrField, + defKindToExtKind, + reservedRootFields, + defaultRootOperationNameLookup +} from './utils'; +import federationDirectives from '../directives'; export function normalizeTypeDefs(typeDefs: DocumentNode) { - return defaultRootOperationTypes( - replaceExtendedDefinitionsWithExtensions(typeDefs), + // The order of this is important - `stripFederationPrimitives` must come after + // `defaultRootOperationTypes` because it depends on the `Query` type being named + // its default: `Query`. + return stripFederationPrimitives( + defaultRootOperationTypes( + replaceExtendedDefinitionsWithExtensions(typeDefs), + ), ); } -// Map of OperationTypeNode to its respective default root operation type name -export const defaultRootOperationNameLookup: { - [node in OperationTypeNode]: DefaultRootOperationTypeName; -} = { - query: 'Query', - mutation: 'Mutation', - subscription: 'Subscription', -}; - export function defaultRootOperationTypes( typeDefs: DocumentNode, ): DocumentNode { @@ -251,3 +254,67 @@ export function replaceExtendedDefinitionsWithExtensions( return typeDefsWithExtendedTypesReplaced; } + +// For non-ApolloServer libraries that support federation, this allows a +// library to report the entire schema's SDL rather than an awkward, stripped out +// subset of the schema. Generally there's no need to include the federation +// primitives, but in many cases it's more difficult to exclude them. +// +// This removes the following from a GraphQL Document: +// directives: @external, @key, @requires, @provides, @extends +// scalars: _Any, _FieldSet +// union: _Entity +// object type: _Service +// Query fields: _service, _entities +export function stripFederationPrimitives(document: DocumentNode) { + const typeDefinitionVisitor: VisitFn< + any, + ObjectTypeDefinitionNode | ObjectTypeExtensionNode + > = (node) => { + // Remove the `_entities` and `_service` fields from the `Query` type + if (node.name.value === defaultRootOperationNameLookup.query) { + const filteredFieldDefinitions = node.fields?.filter( + (fieldDefinition) => + !reservedRootFields.includes(fieldDefinition.name.value), + ); + + // If the 'Query' type is now empty just remove it + if (!filteredFieldDefinitions || filteredFieldDefinitions.length === 0) { + return null; + } + + return { + ...node, + fields: filteredFieldDefinitions, + }; + } + + // Remove the _Service type from the document + const isFederationType = node.name.value === '_Service'; + return isFederationType ? null : node; + }; + + return visit(document, { + // Remove all federation directive definitions from the document + DirectiveDefinition(node) { + const isFederationDirective = federationDirectives.some( + (directive) => directive.name === node.name.value, + ); + return isFederationDirective ? null : node; + }, + // Remove all federation scalar definitions from the document + ScalarTypeDefinition(node) { + const isFederationScalar = ['_Any', '_FieldSet'].includes( + node.name.value, + ); + return isFederationScalar ? null : node; + }, + // Remove all federation union definitions from the document + UnionTypeDefinition(node) { + const isFederationUnion = node.name.value === "_Entity"; + return isFederationUnion ? null : node; + }, + ObjectTypeDefinition: typeDefinitionVisitor, + ObjectTypeExtension: typeDefinitionVisitor, + }); +} diff --git a/packages/apollo-federation/src/composition/utils.ts b/packages/apollo-federation/src/composition/utils.ts index 9422f29c102..6dc2c331aa1 100644 --- a/packages/apollo-federation/src/composition/utils.ts +++ b/packages/apollo-federation/src/composition/utils.ts @@ -29,9 +29,10 @@ import { ASTNode, DirectiveDefinitionNode, GraphQLDirective, + OperationTypeNode, } from 'graphql'; import Maybe from 'graphql/tsutils/Maybe'; -import { ExternalFieldDefinition } from './types'; +import { ExternalFieldDefinition, DefaultRootOperationTypeName } from './types'; import federationDirectives from '../directives'; export function isStringValueNode(node: any): node is StringValueNode { @@ -549,3 +550,14 @@ export const executableDirectiveLocations = [ export function isFederationDirective(directive: GraphQLDirective): boolean { return federationDirectives.some(({ name }) => name === directive.name); } + +export const reservedRootFields = ['_service', '_entities']; + +// Map of OperationTypeNode to its respective default root operation type name +export const defaultRootOperationNameLookup: { + [node in OperationTypeNode]: DefaultRootOperationTypeName; +} = { + query: 'Query', + mutation: 'Mutation', + subscription: 'Subscription', +}; diff --git a/packages/apollo-federation/src/composition/validate/preComposition/reservedFieldUsed.ts b/packages/apollo-federation/src/composition/validate/preComposition/reservedFieldUsed.ts index f5969361f7e..0434f7bd28c 100644 --- a/packages/apollo-federation/src/composition/validate/preComposition/reservedFieldUsed.ts +++ b/packages/apollo-federation/src/composition/validate/preComposition/reservedFieldUsed.ts @@ -1,8 +1,10 @@ import { GraphQLError, visit } from 'graphql'; import { ServiceDefinition } from '../../types'; -import { logServiceAndType, errorWithCode } from '../../utils'; - -const reservedRootFields = ['_service', '_entities']; +import { + logServiceAndType, + errorWithCode, + reservedRootFields +} from '../../utils'; /** * - Schemas should not define the _service or _entitites fields on the query root diff --git a/packages/apollo-federation/src/composition/validate/preNormalization/rootFieldUsed.ts b/packages/apollo-federation/src/composition/validate/preNormalization/rootFieldUsed.ts index 7ff2384b60f..1e6bdc13951 100644 --- a/packages/apollo-federation/src/composition/validate/preNormalization/rootFieldUsed.ts +++ b/packages/apollo-federation/src/composition/validate/preNormalization/rootFieldUsed.ts @@ -1,13 +1,15 @@ import { GraphQLError, visit, - OperationTypeNode, ObjectTypeDefinitionNode, ObjectTypeExtensionNode, } from 'graphql'; import { ServiceDefinition, DefaultRootOperationTypeName } from '../../types'; - -import { logServiceAndType, errorWithCode } from '../../utils'; +import { + logServiceAndType, + errorWithCode, + defaultRootOperationNameLookup +} from '../../utils'; /** * - When a schema definition or extension is provided, warn user against using @@ -20,15 +22,6 @@ export const rootFieldUsed = ({ }: ServiceDefinition) => { const errors: GraphQLError[] = []; - // Map of OperationTypeNode to its respective default root operation type name - const defaultRootOperationNameLookup: { - [node in OperationTypeNode]: DefaultRootOperationTypeName; - } = { - query: 'Query', - mutation: 'Mutation', - subscription: 'Subscription', - }; - // Array of default root operation names const defaultRootOperationNames = Object.values( defaultRootOperationNameLookup,