@graphql-mesh/fusion-composition
Version:
Basic composition utility for Fusion spec
253 lines (252 loc) • 13.2 kB
JavaScript
import { DirectiveLocation, getNamedType, GraphQLDirective, GraphQLList, GraphQLNonNull, GraphQLSchema, GraphQLString, OperationTypeNode, } from 'graphql';
import { asArray, getDirectiveExtensions, getRootTypeMap, MapperKind, mapSchema, } from '@graphql-tools/utils';
import { addFederation2DirectivesToSubgraph, importFederationDirectives, importMeshDirectives, } from '../federation-utils.js';
import { TransformValidationError } from './utils.js';
const federationDirectiveNames = [
'key',
'interfaceObject',
'extends',
'shareable',
'inaccessible',
'override',
'authenticated',
'requiresScopes',
'policy',
'external',
'provides',
'requires',
];
export function createFederationTransform(config) {
return function (subgraphSchema, subgraphConfig) {
const configurationByType = new Map();
const configurationByField = new Map();
for (const coordinate in config) {
const [typeName, fieldName] = coordinate.split('.');
const typeInSchema = subgraphSchema.getType(typeName);
if (!typeInSchema) {
throw new TransformValidationError(`Federation Transform: Type ${typeName} not found in schema for the coordinate: ${coordinate}`);
}
if (fieldName) {
if (!('getFields' in typeInSchema)) {
throw new TransformValidationError(`Federation Transform: Type ${typeName} is not an object type in schema for the coordinate: ${coordinate}`);
}
const fieldsInType = typeInSchema.getFields();
if (!fieldsInType[fieldName]) {
throw new TransformValidationError(`Federation Transform: Field ${fieldName} not found in type ${typeName} for the coordinate: ${coordinate}`);
}
const fieldConfig = config[coordinate];
let fieldMap = configurationByField.get(typeName);
if (!fieldMap) {
fieldMap = new Map();
configurationByField.set(typeName, fieldMap);
}
fieldMap.set(fieldName, fieldConfig);
}
else {
configurationByType.set(typeName, config[coordinate]);
}
}
const mergeDirectiveConfigMap = new Map();
const rootTypeNameOperationMap = new Map([...getRootTypeMap(subgraphSchema)].map(([operationType, type]) => [
type.name,
operationType,
]));
const usedFederationDirectives = new Set();
let mergeDirectiveUsed = false;
subgraphSchema = mapSchema(subgraphSchema, {
[MapperKind.TYPE]: type => {
const configByType = configurationByType.get(type.name);
if (configByType) {
const directiveExtensions = getDirectiveExtensions(type) || {};
for (const directiveName in configByType) {
const directiveConfigs = asArray(configByType[directiveName]);
for (const directiveConfig of directiveConfigs) {
const specificDirectiveExtensions = (directiveExtensions[directiveName] ||= []);
switch (directiveName) {
case 'key': {
const keyConfig = directiveConfig;
specificDirectiveExtensions.push({
fields: keyConfig.fields,
});
if (keyConfig.resolveReference) {
const operation = keyConfig.resolveReference.operation || OperationTypeNode.QUERY;
let operationMergeDirectiveConfig = mergeDirectiveConfigMap.get(operation);
if (!operationMergeDirectiveConfig) {
operationMergeDirectiveConfig = new Map();
mergeDirectiveConfigMap.set(operation, operationMergeDirectiveConfig);
}
operationMergeDirectiveConfig.set(keyConfig.resolveReference.fieldName, {
keyField: keyConfig.fields,
...keyConfig.resolveReference,
});
}
break;
}
default: {
if (directiveConfig) {
specificDirectiveExtensions.push(directiveConfig === true ? {} : directiveConfig);
}
break;
}
}
usedFederationDirectives.add(`@${directiveName}`);
}
}
for (const directiveName of federationDirectiveNames) {
if (directiveExtensions[directiveName]?.length &&
federationDirectiveNames.includes(directiveName)) {
usedFederationDirectives.add(`@${directiveName}`);
}
}
// Existing directives
return new (Object.getPrototypeOf(type).constructor)({
...type.toConfig(),
astNode: undefined,
extensions: {
...(type.extensions || {}),
directives: directiveExtensions,
},
});
}
},
[MapperKind.ROOT_FIELD]: (fieldConfig, fieldName, typeName) => {
const operation = rootTypeNameOperationMap.get(typeName);
if (!operation) {
throw new Error(`Unexpected root field ${fieldName} in type ${typeName}`);
}
const mergeDirectiveConfigForOperation = mergeDirectiveConfigMap.get(operation);
const mergeDirectiveConfig = mergeDirectiveConfigForOperation?.get(fieldName);
const fieldDirectives = getDirectiveExtensions(fieldConfig) || {};
const fieldTransformConfig = configurationByField.get(typeName)?.get(fieldName);
if (fieldTransformConfig) {
for (const directiveName in fieldTransformConfig) {
const directiveConfigs = asArray(fieldTransformConfig[directiveName]);
const specificDirectiveExtensions = (fieldDirectives[directiveName] ||= []);
for (let directiveConfig of directiveConfigs) {
if (typeof directiveConfig === 'boolean') {
directiveConfig = {};
}
specificDirectiveExtensions.push(directiveConfig);
usedFederationDirectives.add(`@${directiveName}`);
}
}
}
if (mergeDirectiveConfig) {
const mergeDirectiveExtensions = (fieldDirectives.merge ||= []);
let argsExpr = mergeDirectiveConfig.argsExpr;
if (!argsExpr && fieldConfig.args && !mergeDirectiveConfig.keyArg) {
const argsExprElems = [];
const returnNamedType = getNamedType(fieldConfig.type);
if ('getFields' in returnNamedType) {
const returnFields = returnNamedType.getFields();
for (const argName in fieldConfig.args) {
const arg = fieldConfig.args[argName];
const argType = getNamedType(arg.type);
const returnField = returnFields[argName];
if (returnField) {
const returnFieldType = getNamedType(returnField.type);
if (argType.name === returnFieldType.name) {
argsExprElems.push(`${argName}: $key.${argName}`);
}
}
}
argsExpr = argsExprElems.join(', ');
}
}
mergeDirectiveExtensions.push({
subgraph: subgraphConfig.name,
key: mergeDirectiveConfig.key,
keyField: mergeDirectiveConfig.keyField,
keyArg: mergeDirectiveConfig.keyArg,
argsExpr,
});
mergeDirectiveUsed = true;
}
for (const directiveName of federationDirectiveNames) {
if (fieldDirectives[directiveName]?.length &&
federationDirectiveNames.includes(directiveName)) {
usedFederationDirectives.add(`@${directiveName}`);
}
}
if (fieldTransformConfig || mergeDirectiveConfig) {
return {
...fieldConfig,
astNode: undefined,
extensions: {
...(fieldConfig.extensions || {}),
directives: fieldDirectives,
},
};
}
},
[MapperKind.FIELD]: (fieldConfig, fieldName, typeName) => {
const fieldTransformConfig = configurationByField.get(typeName)?.get(fieldName);
if (fieldTransformConfig) {
const fieldDirectives = getDirectiveExtensions(fieldConfig) || {};
for (const directiveName in fieldTransformConfig) {
const directiveConfigs = asArray(fieldTransformConfig[directiveName]);
const specificDirectiveExtensions = (fieldDirectives[directiveName] ||= []);
for (let directiveConfig of directiveConfigs) {
if (typeof directiveConfig === 'boolean') {
directiveConfig = {};
}
specificDirectiveExtensions.push(directiveConfig);
usedFederationDirectives.add(`@${directiveName}`);
}
}
for (const directiveName of federationDirectiveNames) {
if (fieldDirectives[directiveName]?.length &&
federationDirectiveNames.includes(directiveName)) {
usedFederationDirectives.add(`@${directiveName}`);
}
}
return {
...fieldConfig,
astNode: undefined,
extensions: {
...(fieldConfig.extensions || {}),
directives: fieldDirectives,
},
};
}
},
});
subgraphSchema = addFederation2DirectivesToSubgraph(subgraphSchema);
subgraphSchema = importFederationDirectives(subgraphSchema, [...usedFederationDirectives]);
if (mergeDirectiveUsed) {
subgraphSchema = importMeshDirectives(subgraphSchema, ['@merge']);
const schemaConfig = subgraphSchema.toConfig();
subgraphSchema = new GraphQLSchema({
...schemaConfig,
directives: [
...schemaConfig.directives,
new GraphQLDirective({
name: 'merge',
locations: [DirectiveLocation.FIELD_DEFINITION],
args: {
subgraph: {
type: new GraphQLNonNull(GraphQLString),
},
argsExpr: {
type: GraphQLString,
},
keyArg: {
type: GraphQLString,
},
keyField: {
type: GraphQLString,
},
key: {
type: new GraphQLList(GraphQLString),
},
additionalArgs: {
type: GraphQLString,
},
},
}),
],
});
}
return subgraphSchema;
};
}