@apollo/federation-internals
Version:
Apollo Federation internal utilities
655 lines • 33.6 kB
JavaScript
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
;