@graphql-mesh/fusion-composition
Version:
Basic composition utility for Fusion spec
613 lines (612 loc) • 30.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getUnifiedGraphGracefully = getUnifiedGraphGracefully;
exports.getAnnotatedSubgraphs = getAnnotatedSubgraphs;
exports.composeSubgraphs = composeSubgraphs;
exports.composeAnnotatedSubgraphs = composeAnnotatedSubgraphs;
const tslib_1 = require("tslib");
const graphql_1 = require("graphql");
const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
const snake_case_1 = require("snake-case");
const stitching_directives_1 = require("@graphql-tools/stitching-directives");
const utils_1 = require("@graphql-tools/utils");
const federation_composition_1 = require("@theguild/federation-composition");
const federation_utils_js_1 = require("./federation-utils.js");
function getUnifiedGraphGracefully(subgraphs) {
const result = composeSubgraphs(subgraphs);
if (result.errors?.length) {
throw new AggregateError(result.errors, `Failed to compose subgraphs; \n${result.errors.map(e => `- ${e.message}`).join('\n')}`);
}
return result.supergraphSdl;
}
function isEmptyObject(obj) {
if (!obj) {
return true;
}
for (const key in obj) {
if (obj[key] != null) {
return false;
}
}
return true;
}
function getAnnotatedSubgraphs(subgraphs, options = {}) {
const annotatedSubgraphs = [];
for (const subgraphConfig of subgraphs) {
const { name: subgraphName, schema, transforms } = subgraphConfig;
let url = subgraphConfig.url;
const subgraphSchemaExtensions = (0, utils_1.getDirectiveExtensions)(schema);
const transportDirectives = subgraphSchemaExtensions?.transport;
const transportDirective = transportDirectives?.[0];
let removeTransportDirective = false;
if (transportDirective) {
url = transportDirective.location;
if (!options.alwaysAddTransportDirective &&
transportDirective.kind === 'http' &&
!transportDirective.headers &&
isEmptyObject(transportDirective.options)) {
removeTransportDirective = true;
}
}
let mergeDirectiveUsed = false;
const sourceDirectiveUsed = transforms?.length > 0;
const normalizedSchema = (0, federation_utils_js_1.normalizeDirectiveExtensions)(schema);
if (removeTransportDirective && normalizedSchema.extensions?.directives?.transport) {
delete (normalizedSchema.extensions?.directives).transport;
}
const annotatedSubgraph = (0, utils_1.mapSchema)(normalizedSchema, {
[utils_1.MapperKind.TYPE]: type => {
if (!sourceDirectiveUsed || (0, graphql_1.isSpecifiedScalarType)(type)) {
return type;
}
const existingDirectives = (0, utils_1.getDirectiveExtensions)(type);
const existingSourceDirectives = existingDirectives?.source || [];
if (existingSourceDirectives.length > 1) {
throw new Error(`Type ${type.name} already has source directives from multiple subgraphs: ${existingSourceDirectives
.map((source) => source.subgraph)
.join(', ')}`);
}
const existingSourceDirective = existingSourceDirectives[0] || {};
const directives = {
...existingDirectives,
source: {
name: type.name,
...existingSourceDirective,
subgraph: subgraphName,
},
};
return new (Object.getPrototypeOf(type).constructor)({
...type.toConfig(),
extensions: {
...type.extensions,
directives,
},
astNode: undefined,
});
},
[utils_1.MapperKind.FIELD]: (fieldConfig, fieldName) => {
if (!sourceDirectiveUsed) {
return fieldConfig;
}
const newArgs = {};
if ('args' in fieldConfig && fieldConfig.args) {
for (const argName in fieldConfig.args) {
const arg = fieldConfig.args[argName];
const directives = (0, utils_1.getDirectiveExtensions)(arg);
const existingSourceDirectives = directives.source || [];
if (existingSourceDirectives.length > 1) {
throw new Error(`Argument ${argName} of field ${fieldName} already has source directives from multiple subgraphs: ${existingSourceDirectives
.map((source) => source.subgraph)
.join(', ')}`);
}
const existingSourceDirective = existingSourceDirectives[0] || {};
newArgs[argName] = {
...arg,
extensions: {
...arg.extensions,
directives: {
...directives,
source: {
name: argName,
type: arg.type.toString(),
...existingSourceDirective,
subgraph: subgraphName,
},
},
},
astNode: undefined,
};
}
}
const existingDirectives = (0, utils_1.getDirectiveExtensions)(fieldConfig);
const existingSourceDirectives = existingDirectives.source || [];
if (existingSourceDirectives.length > 1) {
throw new Error(`Field ${fieldName} already has source directives from multiple subgraphs: ${existingSourceDirectives
.map((source) => source.subgraph)
.join(', ')}`);
}
const existingSourceDirective = existingSourceDirectives[0] || {};
return {
...fieldConfig,
args: newArgs,
extensions: {
...fieldConfig.extensions,
directives: {
...existingDirectives,
source: {
name: fieldName,
type: fieldConfig.type.toString(),
...existingSourceDirective,
subgraph: subgraphName,
},
},
},
astNode: undefined,
};
},
[utils_1.MapperKind.ENUM_VALUE]: (valueConfig, _typeName, _schema, externalValue) => {
if (!sourceDirectiveUsed) {
return valueConfig;
}
const existingDirectives = (0, utils_1.getDirectiveExtensions)(valueConfig);
const existingSourceDirectives = existingDirectives.source || [];
if (existingSourceDirectives.length > 1) {
throw new Error(`Enum value ${externalValue} already has source directives from multiple subgraphs: ${existingSourceDirectives
.map((source) => source.subgraph)
.join(', ')}`);
}
const existingSourceDirective = existingSourceDirectives[0] || {};
return {
...valueConfig,
extensions: {
...valueConfig.extensions,
directives: {
...existingDirectives,
source: {
name: externalValue,
...existingSourceDirective,
subgraph: subgraphName,
},
},
},
astNode: undefined,
};
},
[utils_1.MapperKind.ROOT_FIELD]: (fieldConfig, fieldName) => {
const directiveExtensions = (0, utils_1.getDirectiveExtensions)(fieldConfig);
if (!transforms?.length && !options.ignoreSemanticConventions) {
addAnnotationsForSemanticConventions({
queryFieldName: fieldName,
queryFieldConfig: fieldConfig,
directiveExtensions,
subgraphName,
});
if (directiveExtensions.merge) {
mergeDirectiveUsed = true;
}
return {
...fieldConfig,
extensions: {
...fieldConfig.extensions,
directives: directiveExtensions,
},
astNode: undefined,
};
}
const newArgs = {};
if (fieldConfig.args) {
for (const argName in fieldConfig.args) {
const arg = fieldConfig.args[argName];
const directives = (0, utils_1.getDirectiveExtensions)(arg);
const existingSourceDirectives = directives.source || [];
if (existingSourceDirectives.length > 1) {
throw new Error(`Argument ${argName} of field ${fieldName} already has source directives from multiple subgraphs: ${existingSourceDirectives
.map((source) => source.subgraph)
.join(', ')}`);
}
const existingSourceDirective = existingSourceDirectives[0] || {};
newArgs[argName] = {
...arg,
extensions: {
...arg.extensions,
directives: {
...directives,
source: {
name: argName,
type: arg.type.toString(),
...existingSourceDirective,
subgraph: subgraphName,
},
},
},
astNode: undefined,
};
}
}
const existingSourceDirectives = directiveExtensions.source || [];
if (existingSourceDirectives.length > 1) {
throw new Error(`Field ${fieldName} already has source directives from multiple subgraphs: ${existingSourceDirectives
.map((source) => source.subgraph)
.join(', ')}`);
}
const existingSourceDirective = existingSourceDirectives[0] || {};
return {
...fieldConfig,
extensions: {
...fieldConfig.extensions,
directives: {
...directiveExtensions,
source: {
name: fieldName,
type: fieldConfig.type.toString(),
...existingSourceDirective,
subgraph: subgraphName,
},
},
},
args: newArgs,
astNode: undefined,
};
},
[utils_1.MapperKind.DIRECTIVE]: directive => {
if (directive.name === 'transport' && removeTransportDirective) {
return null;
}
if (!sourceDirectiveUsed && directive.name === 'source') {
return null;
}
},
});
let transformedSubgraph = annotatedSubgraph;
if (transforms?.length) {
for (const transform of transforms) {
transformedSubgraph = transform(transformedSubgraph, subgraphConfig);
}
// Semantic conventions
transformedSubgraph = (0, utils_1.mapSchema)(transformedSubgraph, {
[utils_1.MapperKind.ROOT_FIELD]: (fieldConfig, fieldName) => {
// Automatic type merging configuration based on ById and ByIds naming conventions after transforms
const directiveExtensions = (0, utils_1.getDirectiveExtensions)(fieldConfig);
if (!options.ignoreSemanticConventions) {
addAnnotationsForSemanticConventions({
queryFieldName: fieldName,
queryFieldConfig: fieldConfig,
directiveExtensions,
subgraphName,
});
if (directiveExtensions.merge) {
mergeDirectiveUsed = true;
}
return {
...fieldConfig,
extensions: {
...fieldConfig.extensions,
directives: directiveExtensions,
},
astNode: undefined,
};
}
},
});
}
transformedSubgraph = (0, federation_utils_js_1.convertSubgraphToFederationv2)(transformedSubgraph);
transformedSubgraph = (0, federation_utils_js_1.detectAndAddMeshDirectives)(transformedSubgraph);
const importedDirectives = new Set();
if (mergeDirectiveUsed) {
importedDirectives.add('@merge');
}
if (sourceDirectiveUsed) {
importedDirectives.add('@source');
}
const fieldMapper = (fieldConfig, fieldName) => {
const fieldDirectives = (0, utils_1.getDirectiveExtensions)(fieldConfig);
const sourceDirectives = fieldDirectives?.source;
if (sourceDirectives?.length) {
const filteredSourceDirectives = sourceDirectives.filter(sourceDirective => sourceDirective.name !== fieldName ||
sourceDirective.type !== fieldConfig.type.toString());
fieldDirectives.source = filteredSourceDirectives;
}
if ('args' in fieldConfig) {
const newArgs = {};
for (const argName in fieldConfig.args) {
const arg = fieldConfig.args[argName];
const argDirectives = (0, utils_1.getDirectiveExtensions)(arg);
const sourceDirectives = argDirectives?.source;
if (sourceDirectives?.length) {
const filteredSourceDirectives = sourceDirectives.filter(sourceDirective => sourceDirective.name !== argName || sourceDirective.type !== arg.type.toString());
newArgs[argName] = {
...arg,
extensions: {
...arg.extensions,
directives: {
...argDirectives,
source: filteredSourceDirectives,
},
},
};
}
else {
newArgs[argName] = arg;
}
}
return {
...fieldConfig,
args: newArgs,
extensions: {
...fieldConfig.extensions,
directives: fieldDirectives,
},
astNode: undefined,
};
}
return {
...fieldConfig,
extensions: {
...fieldConfig.extensions,
directives: fieldDirectives,
},
astNode: undefined,
};
};
// Remove unnecessary @source directives
transformedSubgraph = (0, utils_1.mapSchema)(transformedSubgraph, {
[utils_1.MapperKind.TYPE]: type => {
const typeDirectives = (0, utils_1.getDirectiveExtensions)(type);
const sourceDirectives = typeDirectives?.source;
if (sourceDirectives?.length) {
const filteredSourceDirectives = sourceDirectives.filter(sourceDirective => sourceDirective.name !== type.name);
const typeExtensions = (type.extensions ||= {});
typeExtensions.directives = {
...typeDirectives,
source: filteredSourceDirectives,
};
}
return type;
},
[utils_1.MapperKind.ROOT_FIELD]: fieldMapper,
[utils_1.MapperKind.FIELD]: fieldMapper,
[utils_1.MapperKind.ENUM_VALUE]: (valueConfig, _typeName, _schema, externalValue) => {
const valueDirectives = (0, utils_1.getDirectiveExtensions)(valueConfig);
const sourceDirectives = valueDirectives?.source;
if (sourceDirectives?.length) {
const filteredSourceDirectives = sourceDirectives.filter(sourceDirective => sourceDirective.name !== externalValue);
const valueExtensions = (valueConfig.extensions ||= {});
valueExtensions.directives = {
...valueDirectives,
source: filteredSourceDirectives,
};
}
return valueConfig;
},
});
// Workaround to keep directives on unsupported nodes since not all of them are supported by the composition library
let extraSchemaDefinitionDirectives;
const schemaDirectiveExtensions = (0, utils_1.getDirectiveExtensions)(transformedSubgraph);
if (schemaDirectiveExtensions) {
const schemaDirectiveExtensionsEntries = Object.entries(schemaDirectiveExtensions);
const schemaDirectiveExtraEntries = schemaDirectiveExtensionsEntries.filter(([dirName]) => dirName !== 'link' &&
dirName !== 'composeDirective' &&
(removeTransportDirective ? dirName !== 'transport' : true));
if (schemaDirectiveExtraEntries.length) {
transformedSubgraph = new graphql_1.GraphQLSchema({
...transformedSubgraph.toConfig(),
extensions: {
...transformedSubgraph.extensions,
directives: {
link: schemaDirectiveExtensions.link || [],
composeDirective: schemaDirectiveExtensions.composeDirective || [],
},
},
// Cleanup AST Node to avoid conflicts with extensions
astNode: undefined,
extensionASTNodes: [],
});
extraSchemaDefinitionDirectives = Object.fromEntries(schemaDirectiveExtraEntries);
}
}
const importedDirectivesAST = new Set();
if (mergeDirectiveUsed) {
if (!transformedSubgraph.getDirective('merge')) {
const { mergeDirectiveTypeDefs } = (0, stitching_directives_1.stitchingDirectives)();
// Add subgraph argument to @merge directive
importedDirectivesAST.add(mergeDirectiveTypeDefs
.replace('@merge(', '@merge(subgraph: String, ')
.replace('on ', 'repeatable on '));
transformedSubgraph = (0, federation_utils_js_1.importFederationDirectives)(transformedSubgraph, ['@key']);
}
}
if (sourceDirectiveUsed) {
importedDirectivesAST.add(/* GraphQL */ `
directive @source(
name: String!
type: String
subgraph: String!
) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
`);
}
const queryType = transformedSubgraph.getQueryType();
const queryTypeDirectives = (0, utils_1.getDirectiveExtensions)(queryType) || {};
if (extraSchemaDefinitionDirectives) {
importedDirectives.add('@extraSchemaDefinitionDirective');
importedDirectivesAST.add(/* GraphQL */ `
scalar _DirectiveExtensions
`);
importedDirectivesAST.add(/* GraphQL */ `
directive @extraSchemaDefinitionDirective(
directives: _DirectiveExtensions
) repeatable on OBJECT
`);
queryTypeDirectives.extraSchemaDefinitionDirective ||= [];
queryTypeDirectives.extraSchemaDefinitionDirective.push({
directives: extraSchemaDefinitionDirectives,
});
}
const queryTypeExtensions = (queryType.extensions ||= {});
queryTypeExtensions.directives = queryTypeDirectives;
if (importedDirectives.size) {
transformedSubgraph = (0, federation_utils_js_1.importMeshDirectives)(transformedSubgraph, [...importedDirectives]);
}
let subgraphAST = (0, utils_1.getDocumentNodeFromSchema)(transformedSubgraph);
if (importedDirectivesAST.size) {
subgraphAST = (0, graphql_1.concatAST)([
subgraphAST,
(0, graphql_1.parse)([...importedDirectivesAST].join('\n'), { noLocation: true }),
]);
}
annotatedSubgraphs.push({
name: subgraphName,
typeDefs: subgraphAST,
url,
});
}
return annotatedSubgraphs;
}
function composeSubgraphs(subgraphs, options = {}) {
const annotatedSubgraphs = getAnnotatedSubgraphs(subgraphs, options);
const composedSupergraphWithAnnotatedSubgraphs = composeAnnotatedSubgraphs(annotatedSubgraphs);
return {
...composedSupergraphWithAnnotatedSubgraphs,
subgraphs,
};
}
function composeAnnotatedSubgraphs(annotatedSubgraphs) {
const composedSupergraphSdl = (0, federation_composition_1.composeServices)(annotatedSubgraphs);
return {
...composedSupergraphSdl,
supergraphSdl: composedSupergraphSdl.supergraphSdl?.trim(),
annotatedSubgraphs,
};
}
function addAnnotationsForSemanticConventions({ queryFieldName, queryFieldConfig, directiveExtensions, subgraphName, }) {
if (directiveExtensions.merge?.length) {
return;
}
const type = (0, graphql_1.getNamedType)(queryFieldConfig.type);
if (type.astNode?.directives?.some((directive) => directive.name.value === 'key')) {
return;
}
if ((0, graphql_1.isObjectType)(type)) {
const fieldMap = type.getFields();
let fieldNames = Object.keys(fieldMap);
if (fieldNames.includes('id')) {
fieldNames = ['id'];
}
else {
const nonNullOnes = fieldNames.filter(fieldName => (0, graphql_1.isNonNullType)(fieldMap[fieldName].type));
if (nonNullOnes.length) {
fieldNames = nonNullOnes;
}
}
for (const fieldName of fieldNames) {
const objectField = fieldMap[fieldName];
const objectFieldType = (0, graphql_1.getNamedType)(objectField.type);
const argEntries = Object.entries(queryFieldConfig.args);
let argName;
let arg;
if (argEntries.length === 1) {
const argType = (0, graphql_1.getNamedType)(argEntries[0][1].type);
if (argType.name === objectFieldType.name) {
[argName, arg] = argEntries[0];
}
}
else {
for (const [currentArgName, currentArg] of argEntries) {
if (currentArgName === fieldName || (0, pluralize_1.default)(fieldName) === currentArgName) {
argName = currentArgName;
arg = currentArg;
break;
}
}
}
const queryFieldNameSnakeCase = (0, snake_case_1.snakeCase)(queryFieldName);
const pluralTypeName = (0, pluralize_1.default)(type.name);
if (arg) {
const typeDirectives = (0, utils_1.getDirectiveExtensions)(type);
switch (queryFieldNameSnakeCase) {
case (0, snake_case_1.snakeCase)(type.name):
case (0, snake_case_1.snakeCase)(`get_${type.name}_by_${fieldName}`):
case (0, snake_case_1.snakeCase)(`${type.name}_by_${fieldName}`): {
if ((0, graphql_1.isNonNullType)(arg.type)) {
directiveExtensions.merge ||= [];
directiveExtensions.merge.push({
subgraph: subgraphName,
keyField: fieldName,
keyArg: argName,
});
typeDirectives.key ||= [];
if (!typeDirectives.key.some((key) => key.fields === fieldName)) {
typeDirectives.key.push({
fields: fieldName,
});
const typeExtensions = (type.extensions ||= {});
typeExtensions.directives = typeDirectives;
}
}
break;
}
case (0, snake_case_1.snakeCase)(pluralTypeName):
case (0, snake_case_1.snakeCase)(`get_${pluralTypeName}_by_${fieldName}`):
case (0, snake_case_1.snakeCase)(`${pluralTypeName}_by_${fieldName}`):
case (0, snake_case_1.snakeCase)(`get_${pluralTypeName}_by_${fieldName}s`):
case (0, snake_case_1.snakeCase)(`${pluralTypeName}_by_${fieldName}s`): {
directiveExtensions.merge ||= [];
directiveExtensions.merge.push({
subgraph: subgraphName,
keyField: fieldName,
keyArg: argName,
});
typeDirectives.key ||= [];
if (!typeDirectives.key.some((key) => key.fields === fieldName)) {
typeDirectives.key.push({
fields: fieldName,
});
const typeExtensions = (type.extensions ||= {});
typeExtensions.directives = typeDirectives;
}
break;
}
}
}
/** For the schemas with filter in `where` argument */
/** Todo:
if (fieldName === 'id') {
const [, whereArg] = Object.entries(queryFieldConfig.args).find(([argName]) => argName === 'where') || [];
const whereArgType = whereArg && getNamedType(whereArg.type);
const whereArgTypeFields = isInputObjectType(whereArgType) && whereArgType.getFields();
const regularFieldInWhereArg = whereArgTypeFields?.[fieldName];
const regularFieldTypeName =
regularFieldInWhereArg && getNamedType(regularFieldInWhereArg.type)?.name;
const batchFieldInWhereArg = whereArgTypeFields?.[`${fieldName}_in`];
const batchFieldTypeName =
batchFieldInWhereArg && getNamedType(batchFieldInWhereArg.type)?.name;
const objectFieldTypeName = objectFieldType.name;
if (regularFieldTypeName === objectFieldTypeName) {
const operationName = pascalCase(`get_${type.name}_by_${fieldName}`);
const originalFieldName = getOriginalFieldNameForSubgraph(queryField, subgraphName);
const resolverAnnotation: ResolverAnnotation = {
subgraph: subgraphName,
operation: `query ${operationName}($${varName}: ${objectFieldTypeName}!) { ${originalFieldName}(where: { ${fieldName}: $${varName}) } }`,
kind: 'FETCH',
};
directiveExtensions.resolver ||= [];
directiveExtensions.resolver.push(resolverAnnotation);
directiveExtensions.variable ||= [];
}
if (batchFieldTypeName === objectFieldTypeName) {
const pluralFieldName = pluralize(fieldName);
const operationName = pascalCase(`get_${pluralTypeName}_by_${pluralFieldName}`);
const originalFieldName = getOriginalFieldNameForSubgraph(queryField, subgraphName);
const resolverAnnotation: ResolverAnnotation = {
subgraph: subgraphName,
operation: `query ${operationName}($${varName}: [${objectFieldTypeName}!]!) { ${originalFieldName}(where: { ${fieldName}_in: $${varName} }) }`,
kind: 'BATCH',
};
directiveExtensions.resolver ||= [];
directiveExtensions.resolver.push(resolverAnnotation);
directiveExtensions.variable ||= [];
}
}
*/
}
}
}