@apollo/federation
Version:
Apollo Federation Utilities
121 lines (111 loc) • 3.7 kB
text/typescript
import {
visit,
visitWithTypeInfo,
TypeInfo,
parse,
GraphQLSchema,
GraphQLError,
specifiedDirectives,
} from 'graphql';
// Importing from 'dist' is not actually supported as part of the public API,
// but this allows us not to duplicate things in the meantime while the
// @apollo/federation package still exists.
import { buildSchemaFromSDL } from '@apollo/subgraph/dist/schema-helper';
import { knownSubgraphDirectives } from '@apollo/subgraph/dist/directives';
import { ServiceDefinition } from '../../types';
import {
findDirectivesOnNode,
isStringValueNode,
logServiceAndType,
errorWithCode,
} from '../../utils';
import { isNotNullOrUndefined } from '../../../utilities';
/**
* For every @key directive, it must reference a field marked as @external
*/
export const keyFieldsMissingExternal = ({
name: serviceName,
typeDefs,
}: ServiceDefinition) => {
const errors: GraphQLError[] = [];
// Build an array that accounts for all key directives on type extensions.
let keyDirectiveInfoOnTypeExtensions: {
typeName: string;
keyArgument: string;
}[] = [];
visit(typeDefs, {
ObjectTypeExtension(node) {
const keyDirectivesOnTypeExtension = findDirectivesOnNode(
node,
'key',
);
const keyDirectivesInfo = keyDirectivesOnTypeExtension
.map(keyDirective =>
keyDirective.arguments &&
isStringValueNode(keyDirective.arguments[0].value)
? {
typeName: node.name.value,
keyArgument: keyDirective.arguments[0].value.value,
}
: null,
)
.filter(isNotNullOrUndefined);
keyDirectiveInfoOnTypeExtensions.push(...keyDirectivesInfo);
},
});
// this allows us to build a partial schema
let schema = new GraphQLSchema({
query: undefined,
directives: [...specifiedDirectives, ...knownSubgraphDirectives],
});
try {
schema = buildSchemaFromSDL(typeDefs, schema);
} catch (e) {
errors.push(e);
return errors;
}
const typeInfo = new TypeInfo(schema);
for (const { typeName, keyArgument } of keyDirectiveInfoOnTypeExtensions) {
const keyDirectiveSelectionSet = parse(
`fragment __generated on ${typeName} { ${keyArgument} }`,
);
visit(
keyDirectiveSelectionSet,
visitWithTypeInfo(typeInfo, {
Field(node) {
const fieldDef = typeInfo.getFieldDef();
const parentType = typeInfo.getParentType();
if (parentType) {
if (!fieldDef) {
// TODO: find all fields that have @external and suggest them / heursitic match
errors.push(
errorWithCode(
'KEY_FIELDS_MISSING_EXTERNAL',
logServiceAndType(serviceName, parentType.name) +
`A @key directive specifies a field which is not found in this service. Add a field to this type with @external.`,
node,
),
);
return;
}
const externalDirectivesOnField = findDirectivesOnNode(
fieldDef.astNode,
'external',
);
if (externalDirectivesOnField.length === 0) {
errors.push(
errorWithCode(
'KEY_FIELDS_MISSING_EXTERNAL',
logServiceAndType(serviceName, parentType.name) +
`A @key directive specifies the \`${fieldDef.name}\` field which has no matching @external field.`,
fieldDef.astNode ?? undefined,
),
);
}
}
},
}),
);
}
return errors;
};