@apollo/federation
Version:
Apollo Federation Utilities
69 lines (61 loc) • 3.16 kB
text/typescript
import { isObjectType, FieldNode, GraphQLError, FieldDefinitionNode, InputValueDefinitionNode } from 'graphql';
import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode, printFieldSet } from '../../utils';
import { PostCompositionValidator } from '.';
/**
* for every @requires, there should be a matching @external
*/
export const requiresFieldsMissingExternal: 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 requires on it, check that there's a matching
// @external field, and that the types referenced are from the base type
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 @requires federation data, since
// the only case where serviceName wouldn't exist is on a base type, and in that case,
// the `requires` 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;
if (fieldFederationMetadata?.requires) {
const typeFederationMetadata = getFederationMetadata(namedType);
const externalFieldsOnTypeForService =
typeFederationMetadata?.externals?.[serviceName];
const selections = fieldFederationMetadata?.requires as FieldNode[];
for (const selection of selections) {
const foundMatchingExternal = externalFieldsOnTypeForService
? externalFieldsOnTypeForService.some(
ext => ext.field.name.value === selection.name.value,
)
: undefined;
if (!foundMatchingExternal) {
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, 'requires', printFieldSet(selections));
errors.push(
errorWithCode(
'REQUIRES_FIELDS_MISSING_EXTERNAL',
logServiceAndType(serviceName, typeName, fieldName) +
`requires the field \`${selection.name.value}\` to be marked as @external.`,
// TODO (Issue #705): when we can associate locations to service name's this should be the node of the
// field on the other service that needs to be marked external
selectionSetNode,
),
);
}
}
}
}
}
return errors;
};