SchemaTypeChecker.java
package graphql.schema.idl;
import graphql.GraphQLError;
import graphql.Internal;
import graphql.collect.ImmutableKit;
import graphql.introspection.Introspection;
import graphql.language.Argument;
import graphql.language.Directive;
import graphql.language.DirectiveDefinition;
import graphql.language.EnumTypeDefinition;
import graphql.language.EnumValueDefinition;
import graphql.language.FieldDefinition;
import graphql.language.InputObjectTypeDefinition;
import graphql.language.InputValueDefinition;
import graphql.language.InterfaceTypeDefinition;
import graphql.language.Node;
import graphql.language.ObjectTypeDefinition;
import graphql.language.ObjectTypeExtensionDefinition;
import graphql.language.Type;
import graphql.language.TypeDefinition;
import graphql.language.TypeName;
import graphql.language.UnionTypeDefinition;
import graphql.schema.idl.errors.DirectiveIllegalLocationError;
import graphql.schema.idl.errors.MissingInterfaceTypeError;
import graphql.schema.idl.errors.MissingScalarImplementationError;
import graphql.schema.idl.errors.MissingTypeError;
import graphql.schema.idl.errors.MissingTypeResolverError;
import graphql.schema.idl.errors.NonUniqueArgumentError;
import graphql.schema.idl.errors.NonUniqueNameError;
import graphql.schema.idl.errors.SchemaProblem;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import static graphql.introspection.Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION;
import static java.util.stream.Collectors.toList;
/**
* This helps pre check the state of the type system to ensure it can be made into an executable schema.
* <p>
* It looks for missing types and ensure certain invariants are true before a schema can be made.
*/
@Internal
public class SchemaTypeChecker {
public List<GraphQLError> checkTypeRegistry(ImmutableTypeDefinitionRegistry typeRegistry, RuntimeWiring wiring) throws SchemaProblem {
List<GraphQLError> errors = new ArrayList<>();
checkForMissingTypes(errors, typeRegistry);
SchemaTypeExtensionsChecker typeExtensionsChecker = new SchemaTypeExtensionsChecker();
typeExtensionsChecker.checkTypeExtensions(errors, typeRegistry);
ImplementingTypesChecker implementingTypesChecker = new ImplementingTypesChecker();
implementingTypesChecker.checkImplementingTypes(errors, typeRegistry);
UnionTypesChecker unionTypesChecker = new UnionTypesChecker();
unionTypesChecker.checkUnionType(errors, typeRegistry);
SchemaExtensionsChecker.checkSchemaInvariants(errors, typeRegistry);
checkScalarImplementationsArePresent(errors, typeRegistry, wiring);
checkTypeResolversArePresent(errors, typeRegistry, wiring);
checkFieldsAreSensible(errors, typeRegistry);
//check directive definitions before checking directive usages
checkDirectiveDefinitions(typeRegistry, errors);
SchemaTypeDirectivesChecker directivesChecker = new SchemaTypeDirectivesChecker(typeRegistry, wiring);
directivesChecker.checkTypeDirectives(errors);
return errors;
}
private void checkForMissingTypes(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry) {
// type extensions
List<ObjectTypeExtensionDefinition> typeExtensions = typeRegistry.objectTypeExtensions().values().stream().flatMap(Collection::stream).collect(toList());
typeExtensions.forEach(typeExtension -> {
List<Type> implementsTypes = typeExtension.getImplements();
implementsTypes.forEach(checkInterfaceTypeExists(typeRegistry, errors, typeExtension));
checkFieldTypesPresent(typeRegistry, errors, typeExtension, typeExtension.getFieldDefinitions());
});
Map<String, TypeDefinition> typesMap = typeRegistry.types();
// objects
List<ObjectTypeDefinition> objectTypes = filterTo(typesMap, ObjectTypeDefinition.class);
objectTypes.forEach(objectType -> {
List<Type> implementsTypes = objectType.getImplements();
implementsTypes.forEach(checkInterfaceTypeExists(typeRegistry, errors, objectType));
checkFieldTypesPresent(typeRegistry, errors, objectType, objectType.getFieldDefinitions());
});
// interfaces
List<InterfaceTypeDefinition> interfaceTypes = filterTo(typesMap, InterfaceTypeDefinition.class);
interfaceTypes.forEach(interfaceType -> {
List<FieldDefinition> fields = interfaceType.getFieldDefinitions();
checkFieldTypesPresent(typeRegistry, errors, interfaceType, fields);
});
// union types
List<UnionTypeDefinition> unionTypes = filterTo(typesMap, UnionTypeDefinition.class);
unionTypes.forEach(unionType -> {
List<Type> memberTypes = unionType.getMemberTypes();
memberTypes.forEach(checkTypeExists("union member", typeRegistry, errors, unionType));
});
// input types
List<InputObjectTypeDefinition> inputTypes = filterTo(typesMap, InputObjectTypeDefinition.class);
inputTypes.forEach(inputType -> {
List<InputValueDefinition> inputValueDefinitions = inputType.getInputValueDefinitions();
List<Type> inputValueTypes = ImmutableKit.map(inputValueDefinitions, InputValueDefinition::getType);
inputValueTypes.forEach(checkTypeExists("input value", typeRegistry, errors, inputType));
});
}
private void checkDirectiveDefinitions(TypeDefinitionRegistry typeRegistry, List<GraphQLError> errors) {
List<DirectiveDefinition> directiveDefinitions = new ArrayList<>(typeRegistry.getDirectiveDefinitions().values());
directiveDefinitions.forEach(directiveDefinition -> {
List<InputValueDefinition> arguments = directiveDefinition.getInputValueDefinitions();
checkNamedUniqueness(errors, arguments, InputValueDefinition::getName,
(name, arg) -> new NonUniqueNameError(directiveDefinition, arg));
List<Type> inputValueTypes = ImmutableKit.map(arguments, InputValueDefinition::getType);
inputValueTypes.forEach(
checkTypeExists(typeRegistry, errors, "directive definition", directiveDefinition, directiveDefinition.getName())
);
directiveDefinition.getDirectiveLocations().forEach(directiveLocation -> {
String locationName = directiveLocation.getName();
try {
Introspection.DirectiveLocation.valueOf(locationName);
} catch (IllegalArgumentException e) {
errors.add(new DirectiveIllegalLocationError(directiveDefinition, locationName));
}
});
});
}
private void checkScalarImplementationsArePresent(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry, RuntimeWiring wiring) {
typeRegistry.scalars().forEach((scalarName, scalarTypeDefinition) -> {
WiringFactory wiringFactory = wiring.getWiringFactory();
ScalarWiringEnvironment environment = new ScalarWiringEnvironment(typeRegistry, scalarTypeDefinition, ImmutableKit.emptyList());
if (!wiringFactory.providesScalar(environment) && !wiring.getScalars().containsKey(scalarName)) {
errors.add(new MissingScalarImplementationError(scalarName));
}
});
}
private void checkFieldsAreSensible(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry) {
Map<String, TypeDefinition> typesMap = typeRegistry.types();
Map<String, DirectiveDefinition> directiveDefinitionMap = typeRegistry.getDirectiveDefinitions();
// objects
List<ObjectTypeDefinition> objectTypes = filterTo(typesMap, ObjectTypeDefinition.class);
objectTypes.forEach(objectType -> checkObjTypeFields(errors, objectType, objectType.getFieldDefinitions(), directiveDefinitionMap));
// interfaces
List<InterfaceTypeDefinition> interfaceTypes = filterTo(typesMap, InterfaceTypeDefinition.class);
interfaceTypes.forEach(interfaceType -> checkInterfaceFields(errors, interfaceType, interfaceType.getFieldDefinitions(), directiveDefinitionMap));
// enum types
List<EnumTypeDefinition> enumTypes = filterTo(typesMap, EnumTypeDefinition.class);
enumTypes.forEach(enumType -> checkEnumValues(errors, enumType, enumType.getEnumValueDefinitions(), directiveDefinitionMap));
// input types
List<InputObjectTypeDefinition> inputTypes = filterTo(typesMap, InputObjectTypeDefinition.class);
inputTypes.forEach(inputType -> checkInputValues(errors, inputType, inputType.getInputValueDefinitions(), INPUT_FIELD_DEFINITION, directiveDefinitionMap));
}
private void checkObjTypeFields(List<GraphQLError> errors, ObjectTypeDefinition typeDefinition, List<FieldDefinition> fieldDefinitions, Map<String, DirectiveDefinition> directiveDefinitionMap) {
// field unique ness
checkNamedUniqueness(errors, fieldDefinitions, FieldDefinition::getName,
(name, fieldDef) -> new NonUniqueNameError(typeDefinition, fieldDef));
// field arg unique ness
fieldDefinitions.forEach(fld -> checkNamedUniqueness(errors, fld.getInputValueDefinitions(), InputValueDefinition::getName,
(name, inputValueDefinition) -> new NonUniqueArgumentError(typeDefinition, fld, name)));
// directive checks
fieldDefinitions.forEach(fld -> fld.getDirectives().forEach(directive -> {
checkNamedUniqueness(errors, directive.getArguments(), Argument::getName,
(argumentName, argument) -> new NonUniqueArgumentError(typeDefinition, fld, argumentName));
}));
}
private void checkInterfaceFields(List<GraphQLError> errors, InterfaceTypeDefinition interfaceType, List<FieldDefinition> fieldDefinitions, Map<String, DirectiveDefinition> directiveDefinitionMap) {
// field unique ness
checkNamedUniqueness(errors, fieldDefinitions, FieldDefinition::getName,
(name, fieldDef) -> new NonUniqueNameError(interfaceType, fieldDef));
// field arg unique ness
fieldDefinitions.forEach(fld -> checkNamedUniqueness(errors, fld.getInputValueDefinitions(), InputValueDefinition::getName,
(name, inputValueDefinition) -> new NonUniqueArgumentError(interfaceType, fld, name)));
// directive checks
fieldDefinitions.forEach(fieldDefinition -> {
List<Directive> directives = fieldDefinition.getDirectives();
directives.forEach(directive -> checkNamedUniqueness(errors, directive.getArguments(), Argument::getName,
(argumentName, argument) -> new NonUniqueArgumentError(interfaceType, fieldDefinition, argumentName)));
});
}
private void checkEnumValues(List<GraphQLError> errors, EnumTypeDefinition enumType, List<EnumValueDefinition> enumValueDefinitions, Map<String, DirectiveDefinition> directiveDefinitionMap) {
// enum unique ness
checkNamedUniqueness(errors, enumValueDefinitions, EnumValueDefinition::getName,
(name, inputValueDefinition) -> new NonUniqueNameError(enumType, inputValueDefinition));
// directive checks
enumValueDefinitions.forEach(enumValue -> enumValue.getDirectives().forEach(directive -> {
BiFunction<String, Argument, NonUniqueArgumentError> errorFunction = (argumentName, argument) -> new NonUniqueArgumentError(enumType, enumValue, argumentName);
checkNamedUniqueness(errors, directive.getArguments(), Argument::getName, errorFunction);
}));
}
private void checkInputValues(List<GraphQLError> errors, InputObjectTypeDefinition inputType, List<InputValueDefinition> inputValueDefinitions, Introspection.DirectiveLocation directiveLocation, Map<String, DirectiveDefinition> directiveDefinitionMap) {
// field unique ness
checkNamedUniqueness(errors, inputValueDefinitions, InputValueDefinition::getName,
(name, inputValueDefinition) -> {
// not sure why this is needed but inlining breaks it
@SuppressWarnings("UnnecessaryLocalVariable")
InputObjectTypeDefinition as = inputType;
return new NonUniqueNameError(as, inputValueDefinition);
});
// directive checks
inputValueDefinitions.forEach(inputValueDef -> inputValueDef.getDirectives().forEach(directive ->
checkNamedUniqueness(errors, directive.getArguments(), Argument::getName,
(argumentName, argument) -> new NonUniqueArgumentError(inputType, inputValueDef, argumentName))));
}
/**
* A simple function that takes a list of things, asks for their names and checks that the
* names are unique within that list. If not it calls the error handler function
*
* @param errors the error list
* @param listOfNamedThings the list of named things
* @param namer the function naming a thing
* @param errorFunction the function producing an error
*/
static <T, E extends GraphQLError> void checkNamedUniqueness(List<GraphQLError> errors, List<T> listOfNamedThings, Function<T, String> namer, BiFunction<String, T, E> errorFunction) {
Set<String> names = new LinkedHashSet<>();
listOfNamedThings.forEach(thing -> {
String name = namer.apply(thing);
if (names.contains(name)) {
errors.add(errorFunction.apply(name, thing));
} else {
names.add(name);
}
});
}
private void checkTypeResolversArePresent(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry, RuntimeWiring wiring) {
Predicate<InterfaceTypeDefinition> noDynamicResolverForInterface = interfaceTypeDef -> !wiring.getWiringFactory().providesTypeResolver(new InterfaceWiringEnvironment(typeRegistry, interfaceTypeDef));
Predicate<UnionTypeDefinition> noDynamicResolverForUnion = unionTypeDef -> !wiring.getWiringFactory().providesTypeResolver(new UnionWiringEnvironment(typeRegistry, unionTypeDef));
Predicate<TypeDefinition> noTypeResolver = typeDefinition -> !wiring.getTypeResolvers().containsKey(typeDefinition.getName());
Consumer<TypeDefinition> addError = typeDefinition -> errors.add(new MissingTypeResolverError(typeDefinition));
typeRegistry.types().values().stream()
.filter(typeDef -> typeDef instanceof InterfaceTypeDefinition)
.map(InterfaceTypeDefinition.class::cast)
.filter(noDynamicResolverForInterface)
.filter(noTypeResolver)
.forEach(addError);
typeRegistry.types().values().stream()
.filter(typeDef -> typeDef instanceof UnionTypeDefinition)
.map(UnionTypeDefinition.class::cast)
.filter(noDynamicResolverForUnion)
.filter(noTypeResolver)
.forEach(addError);
}
private void checkFieldTypesPresent(TypeDefinitionRegistry typeRegistry, List<GraphQLError> errors, TypeDefinition typeDefinition, List<FieldDefinition> fields) {
List<Type> fieldTypes = ImmutableKit.map(fields, FieldDefinition::getType);
fieldTypes.forEach(checkTypeExists("field", typeRegistry, errors, typeDefinition));
List<Type> fieldInputValues = fields.stream()
.map(f -> f.getInputValueDefinitions()
.stream()
.map(InputValueDefinition::getType)
.collect(toList()))
.flatMap(Collection::stream)
.collect(toList());
fieldInputValues.forEach(checkTypeExists("field input", typeRegistry, errors, typeDefinition));
}
private Consumer<Type> checkTypeExists(String typeOfType, TypeDefinitionRegistry typeRegistry, List<GraphQLError> errors, TypeDefinition typeDefinition) {
return t -> {
String name = TypeInfo.typeName(t);
if (!typeRegistry.hasType(name)) {
TypeName unwrapped = TypeInfo.typeInfo(t).getTypeName();
errors.add(new MissingTypeError(typeOfType, typeDefinition, unwrapped));
}
};
}
private Consumer<Type> checkTypeExists(TypeDefinitionRegistry typeRegistry, List<GraphQLError> errors, String typeOfType, Node element, String elementName) {
return ivType -> {
String name = TypeInfo.typeName(ivType);
if (!typeRegistry.hasType(name)) {
TypeName unwrapped = TypeInfo.typeInfo(ivType).getTypeName();
errors.add(new MissingTypeError(typeOfType, element, elementName, unwrapped));
}
};
}
private Consumer<? super Type> checkInterfaceTypeExists(TypeDefinitionRegistry typeRegistry, List<GraphQLError> errors, TypeDefinition typeDefinition) {
return t -> {
TypeInfo typeInfo = TypeInfo.typeInfo(t);
TypeName unwrapped = typeInfo.getTypeName();
TypeDefinition<?> type = typeRegistry.getTypeOrNull(unwrapped);
if (type == null) {
errors.add(new MissingInterfaceTypeError("interface", typeDefinition, unwrapped));
} else if (!(type instanceof InterfaceTypeDefinition)) {
errors.add(new MissingInterfaceTypeError("interface", typeDefinition, unwrapped));
}
};
}
private <T extends TypeDefinition> List<T> filterTo(Map<String, TypeDefinition> types, Class<? extends T> clazz) {
return ImmutableKit.filterAndMap(types.values(), t -> clazz.equals(t.getClass()),
clazz::cast);
}
}