@theguild/federation-composition
Version:
Open Source Composition library for Apollo Federation
522 lines (521 loc) • 22 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateSubgraphState = validateSubgraphState;
exports.isTypeSubTypeOf = isTypeSubTypeOf;
exports.isInputType = isInputType;
exports.typeExists = typeExists;
exports.isInputObjectType = isInputObjectType;
exports.isScalarType = isScalarType;
exports.isEnumType = isEnumType;
exports.isObjectType = isObjectType;
exports.isInterfaceType = isInterfaceType;
exports.isUnionType = isUnionType;
exports.isDirective = isDirective;
const graphql_1 = require("graphql");
const format_js_1 = require("../../utils/format.js");
const state_js_1 = require("../../utils/state.js");
const state_js_2 = require("../state.js");
const specifiedScalars = new Set(graphql_1.specifiedScalarTypes.map(t => t.name));
const SKIP = Symbol('skip');
function validateSubgraphState(state, context) {
const errors = [];
function reportError(message) {
errors.push(new graphql_1.GraphQLError(message, {
extensions: {
code: 'INVALID_GRAPHQL',
},
}));
}
validateRootTypes(state, reportError);
validateDirectives(state, reportError, context);
validateTypes(state, reportError);
return errors;
}
function validateRootTypes(state, reportError) {
const rootTypesMap = new Map();
for (const key in state.schema) {
const rootTypeKind = key;
const rootTypeName = state.schema[rootTypeKind];
if (rootTypeName) {
const rootType = state.types.get(rootTypeName);
if (!rootType) {
continue;
}
if (!isObjectType(rootType)) {
const operationTypeStr = capitalize(rootTypeKind.replace('Type', ''));
reportError(rootTypeKind === 'queryType'
? `${operationTypeStr} root type must be Object type, it cannot be ${rootTypeName}.`
: `${operationTypeStr} root type must be Object type if provided, it cannot be ${rootTypeName}.`);
}
else {
const existing = rootTypesMap.get(rootTypeName);
if (existing) {
existing.add(rootTypeKind);
}
else {
rootTypesMap.set(rootTypeName, new Set([rootTypeKind]));
}
}
}
}
for (const [rootTypeName, operationTypes] of rootTypesMap) {
if (operationTypes.size > 1) {
const operationList = (0, format_js_1.andList)(Array.from(operationTypes).map(op => capitalize(op.replace('Type', ''))));
reportError(`All root types must be different, "${rootTypeName}" type is used as ${operationList} root types.`);
}
}
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
function validateDirectives(state, reportError, context) {
for (const directive of state.types.values()) {
if (isDirective(directive)) {
if (context.isLinkSpecDirective(directive.name)) {
continue;
}
validateName(reportError, directive.name);
for (const [argName, arg] of directive.args) {
validateName(reportError, argName);
const argInputTypeName = (0, state_js_1.stripTypeModifiers)(arg.type);
if (context.isLinkSpecType(argInputTypeName)) {
continue;
}
const isInput = isInputType(state, argInputTypeName);
if (isInput === SKIP) {
continue;
}
if (!isInput) {
reportError(`The type of @${directive.name}(${arg.name}:) must be Input Type ` +
`but got: ${arg.type}.`);
}
if (isRequiredArgument(arg) && arg.deprecated?.deprecated === true) {
reportError(`Required argument @${directive.name}(${arg.name}:) cannot be deprecated.`);
}
}
}
}
}
function validateTypes(state, reportError) {
const validateInputObjectCircularRefs = createInputObjectCircularRefsValidator(state, reportError);
const implementationsMap = new Map();
for (const type of state.types.values()) {
if (isInterfaceType(type)) {
for (const iface of type.interfaces) {
const interfaceType = state.types.get(iface);
if (interfaceType && isInterfaceType(interfaceType)) {
let implementations = implementationsMap.get(iface);
if (implementations === undefined) {
implementationsMap.set(iface, new Set([type.name]));
}
else {
implementations.add(type.name);
}
}
}
}
else if (isObjectType(type)) {
for (const iface of type.interfaces) {
const interfaceType = state.types.get(iface);
if (interfaceType && isInterfaceType(interfaceType)) {
let implementations = implementationsMap.get(iface);
if (implementations === undefined) {
implementationsMap.set(iface, new Set([type.name]));
}
else {
implementations.add(type.name);
}
}
}
}
}
for (const type of state.types.values()) {
if (!isIntrospectionType(type.name)) {
validateName(reportError, type.name);
}
if (isObjectType(type)) {
validateFields(state, reportError, type);
validateInterfaces(state, implementationsMap, reportError, type);
}
else if (isInterfaceType(type)) {
validateFields(state, reportError, type);
validateInterfaces(state, implementationsMap, reportError, type);
}
else if (isUnionType(type)) {
validateUnionMembers(state, reportError, type);
}
else if (isEnumType(type)) {
validateEnumValues(reportError, type);
}
else if (isInputObjectType(type)) {
validateInputFields(state, reportError, type);
validateInputObjectCircularRefs(type);
}
}
}
function validateName(reportError, name) {
if (name.startsWith('__')) {
reportError(`Name "${name}" must not begin with "__", which is reserved by GraphQL introspection.`);
}
}
function validateFields(state, reportError, type) {
const fields = type.fields;
const isRootType = type.name === state.schema.queryType ||
type.name === state.schema.mutationType ||
type.name === state.schema.subscriptionType;
if (fields.size === 0 && !isRootType) {
reportError(`Type ${type.name} must define one or more fields.`);
}
for (const field of fields.values()) {
validateName(reportError, field.name);
const fieldTypeName = (0, state_js_1.stripTypeModifiers)(field.type);
const fieldTypeExists = typeExists(state, fieldTypeName);
if (!fieldTypeExists) {
continue;
}
const isOutput = isOutputType(state, fieldTypeName);
if (isOutput === SKIP) {
continue;
}
if (!isOutput) {
reportError(`The type of "${type.name}.${field.name}" must be Output Type but got: "${field.type}".`);
}
for (const arg of field.args.values()) {
const argName = arg.name;
validateName(reportError, argName);
const argTypeName = (0, state_js_1.stripTypeModifiers)(arg.type);
const argTypeExists = typeExists(state, argTypeName);
if (!argTypeExists) {
continue;
}
const isInput = isInputType(state, argTypeName);
if (isInput === SKIP) {
continue;
}
if (!isInput) {
const isList = arg.type.endsWith(']');
const isNonNull = arg.type.endsWith('!');
const extra = isList ? ', a ListType' : isNonNull ? ', a NonNullType' : '';
reportError(`The type of "${type.name}.${field.name}(${argName}:)" must be Input Type but got "${arg.type}"${extra}.`);
}
if (isRequiredArgument(arg) && arg.deprecated?.deprecated) {
reportError(`Required argument ${type.name}.${field.name}(${argName}:) cannot be deprecated.`);
}
if (typeof arg.defaultValue !== 'undefined' &&
!isValidateDefaultValue(state, reportError, arg.type, (0, graphql_1.parseValue)(arg.defaultValue))) {
reportError(`Invalid default value (got: ${arg.defaultValue}) provided for argument ${type.name}.${field.name}(${arg.name}:) of type ${arg.type}.`);
}
}
}
}
function isValidateDefaultValue(state, reportError, inputTypePrinted, value) {
if ((0, state_js_1.isNonNull)(inputTypePrinted)) {
if (value.kind === graphql_1.Kind.NULL) {
return false;
}
return isValidateDefaultValue(state, reportError, (0, state_js_1.stripNonNull)(inputTypePrinted), value);
}
if (value.kind === graphql_1.Kind.NULL) {
return true;
}
const inputTypeName = (0, state_js_1.stripTypeModifiers)(inputTypePrinted);
const inputType = state.types.get(inputTypeName);
if (inputType && isScalarType(inputType)) {
return true;
}
if ((0, state_js_1.isList)(inputTypePrinted)) {
if (value.kind === graphql_1.Kind.LIST) {
return value.values.every(val => isValidateDefaultValue(state, reportError, (0, state_js_1.stripList)(inputTypePrinted), val));
}
return isValidateDefaultValue(state, reportError, (0, state_js_1.stripList)(inputTypePrinted), value);
}
if (specifiedScalars.has(inputTypeName)) {
const specifiedScalar = graphql_1.specifiedScalarTypes.find(t => t.name === inputTypeName);
try {
specifiedScalar.parseLiteral(value);
return true;
}
catch (error) {
return false;
}
}
if (!inputType) {
return true;
}
if (isInputObjectType(inputType)) {
if (value.kind !== graphql_1.Kind.OBJECT) {
return false;
}
const fields = inputType.fields;
for (const astField of value.fields) {
const field = fields.get(astField.name.value);
if (!field) {
return false;
}
if (!isValidateDefaultValue(state, reportError, field.type, astField.value)) {
return false;
}
}
return true;
}
if (isEnumType(inputType)) {
if (value.kind !== graphql_1.Kind.ENUM && value.kind !== graphql_1.Kind.STRING) {
return false;
}
return inputType.values.has(value.value);
}
return false;
}
function validateUnionMembers(state, reportError, union) {
const memberTypes = union.members;
if (memberTypes.size === 0) {
reportError(`Union type ${union.name} must define one or more member types.`);
}
const includedTypeNames = new Set();
for (const memberType of memberTypes) {
if (includedTypeNames.has(memberType)) {
reportError(`Union type ${union.name} can only include type ${memberType} once.`);
continue;
}
includedTypeNames.add(memberType);
const type = state.types.get(memberType);
if (!type || !isObjectType(type)) {
reportError(`Union type ${union.name} can only include Object types, ` +
`it cannot include ${memberType}.`);
}
}
}
function validateEnumValues(reportError, enumType) {
const enumValues = enumType.values;
if (enumValues.size === 0) {
reportError(`Enum type ${enumType.name} must define one or more values.`);
}
for (const enumValue of enumValues.keys()) {
validateName(reportError, enumValue);
}
}
function validateInputFields(state, reportError, inputObj) {
const fields = inputObj.fields;
if (fields.size === 0) {
reportError(`Input Object type ${inputObj.name} must define one or more fields.`);
}
for (const field of fields.values()) {
validateName(reportError, field.name);
const fieldTypeName = (0, state_js_1.stripTypeModifiers)(field.type);
const fieldTypeExists = typeExists(state, fieldTypeName);
if (!fieldTypeExists) {
continue;
}
const isInput = isInputType(state, fieldTypeName);
if (isInput === SKIP) {
continue;
}
if (!isInput) {
const isList = field.type.endsWith(']');
const isNonNull = field.type.endsWith('!');
const extra = isList ? ', a ListType' : isNonNull ? ', a NonNullType' : '';
reportError(`The type of ${inputObj.name}.${field.name} must be Input Type but got "${field.type}"${extra}.`);
}
if (isRequiredInputField(field) && field.deprecated?.deprecated) {
reportError(`Required input field ${inputObj.name}.${field.name} cannot be deprecated.`);
}
if (typeof field.defaultValue !== 'undefined' &&
!isValidateDefaultValue(state, reportError, field.type, (0, graphql_1.parseValue)(field.defaultValue))) {
reportError(`Invalid default value (got: ${field.defaultValue}) provided for input field ${inputObj.name}.${field.name} of type ${field.type}.`);
}
}
}
function validateInterfaces(state, implementationsMap, reportError, type) {
const ifaceTypeNames = new Set();
for (const iface of type.interfaces) {
const interfaceType = state.types.get(iface);
if (!interfaceType) {
continue;
}
if (!isInterfaceType(interfaceType)) {
reportError(`Type ${type.name} must only implement Interface types, it cannot implement ${iface}.`);
continue;
}
if (type.name === iface) {
reportError(`Type ${type.name} cannot implement itself because it would create a circular reference.`);
continue;
}
if (ifaceTypeNames.has(iface)) {
reportError(`Type ${type.name} can only implement ${iface} once.`);
continue;
}
ifaceTypeNames.add(iface);
validateTypeImplementsAncestors(reportError, type, interfaceType);
validateTypeImplementsInterface(state, implementationsMap, reportError, type, interfaceType);
}
}
function validateTypeImplementsAncestors(reportError, type, interfaceType) {
const ifaceInterfaces = type.interfaces;
for (const transitive of interfaceType.interfaces) {
if (!ifaceInterfaces.has(transitive)) {
reportError(transitive === type.name
? `Type ${type.name} cannot implement ${interfaceType.name} because it would create a circular reference.`
: `Type ${type.name} must implement ${transitive} because it is implemented by ${interfaceType.name}.`);
}
}
}
function validateTypeImplementsInterface(state, implementationsMap, reportError, type, interfaceType) {
const typeFieldMap = type.fields;
for (const ifaceField of interfaceType.fields.values()) {
const fieldName = ifaceField.name;
const typeField = typeFieldMap.get(fieldName);
if (typeField == null) {
reportError(`Interface field ${interfaceType.name}.${fieldName} expected but ${type.name} does not provide it.`);
continue;
}
if (!isTypeSubTypeOf(state, implementationsMap, typeField.type, ifaceField.type)) {
reportError(`Interface field ${interfaceType.name}.${fieldName} expects type ` +
`${ifaceField.type} but ${type.name}.${fieldName} of type ${typeField.type} is not a proper subtype.`);
}
for (const ifaceArg of ifaceField.args.values()) {
const argName = ifaceArg.name;
const typeArg = typeField.args.get(argName);
if (!typeArg) {
reportError(`Interface field argument ${interfaceType.name}.${fieldName}(${argName}:) expected but ${type.name}.${fieldName} does not provide it.`);
continue;
}
if (ifaceArg.type !== typeArg.type) {
reportError(`Interface field argument ${interfaceType.name}.${fieldName}(${argName}:) ` +
`expects type ${ifaceArg.type} but ` +
`${type.name}.${fieldName}(${argName}:) is type ` +
`${typeArg.type}.`);
}
}
for (const typeArg of typeField.args.values()) {
const argName = typeArg.name;
const ifaceArg = ifaceField.args.get(argName);
if (!ifaceArg && isRequiredArgument(typeArg)) {
reportError(`Object field ${type.name}.${fieldName} includes required argument ${argName} that is missing from the Interface field ${interfaceType.name}.${fieldName}.`);
}
}
}
}
function isTypeSubTypeOf(state, implementationsMap, maybeSubTypeName, superTypeName) {
if (maybeSubTypeName === superTypeName) {
return true;
}
if ((0, state_js_1.isNonNull)(superTypeName)) {
if ((0, state_js_1.isNonNull)(maybeSubTypeName)) {
return isTypeSubTypeOf(state, implementationsMap, (0, state_js_1.stripNonNull)(maybeSubTypeName), (0, state_js_1.stripNonNull)(superTypeName));
}
return false;
}
if ((0, state_js_1.isNonNull)(maybeSubTypeName)) {
return isTypeSubTypeOf(state, implementationsMap, (0, state_js_1.stripNonNull)(maybeSubTypeName), superTypeName);
}
if ((0, state_js_1.isList)(superTypeName)) {
if ((0, state_js_1.isList)(maybeSubTypeName)) {
return isTypeSubTypeOf(state, implementationsMap, (0, state_js_1.stripList)(maybeSubTypeName), (0, state_js_1.stripList)(superTypeName));
}
return false;
}
if ((0, state_js_1.isList)(maybeSubTypeName)) {
return false;
}
const superType = state.types.get(superTypeName);
const maybeSubType = state.types.get(maybeSubTypeName);
if (!superType || !maybeSubType) {
return false;
}
return (isAbstractType(superType) &&
(isInterfaceType(maybeSubType) || isObjectType(maybeSubType)) &&
isSubType(implementationsMap, superType, maybeSubType));
}
function isSubType(implementationsMap, abstractType, maybeSubType) {
if (isUnionType(abstractType)) {
return abstractType.members.has(maybeSubType.name);
}
return implementationsMap.get(abstractType.name)?.has(maybeSubType.name) ?? false;
}
function createInputObjectCircularRefsValidator(state, reportError) {
const visitedTypes = new Set();
const fieldPath = [];
const fieldPathIndexByTypeName = Object.create(null);
return detectCycleRecursive;
function detectCycleRecursive(inputObj) {
if (visitedTypes.has(inputObj)) {
return;
}
visitedTypes.add(inputObj);
fieldPathIndexByTypeName[inputObj.name] = fieldPath.length;
for (const field of inputObj.fields.values()) {
if ((0, state_js_1.isNonNull)(field.type)) {
const fieldType = state.types.get((0, state_js_1.stripNonNull)(field.type));
if (fieldType && isInputObjectType(fieldType)) {
const cycleIndex = fieldPathIndexByTypeName[fieldType.name];
fieldPath.push(field.name);
if (cycleIndex === undefined) {
detectCycleRecursive(fieldType);
}
else {
const cyclePath = fieldPath.slice(cycleIndex);
reportError(`Cannot reference Input Object "${fieldType.name}" within itself through a series of non-null fields: "${cyclePath.join('.')}".`);
}
fieldPath.pop();
}
}
}
fieldPathIndexByTypeName[inputObj.name] = undefined;
}
}
function isIntrospectionType(typeName) {
return graphql_1.introspectionTypes.some(t => t.name === typeName);
}
function isAbstractType(type) {
return isInterfaceType(type) || isUnionType(type);
}
function isRequiredArgument(arg) {
return (0, state_js_1.isNonNull)(arg.type) && arg.defaultValue === undefined;
}
function isRequiredInputField(arg) {
return (0, state_js_1.isNonNull)(arg.type) && arg.defaultValue === undefined;
}
function isOutputType(state, typeName) {
const type = state.types.get(typeName);
if (!type) {
if (specifiedScalars.has(typeName)) {
return true;
}
return SKIP;
}
return !isInputObjectType(type);
}
function isInputType(state, typeName) {
const type = state.types.get(typeName);
if (!type) {
if (specifiedScalars.has(typeName)) {
return true;
}
return SKIP;
}
return isScalarType(type) || isEnumType(type) || isInputObjectType(type);
}
function typeExists(state, typeName) {
return state.types.has(typeName) || specifiedScalars.has(typeName);
}
function isInputObjectType(type) {
return type.kind === state_js_2.TypeKind.INPUT_OBJECT;
}
function isScalarType(type) {
return type.kind === state_js_2.TypeKind.SCALAR;
}
function isEnumType(type) {
return type.kind === state_js_2.TypeKind.ENUM;
}
function isObjectType(type) {
return type.kind === state_js_2.TypeKind.OBJECT;
}
function isInterfaceType(type) {
return type.kind === state_js_2.TypeKind.INTERFACE;
}
function isUnionType(type) {
return type.kind === state_js_2.TypeKind.UNION;
}
function isDirective(type) {
return type.kind === state_js_2.TypeKind.DIRECTIVE;
}
;