UNPKG

@apollo/federation-internals

Version:
655 lines 33.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.removeInaccessibleElements = exports.INACCESSIBLE_VERSIONS = exports.InaccessibleSpecDefinition = exports.inaccessibleIdentity = void 0; const coreSpec_1 = require("./coreSpec"); const definitions_1 = require("../definitions"); const graphql_1 = require("graphql"); const knownCoreFeatures_1 = require("../knownCoreFeatures"); const error_1 = require("../error"); const directiveAndTypeSpecification_1 = require("../directiveAndTypeSpecification"); const utils_1 = require("../utils"); exports.inaccessibleIdentity = 'https://specs.apollo.dev/inaccessible'; class InaccessibleSpecDefinition extends coreSpec_1.FeatureDefinition { constructor(version, minimumFederationVersion) { super(new coreSpec_1.FeatureUrl(exports.inaccessibleIdentity, 'inaccessible', version), minimumFederationVersion); this.inaccessibleLocations = [ graphql_1.DirectiveLocation.FIELD_DEFINITION, graphql_1.DirectiveLocation.OBJECT, graphql_1.DirectiveLocation.INTERFACE, graphql_1.DirectiveLocation.UNION, ]; this.printedInaccessibleDefinition = 'directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION'; if (!this.isV01()) { this.inaccessibleLocations.push(graphql_1.DirectiveLocation.ARGUMENT_DEFINITION, graphql_1.DirectiveLocation.SCALAR, graphql_1.DirectiveLocation.ENUM, graphql_1.DirectiveLocation.ENUM_VALUE, graphql_1.DirectiveLocation.INPUT_OBJECT, graphql_1.DirectiveLocation.INPUT_FIELD_DEFINITION); this.printedInaccessibleDefinition = 'directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION'; } this.inaccessibleDirectiveSpec = (0, directiveAndTypeSpecification_1.createDirectiveSpecification)({ name: 'inaccessible', locations: this.inaccessibleLocations, composes: true, supergraphSpecification: (fedVersion) => exports.INACCESSIBLE_VERSIONS.getMinimumRequiredVersion(fedVersion), }); this.registerDirective(this.inaccessibleDirectiveSpec); } isV01() { return this.version.equals(new coreSpec_1.FeatureVersion(0, 1)); } inaccessibleDirective(schema) { return this.directive(schema, 'inaccessible'); } checkCompatibleDirective(definition) { const hasUnknownArguments = Object.keys(definition.arguments()).length > 0; const hasRepeatable = definition.repeatable; const hasValidLocations = definition.locations.every(loc => this.inaccessibleLocations.includes(loc)); if (hasUnknownArguments || hasRepeatable || !hasValidLocations) { return error_1.ERRORS.DIRECTIVE_DEFINITION_INVALID.err(`Found invalid @inaccessible directive definition. Please ensure the directive definition in your schema's definitions matches the following:\n\t${this.printedInaccessibleDefinition}`); } return undefined; } get defaultCorePurpose() { return 'SECURITY'; } } exports.InaccessibleSpecDefinition = InaccessibleSpecDefinition; exports.INACCESSIBLE_VERSIONS = new coreSpec_1.FeatureDefinitions(exports.inaccessibleIdentity) .add(new InaccessibleSpecDefinition(new coreSpec_1.FeatureVersion(0, 1))) .add(new InaccessibleSpecDefinition(new coreSpec_1.FeatureVersion(0, 2), new coreSpec_1.FeatureVersion(2, 0))); (0, knownCoreFeatures_1.registerKnownFeature)(exports.INACCESSIBLE_VERSIONS); function removeInaccessibleElements(schema) { schema.validate(); const coreFeatures = schema.coreFeatures; if (!coreFeatures) { return; } const inaccessibleFeature = coreFeatures.getByIdentity(exports.inaccessibleIdentity); if (!inaccessibleFeature) { return; } const inaccessibleSpec = exports.INACCESSIBLE_VERSIONS.find(inaccessibleFeature.url.version); if (!inaccessibleSpec) { throw (0, definitions_1.ErrGraphQLAPISchemaValidationFailed)([new graphql_1.GraphQLError(`Cannot remove inaccessible elements: the schema uses unsupported` + ` inaccessible spec version ${inaccessibleFeature.url.version}` + ` (supported versions: ${exports.INACCESSIBLE_VERSIONS.versions().join(', ')})`)]); } const inaccessibleDirective = inaccessibleSpec.inaccessibleDirective(schema); if (!inaccessibleDirective) { throw (0, definitions_1.ErrGraphQLAPISchemaValidationFailed)([new graphql_1.GraphQLError(`Invalid schema: declares ${inaccessibleSpec.url} spec but does not` + ` define a @inaccessible directive.`)]); } const incompatibleError = inaccessibleSpec.checkCompatibleDirective(inaccessibleDirective); if (incompatibleError) { throw (0, definitions_1.ErrGraphQLAPISchemaValidationFailed)([incompatibleError]); } validateInaccessibleElements(schema, coreFeatures, inaccessibleSpec, inaccessibleDirective); removeInaccessibleElementsAssumingValid(schema, inaccessibleDirective); } exports.removeInaccessibleElements = removeInaccessibleElements; function validateInaccessibleElements(schema, coreFeatures, inaccessibleSpec, inaccessibleDirective) { var _a, _b, _c; function isInaccessible(element) { return element.hasAppliedDirective(inaccessibleDirective); } const featureList = [...coreFeatures.allFeatures()]; function isFeatureDefinition(element) { return featureList.some((feature) => feature.isFeatureDefinition(element)); } function isInAPISchema(element) { if (!(element instanceof definitions_1.DirectiveDefinition) && isInaccessible(element)) return false; if ((element instanceof definitions_1.ObjectType) || (element instanceof definitions_1.InterfaceType) || (element instanceof definitions_1.UnionType) || (element instanceof definitions_1.ScalarType) || (element instanceof definitions_1.EnumType) || (element instanceof definitions_1.InputObjectType) || (element instanceof definitions_1.DirectiveDefinition)) { return true; } else if ((element instanceof definitions_1.FieldDefinition) || (element instanceof definitions_1.ArgumentDefinition) || (element instanceof definitions_1.InputFieldDefinition) || (element instanceof definitions_1.EnumValue)) { return isInAPISchema(element.parent); } (0, utils_1.assert)(false, "Unreachable code, element is of unknown type."); } function fetchInaccessibleElementsDeep(element) { const inaccessibleElements = []; if (isInaccessible(element)) { inaccessibleElements.push(element); } if ((element instanceof definitions_1.ObjectType) || (element instanceof definitions_1.InterfaceType) || (element instanceof definitions_1.InputObjectType)) { for (const field of element.fields()) { inaccessibleElements.push(...fetchInaccessibleElementsDeep(field)); } return inaccessibleElements; } else if (element instanceof definitions_1.EnumType) { for (const enumValue of element.values) { inaccessibleElements.push(...fetchInaccessibleElementsDeep(enumValue)); } return inaccessibleElements; } else if ((element instanceof definitions_1.DirectiveDefinition) || (element instanceof definitions_1.FieldDefinition)) { for (const argument of element.arguments()) { inaccessibleElements.push(...fetchInaccessibleElementsDeep(argument)); } return inaccessibleElements; } else if ((element instanceof definitions_1.UnionType) || (element instanceof definitions_1.ScalarType) || (element instanceof definitions_1.ArgumentDefinition) || (element instanceof definitions_1.InputFieldDefinition) || (element instanceof definitions_1.EnumValue)) { return inaccessibleElements; } (0, utils_1.assert)(false, "Unreachable code, element is of unknown type."); } const errors = []; let defaultValueReferencers = undefined; if (!inaccessibleSpec.isV01()) { defaultValueReferencers = computeDefaultValueReferencers(schema); } for (const type of schema.allTypes()) { if (hasBuiltInName(type)) { const inaccessibleElements = fetchInaccessibleElementsDeep(type); if (inaccessibleElements.length > 0) { errors.push(error_1.ERRORS.DISALLOWED_INACCESSIBLE.err(`Built-in type "${type.coordinate}" cannot use @inaccessible.`, { nodes: type.sourceAST, extensions: { inaccessible_elements: inaccessibleElements .map((element) => element.coordinate), inaccessible_referencers: [type.coordinate], } })); } } else if (isFeatureDefinition(type)) { const inaccessibleElements = fetchInaccessibleElementsDeep(type); if (inaccessibleElements.length > 0) { errors.push(error_1.ERRORS.DISALLOWED_INACCESSIBLE.err(`Core feature type "${type.coordinate}" cannot use @inaccessible.`, { nodes: type.sourceAST, extensions: { inaccessible_elements: inaccessibleElements .map((element) => element.coordinate), inaccessible_referencers: [type.coordinate], } })); } } else if (isInaccessible(type)) { const referencers = type.referencers(); for (const referencer of referencers) { if (referencer instanceof definitions_1.FieldDefinition || referencer instanceof definitions_1.ArgumentDefinition || referencer instanceof definitions_1.InputFieldDefinition) { if (isInAPISchema(referencer)) { errors.push(error_1.ERRORS.REFERENCED_INACCESSIBLE.err(`Type "${type.coordinate}" is @inaccessible but is referenced` + ` by "${referencer.coordinate}", which is in the API schema.`, { nodes: type.sourceAST, extensions: { inaccessible_elements: [type.coordinate], inaccessible_referencers: [referencer.coordinate], } })); } } else if (referencer instanceof definitions_1.SchemaDefinition) { if (type === referencer.rootType('query')) { errors.push(error_1.ERRORS.QUERY_ROOT_TYPE_INACCESSIBLE.err(`Type "${type.coordinate}" is @inaccessible but is the root` + ` query type, which must be in the API schema.`, { nodes: type.sourceAST, extensions: { inaccessible_elements: [type.coordinate], } })); } } } } else { if ((type instanceof definitions_1.ObjectType) || (type instanceof definitions_1.InterfaceType) || (type instanceof definitions_1.InputObjectType)) { let isEmpty = true; for (const field of type.fields()) { if (!isInaccessible(field)) isEmpty = false; } if (isEmpty) { errors.push(error_1.ERRORS.ONLY_INACCESSIBLE_CHILDREN.err(`Type "${type.coordinate}" is in the API schema but all of its` + ` ${(type instanceof definitions_1.InputObjectType) ? 'input ' : ''}fields` + ` are @inaccessible.`, { nodes: type.sourceAST, extensions: { inaccessible_elements: type.fields() .map((field) => field.coordinate), inaccessible_referencers: [type.coordinate], } })); } } else if (type instanceof definitions_1.UnionType) { let isEmpty = true; for (const member of type.types()) { if (!isInaccessible(member)) isEmpty = false; } if (isEmpty) { errors.push(error_1.ERRORS.ONLY_INACCESSIBLE_CHILDREN.err(`Type "${type.coordinate}" is in the API schema but all of its` + ` members are @inaccessible.`, { nodes: type.sourceAST, extensions: { inaccessible_elements: type.types() .map((type) => type.coordinate), inaccessible_referencers: [type.coordinate], } })); } } else if (type instanceof definitions_1.EnumType) { let isEmpty = true; for (const enumValue of type.values) { if (!isInaccessible(enumValue)) isEmpty = false; } if (isEmpty) { errors.push(error_1.ERRORS.ONLY_INACCESSIBLE_CHILDREN.err(`Type "${type.coordinate}" is in the API schema but all of its` + ` values are @inaccessible.`, { nodes: type.sourceAST, extensions: { inaccessible_elements: type.values .map((enumValue) => enumValue.coordinate), inaccessible_referencers: [type.coordinate], } })); } } if ((type instanceof definitions_1.ObjectType) || (type instanceof definitions_1.InterfaceType)) { const implementedInterfaces = type.interfaces(); const implementingTypes = []; if (type instanceof definitions_1.InterfaceType) { for (const referencer of type.referencers()) { if ((referencer instanceof definitions_1.ObjectType) || (referencer instanceof definitions_1.InterfaceType)) { implementingTypes.push(referencer); } } } for (const field of type.fields()) { if (isInaccessible(field)) { for (const implementedInterface of implementedInterfaces) { const implementedField = implementedInterface.field(field.name); if (implementedField && isInAPISchema(implementedField)) { errors.push(error_1.ERRORS.IMPLEMENTED_BY_INACCESSIBLE.err(`Field "${field.coordinate}" is @inaccessible but` + ` implements the interface field` + ` "${implementedField.coordinate}", which is in the API` + ` schema.`, { nodes: field.sourceAST, extensions: { inaccessible_elements: [field.coordinate], inaccessible_referencers: [implementedField.coordinate], } })); } } } else { for (const argument of field.arguments()) { if (isInaccessible(argument)) { if (argument.isRequired()) { errors.push(error_1.ERRORS.REQUIRED_INACCESSIBLE.err(`Argument "${argument.coordinate}" is @inaccessible but` + ` is a required argument of its field.`, { nodes: argument.sourceAST, extensions: { inaccessible_elements: [argument.coordinate], inaccessible_referencers: [argument.coordinate], } })); } for (const implementingType of implementingTypes) { const implementingField = implementingType.field(field.name); (0, utils_1.assert)(implementingField, "Schema should have been valid, but an implementing type" + " did not implement one of this type's fields."); const implementingArgument = implementingField .argument(argument.name); (0, utils_1.assert)(implementingArgument, "Schema should have been valid, but an implementing type" + " did not implement one of this type's field's arguments."); if (isInAPISchema(implementingArgument) && implementingArgument.isRequired()) { errors.push(error_1.ERRORS.REQUIRED_INACCESSIBLE.err(`Argument "${argument.coordinate}" is @inaccessible` + ` but is implemented by the required argument` + ` "${implementingArgument.coordinate}", which is` + ` in the API schema.`, { nodes: argument.sourceAST, extensions: { inaccessible_elements: [argument.coordinate], inaccessible_referencers: [ implementingArgument.coordinate, ], } })); } } for (const implementedInterface of implementedInterfaces) { const implementedArgument = (_a = implementedInterface .field(field.name)) === null || _a === void 0 ? void 0 : _a.argument(argument.name); if (implementedArgument && isInAPISchema(implementedArgument)) { errors.push(error_1.ERRORS.IMPLEMENTED_BY_INACCESSIBLE.err(`Argument "${argument.coordinate}" is @inaccessible` + ` but implements the interface argument` + ` "${implementedArgument.coordinate}", which is in` + ` the API schema.`, { nodes: argument.sourceAST, extensions: { inaccessible_elements: [argument.coordinate], inaccessible_referencers: [ implementedArgument.coordinate, ], } })); } } } } } } } else if (type instanceof definitions_1.InputObjectType) { for (const inputField of type.fields()) { if (isInaccessible(inputField)) { if (inputField.isRequired()) { errors.push(error_1.ERRORS.REQUIRED_INACCESSIBLE.err(`Input field "${inputField.coordinate}" is @inaccessible` + ` but is a required input field of its type.`, { nodes: inputField.sourceAST, extensions: { inaccessible_elements: [inputField.coordinate], inaccessible_referencers: [inputField.coordinate], } })); } (0, utils_1.assert)(defaultValueReferencers, "Input fields can't be @inaccessible in v0.1, but default value" + " referencers weren't computed (which is only skipped for v0.1)."); const referencers = (_b = defaultValueReferencers.get(inputField)) !== null && _b !== void 0 ? _b : []; for (const referencer of referencers) { if (isInAPISchema(referencer)) { errors.push(error_1.ERRORS.DEFAULT_VALUE_USES_INACCESSIBLE.err(`Input field "${inputField.coordinate}" is @inaccessible` + ` but is used in the default value of` + ` "${referencer.coordinate}", which is in the API schema.`, { nodes: type.sourceAST, extensions: { inaccessible_elements: [type.coordinate], inaccessible_referencers: [referencer.coordinate], } })); } } } } } else if (type instanceof definitions_1.EnumType) { for (const enumValue of type.values) { if (isInaccessible(enumValue)) { (0, utils_1.assert)(defaultValueReferencers, "Enum values can't be @inaccessible in v0.1, but default value" + " referencers weren't computed (which is only skipped for v0.1)."); const referencers = (_c = defaultValueReferencers.get(enumValue)) !== null && _c !== void 0 ? _c : []; for (const referencer of referencers) { if (isInAPISchema(referencer)) { errors.push(error_1.ERRORS.DEFAULT_VALUE_USES_INACCESSIBLE.err(`Enum value "${enumValue.coordinate}" is @inaccessible` + ` but is used in the default value of` + ` "${referencer.coordinate}", which is in the API schema.`, { nodes: type.sourceAST, extensions: { inaccessible_elements: [type.coordinate], inaccessible_referencers: [referencer.coordinate], } })); } } } } } } } for (const directive of schema.allDirectives()) { const typeSystemLocations = directive.locations.filter((loc) => (0, definitions_1.isTypeSystemDirectiveLocation)(loc)); if (hasBuiltInName(directive)) { const inaccessibleElements = fetchInaccessibleElementsDeep(directive); if (inaccessibleElements.length > 0) { errors.push(error_1.ERRORS.DISALLOWED_INACCESSIBLE.err(`Built-in directive "${directive.coordinate}" cannot use @inaccessible.`, { nodes: directive.sourceAST, extensions: { inaccessible_elements: inaccessibleElements .map((element) => element.coordinate), inaccessible_referencers: [directive.coordinate], } })); } } else if (isFeatureDefinition(directive)) { const inaccessibleElements = fetchInaccessibleElementsDeep(directive); if (inaccessibleElements.length > 0) { errors.push(error_1.ERRORS.DISALLOWED_INACCESSIBLE.err(`Core feature directive "${directive.coordinate}" cannot use @inaccessible.`, { nodes: directive.sourceAST, extensions: { inaccessible_elements: inaccessibleElements .map((element) => element.coordinate), inaccessible_referencers: [directive.coordinate], } })); } } else if (typeSystemLocations.length > 0) { const inaccessibleElements = fetchInaccessibleElementsDeep(directive); if (inaccessibleElements.length > 0) { errors.push(error_1.ERRORS.DISALLOWED_INACCESSIBLE.err(`Directive "${directive.coordinate}" cannot use @inaccessible` + ` because it may be applied to these type-system locations:` + ` ${typeSystemLocations.join(', ')}.`, { nodes: directive.sourceAST, extensions: { inaccessible_elements: inaccessibleElements .map((element) => element.coordinate), inaccessible_referencers: [directive.coordinate], } })); } } else { for (const argument of directive.arguments()) { if (argument.isRequired()) { if (isInaccessible(argument)) { errors.push(error_1.ERRORS.REQUIRED_INACCESSIBLE.err(`Argument "${argument.coordinate}" is @inaccessible but is a` + ` required argument of its directive.`, { nodes: argument.sourceAST, extensions: { inaccessible_elements: [argument.coordinate], inaccessible_referencers: [argument.coordinate], } })); } } } } } if (errors.length > 0) { throw (0, definitions_1.ErrGraphQLAPISchemaValidationFailed)(errors); } } function computeDefaultValueReferencers(schema) { const referencers = new Map(); function addReference(reference, referencer) { var _a; const referencerList = (_a = referencers.get(reference)) !== null && _a !== void 0 ? _a : []; if (referencerList.length === 0) { referencers.set(reference, referencerList); } referencerList.push(referencer); } for (const type of schema.allTypes()) { if (hasBuiltInName(type)) continue; if ((type instanceof definitions_1.ObjectType) || (type instanceof definitions_1.InterfaceType)) { for (const field of type.fields()) { for (const argument of field.arguments()) { for (const reference of computeDefaultValueReferences(argument)) { addReference(reference, argument); } } } } if (type instanceof definitions_1.InputObjectType) { for (const inputField of type.fields()) { for (const reference of computeDefaultValueReferences(inputField)) { addReference(reference, inputField); } } } } for (const directive of schema.allDirectives()) { if (hasBuiltInName(directive)) continue; for (const argument of directive.arguments()) { for (const reference of computeDefaultValueReferences(argument)) { addReference(reference, argument); } } } return referencers; } function computeDefaultValueReferences(element) { const references = []; addValueReferences(element.defaultValue, getInputType(element), references); return references; } function getInputType(element) { const type = element.type; (0, utils_1.assert)(type, "Schema should have been valid, but argument/input field did not have type."); return type; } function addValueReferences(value, type, references) { if (value === undefined || value === null) { return; } if ((0, definitions_1.isNonNullType)(type)) { return addValueReferences(value, type.ofType, references); } if ((0, definitions_1.isScalarType)(type)) { return; } if ((0, definitions_1.isVariable)(value)) { return; } if (Array.isArray(value)) { if ((0, definitions_1.isListType)(type)) { const itemType = type.ofType; for (const item of value) { addValueReferences(item, itemType, references); } } else { } return; } if ((0, definitions_1.isListType)(type)) { return addValueReferences(value, type.ofType, references); } if (typeof value === 'object') { if ((0, definitions_1.isInputObjectType)(type)) { for (const field of type.fields()) { const fieldValue = value[field.name]; if (fieldValue !== undefined) { references.push(field); addValueReferences(fieldValue, field.type, references); } else { } } } else { } return; } if (typeof value === 'string') { if ((0, definitions_1.isEnumType)(type)) { const enumValue = type.value(value); if (enumValue !== undefined) { references.push(enumValue); } else { } } else { } return; } return; } function hasBuiltInName(element) { const schema = element.schema(); if ((element instanceof definitions_1.ObjectType) || (element instanceof definitions_1.InterfaceType) || (element instanceof definitions_1.UnionType) || (element instanceof definitions_1.ScalarType) || (element instanceof definitions_1.EnumType) || (element instanceof definitions_1.InputObjectType)) { return schema.builtInTypes(true).some((type) => type.name === element.name); } else if (element instanceof definitions_1.DirectiveDefinition) { return schema.builtInDirectives(true).some((directive) => directive.name === element.name); } (0, utils_1.assert)(false, "Unreachable code, element is of unknown type."); } function removeInaccessibleElementsAssumingValid(schema, inaccessibleDirective) { function isInaccessible(element) { return element.hasAppliedDirective(inaccessibleDirective); } for (const type of schema.types()) { if (isInaccessible(type)) { type.remove(); } else { if ((type instanceof definitions_1.ObjectType) || (type instanceof definitions_1.InterfaceType)) { for (const field of type.fields()) { if (isInaccessible(field)) { field.remove(); } else { for (const argument of field.arguments()) { if (isInaccessible(argument)) { argument.remove(); } } } } } else if (type instanceof definitions_1.InputObjectType) { for (const inputField of type.fields()) { if (isInaccessible(inputField)) { inputField.remove(); } } } else if (type instanceof definitions_1.EnumType) { for (const enumValue of type.values) { if (isInaccessible(enumValue)) { enumValue.remove(); } } } } } for (const directive of schema.directives()) { for (const argument of directive.arguments()) { if (isInaccessible(argument)) { argument.remove(); } } } } //# sourceMappingURL=inaccessibleSpec.js.map