@apollo/federation-internals
Version:
Apollo Federation internal utilities
1,055 lines (1,001 loc) • 40.3 kB
text/typescript
import { CorePurpose, FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
import {
ArgumentDefinition,
CoreFeatures,
DirectiveDefinition,
EnumType,
EnumValue,
ErrGraphQLAPISchemaValidationFailed,
FieldDefinition,
InputFieldDefinition,
InputObjectType,
InputType,
InterfaceType,
isEnumType,
isInputObjectType,
isListType,
isNonNullType,
isScalarType,
isTypeSystemDirectiveLocation,
isVariable,
NamedType,
ObjectType,
ScalarType,
Schema,
SchemaDefinition,
SchemaElement,
UnionType,
} from "../definitions";
import { GraphQLError, DirectiveLocation } from "graphql";
import { registerKnownFeature } from "../knownCoreFeatures";
import { ERRORS } from "../error";
import { createDirectiveSpecification, DirectiveSpecification } from "../directiveAndTypeSpecification";
import { assert } from "../utils";
export const inaccessibleIdentity = 'https://specs.apollo.dev/inaccessible';
export class InaccessibleSpecDefinition extends FeatureDefinition {
public readonly inaccessibleLocations: DirectiveLocation[];
public readonly inaccessibleDirectiveSpec: DirectiveSpecification;
private readonly printedInaccessibleDefinition: string;
constructor(version: FeatureVersion, minimumFederationVersion?: FeatureVersion) {
super(new FeatureUrl(inaccessibleIdentity, 'inaccessible', version), minimumFederationVersion);
this.inaccessibleLocations = [
DirectiveLocation.FIELD_DEFINITION,
DirectiveLocation.OBJECT,
DirectiveLocation.INTERFACE,
DirectiveLocation.UNION,
];
this.printedInaccessibleDefinition = 'directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION';
if (!this.isV01()) {
this.inaccessibleLocations.push(
DirectiveLocation.ARGUMENT_DEFINITION,
DirectiveLocation.SCALAR,
DirectiveLocation.ENUM,
DirectiveLocation.ENUM_VALUE,
DirectiveLocation.INPUT_OBJECT,
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 = createDirectiveSpecification({
name: 'inaccessible',
locations: this.inaccessibleLocations,
composes: true,
supergraphSpecification: (fedVersion) => INACCESSIBLE_VERSIONS.getMinimumRequiredVersion(fedVersion),
});
this.registerDirective(this.inaccessibleDirectiveSpec);
}
isV01() {
return this.version.equals(new FeatureVersion(0, 1));
}
inaccessibleDirective(schema: Schema): DirectiveDefinition<Record<string, never>> | undefined {
return this.directive(schema, 'inaccessible');
}
checkCompatibleDirective(definition: DirectiveDefinition): GraphQLError | undefined {
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 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(): CorePurpose | undefined {
return 'SECURITY';
}
}
export const INACCESSIBLE_VERSIONS = new FeatureDefinitions<InaccessibleSpecDefinition>(inaccessibleIdentity)
.add(new InaccessibleSpecDefinition(new FeatureVersion(0, 1)))
.add(new InaccessibleSpecDefinition(new FeatureVersion(0, 2), new FeatureVersion(2, 0)));
registerKnownFeature(INACCESSIBLE_VERSIONS);
export function removeInaccessibleElements(schema: Schema) {
// Note it doesn't hurt to validate here, since we expect the schema to be
// validated already, and if it has been, it's cached/inexpensive.
schema.validate();
const coreFeatures = schema.coreFeatures;
if (!coreFeatures) {
return;
}
const inaccessibleFeature = coreFeatures.getByIdentity(inaccessibleIdentity);
if (!inaccessibleFeature) {
return;
}
const inaccessibleSpec = INACCESSIBLE_VERSIONS.find(
inaccessibleFeature.url.version
);
if (!inaccessibleSpec) {
throw ErrGraphQLAPISchemaValidationFailed([new GraphQLError(
`Cannot remove inaccessible elements: the schema uses unsupported` +
` inaccessible spec version ${inaccessibleFeature.url.version}` +
` (supported versions: ${INACCESSIBLE_VERSIONS.versions().join(', ')})`
)]);
}
const inaccessibleDirective = inaccessibleSpec.inaccessibleDirective(schema);
if (!inaccessibleDirective) {
throw ErrGraphQLAPISchemaValidationFailed([new GraphQLError(
`Invalid schema: declares ${inaccessibleSpec.url} spec but does not` +
` define a @inaccessible directive.`
)]);
}
const incompatibleError =
inaccessibleSpec.checkCompatibleDirective(inaccessibleDirective);
if (incompatibleError) {
throw ErrGraphQLAPISchemaValidationFailed([incompatibleError]);
}
validateInaccessibleElements(
schema,
coreFeatures,
inaccessibleSpec,
inaccessibleDirective,
);
removeInaccessibleElementsAssumingValid(
schema,
inaccessibleDirective,
)
}
// These are elements that may be hidden, by either @inaccessible or core
// feature definition hiding.
type HideableElement =
| ObjectType
| InterfaceType
| UnionType
| ScalarType
| EnumType
| InputObjectType
| DirectiveDefinition
| FieldDefinition<ObjectType | InterfaceType>
| ArgumentDefinition<
| DirectiveDefinition
| FieldDefinition<ObjectType | InterfaceType>>
| InputFieldDefinition
| EnumValue
// Validate the applications of @inaccessible in the schema. Some of these may
// technically be caught by Schema.validate() later, but we'd like to give
// clearer error messaging when possible.
function validateInaccessibleElements(
schema: Schema,
coreFeatures: CoreFeatures,
inaccessibleSpec: InaccessibleSpecDefinition,
inaccessibleDirective: DirectiveDefinition,
): void {
function isInaccessible(element: SchemaElement<any, any>): boolean {
return element.hasAppliedDirective(inaccessibleDirective);
}
const featureList = [...coreFeatures.allFeatures()];
function isFeatureDefinition(
element: NamedType | DirectiveDefinition
): boolean {
return featureList.some((feature) => feature.isFeatureDefinition(element));
}
function isInAPISchema(element: HideableElement): boolean {
// If this element is @inaccessible, it's not in the API schema.
if (
!(element instanceof DirectiveDefinition) &&
isInaccessible(element)
) return false;
if (
(element instanceof ObjectType) ||
(element instanceof InterfaceType) ||
(element instanceof UnionType) ||
(element instanceof ScalarType) ||
(element instanceof EnumType) ||
(element instanceof InputObjectType) ||
(element instanceof DirectiveDefinition)
) {
// These are top-level elements. If they're not @inaccessible, the only
// way they won't be in the API schema is if they're definitions of some
// core feature. However, we do intend on introducing mechanisms for
// exposing core feature elements in the API schema in the near feature.
// Because such mechanisms aren't completely nailed down yet, we opt to
// pretend here that all core feature elements are in the API schema for
// simplicity sake.
//
// This has the effect that if a non-core schema element is referenced by
// a core schema element, that non-core schema element can't be marked
// @inaccessible, despite that the core schema element may likely not be
// in the API schema. This may be relaxed in a later version of the
// inaccessible spec.
return true;
} else if (
(element instanceof FieldDefinition) ||
(element instanceof ArgumentDefinition) ||
(element instanceof InputFieldDefinition) ||
(element instanceof EnumValue)
) {
// While this element isn't marked @inaccessible, this element won't be in
// the API schema if its parent isn't.
return isInAPISchema(element.parent);
}
assert(false, "Unreachable code, element is of unknown type.");
}
function fetchInaccessibleElementsDeep(
element: HideableElement
): HideableElement[] {
const inaccessibleElements: HideableElement[] = [];
if (isInaccessible(element)) {
inaccessibleElements.push(element);
}
if (
(element instanceof ObjectType) ||
(element instanceof InterfaceType) ||
(element instanceof InputObjectType)
) {
for (const field of element.fields()) {
inaccessibleElements.push(
...fetchInaccessibleElementsDeep(field),
);
}
return inaccessibleElements;
} else if (element instanceof EnumType) {
for (const enumValue of element.values) {
inaccessibleElements.push(
...fetchInaccessibleElementsDeep(enumValue),
)
}
return inaccessibleElements;
} else if (
(element instanceof DirectiveDefinition) ||
(element instanceof FieldDefinition)
) {
for (const argument of element.arguments()) {
inaccessibleElements.push(
...fetchInaccessibleElementsDeep(argument),
)
}
return inaccessibleElements;
} else if (
(element instanceof UnionType) ||
(element instanceof ScalarType) ||
(element instanceof ArgumentDefinition) ||
(element instanceof InputFieldDefinition) ||
(element instanceof EnumValue)
) {
return inaccessibleElements;
}
assert(false, "Unreachable code, element is of unknown type.");
}
const errors: GraphQLError[] = [];
let defaultValueReferencers: Map<
DefaultValueReference,
SchemaElementWithDefaultValue[]
> | undefined = undefined;
if (!inaccessibleSpec.isV01()) {
// Note that for inaccessible v0.1, enum values and input fields can't be
// @inaccessible, so there's no need to compute references (the inaccessible
// v0.1 spec also doesn't require default values to be valid, so it doesn't
// make sense to compute them).
defaultValueReferencers = computeDefaultValueReferencers(schema);
}
for (const type of schema.allTypes()) {
if (hasBuiltInName(type)) {
// Built-in types (and their descendants) aren't allowed to be
// @inaccessible, regardless of shadowing.
const inaccessibleElements = fetchInaccessibleElementsDeep(type);
if (inaccessibleElements.length > 0) {
errors.push(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)) {
// Core feature types (and their descendants) aren't allowed to be
// @inaccessible.
const inaccessibleElements = fetchInaccessibleElementsDeep(type);
if (inaccessibleElements.length > 0) {
errors.push(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)) {
// Types can be referenced by other schema elements in a few ways:
// 1. Fields, arguments, and input fields may have the type as their base
// type.
// 2. Union types may have the type as a member (for object types).
// 3. Object and interface types may implement the type (for interface
// types).
// 4. Schemas may have the type as a root operation type (for object
// types).
//
// When a type is hidden, the referencer must follow certain rules for the
// schema to be valid. Respectively, these rules are:
// 1. The field/argument/input field must not be in the API schema.
// 2. The union type, if empty, must not be in the API schema.
// 3. No rules are imposed in this case.
// 4. The root operation type must not be the query type.
//
// We validate the 1st and 4th rules above, and leave the 2nd for when we
// look at accessible union types.
const referencers = type.referencers();
for (const referencer of referencers) {
if (
referencer instanceof FieldDefinition ||
referencer instanceof ArgumentDefinition ||
referencer instanceof InputFieldDefinition
) {
if (isInAPISchema(referencer)) {
errors.push(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 SchemaDefinition) {
if (type === referencer.rootType('query')) {
errors.push(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 {
// At this point, we know the type must be in the API schema. For types
// with children (all types except scalar), we check that at least one of
// the children is accessible.
if (
(type instanceof ObjectType) ||
(type instanceof InterfaceType) ||
(type instanceof InputObjectType)
) {
let isEmpty = true;
for (const field of type.fields()) {
if (!isInaccessible(field)) isEmpty = false;
}
if (isEmpty) {
errors.push(ERRORS.ONLY_INACCESSIBLE_CHILDREN.err(
`Type "${type.coordinate}" is in the API schema but all of its` +
` ${(type instanceof 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 UnionType) {
let isEmpty = true;
for (const member of type.types()) {
if (!isInaccessible(member)) isEmpty = false;
}
if (isEmpty) {
errors.push(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 EnumType) {
let isEmpty = true;
for (const enumValue of type.values) {
if (!isInaccessible(enumValue)) isEmpty = false;
}
if (isEmpty) {
errors.push(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],
}
}
));
}
}
// Descend into the type's children if needed.
if (
(type instanceof ObjectType) ||
(type instanceof InterfaceType)
) {
const implementedInterfaces = type.interfaces();
const implementingTypes: (ObjectType | InterfaceType)[] = [];
if (type instanceof InterfaceType) {
for (const referencer of type.referencers()) {
if (
(referencer instanceof ObjectType) ||
(referencer instanceof InterfaceType)
) {
implementingTypes.push(referencer);
}
}
}
for (const field of type.fields()) {
if (isInaccessible(field)) {
// Fields can be "referenced" by the corresponding fields of any
// interfaces their parent type implements. When a field is hidden
// (but its parent isn't), we check that such implemented fields
// aren't in the API schema.
for (const implementedInterface of implementedInterfaces) {
const implementedField = implementedInterface.field(field.name);
if (implementedField && isInAPISchema(implementedField)) {
errors.push(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 {
// Descend into the field's arguments.
for (const argument of field.arguments()) {
if (isInaccessible(argument)) {
// When an argument is hidden (but its ancestors aren't), we
// check that it isn't a required argument of its field.
if (argument.isRequired()) {
errors.push(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],
}
},
));
}
// When an argument is hidden (but its ancestors aren't), we
// check that it isn't a required argument of any implementing
// fields in the API schema. This is because the GraphQL spec
// requires that any arguments of an implementing field that
// aren't in its implemented field are optional.
//
// You might be thinking that a required argument in an
// implementing field would necessitate that the implemented
// field would also require that argument (and thus the check
// above would also always error, removing the need for this
// one), but the GraphQL spec does not enforce this. E.g. it's
// valid GraphQL for the implementing and implemented arguments
// to be both non-nullable, but for just the implemented
// argument to have a default value. Not providing a value for
// the argument when querying the implemented type succeeds
// GraphQL operation validation, but results in input coercion
// failure for the field at runtime.
for (const implementingType of implementingTypes) {
const implementingField = implementingType.field(field.name);
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);
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(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,
],
}
},
));
}
}
// Arguments can be "referenced" by the corresponding arguments
// of any interfaces their parent type implements. When an
// argument is hidden (but its ancestors aren't), we check that
// such implemented arguments aren't in the API schema.
for (const implementedInterface of implementedInterfaces) {
const implementedArgument = implementedInterface
.field(field.name)
?.argument(argument.name);
if (
implementedArgument &&
isInAPISchema(implementedArgument)
) {
errors.push(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 InputObjectType) {
for (const inputField of type.fields()) {
if (isInaccessible(inputField)) {
// When an input field is hidden (but its parent isn't), we check
// that it isn't a required argument of its field.
if (inputField.isRequired()) {
errors.push(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],
}
},
));
}
// Input fields can be referenced by schema default values. When an
// input field is hidden (but its parent isn't), we check that the
// arguments/input fields with such default values aren't in the API
// schema.
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 = defaultValueReferencers.get(inputField) ?? [];
for (const referencer of referencers) {
if (isInAPISchema(referencer)) {
errors.push(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 EnumType) {
for (const enumValue of type.values) {
if (isInaccessible(enumValue)) {
// Enum values can be referenced by schema default values. When an
// enum value is hidden (but its parent isn't), we check that the
// arguments/input fields with such default values aren't in the API
// schema.
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 = defaultValueReferencers.get(enumValue) ?? [];
for (const referencer of referencers) {
if (isInAPISchema(referencer)) {
errors.push(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) => isTypeSystemDirectiveLocation(loc));
if (hasBuiltInName(directive)) {
// Built-in directives (and their descendants) aren't allowed to be
// @inaccessible, regardless of shadowing.
const inaccessibleElements =
fetchInaccessibleElementsDeep(directive);
if (inaccessibleElements.length > 0) {
errors.push(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)) {
// Core feature directives (and their descendants) aren't allowed to be
// @inaccessible.
const inaccessibleElements =
fetchInaccessibleElementsDeep(directive);
if (inaccessibleElements.length > 0) {
errors.push(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) {
// Directives that can appear on type-system locations (and their
// descendants) aren't allowed to be @inaccessible.
const inaccessibleElements =
fetchInaccessibleElementsDeep(directive);
if (inaccessibleElements.length > 0) {
errors.push(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 {
// At this point, we know the directive must be in the API schema. Descend
// into the directive's arguments.
for (const argument of directive.arguments()) {
// When an argument is hidden (but its parent isn't), we check that it
// isn't a required argument of its directive.
if (argument.isRequired()) {
if (isInaccessible(argument)) {
errors.push(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 ErrGraphQLAPISchemaValidationFailed(errors);
}
}
type DefaultValueReference = InputFieldDefinition | EnumValue;
type SchemaElementWithDefaultValue =
| ArgumentDefinition<
| DirectiveDefinition
| FieldDefinition<ObjectType | InterfaceType>>
| InputFieldDefinition;
// Default values in a schema may contain references to selectable elements that
// are @inaccessible (input fields and enum values). For a given schema, this
// function returns a map from such selectable elements to the elements with
// default values referencing them. (The default values of built-ins and their
// descendants are skipped.)
//
// This function assumes default values are coercible to their location types
// (see the comments for addValueReferences() for details).
function computeDefaultValueReferencers(
schema: Schema,
): Map<
DefaultValueReference,
SchemaElementWithDefaultValue[]
> {
const referencers = new Map<
DefaultValueReference,
SchemaElementWithDefaultValue[]
>();
function addReference(
reference: DefaultValueReference,
referencer: SchemaElementWithDefaultValue,
) {
const referencerList = referencers.get(reference) ?? [];
if (referencerList.length === 0) {
referencers.set(reference, referencerList);
}
referencerList.push(referencer);
}
// Note that the fields/arguments/input fields for built-in schema elements
// can presumably only have types that are built-in types. Since built-ins and
// their children aren't allowed to be @inaccessible, this means we shouldn't
// have to worry about references within the default values of arguments and
// input fields of built-ins, which is why we skip them below.
for (const type of schema.allTypes()) {
if (hasBuiltInName(type)) continue;
// Scan object/interface field arguments.
if (
(type instanceof ObjectType) ||
(type instanceof InterfaceType)
) {
for (const field of type.fields()) {
for (const argument of field.arguments()) {
for (
const reference of computeDefaultValueReferences(argument)
) {
addReference(reference, argument);
}
}
}
}
// Scan input object fields.
if (type instanceof InputObjectType) {
for (const inputField of type.fields()) {
for (
const reference of computeDefaultValueReferences(inputField)
) {
addReference(reference, inputField);
}
}
}
}
// Scan directive definition arguments.
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;
}
// For the given element, compute a list of input fields and enum values that
// are referenced in its default value (if any). This function assumes the
// default value is coercible to the element's type (see the comments for
// addValueReferences() for details).
function computeDefaultValueReferences(
element: SchemaElementWithDefaultValue,
): DefaultValueReference[] {
const references: DefaultValueReference[] = [];
addValueReferences(
element.defaultValue,
getInputType(element),
references,
)
return references;
}
function getInputType(element: SchemaElementWithDefaultValue): InputType {
const type = element.type;
assert(
type,
"Schema should have been valid, but argument/input field did not have type."
);
return type;
}
// For the given GraphQL input value (represented in the format implicitly
// defined in buildValue()) and its type, add any references to input fields and
// enum values in that input value to the given references list.
//
// Note that this function requires the input value to be coercible to its type,
// similar to the "Values of Correct Type" validation in the GraphQL spec.
// However, there are two noteable differences:
// 1. Variable references are not allowed.
// 2. Scalar values are not required to be coercible (due to machine-specific differences in input coercion rules).
// As it turns out, building a Schema object validates this (and a bit more)
// already, so in the interests of not duplicating validations/keeping the logic
// centralized, this code assumes the input values it receives satisfy the above
// validations.
//
// Accordingly, this function's code is structured very similarly to the
// valueToString() function, which makes similar assumptions about its given
// value. If any inconsistencies/invalidities are discovered, they will be
// silently ignored.
function addValueReferences(
value: any,
type: InputType,
references: DefaultValueReference[],
): void {
if (value === undefined || value === null) {
return;
}
if (isNonNullType(type)) {
return addValueReferences(value, type.ofType, references);
}
if (isScalarType(type)) {
// No need to look at scalar values.
return;
}
if (isVariable(value)) {
// Values in schemas shouldn't use variables, but we silently ignore it.
return;
}
if (Array.isArray(value)) {
if (isListType(type)) {
const itemType = type.ofType;
for (const item of value) {
addValueReferences(item, itemType, references);
}
} else {
// At this point a JS array can only be a list type, but we silently
// ignore when it's not.
}
return;
}
if (isListType(type)) {
// Note that GraphQL spec coerces non-list items into single-element lists.
return addValueReferences(value, type.ofType, references);
}
if (typeof value === 'object') {
if (isInputObjectType(type)) {
// Silently ignore object keys that aren't in the input object.
for (const field of type.fields()) {
const fieldValue = value[field.name];
if (fieldValue !== undefined) {
references.push(field);
addValueReferences(fieldValue, field.type!, references);
} else {
// Silently ignore when required input fields are omitted.
}
}
} else {
// At this point a JS object can only be an input object type, but we
// silently ignore when it's not.
}
return;
}
if (typeof value === 'string') {
if (isEnumType(type)) {
const enumValue = type.value(value);
if (enumValue !== undefined) {
references.push(enumValue);
} else {
// Silently ignore enum values that aren't in the enum type.
}
} else {
// At this point a JS string can only be an enum type, but we silently
// ignore when it's not.
}
return;
}
// This should be unreachable code, but we silently ignore when it's not.
return;
}
// Determine whether a given schema element has a built-in's name. Note that
// this is not the same as the isBuiltIn flag, due to shadowing definitions
// (which will not have the flag set).
function hasBuiltInName(element: NamedType | DirectiveDefinition): boolean {
const schema = element.schema();
if (
(element instanceof ObjectType) ||
(element instanceof InterfaceType) ||
(element instanceof UnionType) ||
(element instanceof ScalarType) ||
(element instanceof EnumType) ||
(element instanceof InputObjectType)
) {
return schema.builtInTypes(true).some((type) =>
type.name === element.name
);
} else if (element instanceof DirectiveDefinition) {
return schema.builtInDirectives(true).some((directive) =>
directive.name === element.name
);
}
assert(false, "Unreachable code, element is of unknown type.")
}
// Remove schema elements marked with @inaccessible in the schema, assuming the
// schema has been validated with validateInaccessibleElements().
//
// Note the schema that results from this may not necessarily be valid GraphQL
// until core feature definitions have been removed by removeFeatureElements().
function removeInaccessibleElementsAssumingValid(
schema: Schema,
inaccessibleDirective: DirectiveDefinition,
): void {
function isInaccessible(element: SchemaElement<any, any>): boolean {
return element.hasAppliedDirective(inaccessibleDirective);
}
for (const type of schema.types()) {
if (isInaccessible(type)) {
type.remove();
} else {
if ((type instanceof ObjectType) || (type instanceof 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 InputObjectType) {
for (const inputField of type.fields()) {
if (isInaccessible(inputField)) {
inputField.remove();
}
}
} else if (type instanceof 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();
}
}
}
}