@apollo/federation
Version:
Apollo Federation Utilities
120 lines (109 loc) • 4.66 kB
text/typescript
import {
GraphQLError,
isObjectType,
FieldNode,
isListType,
isInterfaceType,
isNonNullType,
getNullableType,
isUnionType,
InputValueDefinitionNode,
FieldDefinitionNode,
} from 'graphql';
import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode, printFieldSet } from '../../utils';
import { PostCompositionValidator } from '.';
/**
* - The fields argument can not have root fields that result in a list
* - The fields argument can not have root fields that result in an interface
* - The fields argument can not have root fields that result in a union type
*/
export const providesFieldsSelectInvalidType: PostCompositionValidator = ({
schema,
serviceList,
}) => {
const errors: GraphQLError[] = [];
const types = schema.getTypeMap();
for (const [typeName, namedType] of Object.entries(types)) {
if (!isObjectType(namedType)) continue;
// for each field, if there's a provides on it, check the type of the field
// it references
for (const [fieldName, field] of Object.entries(namedType.getFields())) {
const fieldFederationMetadata = getFederationMetadata(field);
const serviceName = fieldFederationMetadata?.serviceName;
// serviceName should always exist on fields that have @provides federation data, since
// the only case where serviceName wouldn't exist is on a base type, and in that case,
// the `provides` metadata should never get added to begin with. This should be caught in
// composition work. This kind of error should be validated _before_ composition.
if (!serviceName) continue;
const fieldType = field.type;
if (!isObjectType(fieldType)) continue;
const allFields = fieldType.getFields();
if (fieldFederationMetadata?.provides) {
const selections = fieldFederationMetadata.provides as FieldNode[];
for (const selection of selections) {
const name = selection.name.value;
const matchingField = allFields[name];
const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList);
const fieldNode = typeNode && 'fields' in typeNode ?
(typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])
?.find(field => field.name.value === fieldName) : undefined;
const selectionSetNode = findSelectionSetOnNode(fieldNode, 'provides', printFieldSet(selections));
if (!matchingField) {
errors.push(
errorWithCode(
'PROVIDES_FIELDS_SELECT_INVALID_TYPE',
logServiceAndType(serviceName, typeName, fieldName) +
`A @provides selects ${name}, but ${fieldType.name}.${name} could not be found`,
selectionSetNode,
),
);
continue;
}
if (
isListType(matchingField.type) ||
(isNonNullType(matchingField.type) &&
isListType(getNullableType(matchingField.type)))
) {
errors.push(
errorWithCode(
'PROVIDES_FIELDS_SELECT_INVALID_TYPE',
logServiceAndType(serviceName, typeName, fieldName) +
`A @provides selects ${fieldType.name}.${name}, which is a list type. A field cannot @provide lists.`,
selectionSetNode,
),
);
}
if (
isInterfaceType(matchingField.type) ||
(isNonNullType(matchingField.type) &&
isInterfaceType(getNullableType(matchingField.type)))
) {
errors.push(
errorWithCode(
'PROVIDES_FIELDS_SELECT_INVALID_TYPE',
logServiceAndType(serviceName, typeName, fieldName) +
`A @provides selects ${fieldType.name}.${name}, which is an interface type. A field cannot @provide interfaces.`,
selectionSetNode,
),
);
}
if (
isUnionType(matchingField.type) ||
(isNonNullType(matchingField.type) &&
isUnionType(getNullableType(matchingField.type)))
) {
errors.push(
errorWithCode(
'PROVIDES_FIELDS_SELECT_INVALID_TYPE',
logServiceAndType(serviceName, typeName, fieldName) +
`A @provides selects ${fieldType.name}.${name}, which is a union type. A field cannot @provide union types.`,
selectionSetNode,
),
);
}
}
}
}
}
return errors;
};