UNPKG

@graphql-tools/federation

Version:

Useful tools to create and manipulate GraphQL schemas.

902 lines • 51.8 kB
import { buildASTSchema, isInputObjectType, isInterfaceType, isObjectType, Kind, parse, parseType, print, TypeInfo, visit, visitWithTypeInfo, } from 'graphql'; import { defaultMergedResolver, delegateToSchema, extractUnavailableFieldsFromSelectionSet, subtractSelectionSets, } from '@graphql-tools/delegate'; import { buildHTTPExecutor } from '@graphql-tools/executor-http'; import { mergeSchemas } from '@graphql-tools/schema'; import { calculateSelectionScore, getDefaultFieldConfigMerger, stitchSchemas, ValidationLevel, } from '@graphql-tools/stitch'; import { createGraphQLError, isPromise, memoize1, mergeDeep, parseSelectionSet, } from '@graphql-tools/utils'; import { filterInternalFieldsAndTypes, getArgsFromKeysForFederation, getCacheKeyFnFromKey, getKeyFnForFederation, getNamedTypeNode, } from './utils.js'; export function ensureSupergraphSDLAst(supergraphSdl) { return typeof supergraphSdl === 'string' ? parse(supergraphSdl, { noLocation: true }) : supergraphSdl; } function getTypeFieldMapFromSupergraphAST(supergraphAST) { const typeFieldASTMap = new Map(); for (const definition of supergraphAST.definitions) { if ('fields' in definition) { const fieldMap = new Map(); typeFieldASTMap.set(definition.name.value, fieldMap); for (const field of definition.fields || []) { fieldMap.set(field.name.value, field); } } } return typeFieldASTMap; } const rootTypeMap = new Map([ ['Query', 'query'], ['Mutation', 'mutation'], ['Subscription', 'subscription'], ]); export function getFieldMergerFromSupergraphSdl(supergraphSdl) { const supergraphAST = ensureSupergraphSDLAst(supergraphSdl); const typeFieldASTMap = getTypeFieldMapFromSupergraphAST(supergraphAST); const defaultMerger = getDefaultFieldConfigMerger(true); const memoizedASTPrint = memoize1(print); const memoizedTypePrint = memoize1((type) => type.toString()); return function (candidates) { if (candidates.length === 1 || candidates.some(candidate => candidate.fieldName === '_entities')) { return candidates[0].fieldConfig; } if (candidates.some(candidate => rootTypeMap.has(candidate.type.name))) { const candidateNames = new Set(); const realCandidates = []; for (const candidate of candidates.toReversed ? candidates.toReversed() : [...candidates].reverse()) { if (candidate.transformedSubschema?.name && !candidateNames.has(candidate.transformedSubschema.name)) { candidateNames.add(candidate.transformedSubschema.name); realCandidates.push(candidate); } } const defaultMergedField = defaultMerger(candidates); return { ...defaultMergedField, resolve(_root, _args, context, info) { let currentSubschema; let currentScore = Infinity; let currentUnavailableSelectionSet; let currentFriendSubschemas; let currentAvailableSelectionSet; const originalSelectionSet = { kind: Kind.SELECTION_SET, selections: info.fieldNodes, }; // Find the best subschema to delegate this selection for (const candidate of realCandidates) { if (candidate.transformedSubschema) { const unavailableFields = extractUnavailableFieldsFromSelectionSet(candidate.transformedSubschema.transformedSchema, candidate.type, originalSelectionSet, () => true); const score = calculateSelectionScore(unavailableFields); if (score < currentScore) { currentScore = score; currentSubschema = candidate.transformedSubschema; currentFriendSubschemas = new Map(); currentUnavailableSelectionSet = { kind: Kind.SELECTION_SET, selections: unavailableFields, }; currentAvailableSelectionSet = subtractSelectionSets(originalSelectionSet, currentUnavailableSelectionSet); // Make parallel requests if there are other subschemas // that can resolve the remaining fields for this selection directly from the root field // instead of applying a type merging in advance for (const friendCandidate of realCandidates) { if (friendCandidate === candidate || !friendCandidate.transformedSubschema) { continue; } const unavailableFieldsInFriend = extractUnavailableFieldsFromSelectionSet(friendCandidate.transformedSubschema.transformedSchema, friendCandidate.type, currentUnavailableSelectionSet, () => true); const friendScore = calculateSelectionScore(unavailableFieldsInFriend); if (friendScore < score) { const unavailableInFriendSelectionSet = { kind: Kind.SELECTION_SET, selections: unavailableFieldsInFriend, }; const subschemaSelectionSet = subtractSelectionSets(currentUnavailableSelectionSet, unavailableInFriendSelectionSet); currentFriendSubschemas.set(friendCandidate.transformedSubschema, subschemaSelectionSet); currentUnavailableSelectionSet = unavailableInFriendSelectionSet; } } } } } if (!currentSubschema) { throw new Error('Could not determine subschema'); } const jobs = []; let hasPromise = false; const mainJob = delegateToSchema({ schema: currentSubschema, operation: rootTypeMap.get(info.parentType.name) || 'query', context, info: currentFriendSubschemas?.size ? { ...info, fieldNodes: [ ...(currentAvailableSelectionSet?.selections || []), ...(currentUnavailableSelectionSet?.selections || []), ], } : info, }); if (isPromise(mainJob)) { hasPromise = true; } jobs.push(mainJob); if (currentFriendSubschemas?.size) { for (const [friendSubschema, friendSelectionSet] of currentFriendSubschemas) { const friendJob = delegateToSchema({ schema: friendSubschema, operation: rootTypeMap.get(info.parentType.name) || 'query', context, info: { ...info, fieldNodes: friendSelectionSet.selections, }, skipTypeMerging: true, }); if (isPromise(friendJob)) { hasPromise = true; } jobs.push(friendJob); } } if (jobs.length === 1) { return jobs[0]; } if (hasPromise) { return Promise.all(jobs).then(results => mergeDeep(results)); } return mergeDeep(jobs); }, }; } const filteredCandidates = candidates.filter(candidate => { const fieldASTMap = typeFieldASTMap.get(candidate.type.name); if (fieldASTMap) { const fieldAST = fieldASTMap.get(candidate.fieldName); if (fieldAST) { const typeNodeInAST = memoizedASTPrint(fieldAST.type); const typeNodeInCandidate = memoizedTypePrint(candidate.fieldConfig.type); return typeNodeInAST === typeNodeInCandidate; } } return false; }); return defaultMerger(filteredCandidates.length ? filteredCandidates : candidates); }; } export function getSubschemasFromSupergraphSdl({ supergraphSdl, onExecutor = ({ endpoint }) => buildHTTPExecutor({ endpoint }), batch = false, }) { const supergraphAst = ensureSupergraphSDLAst(supergraphSdl); const subgraphEndpointMap = new Map(); const subgraphTypesMap = new Map(); const typeNameKeysBySubgraphMap = new Map(); const typeNameFieldsKeyBySubgraphMap = new Map(); const typeNameCanonicalMap = new Map(); const subgraphTypeNameExtraFieldsMap = new Map(); const subgraphTypeNameProvidedMap = new Map(); const orphanTypeMap = new Map(); // To detect unresolvable interface fields const subgraphExternalFieldMap = new Map(); // TODO: Temporary fix to add missing join__type directives to Query const subgraphNames = []; visit(supergraphAst, { EnumTypeDefinition(node) { if (node.name.value === 'join__Graph') { node.values?.forEach(valueNode => { subgraphNames.push(valueNode.name.value); }); } }, }); // END TODO function TypeWithFieldsVisitor(typeNode) { // TODO: Temporary fix to add missing join__type directives to Query if (typeNode.name.value === 'Query' || (typeNode.name.value === 'Mutation' && !typeNode.directives?.some(directiveNode => directiveNode.name.value === 'join__type'))) { typeNode.directives = [ ...(typeNode.directives || []), ...subgraphNames.map(subgraphName => ({ kind: Kind.DIRECTIVE, name: { kind: Kind.NAME, value: 'join__type', }, arguments: [ { kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: 'graph', }, value: { kind: Kind.ENUM, value: subgraphName, }, }, ], })), ]; } let isOrphan = true; // END TODO const fieldDefinitionNodesByGraphName = new Map(); typeNode.directives?.forEach(directiveNode => { if (typeNode.kind === Kind.OBJECT_TYPE_DEFINITION) { if (directiveNode.name.value === 'join__owner') { directiveNode.arguments?.forEach(argumentNode => { if (argumentNode.name.value === 'graph' && argumentNode.value?.kind === Kind.ENUM) { typeNameCanonicalMap.set(typeNode.name.value, argumentNode.value.value); } }); } } if (directiveNode.name.value === 'join__type') { isOrphan = false; const joinTypeGraphArgNode = directiveNode.arguments?.find(argumentNode => argumentNode.name.value === 'graph'); if (joinTypeGraphArgNode?.value?.kind === Kind.ENUM) { const graphName = joinTypeGraphArgNode.value.value; if (typeNode.kind === Kind.OBJECT_TYPE_DEFINITION || typeNode.kind === Kind.INTERFACE_TYPE_DEFINITION) { const keyArgumentNode = directiveNode.arguments?.find(argumentNode => argumentNode.name.value === 'key'); if (keyArgumentNode?.value?.kind === Kind.STRING) { let typeNameKeysMap = typeNameKeysBySubgraphMap.get(graphName); if (!typeNameKeysMap) { typeNameKeysMap = new Map(); typeNameKeysBySubgraphMap.set(graphName, typeNameKeysMap); } let keys = typeNameKeysMap.get(typeNode.name.value); if (!keys) { keys = []; typeNameKeysMap.set(typeNode.name.value, keys); } keys.push(keyArgumentNode.value.value); } } const fieldDefinitionNodesOfSubgraph = []; typeNode.fields?.forEach(fieldNode => { const joinFieldDirectives = fieldNode.directives?.filter(directiveNode => directiveNode.name.value === 'join__field'); let notInSubgraph = true; joinFieldDirectives?.forEach(joinFieldDirectiveNode => { const joinFieldGraphArgNode = joinFieldDirectiveNode.arguments?.find(argumentNode => argumentNode.name.value === 'graph'); if (joinFieldGraphArgNode?.value?.kind === Kind.ENUM && joinFieldGraphArgNode.value.value === graphName) { notInSubgraph = false; const isExternal = joinFieldDirectiveNode.arguments?.some(argumentNode => argumentNode.name.value === 'external' && argumentNode.value?.kind === Kind.BOOLEAN && argumentNode.value.value === true); const isOverridden = joinFieldDirectiveNode.arguments?.some(argumentNode => argumentNode.name.value === 'usedOverridden' && argumentNode.value?.kind === Kind.BOOLEAN && argumentNode.value.value === true); if (isExternal) { let externalFieldsByType = subgraphExternalFieldMap.get(graphName); if (!externalFieldsByType) { externalFieldsByType = new Map(); subgraphExternalFieldMap.set(graphName, externalFieldsByType); } let externalFields = externalFieldsByType.get(typeNode.name.value); if (!externalFields) { externalFields = new Set(); externalFieldsByType.set(typeNode.name.value, externalFields); } externalFields.add(fieldNode.name.value); } if (!isExternal && !isOverridden) { const typeArg = joinFieldDirectiveNode.arguments?.find(argumentNode => argumentNode.name.value === 'type'); const typeNode = typeArg?.value.kind === Kind.STRING ? parseType(typeArg.value.value) : fieldNode.type; fieldDefinitionNodesOfSubgraph.push({ ...fieldNode, type: typeNode, directives: fieldNode.directives?.filter(directiveNode => directiveNode.name.value !== 'join__field'), }); } const providedExtraField = joinFieldDirectiveNode.arguments?.find(argumentNode => argumentNode.name.value === 'provides'); if (providedExtraField?.value?.kind === Kind.STRING) { const providesSelectionSet = parseSelectionSet( /* GraphQL */ `{ ${providedExtraField.value.value} }`); function handleSelection(fieldNodeTypeName, selection) { let typeNameExtraFieldsMap = subgraphTypeNameExtraFieldsMap.get(graphName); if (!typeNameExtraFieldsMap) { typeNameExtraFieldsMap = new Map(); subgraphTypeNameExtraFieldsMap.set(graphName, typeNameExtraFieldsMap); } switch (selection.kind) { case Kind.FIELD: { const extraFieldTypeNode = supergraphAst.definitions.find(def => 'name' in def && def.name?.value === fieldNodeTypeName); const extraFieldNodeInType = extraFieldTypeNode.fields?.find(fieldNode => fieldNode.name.value === selection.name.value); if (extraFieldNodeInType) { let extraFields = typeNameExtraFieldsMap.get(fieldNodeTypeName); if (!extraFields) { extraFields = []; typeNameExtraFieldsMap.set(fieldNodeTypeName, extraFields); } extraFields.push({ ...extraFieldNodeInType, directives: extraFieldNodeInType.directives?.filter(directiveNode => directiveNode.name.value !== 'join__field'), }); let typeNameProvidedMap = subgraphTypeNameProvidedMap.get(graphName); if (!typeNameProvidedMap) { typeNameProvidedMap = new Map(); subgraphTypeNameProvidedMap.set(graphName, typeNameProvidedMap); } let providedFields = typeNameProvidedMap.get(fieldNodeTypeName); if (!providedFields) { providedFields = new Set(); typeNameProvidedMap.set(fieldNodeTypeName, providedFields); } providedFields.add(selection.name.value); if (selection.selectionSet) { const extraFieldNodeNamedType = getNamedTypeNode(extraFieldNodeInType.type); const extraFieldNodeTypeName = extraFieldNodeNamedType.name.value; for (const subSelection of selection.selectionSet.selections) { handleSelection(extraFieldNodeTypeName, subSelection); } } } } break; case Kind.INLINE_FRAGMENT: { const fragmentType = selection.typeCondition?.name?.value || fieldNodeType.name.value; if (selection.selectionSet) { for (const subSelection of selection.selectionSet.selections) { handleSelection(fragmentType, subSelection); } } } break; } } const fieldNodeType = getNamedTypeNode(fieldNode.type); const fieldNodeTypeName = fieldNodeType.name.value; for (const selection of providesSelectionSet.selections) { handleSelection(fieldNodeTypeName, selection); } } const requiresArgumentNode = joinFieldDirectiveNode.arguments?.find(argumentNode => argumentNode.name.value === 'requires'); if (requiresArgumentNode?.value?.kind === Kind.STRING) { let typeNameFieldsKeyMap = typeNameFieldsKeyBySubgraphMap.get(graphName); if (!typeNameFieldsKeyMap) { typeNameFieldsKeyMap = new Map(); typeNameFieldsKeyBySubgraphMap.set(graphName, typeNameFieldsKeyMap); } let fieldsKeyMap = typeNameFieldsKeyMap.get(typeNode.name.value); if (!fieldsKeyMap) { fieldsKeyMap = new Map(); typeNameFieldsKeyMap.set(typeNode.name.value, fieldsKeyMap); } fieldsKeyMap.set(fieldNode.name.value, requiresArgumentNode.value.value); } } }); // Add if no join__field directive if (!joinFieldDirectives?.length) { fieldDefinitionNodesOfSubgraph.push({ ...fieldNode, directives: fieldNode.directives?.filter(directiveNode => directiveNode.name.value !== 'join__field'), }); } else if (notInSubgraph && typeNameKeysBySubgraphMap .get(graphName) ?.get(typeNode.name.value) ?.some(key => key.split(' ').includes(fieldNode.name.value))) { fieldDefinitionNodesOfSubgraph.push({ ...fieldNode, directives: fieldNode.directives?.filter(directiveNode => directiveNode.name.value !== 'join__field'), }); } }); fieldDefinitionNodesByGraphName.set(graphName, fieldDefinitionNodesOfSubgraph); } } }); const joinImplementsDirectives = typeNode.directives?.filter(directiveNode => directiveNode.name.value === 'join__implements'); fieldDefinitionNodesByGraphName.forEach((fieldDefinitionNodesOfSubgraph, graphName) => { const interfaces = []; typeNode.interfaces?.forEach(interfaceNode => { const implementedSubgraphs = joinImplementsDirectives?.filter(directiveNode => { const argumentNode = directiveNode.arguments?.find(argumentNode => argumentNode.name.value === 'interface'); return (argumentNode?.value?.kind === Kind.STRING && argumentNode.value.value === interfaceNode.name.value); }); if (!implementedSubgraphs?.length || implementedSubgraphs.some(directiveNode => { const argumentNode = directiveNode.arguments?.find(argumentNode => argumentNode.name.value === 'graph'); return (argumentNode?.value?.kind === Kind.ENUM && argumentNode.value.value === graphName); })) { interfaces.push(interfaceNode); } }); if (typeNode.name.value === 'Query') { fieldDefinitionNodesOfSubgraph.push(entitiesFieldDefinitionNode); } const objectTypedDefNodeForSubgraph = { ...typeNode, interfaces, fields: fieldDefinitionNodesOfSubgraph, directives: typeNode.directives?.filter(directiveNode => directiveNode.name.value !== 'join__type' && directiveNode.name.value !== 'join__owner' && directiveNode.name.value !== 'join__implements'), }; let subgraphTypes = subgraphTypesMap.get(graphName); if (!subgraphTypes) { subgraphTypes = []; subgraphTypesMap.set(graphName, subgraphTypes); } subgraphTypes.push(objectTypedDefNodeForSubgraph); }); if (isOrphan) { orphanTypeMap.set(typeNode.name.value, typeNode); } } visit(supergraphAst, { ScalarTypeDefinition(node) { let isOrphan = !node.name.value.startsWith('link__') && !node.name.value.startsWith('join__'); node.directives?.forEach(directiveNode => { if (directiveNode.name.value === 'join__type') { directiveNode.arguments?.forEach(argumentNode => { if (argumentNode.name.value === 'graph' && argumentNode?.value?.kind === Kind.ENUM) { isOrphan = false; const graphName = argumentNode.value.value; let subgraphTypes = subgraphTypesMap.get(graphName); if (!subgraphTypes) { subgraphTypes = []; subgraphTypesMap.set(graphName, subgraphTypes); } subgraphTypes.push({ ...node, directives: node.directives?.filter(directiveNode => directiveNode.name.value !== 'join__type'), }); } }); } }); if (isOrphan) { orphanTypeMap.set(node.name.value, node); } }, InputObjectTypeDefinition(node) { let isOrphan = true; node.directives?.forEach(directiveNode => { if (directiveNode.name.value === 'join__type') { directiveNode.arguments?.forEach(argumentNode => { if (argumentNode.name.value === 'graph' && argumentNode?.value?.kind === Kind.ENUM) { isOrphan = false; const graphName = argumentNode.value.value; let subgraphTypes = subgraphTypesMap.get(graphName); if (!subgraphTypes) { subgraphTypes = []; subgraphTypesMap.set(graphName, subgraphTypes); } subgraphTypes.push({ ...node, directives: node.directives?.filter(directiveNode => directiveNode.name.value !== 'join__type'), }); } }); } }); if (isOrphan) { orphanTypeMap.set(node.name.value, node); } }, InterfaceTypeDefinition: TypeWithFieldsVisitor, UnionTypeDefinition(node) { let isOrphan = true; node.directives?.forEach(directiveNode => { if (directiveNode.name.value === 'join__type') { directiveNode.arguments?.forEach(argumentNode => { if (argumentNode.name.value === 'graph' && argumentNode?.value?.kind === Kind.ENUM) { isOrphan = false; const graphName = argumentNode.value.value; const unionMembers = []; node.directives?.forEach(directiveNode => { if (directiveNode.name.value === 'join__unionMember') { const graphArgumentNode = directiveNode.arguments?.find(argumentNode => argumentNode.name.value === 'graph'); const memberArgumentNode = directiveNode.arguments?.find(argumentNode => argumentNode.name.value === 'member'); if (graphArgumentNode?.value?.kind === Kind.ENUM && graphArgumentNode.value.value === graphName && memberArgumentNode?.value?.kind === Kind.STRING) { unionMembers.push({ kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value: memberArgumentNode.value.value, }, }); } } }); if (unionMembers.length > 0) { let subgraphTypes = subgraphTypesMap.get(graphName); if (!subgraphTypes) { subgraphTypes = []; subgraphTypesMap.set(graphName, subgraphTypes); } subgraphTypes.push({ ...node, types: unionMembers, directives: node.directives?.filter(directiveNode => directiveNode.name.value !== 'join__type' && directiveNode.name.value !== 'join__unionMember'), }); } } }); } }); if (isOrphan && node.name.value !== '_Entity') { orphanTypeMap.set(node.name.value, node); } }, EnumTypeDefinition(node) { let isOrphan = true; if (node.name.value === 'join__Graph') { node.values?.forEach(valueNode => { isOrphan = false; valueNode.directives?.forEach(directiveNode => { if (directiveNode.name.value === 'join__graph') { directiveNode.arguments?.forEach(argumentNode => { if (argumentNode.name.value === 'url' && argumentNode.value?.kind === Kind.STRING) { subgraphEndpointMap.set(valueNode.name.value, argumentNode.value.value); } }); } }); }); } node.directives?.forEach(directiveNode => { if (directiveNode.name.value === 'join__type') { isOrphan = false; directiveNode.arguments?.forEach(argumentNode => { if (argumentNode.name.value === 'graph' && argumentNode.value?.kind === Kind.ENUM) { const graphName = argumentNode.value.value; const enumValueNodes = []; node.values?.forEach(valueNode => { const joinEnumValueDirectives = valueNode.directives?.filter(directiveNode => directiveNode.name.value === 'join__enumValue'); if (joinEnumValueDirectives?.length) { joinEnumValueDirectives.forEach(joinEnumValueDirectiveNode => { joinEnumValueDirectiveNode.arguments?.forEach(argumentNode => { if (argumentNode.name.value === 'graph' && argumentNode.value?.kind === Kind.ENUM && argumentNode.value.value === graphName) { enumValueNodes.push({ ...valueNode, directives: valueNode.directives?.filter(directiveNode => directiveNode.name.value !== 'join__enumValue'), }); } }); }); } else { enumValueNodes.push(valueNode); } }); const enumTypedDefNodeForSubgraph = { ...node, directives: node.directives?.filter(directiveNode => directiveNode.name.value !== 'join__type'), values: enumValueNodes, }; let subgraphTypes = subgraphTypesMap.get(graphName); if (!subgraphTypes) { subgraphTypes = []; subgraphTypesMap.set(graphName, subgraphTypes); } subgraphTypes.push(enumTypedDefNodeForSubgraph); } }); } }); if (isOrphan) { orphanTypeMap.set(node.name.value, node); } }, ObjectTypeDefinition: TypeWithFieldsVisitor, }); const subschemaMap = new Map(); for (const [subgraphName, endpoint] of subgraphEndpointMap) { const mergeConfig = {}; const typeNameKeyMap = typeNameKeysBySubgraphMap.get(subgraphName); const unionTypeNodes = []; if (typeNameKeyMap) { const typeNameFieldsKeyMap = typeNameFieldsKeyBySubgraphMap.get(subgraphName); for (const [typeName, keys] of typeNameKeyMap) { const mergedTypeConfig = (mergeConfig[typeName] = {}); const fieldsKeyMap = typeNameFieldsKeyMap?.get(typeName); const extraKeys = []; if (fieldsKeyMap) { const fieldsConfig = (mergedTypeConfig.fields = {}); for (const [fieldName, fieldNameKey] of fieldsKeyMap) { extraKeys.push(fieldNameKey); fieldsConfig[fieldName] = { selectionSet: `{ ${fieldNameKey} }`, computed: true, }; } } if (typeNameCanonicalMap.get(typeName) === subgraphName) { mergedTypeConfig.canonical = true; } mergedTypeConfig.entryPoints = keys.map(key => ({ selectionSet: `{ ${key} }`, argsFromKeys: getArgsFromKeysForFederation, key: getKeyFnForFederation(typeName, [key, ...extraKeys]), fieldName: `_entities`, dataLoaderOptions: { cacheKeyFn: getCacheKeyFnFromKey(key), }, })); unionTypeNodes.push({ kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value: typeName, }, }); } } const entitiesUnionTypeDefinitionNode = { name: { kind: Kind.NAME, value: '_Entity', }, kind: Kind.UNION_TYPE_DEFINITION, types: unionTypeNodes, }; const extraOrphanTypesForSubgraph = new Map(); function visitTypeDefinitionsForOrphanTypes(node) { function visitNamedTypeNode(namedTypeNode) { const typeName = namedTypeNode.name.value; if (specifiedTypeNames.includes(typeName)) { return node; } const orphanType = orphanTypeMap.get(typeName); if (orphanType) { if (!extraOrphanTypesForSubgraph.has(typeName)) { extraOrphanTypesForSubgraph.set(typeName, {}); const extraOrphanType = visitTypeDefinitionsForOrphanTypes(orphanType); extraOrphanTypesForSubgraph.set(typeName, extraOrphanType); } } else if (!subgraphTypes.some(typeNode => typeNode.name.value === typeName)) { return null; } return node; } function visitFieldDefs(nodeFields) { const fields = []; for (const field of nodeFields || []) { const isTypeNodeOk = visitNamedTypeNode(getNamedTypeNode(field.type)); if (!isTypeNodeOk) { continue; } if (field.kind === Kind.FIELD_DEFINITION) { const args = visitFieldDefs(field.arguments); fields.push({ ...field, arguments: args, }); } else { fields.push(field); } } return fields; } function visitObjectAndInterfaceDefs(node) { const fields = visitFieldDefs(node.fields); const interfaces = []; for (const iface of node.interfaces || []) { const isTypeNodeOk = visitNamedTypeNode(iface); if (!isTypeNodeOk) { continue; } interfaces.push(iface); } return { ...node, fields, interfaces, }; } return visit(node, { [Kind.OBJECT_TYPE_DEFINITION]: visitObjectAndInterfaceDefs, [Kind.OBJECT_TYPE_EXTENSION]: visitObjectAndInterfaceDefs, [Kind.INTERFACE_TYPE_DEFINITION]: visitObjectAndInterfaceDefs, [Kind.INTERFACE_TYPE_EXTENSION]: visitObjectAndInterfaceDefs, [Kind.UNION_TYPE_DEFINITION](node) { const types = []; for (const type of node.types || []) { const isTypeNodeOk = visitNamedTypeNode(type); if (!isTypeNodeOk) { continue; } types.push(type); } return { ...node, types, }; }, [Kind.UNION_TYPE_EXTENSION](node) { const types = []; for (const type of node.types || []) { const isTypeNodeOk = visitNamedTypeNode(type); if (!isTypeNodeOk) { continue; } types.push(type); } return { ...node, types, }; }, [Kind.INPUT_OBJECT_TYPE_DEFINITION](node) { const fields = visitFieldDefs(node.fields); return { ...node, fields, }; }, [Kind.INPUT_OBJECT_TYPE_EXTENSION](node) { const fields = visitFieldDefs(node.fields); return { ...node, fields, }; }, }); } const subgraphTypes = subgraphTypesMap.get(subgraphName) || []; const typeNameExtraFieldsMap = subgraphTypeNameExtraFieldsMap.get(subgraphName); subgraphTypes.forEach(typeNode => { if (typeNameExtraFieldsMap && 'fields' in typeNode) { const extraFields = typeNameExtraFieldsMap.get(typeNode.name.value); if (extraFields) { typeNode.fields.push(...extraFields); } } visitTypeDefinitionsForOrphanTypes(typeNode); }); const extendedSubgraphTypes = [...subgraphTypes, ...extraOrphanTypesForSubgraph.values()]; // We should add implemented objects from other subgraphs implemented by this interface for (const interfaceInSubgraph of extendedSubgraphTypes) { if (interfaceInSubgraph.kind === Kind.INTERFACE_TYPE_DEFINITION) { let isOrphan = true; for (const definitionNode of supergraphAst.definitions) { if (definitionNode.kind === Kind.OBJECT_TYPE_DEFINITION && definitionNode.interfaces?.some(interfaceNode => interfaceNode.name.value === interfaceInSubgraph.name.value)) { isOrphan = false; } } if (isOrphan) { // @ts-expect-error `kind` property is a readonly field in TS definitions but it is not actually interfaceInSubgraph.kind = Kind.OBJECT_TYPE_DEFINITION; } } } let schema; const schemaAst = { kind: Kind.DOCUMENT, definitions: [ ...extendedSubgraphTypes, entitiesUnionTypeDefinitionNode, anyTypeDefinitionNode, ], }; try { schema = buildASTSchema(schemaAst, { assumeValidSDL: true, assumeValid: true, }); } catch (e) { throw new Error(`Error building schema for subgraph ${subgraphName}: ${e?.stack || e?.message || e.toString()}`); } let executor = onExecutor({ subgraphName, endpoint, subgraphSchema: schema }); if (globalThis.process?.env?.['DEBUG']) { const origExecutor = executor; executor = async function debugExecutor(execReq) { console.log(`Executing ${subgraphName} with args:`, { document: print(execReq.document), variables: JSON.stringify(execReq.variables, null, 2), }); const res = await origExecutor(execReq); console.log(`Response from ${subgraphName}:`, JSON.stringify(res, null, 2)); return res; }; } const typeNameProvidedMap = subgraphTypeNameProvidedMap.get(subgraphName); const externalFieldMap = subgraphExternalFieldMap.get(subgraphName); subschemaMap.set(subgraphName, { name: subgraphName, schema, executor, merge: mergeConfig, batch, transforms: [ { transformRequest(request) { const typeInfo = new TypeInfo(schema); return { ...request, document: visit(request.document, visitWithTypeInfo(typeInfo, { // To avoid resolving unresolvable interface fields [Kind.FIELD](node) { if (node.name.value !== '__typename') { const parentType = typeInfo.getParentType(); if (isInterfaceType(parentType)) { const providedInterfaceFields = typeNameProvidedMap?.get(parentType.name); const implementations = schema.getPossibleTypes(parentType); for (const implementation of implementations) { const externalFields = externalFieldMap?.get(implementation.name); const providedFields = typeNameProvidedMap?.get(implementation.name); if (!providedInterfaceFields?.has(node.name.value) && !providedFields?.has(node.name.value) && externalFields?.has(node.name.value)) { throw createGraphQLError(`Was not able to find any options for ${node.name.value}: This shouldn't have happened.`); } } } } }, })), }; }, }, ], }); } return subschemaMap; } export function getStitchedSchemaFromSupergraphSdl(opts) { const subschemaMap = getSubschemasFromSupergraphSdl(opts); const supergraphSchema = stitchSchemas({ subschemas: [...subschemaMap.values()], assumeValid: true, assumeValidSDL: true, typeMergingOptions: { useNonNullableFieldOnConflict: true, validationSettings: { validationLevel: ValidationLevel.Off, }, fieldConfigMerger: getFieldMergerFromSupergraphSdl(opts.supergraphSdl), }, }); const extraDefinitions = []; for (const definition of ensureSupergraphSDLAst(opts.supergraphSdl).definitions) { if ('fields' in definition && definition.fields) { const typeInSchema = supergraphSchema.getType(definition.name.value); if (!typeInSchema || !('getFields' in typeInSchema)) { extraDefinitions.push(definition); } else { const fieldsInSchema = typeInSchema.getFields(); const extraFields = []; for (const field of definition.fields) { if (!fieldsInSchema[field.name.value]) { extraFields.push(field); } } if (extraFields.length) { let definitionKind; if (isObjectType(typeInSchema)) { definitionKind = Kind.OBJECT_TYPE_DEFINITION; } else if (isInterfaceType(typeInSchema)) { definitionKind = Kind.INTERFACE_TYPE_DEFINITION; } else if (isInputObjectType(typeInSchema)) { definitionKind = Kind.INPUT_OBJECT_TYPE_DEFINITION; } if (definitionKind) { extraDefinitions.push({ kind: definitionKind, name: definition.name, fields: extraFields, // `fields` are diff