@theguild/federation-composition
Version:
Open Source Composition library for Apollo Federation
214 lines (213 loc) • 13.3 kB
JavaScript
import { GraphQLError, Kind, } from "graphql";
import { print } from "../../../../graphql/printer.js";
import { getFieldsArgument, namedTypeFromTypeNode, parseFields, validateDirectiveAgainstOriginal, visitFields, } from "../../../helpers.js";
export function ProvidesRules(context) {
return {
DirectiveDefinition(node) {
validateDirectiveAgainstOriginal(node, "provides", context);
},
Directive(directiveNode) {
if (!context.isAvailableFederationDirective("provides", directiveNode)) {
return;
}
const annotatedType = context.typeNodeInfo.getTypeDef();
const annotatedField = context.typeNodeInfo.getFieldDef();
if (!annotatedType || !annotatedField) {
return;
}
const fieldCoordinate = `${annotatedType.name.value}.${annotatedField.name.value}`;
const usedOnInterface = annotatedType.kind === Kind.INTERFACE_TYPE_DEFINITION ||
annotatedType?.kind === Kind.INTERFACE_TYPE_EXTENSION;
const knownObjectsAndInterfaces = context.getSubgraphObjectOrInterfaceTypes();
const outputType = namedTypeFromTypeNode(annotatedField.type);
const targetType = knownObjectsAndInterfaces.get(outputType.name.value);
const processTargetType = (() => function processTargetTypeImpl(targetType) {
if (!targetType) {
context.reportError(new GraphQLError(`Invalid directive on field "${fieldCoordinate}": field has type "${print(annotatedField.type)}" which is not a Composite Type`, {
nodes: directiveNode,
extensions: {
code: "PROVIDES_ON_NON_OBJECT_FIELD",
},
}));
return;
}
if (usedOnInterface) {
context.reportError(new GraphQLError(`Cannot use on field "${fieldCoordinate}" of parent type "${annotatedType.name.value}": is not yet supported within interfaces`, {
nodes: directiveNode,
extensions: { code: "PROVIDES_UNSUPPORTED_ON_INTERFACE" },
}));
return;
}
const fieldsArg = getFieldsArgument(directiveNode);
if (!fieldsArg) {
return;
}
const printedFieldsValue = print(fieldsArg.value);
if (fieldsArg.value.kind !== Kind.STRING) {
context.reportError(new GraphQLError(`On field "${fieldCoordinate}", for : Invalid value for argument "fields": must be a string.`, {
nodes: directiveNode,
extensions: {
code: "PROVIDES_INVALID_FIELDS_TYPE",
},
}));
return;
}
let selectionSet;
try {
selectionSet = parseFields(fieldsArg.value.value);
}
catch (error) {
if (error instanceof GraphQLError) {
context.reportError(new GraphQLError(`On field "${fieldCoordinate}", for : ${error.message}`, {
nodes: directiveNode,
extensions: {
code: "PROVIDES_INVALID_FIELDS",
},
}));
return;
}
throw error;
}
if (!selectionSet) {
return;
}
let isValid = true;
visitFields({
context,
selectionSet,
typeDefinition: targetType,
interceptFieldWithMissingSelectionSet(info) {
isValid = false;
context.reportError(new GraphQLError(`On field "${fieldCoordinate}", for : Invalid empty selection set for field "${info.typeDefinition.name.value}.${info.fieldName}" of non-leaf type ${info.outputType}`, {
nodes: directiveNode,
extensions: { code: "PROVIDES_INVALID_FIELDS" },
}));
},
interceptUnknownField(info) {
isValid = false;
context.reportError(new GraphQLError(`On field "${fieldCoordinate}", for : Cannot query field "${info.fieldName}" on type "${info.typeDefinition.name.value}" (if the field is defined in another subgraph, you need to add it to this subgraph with ).`, {
nodes: directiveNode,
extensions: { code: "PROVIDES_INVALID_FIELDS" },
}));
},
interceptDirective(info) {
isValid = false;
if (info.isKnown) {
context.reportError(new GraphQLError(`On field "${fieldCoordinate}", for : cannot have directive applications in the argument but found @${info.directiveName}.`, {
nodes: directiveNode,
extensions: { code: "PROVIDES_DIRECTIVE_IN_FIELDS_ARG" },
}));
}
else {
context.reportError(new GraphQLError(`On field "${fieldCoordinate}", for : Unknown directive "@${info.directiveName}" in selection`, {
nodes: directiveNode,
extensions: { code: "PROVIDES_INVALID_FIELDS" },
}));
}
},
interceptArguments(info) {
isValid = false;
context.reportError(new GraphQLError(`On field "${fieldCoordinate}", for : field ${info.typeDefinition.name.value}.${info.fieldName} cannot be included because it has arguments (fields with argument are not allowed in )`, {
nodes: directiveNode,
extensions: { code: "PROVIDES_FIELDS_HAS_ARGS" },
}));
},
interceptNonExternalField(info) {
if (context.satisfiesVersionRange("> v1.0")) {
isValid = false;
context.reportError(new GraphQLError(`On field "${fieldCoordinate}", for : field "${info.typeDefinition.name.value}.${info.fieldName}" should not be part of a since it is already provided by this subgraph (it is not marked )`, {
extensions: {
code: "PROVIDES_FIELDS_MISSING_EXTERNAL",
},
}));
}
},
interceptExternalField(info) {
const keyDirectives = info.typeDefinition.directives?.filter((directive) => context.isAvailableFederationDirective("key", directive));
if (!keyDirectives?.length) {
return;
}
let interceptedFieldIsPrimaryKeyFromExtension = false;
for (const keyDirective of keyDirectives) {
if (interceptedFieldIsPrimaryKeyFromExtension) {
break;
}
const fieldsArg = keyDirective.arguments?.find((arg) => arg.name.value === "fields" &&
arg.value.kind === Kind.STRING);
if (fieldsArg) {
const keyFields = parseFields(fieldsArg.value.value);
const mergedTypeDef = context
.getSubgraphObjectOrInterfaceTypes()
.get(info.typeDefinition.name.value);
if (!mergedTypeDef) {
throw new Error(`Could not find type "${info.typeDefinition.name.value}"`);
}
if (keyFields) {
visitFields({
context,
selectionSet: keyFields,
typeDefinition: mergedTypeDef,
interceptField(keyFieldInfo) {
if (keyFieldInfo.typeDefinition.name.value ===
info.typeDefinition.name.value &&
keyFieldInfo.fieldName === info.fieldName) {
const isInterfaceType = keyFieldInfo.typeDefinition.kind ===
Kind.INTERFACE_TYPE_DEFINITION ||
keyFieldInfo.typeDefinition.kind ===
Kind.INTERFACE_TYPE_EXTENSION;
if (isInterfaceType) {
return;
}
const isExtension = keyFieldInfo.typeDefinition.kind ===
Kind.OBJECT_TYPE_EXTENSION ||
keyFieldInfo.typeDefinition.kind ===
Kind.INTERFACE_TYPE_EXTENSION ||
keyFieldInfo.typeDefinition.directives?.some((directive) => context.isAvailableFederationDirective("extends", directive));
if (isExtension) {
interceptedFieldIsPrimaryKeyFromExtension = true;
}
}
if (info.typeDefinition.kind ===
Kind.OBJECT_TYPE_DEFINITION ||
info.typeDefinition.kind ===
Kind.OBJECT_TYPE_EXTENSION) {
if (info.fieldName !== "__typename") {
context.stateBuilder.objectType.field.markAsProvided(info.typeDefinition.name.value, info.fieldName);
}
}
},
});
}
}
}
if (context.satisfiesVersionRange(">= v2.0") &&
interceptedFieldIsPrimaryKeyFromExtension) {
isValid = false;
context.reportError(new GraphQLError(`On field "${fieldCoordinate}", for : field "${info.typeDefinition.name.value}.${info.fieldName}" should not be part of a since it is already "effectively" provided by this subgraph (while it is marked , it is a field of an extension type, which are not internally considered external for historical/backward compatibility reasons)`, {
extensions: {
code: "PROVIDES_FIELDS_MISSING_EXTERNAL",
},
}));
}
},
});
if (isValid) {
context.stateBuilder.objectType.field.setProvides(annotatedType.name.value, annotatedField.name.value, fieldsArg.value.value);
}
})();
if (!targetType) {
const namedTypes = context
.getSubgraphUnionTypes()
.get(outputType.name.value);
if (namedTypes != null) {
for (const namedType of namedTypes) {
const targetType = knownObjectsAndInterfaces.get(namedType);
processTargetType(targetType);
}
return;
}
}
processTargetType(targetType);
},
};
}