@apollo/federation
Version:
Apollo Federation Utilities
325 lines (304 loc) • 10.2 kB
text/typescript
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,
});
}