@apollo/federation
Version:
Apollo Federation Utilities
94 lines (83 loc) • 3.57 kB
text/typescript
import {
isObjectType,
GraphQLError,
isListType,
isNonNullType,
FieldDefinitionNode,
InputValueDefinitionNode,
} from 'graphql';
import {
logServiceAndType,
errorWithCode,
getFederationMetadata,
findTypeNodeInServiceList,
findDirectivesOnNode
} from '../../utils';
import { PostCompositionValidator } from '.';
/**
* Provides directive can only be added to return types that are entities
*/
export const providesNotOnEntity: PostCompositionValidator = ({ schema, serviceList }) => {
const errors: GraphQLError[] = [];
const types = schema.getTypeMap();
for (const [typeName, namedType] of Object.entries(types)) {
// Only object types have fields
if (!isObjectType(namedType)) continue;
// for each field, if there's a provides on it, check that the containing
// type has a `key` field under the federation metadata.
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 &&
fieldFederationMetadata?.provides &&
!fieldFederationMetadata?.belongsToValueType
)
throw Error(
'Internal Consistency Error: field with provides information does not have service name.',
);
if (!serviceName) continue;
const getBaseType = (type: any): any =>
isListType(type) || isNonNullType(type)
? getBaseType(type.ofType)
: type;
const baseType = getBaseType(field.type);
// field has a @provides directive on it
if (fieldFederationMetadata?.provides) {
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 providesDirectiveNode = findDirectivesOnNode(fieldNode, 'provides');
if (!isObjectType(baseType)) {
errors.push(
errorWithCode(
'PROVIDES_NOT_ON_ENTITY',
logServiceAndType(serviceName, typeName, fieldName) +
`uses the @provides directive but \`${typeName}.${fieldName}\` returns \`${field.type}\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.`,
providesDirectiveNode,
),
);
continue;
}
const fieldType = types[baseType.name];
const selectedFieldIsEntity = getFederationMetadata(fieldType)?.keys;
if (!selectedFieldIsEntity) {
errors.push(
errorWithCode(
'PROVIDES_NOT_ON_ENTITY',
logServiceAndType(serviceName, typeName, fieldName) +
`uses the @provides directive but \`${typeName}.${fieldName}\` does not return a type that has a @key. Try adding a @key to the \`${baseType}\` type.`,
providesDirectiveNode,
),
);
}
}
}
}
return errors;
};