UNPKG

@apollo/federation

Version:
325 lines (304 loc) 10.2 kB
import { DefaultRootOperationTypeName } from './types'; import { DocumentNode, visit, ObjectTypeDefinitionNode, ObjectTypeExtensionNode, Kind, InterfaceTypeDefinitionNode, specifiedDirectives, } from 'graphql'; import { findDirectivesOnNode, defKindToExtKind, reservedRootFields, defaultRootOperationNameLookup } from './utils'; // Importing from 'dist' is not actually supported as part of the public API, // but this allows us not to duplicate things in the meantime while the // @apollo/federation package still exists. import { knownSubgraphDirectives } from '@apollo/subgraph/dist/directives'; import { ASTVisitFn } from '../gql16types'; export function normalizeTypeDefs(typeDefs: DocumentNode) { // The order of this is important - `stripCommonPrimitives` must come after // `defaultRootOperationTypes` because it depends on the `Query` type being named // its default: `Query`. return stripCommonPrimitives( defaultRootOperationTypes( replaceExtendedDefinitionsWithExtensions(typeDefs), ), ); } export function defaultRootOperationTypes( typeDefs: DocumentNode, ): DocumentNode { // Array of default root operation names const defaultRootOperationNames = Object.values( defaultRootOperationNameLookup, ); // Map of the given root operation type names to their respective default operation // type names, i.e. {RootQuery: 'Query'} let rootOperationTypeMap: { [key: string]: DefaultRootOperationTypeName; } = Object.create(null); let hasSchemaDefinitionOrExtension = false; visit(typeDefs, { OperationTypeDefinition(node) { // If we find at least one root operation type definition, we know the user has // specified either a schema definition or extension. hasSchemaDefinitionOrExtension = true; // Build the map of root operation type name to its respective default rootOperationTypeMap[node.type.name.value] = defaultRootOperationNameLookup[node.operation]; }, }); // In this case, there's no defined schema or schema extension, so we use defaults if (!hasSchemaDefinitionOrExtension) { rootOperationTypeMap = { Query: 'Query', Mutation: 'Mutation', Subscription: 'Subscription', }; } // A conflicting default definition exists when the user provides a schema // definition, but also defines types that use the default root operation // names (Query, Mutation, Subscription). Those types need to be removed. let schemaWithoutConflictingDefaultDefinitions; if (!hasSchemaDefinitionOrExtension) { // If no schema definition or extension exists, then there aren't any // conflicting defaults to worry about. schemaWithoutConflictingDefaultDefinitions = typeDefs; } else { // If the user provides a schema definition or extension, then using default // root operation names is considered an error for composition. This visit // drops the invalid type definitions/extensions altogether, as well as // fields that reference them. // // Example: // // schema { // query: RootQuery // } // // type Query { <--- this type definition is invalid (as well as Mutation or Subscription) // ... // } schemaWithoutConflictingDefaultDefinitions = visit(typeDefs, { ObjectTypeDefinition(node) { if ( (defaultRootOperationNames as string[]).includes(node.name.value) && !rootOperationTypeMap[node.name.value] ) { return null; } return; }, ObjectTypeExtension(node) { if ( (defaultRootOperationNames as string[]).includes(node.name.value) && !rootOperationTypeMap[node.name.value] ) { return null; } return; }, // This visitor handles the case where: // 1) A schema definition or extension is provided by the user // 2) A field exists that is of a _default_ root operation type. (Query, Mutation, Subscription) // // Example: // // schema { // mutation: RootMutation // } // // type RootMutation { // updateProduct: Query <--- remove this field altogether // } FieldDefinition(node) { if ( node.type.kind === Kind.NAMED_TYPE && (defaultRootOperationNames as string[]).includes(node.type.name.value) ) { return null; } if ( node.type.kind === Kind.NON_NULL_TYPE && node.type.type.kind === Kind.NAMED_TYPE && (defaultRootOperationNames as string[]).includes( node.type.type.name.value, ) ) { return null; } return; }, }); } const schemaWithDefaultRootTypes = visit( schemaWithoutConflictingDefaultDefinitions, { // Schema definitions and extensions are extraneous since we're transforming // the root operation types to their defaults. SchemaDefinition() { return null; }, SchemaExtension() { return null; }, ObjectTypeDefinition(node) { if ( node.name.value in rootOperationTypeMap || (defaultRootOperationNames as string[]).includes(node.name.value) ) { return { ...node, name: { ...node.name, value: rootOperationTypeMap[node.name.value] || node.name.value, }, kind: Kind.OBJECT_TYPE_EXTENSION, }; } return; }, // schema { // query: RootQuery // } // // extend type RootQuery { <--- update this to `extend type Query` // ... // } ObjectTypeExtension(node) { if ( node.name.value in rootOperationTypeMap || (defaultRootOperationNames as string[]).includes(node.name.value) ) { return { ...node, name: { ...node.name, value: rootOperationTypeMap[node.name.value] || node.name.value, }, }; } return; }, // Corresponding NamedTypes must also make the name switch, in the case that // they reference a root operation type that we've transformed // // schema { // query: RootQuery // mutation: RootMutation // } // // type RootQuery { // ... // } // // type RootMutation { // updateProduct: RootQuery <--- rename `RootQuery` to `Query` // } NamedType(node) { if (node.name.value in rootOperationTypeMap) { return { ...node, name: { ...node.name, value: rootOperationTypeMap[node.name.value], }, }; } return; }, }, ); return schemaWithDefaultRootTypes; } // type definitions with the @extends directive should be treated // as type extensions. export function replaceExtendedDefinitionsWithExtensions( typeDefs: DocumentNode, ) { const typeDefsWithExtendedTypesReplaced = visit(typeDefs, { ObjectTypeDefinition: visitor, InterfaceTypeDefinition: visitor, }); function visitor( node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, ) { const isExtensionDefinition = findDirectivesOnNode(node, 'extends').length > 0; if (!isExtensionDefinition) { return node; } const filteredDirectives = node.directives && node.directives.filter(directive => directive.name.value !== 'extends'); return { ...node, ...(filteredDirectives && { directives: filteredDirectives }), kind: defKindToExtKind[node.kind], }; } 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, @skip, @include, @deprecated, @specifiedBy // scalars: _Any, _FieldSet // union: _Entity // object type: _Service // Query fields: _service, _entities export function stripCommonPrimitives(document: DocumentNode) { const typeDefinitionVisitor: ASTVisitFn< 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 common directive definitions from the document DirectiveDefinition(node) { const isCommonDirective = [ ...knownSubgraphDirectives, ...specifiedDirectives, ].some((directive) => directive.name === node.name.value); return isCommonDirective ? 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, }); }