UNPKG

@theguild/federation-composition

Version:

Open Source Composition library for Apollo Federation

256 lines (255 loc) 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.printOutputType = exports.isDirectiveDefinitionNode = exports.namedTypeFromTypeNode = exports.parseFields = exports.getFieldsArgument = exports.visitFields = exports.validateDirectiveAgainstOriginal = void 0; const graphql_1 = require("graphql"); const printer_js_1 = require("../graphql/printer.js"); function validateDirectiveAgainstOriginal(providedDirectiveNode, directiveName, context) { if (!context.isAvailableFederationDirective(directiveName, providedDirectiveNode)) { return; } const isFederationV2 = context.satisfiesVersionRange('>= v2.0'); const federationDirective = context .getKnownFederationDirectives() .find(d => context.isAvailableFederationDirective(directiveName, d)); if (!federationDirective) { throw new Error(`Federation directive @${directiveName} not found`); } const errors = []; const original = { args: new Map(federationDirective.arguments?.map(arg => [arg.name.value, arg])), locations: federationDirective.locations.map(loc => loc.value), }; const provided = { args: new Map(providedDirectiveNode.arguments?.map(arg => [arg.name.value, arg])), locations: providedDirectiveNode.locations.map(loc => loc.value), }; for (const [argName, argDef] of original.args.entries()) { const providedArgNode = provided.args.get(argName); if (isNonNullTypeNode(argDef.type) && !providedArgNode) { errors.push(new graphql_1.GraphQLError(`Invalid definition for directive "@${directiveName}": missing required argument "${argName}"`, { nodes: providedDirectiveNode, extensions: { code: 'DIRECTIVE_DEFINITION_INVALID' }, })); } if (providedArgNode) { const expectedType = (0, printer_js_1.print)(argDef.type); const providedType = (0, printer_js_1.print)(providedArgNode.type); if (expectedType !== providedType) { const isNonNullableString = providedType === 'String!'; const allowedFieldSetTypes = isFederationV2 ? ['FieldSet!', 'federation__FieldSet!', '_FieldSet!'] : ['_FieldSet!', 'String']; const fieldSetTypesInSpec = isFederationV2 ? ['FieldSet!', 'federation__FieldSet!', '_FieldSet!'] : ['_FieldSet!', 'FieldSet!', 'String']; const expectsFieldSet = fieldSetTypesInSpec.includes(expectedType); if (!isNonNullableString && expectsFieldSet) { const isOneOfExpected = allowedFieldSetTypes.includes(providedType); if (!isOneOfExpected) { errors.push(new graphql_1.GraphQLError(`Invalid definition for directive "@${directiveName}": argument "${argName}" should have type "${expectedType}" but found type "${providedType}"`, { nodes: providedDirectiveNode, extensions: { code: 'DIRECTIVE_DEFINITION_INVALID' }, })); } } } if (expectedType === 'Boolean' && argDef.defaultValue?.kind === graphql_1.Kind.BOOLEAN) { let providedValue = null; if (providedArgNode.defaultValue) { if (providedArgNode.defaultValue.kind !== graphql_1.Kind.BOOLEAN) { throw new Error('Expected a Boolean'); } providedValue = providedArgNode.defaultValue.value; } if (argDef.defaultValue?.value !== providedValue) { errors.push(new graphql_1.GraphQLError(`Invalid definition for directive "@${directiveName}": argument "${argName}" should have default value ${argDef.defaultValue ? 'true' : 'false'} but found default value ${providedValue ?? 'null'}`, { nodes: providedDirectiveNode, extensions: { code: 'DIRECTIVE_DEFINITION_INVALID' }, })); } } } } const locationIntersection = provided.locations.filter(loc => original.locations.includes(loc)); if (!locationIntersection.length) { errors.push(new graphql_1.GraphQLError(`Invalid definition for directive "@${directiveName}": "@${directiveName}" should have locations ${Array.from(original.locations).join(', ')}, but found (non-subset) ${Array.from(provided.locations).join(', ')}`, { nodes: providedDirectiveNode, extensions: { code: 'DIRECTIVE_DEFINITION_INVALID' }, })); } if (errors.length) { for (const error of errors) { context.reportError(error); } } else { context.markAsFederationDefinitionReplacement(providedDirectiveNode.name.value); } } exports.validateDirectiveAgainstOriginal = validateDirectiveAgainstOriginal; function visitFields({ context, selectionSet, typeDefinition, interceptField, interceptArguments, interceptUnknownField, interceptDirective, interceptInterfaceType, interceptExternalField, interceptNonExternalField, }) { for (const selection of selectionSet.selections) { if (selection.kind === graphql_1.Kind.FRAGMENT_SPREAD) { continue; } if (selection.kind === graphql_1.Kind.INLINE_FRAGMENT) { if (!selection.typeCondition) { continue; } const interfaceName = selection.typeCondition.name.value; const interfaceDefinition = context.getSubgraphObjectOrInterfaceTypes().get(interfaceName); if (!interfaceDefinition) { continue; } visitFields({ context, selectionSet: selection.selectionSet, typeDefinition: interfaceDefinition, interceptArguments, interceptUnknownField, interceptInterfaceType, }); break; } const selectionFieldDef = selection.name.value === '__typename' ? { kind: graphql_1.Kind.FIELD_DEFINITION, name: { kind: graphql_1.Kind.NAME, value: '__typename', }, type: { kind: graphql_1.Kind.NAMED_TYPE, name: { kind: graphql_1.Kind.NAME, value: 'String', }, }, } : typeDefinition.fields?.find(field => field.name.value === selection.name.value); if (!selectionFieldDef) { if (interceptUnknownField) { interceptUnknownField({ typeDefinition, fieldName: selection.name.value, }); } break; } if (interceptDirective && selection.directives?.length) { for (const directive of selection.directives) { interceptDirective({ directiveName: directive.name.value, isKnown: context.getSubgraphDirectiveDefinitions().has(directive.name.value), }); } } context.markAsUsed('fields', typeDefinition.kind, typeDefinition.name.value, selectionFieldDef.name.value); if (interceptField) { interceptField({ typeDefinition, fieldName: selection.name.value, }); } if (selectionFieldDef.arguments?.length && interceptArguments) { interceptArguments({ typeDefinition, fieldName: selection.name.value, }); continue; } if (interceptNonExternalField || interceptExternalField) { const isExternal = selectionFieldDef.directives?.some(d => context.isAvailableFederationDirective('external', d)); const fieldName = selection.name.value; const fieldDef = typeDefinition.fields?.find(field => field.name.value === fieldName); if (!fieldDef) { continue; } const namedType = namedTypeFromTypeNode(fieldDef.type); const isLeaf = context.isLeafType(namedType.name.value); if (isLeaf) { if (isExternal && interceptExternalField) { interceptExternalField({ typeDefinition, fieldName, }); } else if (!isExternal && interceptNonExternalField) { interceptNonExternalField({ typeDefinition, fieldName, }); } } } const outputType = namedTypeFromTypeNode(selectionFieldDef.type).name.value; const innerTypeDef = context.getSubgraphObjectOrInterfaceTypes().get(outputType); if (!innerTypeDef) { continue; } if (interceptInterfaceType && (innerTypeDef.kind === graphql_1.Kind.INTERFACE_TYPE_DEFINITION || innerTypeDef.kind === graphql_1.Kind.INTERFACE_TYPE_EXTENSION)) { interceptInterfaceType({ typeDefinition, fieldName: selection.name.value, }); } const innerSelection = selection.selectionSet; if (!innerSelection) { continue; } visitFields({ context, selectionSet: innerSelection, typeDefinition: innerTypeDef, interceptArguments, interceptUnknownField, interceptInterfaceType, }); } } exports.visitFields = visitFields; function getFieldsArgument(directiveNode) { const fieldsArg = directiveNode.arguments?.find(arg => arg.name.value === 'fields'); if (!fieldsArg) { return; } return fieldsArg; } exports.getFieldsArgument = getFieldsArgument; function parseFields(fields) { const parsed = (0, graphql_1.parse)(fields.trim().startsWith(`{`) ? `query ${fields}` : `query { ${fields} }`).definitions.find(d => d.kind === graphql_1.Kind.OPERATION_DEFINITION); return parsed?.selectionSet; } exports.parseFields = parseFields; function namedTypeFromTypeNode(type) { if (type.kind === graphql_1.Kind.NAMED_TYPE) { return type; } if (type.kind === graphql_1.Kind.LIST_TYPE) { return namedTypeFromTypeNode(type.type); } if (type.kind === graphql_1.Kind.NON_NULL_TYPE) { return namedTypeFromTypeNode(type.type); } throw new Error('Unknown type node: ' + type); } exports.namedTypeFromTypeNode = namedTypeFromTypeNode; function isDirectiveDefinitionNode(node) { return node.kind === graphql_1.Kind.DIRECTIVE_DEFINITION; } exports.isDirectiveDefinitionNode = isDirectiveDefinitionNode; function printOutputType(type) { if (type.kind === graphql_1.Kind.NAMED_TYPE) { return type.name.value; } if (type.kind === graphql_1.Kind.LIST_TYPE) { return `[${printOutputType(type.type)}]`; } return `${printOutputType(type.type)}!`; } exports.printOutputType = printOutputType; function isNonNullTypeNode(node) { return node.kind === graphql_1.Kind.NON_NULL_TYPE; }