@theguild/federation-composition
Version:
Open Source Composition library for Apollo Federation
375 lines (370 loc) • 16 kB
JavaScript
import { concatAST, GraphQLError, Kind, OperationTypeNode, parse, visit, visitInParallel, } from "graphql";
import { TypeNodeInfo, visitWithTypeNodeInfo, } from "../../graphql/type-node-info.js";
import { createSpecSchema, } from "../../specifications/federation.js";
import { parseLinkDirective, } from "../../specifications/link.js";
import { AuthenticatedRule } from "./rules/elements/authenticated.js";
import { ComposeDirectiveRules } from "./rules/elements/compose-directive.js";
import { ContextDirectiveRules } from "./rules/elements/context.js";
import { CostRule } from "./rules/elements/cost.js";
import { ExtendsRules } from "./rules/elements/extends.js";
import { ExternalRules } from "./rules/elements/external.js";
import { FieldSetRules } from "./rules/elements/field-set.js";
import { FromContextDirectiveRules } from "./rules/elements/from-context.js";
import { InaccessibleRules } from "./rules/elements/inaccessible.js";
import { InterfaceObjectRules } from "./rules/elements/interface-object.js";
import { KeyRules } from "./rules/elements/key.js";
import { ListSizeRule } from "./rules/elements/list-size.js";
import { OverrideRules } from "./rules/elements/override.js";
import { PolicyRule } from "./rules/elements/policy.js";
import { ProvidesRules } from "./rules/elements/provides.js";
import { RequiresScopesRule } from "./rules/elements/requires-scopes.js";
import { RequiresRules } from "./rules/elements/requires.js";
import { ShareableRules } from "./rules/elements/shareable.js";
import { TagRules } from "./rules/elements/tag.js";
import { KnownArgumentNamesOnDirectivesRule } from "./rules/known-argument-names-on-directives-rule.js";
import { KnownDirectivesRule } from "./rules/known-directives-rule.js";
import { KnownFederationDirectivesRule } from "./rules/known-federation-directive-rule.js";
import { KnownRootTypeRule } from "./rules/known-root-type-rule.js";
import { KnownTypeNamesRule } from "./rules/known-type-names-rule.js";
import { LoneSchemaDefinitionRule } from "./rules/lone-schema-definition-rule.js";
import { OnlyInterfaceImplementationRule } from "./rules/only-interface-implementation-rule.js";
import { ProvidedArgumentsOnDirectivesRule } from "./rules/provided-arguments-on-directives-rule.js";
import { ProvidedRequiredArgumentsOnDirectivesRule } from "./rules/provided-required-arguments-on-directives-rule.js";
import { QueryRootTypeInaccessibleRule } from "./rules/query-root-type-inaccessible-rule.js";
import { ReservedSubgraphNameRule } from "./rules/reserved-subgraph-name-rule.js";
import { RootTypeUsedRule } from "./rules/root-type-used-rule.js";
import { UniqueArgumentDefinitionNamesRule } from "./rules/unique-argument-definition-names-rule.js";
import { UniqueArgumentNamesRule } from "./rules/unique-argument-names-rule.js";
import { UniqueDirectiveNamesRule } from "./rules/unique-directive-names-rule.js";
import { UniqueDirectivesPerLocationRule } from "./rules/unique-directives-per-location-rule.js";
import { UniqueEnumValueNamesRule } from "./rules/unique-enum-value-names-rule.js";
import { UniqueFieldDefinitionNamesRule } from "./rules/unique-field-definition-names-rule.js";
import { UniqueInputFieldNamesRule } from "./rules/unique-input-field-names-rule.js";
import { UniqueOperationTypesRule } from "./rules/unique-operation-types-rule.js";
import { UniqueTypeNamesRule } from "./rules/unique-type-names-rule.js";
import { validateSubgraphState } from "./validate-state.js";
import { createSimpleValidationContext, createSubgraphValidationContext, } from "./validation-context.js";
export function assertUniqueSubgraphNames(subgraphs) {
const names = new Set();
for (const subgraph of subgraphs) {
if (names.has(subgraph.name)) {
throw new Error(`A subgraph named ${subgraph.name} already exists`);
}
names.add(subgraph.name);
}
}
export function validateSubgraphCore(subgraph) {
const extractedLinks = extractLinks(subgraph);
if (extractedLinks.errors) {
extractedLinks.errors.forEach((error) => enrichErrorWithSubgraphName(error, subgraph.name));
}
return extractedLinks;
}
export function validateSubgraph(subgraph, stateBuilder, federation, __internal) {
subgraph.typeDefs = cleanSubgraphTypeDefsFromSubgraphSpec(subgraph.typeDefs);
const linkSpecDefinitions = parse(`
enum Purpose {
EXECUTION
SECURITY
}
directive @link(
url: String
as: String
for: link__Purpose
import: [link__Import]
) repeatable on SCHEMA
scalar link__Import
enum link__Purpose {
"""
\`SECURITY\` features provide metadata necessary to securely resolve fields.
"""
SECURITY
"""
\`EXECUTION\` features provide metadata necessary for operation execution.
"""
EXECUTION
}
`).definitions;
const rulesToSkip = __internal?.disableValidationRules ?? [];
const typeNodeInfo = new TypeNodeInfo();
const validationContext = createSubgraphValidationContext(subgraph, federation, typeNodeInfo, stateBuilder);
const federationRules = [
ReservedSubgraphNameRule,
KnownFederationDirectivesRule,
FieldSetRules,
InaccessibleRules,
InterfaceObjectRules,
AuthenticatedRule,
PolicyRule,
RequiresScopesRule,
CostRule,
ListSizeRule,
OverrideRules,
ContextDirectiveRules,
FromContextDirectiveRules,
ExtendsRules,
QueryRootTypeInaccessibleRule,
KnownTypeNamesRule,
KnownRootTypeRule,
RootTypeUsedRule,
ShareableRules,
KeyRules,
ProvidesRules,
RequiresRules,
ExternalRules,
TagRules,
ComposeDirectiveRules,
];
const graphqlRules = [
OnlyInterfaceImplementationRule,
LoneSchemaDefinitionRule,
UniqueOperationTypesRule,
UniqueTypeNamesRule,
UniqueEnumValueNamesRule,
UniqueFieldDefinitionNamesRule,
UniqueArgumentDefinitionNamesRule,
KnownDirectivesRule,
UniqueDirectivesPerLocationRule,
KnownArgumentNamesOnDirectivesRule,
UniqueArgumentNamesRule,
UniqueInputFieldNamesRule,
UniqueDirectiveNamesRule,
ProvidedRequiredArgumentsOnDirectivesRule,
ProvidedArgumentsOnDirectivesRule,
];
visit(subgraph.typeDefs, visitWithTypeNodeInfo(typeNodeInfo, visitInParallel([stateBuilder.visitor(typeNodeInfo)].concat(federationRules.map((rule) => {
if (rulesToSkip.includes(rule.name)) {
return {};
}
return rule(validationContext);
})))));
const federationDefinitionReplacements = validationContext.collectFederationDefinitionReplacements();
const linkSpecDefinitionsToInclude = linkSpecDefinitions.filter((def) => {
if ("name" in def && typeof def.name?.value === "string") {
return !stateBuilder.state.types.has(def.name.value);
}
return true;
});
const fullTypeDefs = concatAST([
{
kind: Kind.DOCUMENT,
definitions: validationContext
.getAvailableFederationTypeAndDirectiveDefinitions()
.filter((def) => !federationDefinitionReplacements.has(def.name.value)),
},
validationContext.satisfiesVersionRange("> v1.0") &&
!stateBuilder.state.specs.link
?
linkSpecDefinitionsToInclude.length > 0
? {
kind: Kind.DOCUMENT,
definitions: linkSpecDefinitionsToInclude,
}
: null
: null,
subgraph.typeDefs,
].filter(onlyDocumentNode));
const subgraphStateErrors = validateSubgraphState(stateBuilder.state, validationContext);
const simpleValidationContext = createSimpleValidationContext(fullTypeDefs, typeNodeInfo);
visit(fullTypeDefs, visitInParallel(graphqlRules.map((rule) => {
if (rulesToSkip.includes(rule.name)) {
return {};
}
return rule(simpleValidationContext);
})));
for (const { typeName, fieldName, } of validationContext.getFieldsToMarkAsShareable()) {
if (validationContext.stateBuilder.isInterfaceObject(typeName)) {
continue;
}
validationContext.stateBuilder.objectType.field.setShareable(typeName, fieldName);
}
return validationContext
.collectReportedErrors()
.concat(validationContext.collectUnusedExternal().map((coordinate) => enrichErrorWithSubgraphName(new GraphQLError(`Field "${coordinate}" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).`, {
extensions: {
code: "EXTERNAL_UNUSED",
},
}), subgraph.name)))
.concat(simpleValidationContext.collectReportedErrors())
.concat(subgraphStateErrors)
.map((error) => enrichErrorWithSubgraphName(error, subgraph.name));
}
function enrichErrorWithSubgraphName(error, subgraphName) {
if (error.extensions.subgraphName) {
return error;
}
error.message = `[${subgraphName}] ${error.message}`;
error.extensions.subgraphName = subgraphName;
return error;
}
const availableFeatures = {
link: ["v1.0"],
tag: ["v0.1", "v0.2"],
kotlin_labs: ["v0.1", "v0.2"],
join: ["v0.1", "v0.2", "v0.3", "v0.4", "v0.5"],
inaccessible: ["v0.1", "v0.2"],
core: ["v0.1", "v0.2"],
};
function extractLinks(subgraph) {
const schemaNodes = subgraph.typeDefs.definitions.filter(isSchemaDefinitionOrExtensionNode);
if (schemaNodes.length === 0) {
return {
links: [],
};
}
const linkDirectives = [];
for (const schemaNode of schemaNodes) {
if (schemaNode.directives?.length) {
for (const directiveNode of schemaNode.directives) {
if (directiveNode.name.value === "link") {
linkDirectives.push(directiveNode);
}
}
}
}
if (!linkDirectives) {
return {
links: [],
};
}
const errors = [];
const links = [];
const identities = new Set();
const reportedAsDuplicate = new Set();
for (let i = 0; i < linkDirectives.length; i++) {
const linkDirective = linkDirectives[i];
try {
const link = parseLinkDirective(linkDirective);
if (!link) {
continue;
}
if (identities.has(link.identity) &&
!reportedAsDuplicate.has(link.identity)) {
errors.push(new GraphQLError(`Duplicate inclusion of feature ${link.identity}`, {
extensions: {
code: "INVALID_LINK_DIRECTIVE_USAGE",
},
}));
reportedAsDuplicate.add(link.identity);
}
identities.add(link.identity);
if (link.version && !/^v\d+\.\d+/.test(link.version)) {
errors.push(new GraphQLError(`Expected a version string (of the form v1.2), got ${link.version}`, {
extensions: {
code: "INVALID_LINK_IDENTIFIER",
},
}));
continue;
}
if (!link.name) {
errors.push(new GraphQLError(`Missing path in feature url '${link.identity}'`, {
extensions: {
code: "INVALID_LINK_IDENTIFIER",
},
}));
continue;
}
if (link.identity.startsWith("https://specs.apollo.dev/")) {
if (link.name === "federation") {
if (!link.version) {
errors.push(new GraphQLError(`Missing version in feature url '${link.identity}'`, {
extensions: {
code: "TODO",
},
}));
continue;
}
const spec = createSpecSchema(link.version);
const availableElements = new Set(spec.directives
.map((d) => d.name.value)
.concat(spec.types.map((t) => t.name.value)));
let pushedError = false;
for (const im of link.imports) {
if (!availableElements.has(im.name.replace(/^@/, ""))) {
pushedError = true;
errors.push(new GraphQLError(`Cannot import unknown element "${im.name}".`, {
extensions: {
code: "INVALID_LINK_DIRECTIVE_USAGE",
},
}));
}
}
if (pushedError) {
continue;
}
}
else if (link.version && availableFeatures[link.name]) {
if (!availableFeatures[link.name].includes(link.version)) {
errors.push(new GraphQLError(`Schema uses unknown version ${link.version} of the ${link.name} spec`, {
extensions: {
code: "UNKNOWN_LINK_VERSION",
},
}));
continue;
}
}
}
links.push(link);
}
catch (error) {
errors.push(error instanceof GraphQLError ? error : new GraphQLError(String(error)));
}
}
if (errors.length > 0) {
return {
errors,
};
}
return {
links,
};
}
function isSchemaDefinitionOrExtensionNode(node) {
return (node.kind === Kind.SCHEMA_DEFINITION ||
node.kind === Kind.SCHEMA_EXTENSION);
}
function onlyDocumentNode(item) {
return item != null;
}
function cleanSubgraphTypeDefsFromSubgraphSpec(typeDefs) {
let queryTypes = [];
const schemaDef = typeDefs.definitions.find((node) => (node.kind === Kind.SCHEMA_DEFINITION ||
node.kind === Kind.SCHEMA_EXTENSION) &&
node.operationTypes?.some((op) => op.operation === OperationTypeNode.QUERY));
const queryTypeName = schemaDef?.operationTypes?.find((op) => op.operation === OperationTypeNode.QUERY)?.type.name.value ?? "Query";
typeDefs.definitions =
typeDefs.definitions.filter((def) => {
if ((def.kind === Kind.SCALAR_TYPE_DEFINITION ||
def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) &&
def.name.value === "_Any") {
return false;
}
if (def.kind === Kind.UNION_TYPE_DEFINITION &&
def.name.value === "_Entity") {
return false;
}
if (def.kind === Kind.OBJECT_TYPE_DEFINITION &&
def.name.value === "_Service") {
return false;
}
if ((def.kind === Kind.OBJECT_TYPE_DEFINITION ||
def.kind === Kind.OBJECT_TYPE_EXTENSION) &&
def.name.value === queryTypeName) {
queryTypes.push(def);
}
return true;
});
if (queryTypes.length > 0) {
for (const queryType of queryTypes) {
queryType.fields =
queryType.fields?.filter((field) => {
if (field.name.value === "_service" ||
field.name.value === "_entities") {
return false;
}
return true;
}) ?? [];
}
}
return typeDefs;
}