@theguild/federation-composition
Version:
Open Source Composition library for Apollo Federation
147 lines (146 loc) • 7.11 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.FieldsOfTheSameTypeRule = FieldsOfTheSameTypeRule;
const graphql_1 = require("graphql");
const state_js_1 = require("../../../subgraph/state.js");
function stripNonNull(type) {
return type.replace(/!$/, '');
}
function stripList(type) {
const startsAt = type.indexOf('[');
const endsAt = type.lastIndexOf(']');
return type.slice(startsAt + 1, endsAt);
}
function normalizeOutputTypeStrings({ superType, localType, }) {
let superTypeNormalized = superType;
let localTypeNormalized = localType;
if (!superTypeNormalized.endsWith('!')) {
localTypeNormalized = stripNonNull(localTypeNormalized);
}
if (superTypeNormalized.startsWith('[') && localTypeNormalized.startsWith('[')) {
const innerSuper = stripList(superTypeNormalized);
let innerLocal = stripList(localTypeNormalized);
if (!innerSuper.endsWith('!')) {
innerLocal = stripNonNull(innerLocal);
}
const superSign = superTypeNormalized.endsWith('!') ? '!' : '';
const localSign = localTypeNormalized.endsWith('!') ? '!' : '';
superTypeNormalized = `[${innerSuper}]${superSign}`;
localTypeNormalized = `[${innerLocal}]${localSign}`;
}
return {
superType: superTypeNormalized,
localType: localTypeNormalized,
};
}
function FieldsOfTheSameTypeRule(context) {
return {
ObjectTypeField(objectTypeState, fieldState) {
const typeToGraphs = new Map();
const typeNameToPossibleTypeNames = new Map();
fieldState.byGraph.forEach((field, graphName) => {
const typeName = field.type.replaceAll('!', '').replaceAll('[', '').replaceAll(']', '');
const typeState = context.subgraphStates.get(graphName)?.types.get(typeName);
if (typeState?.kind === state_js_1.TypeKind.UNION) {
if (!typeNameToPossibleTypeNames.has(typeName)) {
typeNameToPossibleTypeNames.set(typeName, new Set());
}
const list = typeNameToPossibleTypeNames.get(typeName);
typeState.members.forEach(member => {
list.add(member);
});
}
else if (typeState?.kind === state_js_1.TypeKind.INTERFACE) {
if (!typeNameToPossibleTypeNames.has(typeName)) {
typeNameToPossibleTypeNames.set(typeName, new Set());
}
const list = typeNameToPossibleTypeNames.get(typeName);
typeState.implementedBy.forEach(member => {
list.add(member);
});
}
const normalizedOutputTypes = normalizeOutputTypeStrings({
superType: fieldState.type,
localType: field.type,
});
const fieldOutputType = normalizedOutputTypes.superType === normalizedOutputTypes.localType
? normalizedOutputTypes.localType
: field.type;
const existing = typeToGraphs.get(fieldOutputType);
if (existing) {
existing.push(graphName);
}
else {
typeToGraphs.set(fieldOutputType, [graphName]);
}
});
if (typeToGraphs.size > 1) {
if (typeNameToPossibleTypeNames.size === 1) {
const possibleTypeNames = [];
typeNameToPossibleTypeNames.forEach((list, unionOrInterfaceName) => {
possibleTypeNames.push(unionOrInterfaceName);
for (const typeName of list) {
possibleTypeNames.push(typeName);
}
});
const outputTypeNames = Array.from(typeToGraphs.keys()).map(t => t.replaceAll('!', '').replaceAll('[', '').replaceAll(']', ''));
if (outputTypeNames.every(t => possibleTypeNames.includes(t))) {
return;
}
}
const groups = Array.from(typeToGraphs.entries()).map(([outputType, graphs]) => {
const plural = graphs.length > 1 ? 's' : '';
return `type "${outputType}" in subgraph${plural} "${graphs
.map(context.graphIdToName)
.join('", "')}"`;
});
const [first, second, ...rest] = groups;
context.reportError(new graphql_1.GraphQLError(`Type of field "${objectTypeState.name}.${fieldState.name}" is incompatible across subgraphs: it has ${first} but ${second}${rest.length ? ` and ${rest.join(' and ')}` : ''}`, {
extensions: {
code: 'FIELD_TYPE_MISMATCH',
},
}));
}
},
InputObjectTypeField(inputObjectTypeState, fieldState) {
const typeToGraphs = new Map();
fieldState.byGraph.forEach((field, graphName) => {
const isNonNullable = field.type.endsWith('!');
const isNonNullableInSupergraph = fieldState.type.endsWith('!');
const isMatchingNonNullablePart = fieldState.type.replace(/!$/, '') === field.type.replace(/!$/, '');
let normalizedOutputType;
if (isMatchingNonNullablePart) {
normalizedOutputType = isNonNullableInSupergraph
? isNonNullable
? field.type
: field.type + '!'
: field.type;
}
else {
normalizedOutputType = field.type;
}
const existing = typeToGraphs.get(normalizedOutputType);
if (existing) {
existing.push(graphName);
}
else {
typeToGraphs.set(normalizedOutputType, [graphName]);
}
});
if (typeToGraphs.size > 1) {
const groups = Array.from(typeToGraphs.entries()).map(([outputType, graphs]) => {
const plural = graphs.length > 1 ? 's' : '';
return `type "${outputType}" in subgraph${plural} "${graphs
.map(context.graphIdToName)
.join('", "')}"`;
});
const [first, second, ...rest] = groups;
context.reportError(new graphql_1.GraphQLError(`Type of field "${inputObjectTypeState.name}.${fieldState.name}" is incompatible across subgraphs: it has ${first} but ${second}${rest.length ? ` and ${rest.join(' and ')}` : ''}`, {
extensions: {
code: 'FIELD_TYPE_MISMATCH',
},
}));
}
},
};
}
;