@theguild/federation-composition
Version:
Open Source Composition library for Apollo Federation
364 lines (359 loc) • 17.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.assertUniqueSubgraphNames = assertUniqueSubgraphNames;
exports.validateSubgraphCore = validateSubgraphCore;
exports.validateSubgraph = validateSubgraph;
const graphql_1 = require("graphql");
const type_node_info_js_1 = require("../../graphql/type-node-info.js");
const federation_js_1 = require("../../specifications/federation.js");
const link_js_1 = require("../../specifications/link.js");
const authenticated_js_1 = require("./rules/elements/authenticated.js");
const compose_directive_js_1 = require("./rules/elements/compose-directive.js");
const context_js_1 = require("./rules/elements/context.js");
const cost_js_1 = require("./rules/elements/cost.js");
const extends_js_1 = require("./rules/elements/extends.js");
const external_js_1 = require("./rules/elements/external.js");
const field_set_js_1 = require("./rules/elements/field-set.js");
const from_context_js_1 = require("./rules/elements/from-context.js");
const inaccessible_js_1 = require("./rules/elements/inaccessible.js");
const interface_object_js_1 = require("./rules/elements/interface-object.js");
const key_js_1 = require("./rules/elements/key.js");
const list_size_js_1 = require("./rules/elements/list-size.js");
const override_js_1 = require("./rules/elements/override.js");
const policy_js_1 = require("./rules/elements/policy.js");
const provides_js_1 = require("./rules/elements/provides.js");
const requires_scopes_js_1 = require("./rules/elements/requires-scopes.js");
const requires_js_1 = require("./rules/elements/requires.js");
const shareable_js_1 = require("./rules/elements/shareable.js");
const tag_js_1 = require("./rules/elements/tag.js");
const known_argument_names_on_directives_rule_js_1 = require("./rules/known-argument-names-on-directives-rule.js");
const known_directives_rule_js_1 = require("./rules/known-directives-rule.js");
const known_federation_directive_rule_js_1 = require("./rules/known-federation-directive-rule.js");
const known_root_type_rule_js_1 = require("./rules/known-root-type-rule.js");
const known_type_names_rule_js_1 = require("./rules/known-type-names-rule.js");
const lone_schema_definition_rule_js_1 = require("./rules/lone-schema-definition-rule.js");
const only_interface_implementation_rule_js_1 = require("./rules/only-interface-implementation-rule.js");
const provided_arguments_on_directives_rule_js_1 = require("./rules/provided-arguments-on-directives-rule.js");
const provided_required_arguments_on_directives_rule_js_1 = require("./rules/provided-required-arguments-on-directives-rule.js");
const query_root_type_inaccessible_rule_js_1 = require("./rules/query-root-type-inaccessible-rule.js");
const reserved_subgraph_name_rule_js_1 = require("./rules/reserved-subgraph-name-rule.js");
const root_type_used_rule_js_1 = require("./rules/root-type-used-rule.js");
const unique_argument_definition_names_rule_js_1 = require("./rules/unique-argument-definition-names-rule.js");
const unique_argument_names_rule_js_1 = require("./rules/unique-argument-names-rule.js");
const unique_directive_names_rule_js_1 = require("./rules/unique-directive-names-rule.js");
const unique_directives_per_location_rule_js_1 = require("./rules/unique-directives-per-location-rule.js");
const unique_enum_value_names_rule_js_1 = require("./rules/unique-enum-value-names-rule.js");
const unique_field_definition_names_rule_js_1 = require("./rules/unique-field-definition-names-rule.js");
const unique_input_field_names_rule_js_1 = require("./rules/unique-input-field-names-rule.js");
const unique_operation_types_rule_js_1 = require("./rules/unique-operation-types-rule.js");
const unique_type_names_rule_js_1 = require("./rules/unique-type-names-rule.js");
const validate_state_js_1 = require("./validate-state.js");
const validation_context_js_1 = require("./validation-context.js");
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);
}
}
function validateSubgraphCore(subgraph) {
const extractedLinks = extractLinks(subgraph);
if (extractedLinks.errors) {
extractedLinks.errors.forEach(error => enrichErrorWithSubgraphName(error, subgraph.name));
}
return extractedLinks;
}
function validateSubgraph(subgraph, stateBuilder, federation, __internal) {
subgraph.typeDefs = cleanSubgraphTypeDefsFromSubgraphSpec(subgraph.typeDefs);
const linkSpecDefinitions = (0, graphql_1.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 type_node_info_js_1.TypeNodeInfo();
const validationContext = (0, validation_context_js_1.createSubgraphValidationContext)(subgraph, federation, typeNodeInfo, stateBuilder);
const federationRules = [
reserved_subgraph_name_rule_js_1.ReservedSubgraphNameRule,
known_federation_directive_rule_js_1.KnownFederationDirectivesRule,
field_set_js_1.FieldSetRules,
inaccessible_js_1.InaccessibleRules,
interface_object_js_1.InterfaceObjectRules,
authenticated_js_1.AuthenticatedRule,
policy_js_1.PolicyRule,
requires_scopes_js_1.RequiresScopesRule,
cost_js_1.CostRule,
list_size_js_1.ListSizeRule,
override_js_1.OverrideRules,
context_js_1.ContextDirectiveRules,
from_context_js_1.FromContextDirectiveRules,
extends_js_1.ExtendsRules,
query_root_type_inaccessible_rule_js_1.QueryRootTypeInaccessibleRule,
known_type_names_rule_js_1.KnownTypeNamesRule,
known_root_type_rule_js_1.KnownRootTypeRule,
root_type_used_rule_js_1.RootTypeUsedRule,
shareable_js_1.ShareableRules,
key_js_1.KeyRules,
provides_js_1.ProvidesRules,
requires_js_1.RequiresRules,
external_js_1.ExternalRules,
tag_js_1.TagRules,
compose_directive_js_1.ComposeDirectiveRules,
];
const graphqlRules = [
only_interface_implementation_rule_js_1.OnlyInterfaceImplementationRule,
lone_schema_definition_rule_js_1.LoneSchemaDefinitionRule,
unique_operation_types_rule_js_1.UniqueOperationTypesRule,
unique_type_names_rule_js_1.UniqueTypeNamesRule,
unique_enum_value_names_rule_js_1.UniqueEnumValueNamesRule,
unique_field_definition_names_rule_js_1.UniqueFieldDefinitionNamesRule,
unique_argument_definition_names_rule_js_1.UniqueArgumentDefinitionNamesRule,
known_directives_rule_js_1.KnownDirectivesRule,
unique_directives_per_location_rule_js_1.UniqueDirectivesPerLocationRule,
known_argument_names_on_directives_rule_js_1.KnownArgumentNamesOnDirectivesRule,
unique_argument_names_rule_js_1.UniqueArgumentNamesRule,
unique_input_field_names_rule_js_1.UniqueInputFieldNamesRule,
unique_directive_names_rule_js_1.UniqueDirectiveNamesRule,
provided_required_arguments_on_directives_rule_js_1.ProvidedRequiredArgumentsOnDirectivesRule,
provided_arguments_on_directives_rule_js_1.ProvidedArgumentsOnDirectivesRule,
];
(0, graphql_1.visit)(subgraph.typeDefs, (0, type_node_info_js_1.visitWithTypeNodeInfo)(typeNodeInfo, (0, graphql_1.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 = (0, graphql_1.concatAST)([
{
kind: graphql_1.Kind.DOCUMENT,
definitions: validationContext
.getAvailableFederationTypeAndDirectiveDefinitions()
.filter(def => !federationDefinitionReplacements.has(def.name.value)),
},
validationContext.satisfiesVersionRange('> v1.0') && !stateBuilder.state.specs.link
?
linkSpecDefinitionsToInclude.length > 0
? {
kind: graphql_1.Kind.DOCUMENT,
definitions: linkSpecDefinitionsToInclude,
}
: null
: null,
subgraph.typeDefs,
].filter(onlyDocumentNode));
const subgraphStateErrors = (0, validate_state_js_1.validateSubgraphState)(stateBuilder.state, validationContext);
const simpleValidationContext = (0, validation_context_js_1.createSimpleValidationContext)(fullTypeDefs, typeNodeInfo);
(0, graphql_1.visit)(fullTypeDefs, (0, graphql_1.visitInParallel)(graphqlRules.map(rule => {
if (rulesToSkip.includes(rule.name)) {
return {};
}
return rule(simpleValidationContext);
})));
return validationContext
.collectReportedErrors()
.concat(validationContext.collectUnusedExternal().map(coordinate => enrichErrorWithSubgraphName(new graphql_1.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 = (0, link_js_1.parseLinkDirective)(linkDirective);
if (!link) {
continue;
}
if (identities.has(link.identity) && !reportedAsDuplicate.has(link.identity)) {
errors.push(new graphql_1.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 graphql_1.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 graphql_1.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 graphql_1.GraphQLError(`Missing version in feature url '${link.identity}'`, {
extensions: {
code: 'TODO',
},
}));
continue;
}
const spec = (0, federation_js_1.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 graphql_1.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 graphql_1.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 graphql_1.GraphQLError ? error : new graphql_1.GraphQLError(String(error)));
}
}
if (errors.length > 0) {
return {
errors,
};
}
return {
links,
};
}
function isSchemaDefinitionOrExtensionNode(node) {
return (node.kind === graphql_1.Kind.SCHEMA_DEFINITION || node.kind === graphql_1.Kind.SCHEMA_EXTENSION);
}
function onlyDocumentNode(item) {
return item != null;
}
function cleanSubgraphTypeDefsFromSubgraphSpec(typeDefs) {
let queryTypes = [];
const schemaDef = typeDefs.definitions.find(node => (node.kind === graphql_1.Kind.SCHEMA_DEFINITION || node.kind === graphql_1.Kind.SCHEMA_EXTENSION) &&
node.operationTypes?.some(op => op.operation === graphql_1.OperationTypeNode.QUERY));
const queryTypeName = schemaDef?.operationTypes?.find(op => op.operation === graphql_1.OperationTypeNode.QUERY)?.type.name
.value ?? 'Query';
typeDefs.definitions = typeDefs.definitions.filter(def => {
if ((def.kind === graphql_1.Kind.SCALAR_TYPE_DEFINITION ||
def.kind === graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION) &&
def.name.value === '_Any') {
return false;
}
if (def.kind === graphql_1.Kind.UNION_TYPE_DEFINITION && def.name.value === '_Entity') {
return false;
}
if (def.kind === graphql_1.Kind.OBJECT_TYPE_DEFINITION && def.name.value === '_Service') {
return false;
}
if ((def.kind === graphql_1.Kind.OBJECT_TYPE_DEFINITION || def.kind === graphql_1.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;
}