UNPKG

@graphql-tools/stitching-directives

Version:

A set of utils for faster development of GraphQL tools

1,194 lines (1,181 loc) • 42.7 kB
import { visit, Kind, TypeNameMetaFieldDef, parseValue, valueFromASTUntyped, getNamedType, getNullableType, isListType, print, isInterfaceType, isUnionType, isObjectType, isNamedType, isAbstractType, GraphQLDirective, GraphQLNonNull, GraphQLString, GraphQLList, parse } from 'graphql'; import { cloneSubschemaConfig } from '@graphql-tools/delegate'; import { mapSchema, MapperKind, getDirective, parseSelectionSet, getImplementingTypes, mergeDeep, isSome } from '@graphql-tools/utils'; const defaultStitchingDirectiveOptions = { keyDirectiveName: "key", computedDirectiveName: "computed", canonicalDirectiveName: "canonical", mergeDirectiveName: "merge", pathToDirectivesInExtensions: ["directives"] }; function extractVariables(inputValue) { const path = []; const variablePaths = /* @__PURE__ */ Object.create(null); const keyPathVisitor = { enter: (_node, key) => { if (typeof key === "number") { path.push(key); } }, leave: (_node, key) => { if (typeof key === "number") { path.pop(); } } }; const fieldPathVisitor = { enter: (node) => { path.push(node.name.value); }, leave: () => { path.pop(); } }; const variableVisitor = { enter: (node, key) => { if (typeof key === "number") { variablePaths[node.name.value] = path.concat([key]); } else { variablePaths[node.name.value] = path.slice(); } return { kind: Kind.NULL }; } }; const newInputValue = visit(inputValue, { [Kind.OBJECT]: keyPathVisitor, [Kind.LIST]: keyPathVisitor, [Kind.OBJECT_FIELD]: fieldPathVisitor, [Kind.VARIABLE]: variableVisitor }); return { inputValue: newInputValue, variablePaths }; } function pathsFromSelectionSet(selectionSet, path = []) { const paths = []; for (const selection of selectionSet.selections) { const additions = pathsFromSelection(selection, path) ?? []; for (const addition of additions) { paths.push(addition); } } return paths; } function pathsFromSelection(selection, path) { if (selection.kind === Kind.FIELD) { const responseKey = selection.alias?.value ?? selection.name.value; if (selection.selectionSet) { return pathsFromSelectionSet( selection.selectionSet, path.concat([responseKey]) ); } else { return [path.concat([responseKey])]; } } else if (selection.kind === Kind.INLINE_FRAGMENT) { return pathsFromSelectionSet(selection.selectionSet, path); } return void 0; } function getSourcePaths(mappingInstructions, selectionSet) { const sourcePaths = []; for (const mappingInstruction of mappingInstructions) { const { sourcePath } = mappingInstruction; if (sourcePath.length) { sourcePaths.push(sourcePath); continue; } if (selectionSet == null) { continue; } const paths = pathsFromSelectionSet(selectionSet); for (const path of paths) { sourcePaths.push(path); } sourcePaths.push([TypeNameMetaFieldDef.name]); } return sourcePaths; } const KEY_DELIMITER = "__dot__"; const EXPANSION_PREFIX = "__exp"; function preparseMergeArgsExpr(mergeArgsExpr) { const variableRegex = /\$[_A-Za-z][_A-Za-z0-9.]*/g; const dotRegex = /\./g; mergeArgsExpr = mergeArgsExpr.replace( variableRegex, (variable) => variable.replace(dotRegex, KEY_DELIMITER) ); const segments = mergeArgsExpr.split("[["); const expansionExpressions = /* @__PURE__ */ Object.create(null); if (segments.length === 1) { return { mergeArgsExpr, expansionExpressions }; } let finalSegments = [segments[0]]; for (let i = 1; i < segments.length; i++) { const additionalSegments = segments[i]?.split("]]"); if (additionalSegments?.length !== 2) { throw new Error( `Each opening "[[" must be matched by a closing "]]" without nesting.` ); } finalSegments = finalSegments.concat(additionalSegments); } let finalMergeArgsExpr = finalSegments[0]; for (let i = 1; i < finalSegments.length - 1; i += 2) { const variableName = `${EXPANSION_PREFIX}${(i - 1) / 2 + 1}`; expansionExpressions[variableName] = finalSegments[i]; finalMergeArgsExpr += `$${variableName}${finalSegments[i + 1]}`; } return { mergeArgsExpr: finalMergeArgsExpr, expansionExpressions }; } function addProperty(object, path, value) { const initialSegment = path[0]; if (path.length === 1) { object[initialSegment] = value; return; } let field = object[initialSegment]; if (field != null) { addProperty(field, path.slice(1), value); return; } if (typeof path[1] === "string") { field = /* @__PURE__ */ Object.create(null); } else { field = []; } addProperty(field, path.slice(1), value); object[initialSegment] = field; } function getProperty(object, path) { if (!path.length || object == null) { return object; } const newPath = path.slice(); const key = newPath.shift(); if (key == null) { return; } const prop = object[key]; return getProperty(prop, newPath); } function getProperties(object, propertyTree) { if (object == null) { return object; } const newObject = /* @__PURE__ */ Object.create(null); for (const key in propertyTree) { const subKey = propertyTree[key]; if (subKey == null) { newObject[key] = object[key]; continue; } const prop = object[key]; newObject[key] = deepMap(prop, function deepMapFn(item) { return getProperties(item, subKey); }); } return newObject; } function propertyTreeFromPaths(paths) { const propertyTree = /* @__PURE__ */ Object.create(null); for (const path of paths) { addProperty(propertyTree, path, null); } return propertyTree; } function deepMap(arrayOrItem, fn) { if (Array.isArray(arrayOrItem)) { return arrayOrItem.map( (nestedArrayOrItem) => deepMap(nestedArrayOrItem, fn) ); } return fn(arrayOrItem); } function parseMergeArgsExpr(mergeArgsExpr, selectionSet) { const { mergeArgsExpr: newMergeArgsExpr, expansionExpressions } = preparseMergeArgsExpr(mergeArgsExpr); const inputValue = parseValue(`{ ${newMergeArgsExpr} }`, { noLocation: true }); const { inputValue: newInputValue, variablePaths } = extractVariables(inputValue); if (!Object.keys(expansionExpressions).length) { if (!Object.keys(variablePaths).length) { throw new Error("Merge arguments must declare a key."); } const mappingInstructions = getMappingInstructions(variablePaths); const usedProperties2 = propertyTreeFromPaths( getSourcePaths(mappingInstructions, selectionSet) ); return { args: valueFromASTUntyped(newInputValue), usedProperties: usedProperties2, mappingInstructions }; } const expansionRegEx = new RegExp(`^${EXPANSION_PREFIX}[0-9]+$`); for (const variableName in variablePaths) { if (!variableName.match(expansionRegEx)) { throw new Error( "Expansions cannot be mixed with single key declarations." ); } } const expansions = []; const sourcePaths = []; for (const variableName in expansionExpressions) { const str = expansionExpressions[variableName]; const valuePath = variablePaths[variableName]; const { inputValue: expansionInputValue, variablePaths: expansionVariablePaths } = extractVariables(parseValue(`${str}`, { noLocation: true })); if (!Object.keys(expansionVariablePaths).length) { throw new Error("Merge arguments must declare a key."); } const mappingInstructions = getMappingInstructions(expansionVariablePaths); const value = valueFromASTUntyped(expansionInputValue); sourcePaths.push(...getSourcePaths(mappingInstructions, selectionSet)); assertNotWithinList(valuePath); expansions.push({ valuePath, value, mappingInstructions }); } const usedProperties = propertyTreeFromPaths(sourcePaths); return { args: valueFromASTUntyped(newInputValue), usedProperties, expansions }; } function getMappingInstructions(variablePaths) { const mappingInstructions = []; for (const keyPath in variablePaths) { const valuePath = variablePaths[keyPath]; const splitKeyPath = keyPath.split(KEY_DELIMITER).slice(1); assertNotWithinList(valuePath); mappingInstructions.push({ destinationPath: valuePath, sourcePath: splitKeyPath }); } return mappingInstructions; } function assertNotWithinList(path) { for (const pathSegment of path) { if (typeof pathSegment === "number") { throw new Error("Insertions cannot be made into a list."); } } } function stitchingDirectivesTransformer(options = {}) { const { keyDirectiveName, computedDirectiveName, mergeDirectiveName, canonicalDirectiveName, pathToDirectivesInExtensions } = { ...defaultStitchingDirectiveOptions, ...options }; return (subschemaConfig) => { const newSubschemaConfig = cloneSubschemaConfig(subschemaConfig); const selectionSetsByType = /* @__PURE__ */ Object.create(null); const computedFieldSelectionSets = /* @__PURE__ */ Object.create(null); const mergedTypesResolversInfo = /* @__PURE__ */ Object.create(null); const canonicalTypesInfo = /* @__PURE__ */ Object.create(null); const selectionSetsByTypeAndEntryField = /* @__PURE__ */ Object.create(null); const mergedTypesResolversInfoByEntryField = /* @__PURE__ */ Object.create(null); const schema = subschemaConfig.schema; function setCanonicalDefinition(typeName, fieldName) { canonicalTypesInfo[typeName] = canonicalTypesInfo[typeName] || /* @__PURE__ */ Object.create(null); if (fieldName) { const fields = canonicalTypesInfo[typeName]?.fields ?? /* @__PURE__ */ Object.create(null); canonicalTypesInfo[typeName].fields = fields; fields[fieldName] = true; } else { canonicalTypesInfo[typeName].canonical = true; } } mapSchema(schema, { [MapperKind.OBJECT_TYPE]: (type) => { const keyDirective = getDirective( schema, type, keyDirectiveName, pathToDirectivesInExtensions )?.[0]; if (keyDirective != null) { const selectionSet = parseSelectionSet(keyDirective["selectionSet"], { noLocation: true }); selectionSetsByType[type.name] = selectionSet; } const canonicalDirective = getDirective( schema, type, canonicalDirectiveName, pathToDirectivesInExtensions )?.[0]; if (canonicalDirective != null) { setCanonicalDefinition(type.name); } return void 0; }, [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => { const computedDirective = getDirective( schema, fieldConfig, computedDirectiveName, pathToDirectivesInExtensions )?.[0]; if (computedDirective != null) { const selectionSet = parseSelectionSet( computedDirective["selectionSet"], { noLocation: true } ); if (!computedFieldSelectionSets[typeName]) { computedFieldSelectionSets[typeName] = /* @__PURE__ */ Object.create(null); } computedFieldSelectionSets[typeName][fieldName] = selectionSet; } const mergeDirective = getDirective( schema, fieldConfig, mergeDirectiveName, pathToDirectivesInExtensions )?.[0]; if (mergeDirective?.["keyField"] != null) { const mergeDirectiveKeyField = mergeDirective["keyField"]; const selectionSet = parseSelectionSet( `{ ${mergeDirectiveKeyField}}`, { noLocation: true } ); const typeNames = mergeDirective["types"]; const returnType = getNamedType(fieldConfig.type); forEachConcreteType(schema, returnType, typeNames, (typeName2) => { if (typeNames == null || typeNames.includes(typeName2)) { let existingEntryFieldMap = selectionSetsByTypeAndEntryField[typeName2]; if (existingEntryFieldMap == null) { existingEntryFieldMap = /* @__PURE__ */ Object.create(null); selectionSetsByTypeAndEntryField[typeName2] = existingEntryFieldMap; } let existingSelectionSet = existingEntryFieldMap[fieldName]; if (existingSelectionSet == null) { existingSelectionSet = selectionSetsByType[typeName2]; } existingEntryFieldMap[fieldName] = existingSelectionSet ? mergeSelectionSets(existingSelectionSet, selectionSet) : selectionSet; } }); } const canonicalDirective = getDirective( schema, fieldConfig, canonicalDirectiveName, pathToDirectivesInExtensions )?.[0]; if (canonicalDirective != null) { setCanonicalDefinition(typeName, fieldName); } return void 0; }, [MapperKind.INTERFACE_TYPE]: (type) => { const canonicalDirective = getDirective( schema, type, canonicalDirectiveName, pathToDirectivesInExtensions )?.[0]; if (canonicalDirective) { setCanonicalDefinition(type.name); } return void 0; }, [MapperKind.INTERFACE_FIELD]: (fieldConfig, fieldName, typeName) => { const canonicalDirective = getDirective( schema, fieldConfig, canonicalDirectiveName, pathToDirectivesInExtensions )?.[0]; if (canonicalDirective) { setCanonicalDefinition(typeName, fieldName); } return void 0; }, [MapperKind.INPUT_OBJECT_TYPE]: (type) => { const canonicalDirective = getDirective( schema, type, canonicalDirectiveName, pathToDirectivesInExtensions )?.[0]; if (canonicalDirective) { setCanonicalDefinition(type.name); } return void 0; }, [MapperKind.INPUT_OBJECT_FIELD]: (inputFieldConfig, fieldName, typeName) => { const canonicalDirective = getDirective( schema, inputFieldConfig, canonicalDirectiveName, pathToDirectivesInExtensions )?.[0]; if (canonicalDirective != null) { setCanonicalDefinition(typeName, fieldName); } return void 0; }, [MapperKind.UNION_TYPE]: (type) => { const canonicalDirective = getDirective( schema, type, canonicalDirectiveName, pathToDirectivesInExtensions )?.[0]; if (canonicalDirective != null) { setCanonicalDefinition(type.name); } return void 0; }, [MapperKind.ENUM_TYPE]: (type) => { const canonicalDirective = getDirective( schema, type, canonicalDirectiveName, pathToDirectivesInExtensions )?.[0]; if (canonicalDirective != null) { setCanonicalDefinition(type.name); } return void 0; }, [MapperKind.SCALAR_TYPE]: (type) => { const canonicalDirective = getDirective( schema, type, canonicalDirectiveName, pathToDirectivesInExtensions )?.[0]; if (canonicalDirective != null) { setCanonicalDefinition(type.name); } return void 0; } }); if (subschemaConfig.merge) { for (const typeName in subschemaConfig.merge) { const mergedTypeConfig = subschemaConfig.merge[typeName]; if (mergedTypeConfig?.selectionSet) { const selectionSet = parseSelectionSet( mergedTypeConfig.selectionSet, { noLocation: true } ); if (selectionSet) { if (selectionSetsByType[typeName]) { selectionSetsByType[typeName] = mergeSelectionSets( selectionSetsByType[typeName], selectionSet ); } else { selectionSetsByType[typeName] = selectionSet; } } } if (mergedTypeConfig?.fields) { for (const fieldName in mergedTypeConfig.fields) { const fieldConfig = mergedTypeConfig.fields[fieldName]; if (!fieldConfig?.selectionSet) continue; const selectionSet = parseSelectionSet(fieldConfig.selectionSet, { noLocation: true }); if (selectionSet) { if (computedFieldSelectionSets[typeName]?.[fieldName]) { computedFieldSelectionSets[typeName][fieldName] = mergeSelectionSets( computedFieldSelectionSets[typeName][fieldName], selectionSet ); } else { if (computedFieldSelectionSets[typeName] == null) { computedFieldSelectionSets[typeName] = /* @__PURE__ */ Object.create(null); } computedFieldSelectionSets[typeName][fieldName] = selectionSet; } } } } } } const allSelectionSetsByType = /* @__PURE__ */ Object.create(null); for (const typeName in selectionSetsByType) { allSelectionSetsByType[typeName] = allSelectionSetsByType[typeName] || []; const selectionSet = selectionSetsByType[typeName]; allSelectionSetsByType[typeName].push(selectionSet); } for (const typeName in computedFieldSelectionSets) { const selectionSets = computedFieldSelectionSets[typeName]; for (const i in selectionSets) { allSelectionSetsByType[typeName] = allSelectionSetsByType[typeName] || []; const selectionSet = selectionSets[i]; allSelectionSetsByType[typeName].push(selectionSet); } } mapSchema(schema, { [MapperKind.OBJECT_FIELD]: function objectFieldMapper(fieldConfig, fieldName) { const mergeDirective = getDirective( schema, fieldConfig, mergeDirectiveName, pathToDirectivesInExtensions )?.[0]; if (mergeDirective != null) { const returnType = getNullableType(fieldConfig.type); const returnsList = isListType(returnType); const namedType = getNamedType(returnType); let mergeArgsExpr = mergeDirective["argsExpr"]; if (mergeArgsExpr == null) { const key = mergeDirective["key"]; const keyField = mergeDirective["keyField"]; const keyExpr = key != null ? buildKeyExpr(key) : keyField != null ? `$key.${keyField}` : "$key"; const keyArg = mergeDirective["keyArg"]; const argNames = keyArg == null ? [Object.keys(fieldConfig.args ?? {})[0]] : keyArg.split("."); const lastArgName = argNames.pop(); mergeArgsExpr = returnsList ? `${lastArgName}: [[${keyExpr}]]` : `${lastArgName}: ${keyExpr}`; for (const argName of argNames.reverse()) { mergeArgsExpr = `${argName}: { ${mergeArgsExpr} }`; } } const typeNames = mergeDirective["types"]; forEachConcreteTypeName( namedType, schema, typeNames, function generateResolveInfo(typeName) { const mergedSelectionSets = []; if (allSelectionSetsByType[typeName]) { mergedSelectionSets.push(...allSelectionSetsByType[typeName]); } if (selectionSetsByTypeAndEntryField[typeName]?.[fieldName]) { mergedSelectionSets.push( selectionSetsByTypeAndEntryField[typeName][fieldName] ); } const parsedMergeArgsExpr = parseMergeArgsExpr( mergeArgsExpr, allSelectionSetsByType[typeName] == null ? void 0 : mergeSelectionSets(...mergedSelectionSets) ); const additionalArgs = mergeDirective["additionalArgs"]; if (additionalArgs != null) { parsedMergeArgsExpr.args = mergeDeep([ parsedMergeArgsExpr.args, valueFromASTUntyped( parseValue(`{ ${additionalArgs} }`, { noLocation: true }) ) ]); } if (selectionSetsByTypeAndEntryField[typeName]?.[fieldName] != null) { const typeConfigByField = mergedTypesResolversInfoByEntryField[typeName] ||= /* @__PURE__ */ Object.create(null); typeConfigByField[fieldName] = { fieldName, returnsList, ...parsedMergeArgsExpr }; } else { mergedTypesResolversInfo[typeName] = { fieldName, returnsList, ...parsedMergeArgsExpr }; } } ); } return void 0; } }); for (const typeName in selectionSetsByType) { const selectionSet = selectionSetsByType[typeName]; const mergeConfig = newSubschemaConfig.merge ?? /* @__PURE__ */ Object.create(null); newSubschemaConfig.merge = mergeConfig; if (mergeConfig[typeName] == null) { newSubschemaConfig.merge[typeName] = /* @__PURE__ */ Object.create(null); } const mergeTypeConfig = mergeConfig[typeName]; mergeTypeConfig.selectionSet = print(selectionSet); } for (const typeName in computedFieldSelectionSets) { const selectionSets = computedFieldSelectionSets[typeName]; const mergeConfig = newSubschemaConfig.merge ?? /* @__PURE__ */ Object.create(null); newSubschemaConfig.merge = mergeConfig; if (mergeConfig[typeName] == null) { mergeConfig[typeName] = /* @__PURE__ */ Object.create(null); } const mergeTypeConfig = newSubschemaConfig.merge[typeName]; const mergeTypeConfigFields = mergeTypeConfig.fields ?? /* @__PURE__ */ Object.create(null); mergeTypeConfig.fields = mergeTypeConfigFields; for (const fieldName in selectionSets) { const selectionSet = selectionSets[fieldName]; const fieldConfig = mergeTypeConfigFields[fieldName] ?? /* @__PURE__ */ Object.create(null); mergeTypeConfigFields[fieldName] = fieldConfig; fieldConfig.selectionSet = print(selectionSet); fieldConfig.computed = true; } } for (const typeName in mergedTypesResolversInfo) { const mergedTypeResolverInfo = mergedTypesResolversInfo[typeName]; const mergeConfig = newSubschemaConfig.merge ?? /* @__PURE__ */ Object.create(null); newSubschemaConfig.merge = mergeConfig; if (newSubschemaConfig.merge[typeName] == null) { newSubschemaConfig.merge[typeName] = /* @__PURE__ */ Object.create(null); } const mergeTypeConfig = newSubschemaConfig.merge[typeName]; mergeTypeConfig.fieldName = mergedTypeResolverInfo.fieldName; if (mergedTypeResolverInfo.returnsList) { mergeTypeConfig.key = generateKeyFn(mergedTypeResolverInfo); mergeTypeConfig.argsFromKeys = generateArgsFromKeysFn( mergedTypeResolverInfo ); } else { mergeTypeConfig.args = generateArgsFn(mergedTypeResolverInfo); } } for (const typeName in canonicalTypesInfo) { const canonicalTypeInfo = canonicalTypesInfo[typeName]; const mergeConfig = newSubschemaConfig.merge ?? /* @__PURE__ */ Object.create(null); newSubschemaConfig.merge = mergeConfig; if (newSubschemaConfig.merge[typeName] == null) { newSubschemaConfig.merge[typeName] = /* @__PURE__ */ Object.create(null); } const mergeTypeConfig = newSubschemaConfig.merge[typeName]; if (canonicalTypeInfo.canonical) { mergeTypeConfig.canonical = true; } if (canonicalTypeInfo.fields) { const mergeTypeConfigFields = mergeTypeConfig.fields ?? /* @__PURE__ */ Object.create(null); mergeTypeConfig.fields = mergeTypeConfigFields; for (const fieldName in canonicalTypeInfo.fields) { if (mergeTypeConfigFields[fieldName] == null) { mergeTypeConfigFields[fieldName] = /* @__PURE__ */ Object.create(null); } mergeTypeConfigFields[fieldName].canonical = true; } } } for (const typeName in mergedTypesResolversInfoByEntryField) { const entryPoints = []; const existingMergeConfig = newSubschemaConfig.merge?.[typeName]; const newMergeConfig = newSubschemaConfig.merge ||= /* @__PURE__ */ Object.create(null); if (existingMergeConfig) { const { fields, canonical, ...baseEntryPoint } = existingMergeConfig; newMergeConfig[typeName] = { fields, canonical, entryPoints }; entryPoints.push(baseEntryPoint); } else { newMergeConfig[typeName] = { entryPoints }; } for (const fieldName in mergedTypesResolversInfoByEntryField[typeName]) { const mergedTypeResolverInfo = mergedTypesResolversInfoByEntryField[typeName][fieldName]; const newEntryPoint = { fieldName, selectionSet: print( selectionSetsByTypeAndEntryField[typeName][fieldName] ) }; if (mergedTypeResolverInfo.returnsList) { newEntryPoint.key = generateKeyFn(mergedTypeResolverInfo); newEntryPoint.argsFromKeys = generateArgsFromKeysFn( mergedTypeResolverInfo ); } else { newEntryPoint.args = generateArgsFn(mergedTypeResolverInfo); } entryPoints.push(newEntryPoint); } if (entryPoints.length === 1) { const [entryPoint] = entryPoints; const { fields, canonical } = newMergeConfig[typeName]; newMergeConfig[typeName] = { ...entryPoint, fields, canonical }; } } return newSubschemaConfig; }; } function forEachConcreteType(schema, type, typeNames, fn) { if (isInterfaceType(type)) { for (const typeName of getImplementingTypes(type.name, schema)) { if (typeNames == null || typeNames.includes(typeName)) { fn(typeName); } } } else if (isUnionType(type)) { for (const { name: typeName } of type.getTypes()) { if (typeNames == null || typeNames.includes(typeName)) { fn(typeName); } } } else if (isObjectType(type)) { fn(type.name); } } function generateKeyFn(mergedTypeResolverInfo) { return function keyFn(originalResult) { return getProperties(originalResult, mergedTypeResolverInfo.usedProperties); }; } function generateArgsFromKeysFn(mergedTypeResolverInfo) { const { expansions, args } = mergedTypeResolverInfo; return function generateArgsFromKeys(keys) { const newArgs = { ...args }; if (expansions) { for (const expansion of expansions) { const mappingInstructions = expansion.mappingInstructions; const expanded = []; for (const key of keys) { let newValue = {}; for (const { destinationPath, sourcePath } of mappingInstructions) { if (destinationPath.length) { addProperty( newValue, destinationPath, getProperty(key, sourcePath) ); } else { newValue = getProperty(key, sourcePath); } } expanded.push(newValue); } addProperty(newArgs, expansion.valuePath, expanded); } } return newArgs; }; } function generateArgsFn(mergedTypeResolverInfo) { const { mappingInstructions, args, usedProperties } = mergedTypeResolverInfo; return function generateArgs(originalResult) { const newArgs = { ...args }; const filteredResult = getProperties(originalResult, usedProperties); if (mappingInstructions) { for (const mappingInstruction of mappingInstructions) { const { destinationPath, sourcePath } = mappingInstruction; addProperty( newArgs, destinationPath, getProperty(filteredResult, sourcePath) ); } } return newArgs; }; } function buildKeyExpr(key) { let mergedObject = {}; for (const keyDef of key) { let [aliasOrKeyPath, keyPath] = keyDef.split(":"); let aliasPath; if (keyPath == null) { keyPath = aliasPath = aliasOrKeyPath; } else { aliasPath = aliasOrKeyPath; } const aliasParts = aliasPath.split("."); const lastAliasPart = aliasParts.pop(); if (lastAliasPart == null) { throw new Error(`Key "${key}" is invalid, no path provided.`); } let object = { [lastAliasPart]: `$key.${keyPath}` }; for (const aliasPart of aliasParts.reverse()) { object = { [aliasPart]: object }; } mergedObject = mergeDeep([mergedObject, object]); } return JSON.stringify(mergedObject).replace(/"/g, ""); } function mergeSelectionSets(...selectionSets) { const normalizedSelections = /* @__PURE__ */ Object.create(null); for (const selectionSet of selectionSets) { for (const selection of selectionSet.selections) { const normalizedSelection = print(selection); normalizedSelections[normalizedSelection] = selection; } } const newSelectionSet = { kind: Kind.SELECTION_SET, selections: Object.values(normalizedSelections) }; return newSelectionSet; } function forEachConcreteTypeName(returnType, schema, typeNames, fn) { if (isInterfaceType(returnType)) { for (const typeName of getImplementingTypes(returnType.name, schema)) { if (typeNames == null || typeNames.includes(typeName)) { fn(typeName); } } } else if (isUnionType(returnType)) { for (const type of returnType.getTypes()) { if (typeNames == null || typeNames.includes(type.name)) { fn(type.name); } } } else if (isObjectType(returnType) && (typeNames == null || typeNames.includes(returnType.name))) { fn(returnType.name); } } const dottedNameRegEx = /^[_A-Za-z][_0-9A-Za-z]*(.[_A-Za-z][_0-9A-Za-z]*)*$/; function stitchingDirectivesValidator(options = {}) { const { keyDirectiveName, computedDirectiveName, mergeDirectiveName, pathToDirectivesInExtensions } = { ...defaultStitchingDirectiveOptions, ...options }; return (schema) => { const queryTypeName = schema.getQueryType()?.name; mapSchema(schema, { [MapperKind.OBJECT_TYPE]: (type) => { const keyDirective = getDirective( schema, type, keyDirectiveName, pathToDirectivesInExtensions )?.[0]; if (keyDirective != null) { parseSelectionSet(keyDirective["selectionSet"]); } return void 0; }, [MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => { const computedDirective = getDirective( schema, fieldConfig, computedDirectiveName, pathToDirectivesInExtensions )?.[0]; if (computedDirective != null) { parseSelectionSet(computedDirective["selectionSet"]); } const mergeDirective = getDirective( schema, fieldConfig, mergeDirectiveName, pathToDirectivesInExtensions )?.[0]; if (mergeDirective != null) { if (typeName !== queryTypeName) { throw new Error( "@merge directive may be used only for root fields of the root Query type." ); } let returnType = getNullableType(fieldConfig.type); if (isListType(returnType)) { returnType = getNullableType(returnType.ofType); } if (!isNamedType(returnType)) { throw new Error( "@merge directive must be used on a field that returns an object or a list of objects." ); } const mergeArgsExpr = mergeDirective["argsExpr"]; if (mergeArgsExpr != null) { parseMergeArgsExpr(mergeArgsExpr); } const args = Object.keys(fieldConfig.args ?? {}); const keyArg = mergeDirective["keyArg"]; if (keyArg == null) { if (!mergeArgsExpr && args.length !== 1) { throw new Error( "Cannot use @merge directive without `keyArg` argument if resolver takes more than one argument." ); } } else if (!keyArg.match(dottedNameRegEx)) { throw new Error( "`keyArg` argument for @merge directive must be a set of valid GraphQL SDL names separated by periods." ); } const keyField = mergeDirective["keyField"]; if (keyField != null && !keyField.match(dottedNameRegEx)) { throw new Error( "`keyField` argument for @merge directive must be a set of valid GraphQL SDL names separated by periods." ); } const key = mergeDirective["key"]; if (key != null) { if (keyField != null) { throw new Error( "Cannot use @merge directive with both `keyField` and `key` arguments." ); } for (const keyDef of key) { let [aliasOrKeyPath, keyPath] = keyDef.split(":"); let aliasPath; if (keyPath == null) { keyPath = aliasPath = aliasOrKeyPath; } else { aliasPath = aliasOrKeyPath; } if (keyPath != null && !keyPath.match(dottedNameRegEx)) { throw new Error( "Each partial key within the `key` argument for @merge directive must be a set of valid GraphQL SDL names separated by periods." ); } if (aliasPath != null && !aliasOrKeyPath.match(dottedNameRegEx)) { throw new Error( "Each alias within the `key` argument for @merge directive must be a set of valid GraphQL SDL names separated by periods." ); } } } const additionalArgs = mergeDirective["additionalArgs"]; if (additionalArgs != null) { parseValue(`{ ${additionalArgs} }`, { noLocation: true }); } if (mergeArgsExpr != null && (keyArg != null || additionalArgs != null)) { throw new Error( "Cannot use @merge directive with both `argsExpr` argument and any additional argument." ); } if (!isInterfaceType(returnType) && !isUnionType(returnType) && !isObjectType(returnType)) { throw new Error( "@merge directive may be used only with resolver that return an object, interface, or union." ); } const typeNames = mergeDirective["types"]; if (typeNames != null) { if (!isAbstractType(returnType)) { throw new Error( "Types argument can only be used with a field that returns an abstract type." ); } const implementingTypes = isInterfaceType(returnType) ? getImplementingTypes(returnType.name, schema).map( (typeName2) => schema.getType(typeName2) ) : returnType.getTypes(); const implementingTypeNames = implementingTypes.map((type) => type?.name).filter(isSome); for (const typeName2 of typeNames) { if (!implementingTypeNames.includes(typeName2)) { throw new Error( `Types argument can only include only type names that implement the field return type's abstract type.` ); } } } } return void 0; } }); return schema; }; } function stitchingDirectives(options = {}) { const finalOptions = { ...defaultStitchingDirectiveOptions, ...options }; const { keyDirectiveName, computedDirectiveName, mergeDirectiveName, canonicalDirectiveName } = finalOptions; const keyDirectiveTypeDefs = ( /* GraphQL */ `directive @${keyDirectiveName}(selectionSet: String!) on OBJECT` ); const computedDirectiveTypeDefs = ( /* GraphQL */ `directive @${computedDirectiveName}(selectionSet: String!) on FIELD_DEFINITION` ); const mergeDirectiveTypeDefs = ( /* GraphQL */ `directive @${mergeDirectiveName}(argsExpr: String, keyArg: String, keyField: String, key: [String!], additionalArgs: String) on FIELD_DEFINITION` ); const canonicalDirectiveTypeDefs = ( /* GraphQL */ `directive @${canonicalDirectiveName} on OBJECT | INTERFACE | INPUT_OBJECT | UNION | ENUM | SCALAR | FIELD_DEFINITION | INPUT_FIELD_DEFINITION` ); const keyDirective = new GraphQLDirective({ name: keyDirectiveName, locations: ["OBJECT"], args: { selectionSet: { type: new GraphQLNonNull(GraphQLString) } } }); const computedDirective = new GraphQLDirective({ name: computedDirectiveName, locations: ["FIELD_DEFINITION"], args: { selectionSet: { type: new GraphQLNonNull(GraphQLString) } } }); const mergeDirective = new GraphQLDirective({ name: mergeDirectiveName, locations: ["FIELD_DEFINITION"], args: { argsExpr: { type: GraphQLString }, keyArg: { type: GraphQLString }, keyField: { type: GraphQLString }, key: { type: new GraphQLList(new GraphQLNonNull(GraphQLString)) }, additionalArgs: { type: GraphQLString } } }); const canonicalDirective = new GraphQLDirective({ name: canonicalDirectiveName, locations: [ "OBJECT", "INTERFACE", "INPUT_OBJECT", "UNION", "ENUM", "SCALAR", "FIELD_DEFINITION", "INPUT_FIELD_DEFINITION" ] }); const allStitchingDirectivesTypeDefs = [ keyDirectiveTypeDefs, computedDirectiveTypeDefs, mergeDirectiveTypeDefs, canonicalDirectiveTypeDefs ].join("\n"); return { keyDirectiveTypeDefs, computedDirectiveTypeDefs, mergeDirectiveTypeDefs, canonicalDirectiveTypeDefs, stitchingDirectivesTypeDefs: allStitchingDirectivesTypeDefs, // for backwards compatibility allStitchingDirectivesTypeDefs, keyDirective, computedDirective, mergeDirective, canonicalDirective, allStitchingDirectives: [ keyDirective, computedDirective, mergeDirective, canonicalDirective ], stitchingDirectivesValidator: stitchingDirectivesValidator(finalOptions), stitchingDirectivesTransformer: stitchingDirectivesTransformer(finalOptions) }; } const extensionKind = /Extension$/; const entityKinds = [ Kind.OBJECT_TYPE_DEFINITION, Kind.OBJECT_TYPE_EXTENSION, Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION ]; function isEntityKind(def) { return entityKinds.includes(def.kind); } function getQueryTypeDef(definitions) { const schemaDef = definitions.find( (def) => def.kind === Kind.SCHEMA_DEFINITION ); const typeName = schemaDef ? schemaDef.operationTypes.find(({ operation }) => operation === "query")?.type.name.value : "Query"; return definitions.find( (def) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === typeName ); } function federationToStitchingSDL(federationSDL, stitchingConfig = stitchingDirectives()) { const doc = parse(federationSDL); const entityTypes = []; const baseTypeNames = doc.definitions.reduce((memo, typeDef) => { if (!extensionKind.test(typeDef.kind) && "name" in typeDef && typeDef.name) { memo[typeDef.name.value] = true; } return memo; }, {}); doc.definitions.forEach((typeDef) => { if (extensionKind.test(typeDef.kind) && "name" in typeDef && typeDef.name && !baseTypeNames[typeDef.name.value]) { typeDef.kind = typeDef.kind.replace( extensionKind, "Definition" ); } if (!isEntityKind(typeDef)) return; const keyDirs = []; const otherDirs = []; typeDef.directives?.forEach((dir) => { if (dir.name.value === "key") { keyDirs.push(dir); } else { otherDirs.push(dir); } }); if (!keyDirs.length) return; const selectionSet = `{ ${keyDirs.map((dir) => dir.arguments[0].value.value).join(" ")} }`; const keyFields = parse(selectionSet).definitions[0].selectionSet.selections.map((sel) => sel.name.value); const keyDir = keyDirs[0]; keyDir.name.value = stitchingConfig.keyDirective.name; keyDir.arguments[0].name.value = "selectionSet"; keyDir.arguments[0].value.value = selectionSet; typeDef.directives = [keyDir, ...otherDirs]; typeDef.fields = typeDef.fields?.filter((fieldDef) => { return keyFields.includes(fieldDef.name.value) || !fieldDef.directives?.find((dir) => dir.name.value === "external"); }); typeDef.fields?.forEach((fieldDef) => { fieldDef.directives = fieldDef.directives.filter( (dir) => !/^(external|provides)$/.test(dir.name.value) ); fieldDef.directives.forEach((dir) => { if (dir.name.value === "requires") { dir.name.value = stitchingConfig.computedDirective.name; dir.arguments[0].name.value = "selectionSet"; dir.arguments[0].value.value = `{ ${dir.arguments[0].value.value} }`; } }); }); if (typeDef.kind === Kind.OBJECT_TYPE_DEFINITION || typeDef.kind === Kind.OBJECT_TYPE_EXTENSION) { entityTypes.push(typeDef.name.value); } }); if (entityTypes.length) { const queryDef = getQueryTypeDef(doc.definitions); const entitiesSchema = parse( /* GraphQL */ ` scalar _Any union _Entity = ${entityTypes.filter((v, i, a) => a.indexOf(v) === i).join(" | ")} type Query { _entities(representations: [_Any!]!): [_Entity]! @${stitchingConfig.mergeDirective.name} } ` ).definitions; doc.definitions.push(entitiesSchema[0]); doc.definitions.push(entitiesSchema[1]); if (queryDef) { queryDef.fields.push(entitiesSchema[2].fields[0]); } else { doc.definitions.push(entitiesSchema[2]); } } return [stitchingConfig.stitchingDirectivesTypeDefs, print(doc)].join("\n"); } export { federationToStitchingSDL, stitchingDirectives };