UNPKG

@theguild/federation-composition

Version:
359 lines (354 loc) 15.4 kB
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); }))); 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; }