UNPKG

@graphql-tools/stitch

Version:

A set of utils for faster development of GraphQL tools

459 lines (458 loc) • 20.2 kB
import { getNullableType, GraphQLEnumType, GraphQLInputObjectType, GraphQLInterfaceType, GraphQLObjectType, GraphQLScalarType, GraphQLUnionType, isEnumType, isInputObjectType, isInterfaceType, isNullableType, isObjectType, isScalarType, isUnionType, } from 'graphql'; import { isSubschemaConfig } from '@graphql-tools/delegate'; import { mergeEnum, mergeInputType, mergeInterface, mergeScalar, mergeType, mergeUnion, } from '@graphql-tools/merge'; import { validateFieldConsistency, validateInputFieldConsistency, validateInputObjectConsistency, } from './mergeValidations.js'; export function mergeCandidates(typeName, candidates, typeMergingOptions) { const initialCandidateType = candidates[0].type; if (candidates.some(candidate => candidate.type.constructor !== initialCandidateType.constructor)) { throw new Error(`Cannot merge different type categories into common type ${typeName}.`); } if (isObjectType(initialCandidateType)) { return mergeObjectTypeCandidates(typeName, candidates, typeMergingOptions); } else if (isInputObjectType(initialCandidateType)) { return mergeInputObjectTypeCandidates(typeName, candidates, typeMergingOptions); } else if (isInterfaceType(initialCandidateType)) { return mergeInterfaceTypeCandidates(typeName, candidates, typeMergingOptions); } else if (isUnionType(initialCandidateType)) { return mergeUnionTypeCandidates(typeName, candidates, typeMergingOptions); } else if (isEnumType(initialCandidateType)) { return mergeEnumTypeCandidates(typeName, candidates, typeMergingOptions); } else if (isScalarType(initialCandidateType)) { return mergeScalarTypeCandidates(typeName, candidates, typeMergingOptions); } else { // not reachable. throw new Error(`Type ${typeName} has unknown GraphQL type.`); } } function mergeObjectTypeCandidates(typeName, candidates, typeMergingOptions) { candidates = orderedTypeCandidates(candidates, typeMergingOptions); const description = mergeTypeDescriptions(candidates, typeMergingOptions); const fields = fieldConfigMapFromTypeCandidates(candidates, typeMergingOptions); const typeConfigs = candidates.map(candidate => candidate.type.toConfig()); const interfaceMap = typeConfigs .map(typeConfig => typeConfig.interfaces) .reduce((acc, interfaces) => { if (interfaces != null) { for (const iface of interfaces) { acc[iface.name] = iface; } } return acc; }, Object.create(null)); const interfaces = Object.values(interfaceMap); const astNodes = pluck('astNode', candidates); const fieldAstNodes = canonicalFieldNamesForType(candidates) .map(fieldName => fields[fieldName]?.astNode) .filter(n => n != null); if (astNodes.length > 1 && fieldAstNodes.length) { astNodes.push({ ...astNodes[astNodes.length - 1], fields: JSON.parse(JSON.stringify(fieldAstNodes)), }); } const astNode = astNodes.slice(1).reduce((acc, astNode) => mergeType(astNode, acc, { ignoreFieldConflicts: true, }), astNodes[0]); const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck('extensions', candidates)); const typeConfig = { name: typeName, description, fields, interfaces, astNode, extensionASTNodes, extensions, }; return new GraphQLObjectType(typeConfig); } function mergeInputObjectTypeCandidates(typeName, candidates, typeMergingOptions) { candidates = orderedTypeCandidates(candidates, typeMergingOptions); const description = mergeTypeDescriptions(candidates, typeMergingOptions); const fields = inputFieldConfigMapFromTypeCandidates(candidates, typeMergingOptions); const astNodes = pluck('astNode', candidates); const fieldAstNodes = canonicalFieldNamesForType(candidates) .map(fieldName => fields[fieldName]?.astNode) .filter(n => n != null); if (astNodes.length > 1 && fieldAstNodes.length) { astNodes.push({ ...astNodes[astNodes.length - 1], fields: JSON.parse(JSON.stringify(fieldAstNodes)), }); } const astNode = astNodes.slice(1).reduce((acc, astNode) => mergeInputType(astNode, acc, { ignoreFieldConflicts: true, }), astNodes[0]); const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck('extensions', candidates)); const typeConfig = { name: typeName, description, fields, astNode, extensionASTNodes, extensions, }; return new GraphQLInputObjectType(typeConfig); } function pluck(typeProperty, candidates) { return candidates .map(candidate => candidate.type[typeProperty]) .filter(value => value != null); } function mergeInterfaceTypeCandidates(typeName, candidates, typeMergingOptions) { candidates = orderedTypeCandidates(candidates, typeMergingOptions); const description = mergeTypeDescriptions(candidates, typeMergingOptions); const fields = fieldConfigMapFromTypeCandidates(candidates, typeMergingOptions); const typeConfigs = candidates.map(candidate => candidate.type.toConfig()); const interfaceMap = typeConfigs .map(typeConfig => ('interfaces' in typeConfig ? typeConfig.interfaces : [])) .reduce((acc, interfaces) => { if (interfaces != null) { for (const iface of interfaces) { acc[iface.name] = iface; } } return acc; }, Object.create(null)); const interfaces = Object.values(interfaceMap); const astNodes = pluck('astNode', candidates); const fieldAstNodes = canonicalFieldNamesForType(candidates) .map(fieldName => fields[fieldName]?.astNode) .filter(n => n != null); if (astNodes.length > 1 && fieldAstNodes.length) { astNodes.push({ ...astNodes[astNodes.length - 1], fields: JSON.parse(JSON.stringify(fieldAstNodes)), }); } const astNode = astNodes.slice(1).reduce((acc, astNode) => mergeInterface(astNode, acc, { ignoreFieldConflicts: true, }), astNodes[0]); const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck('extensions', candidates)); const typeConfig = { name: typeName, description, fields, interfaces, astNode, extensionASTNodes, extensions, }; return new GraphQLInterfaceType(typeConfig); } function mergeUnionTypeCandidates(typeName, candidates, typeMergingOptions) { candidates = orderedTypeCandidates(candidates, typeMergingOptions); const description = mergeTypeDescriptions(candidates, typeMergingOptions); const typeConfigs = candidates.map(candidate => { if (!isUnionType(candidate.type)) { throw new Error(`Expected ${candidate.type} to be a union type!`); } return candidate.type.toConfig(); }); const typeMap = typeConfigs.reduce((acc, typeConfig) => { for (const type of typeConfig.types) { acc[type.name] = type; } return acc; }, Object.create(null)); const types = Object.values(typeMap); const astNodes = pluck('astNode', candidates); const astNode = astNodes .slice(1) .reduce((acc, astNode) => mergeUnion(astNode, acc), astNodes[0]); const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck('extensions', candidates)); const typeConfig = { name: typeName, description, types, astNode, extensionASTNodes, extensions, }; return new GraphQLUnionType(typeConfig); } function mergeEnumTypeCandidates(typeName, candidates, typeMergingOptions) { candidates = orderedTypeCandidates(candidates, typeMergingOptions); const description = mergeTypeDescriptions(candidates, typeMergingOptions); const values = enumValueConfigMapFromTypeCandidates(candidates, typeMergingOptions); const astNodes = pluck('astNode', candidates); const astNode = astNodes.slice(1).reduce((acc, astNode) => mergeEnum(astNode, acc, { consistentEnumMerge: true, }), astNodes[0]); const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck('extensions', candidates)); const typeConfig = { name: typeName, description, values, astNode, extensionASTNodes, extensions, }; return new GraphQLEnumType(typeConfig); } function enumValueConfigMapFromTypeCandidates(candidates, typeMergingOptions) { const enumValueConfigCandidatesMap = Object.create(null); for (const candidate of candidates) { const valueMap = candidate.type.toConfig().values; for (const enumValue in valueMap) { const enumValueConfigCandidate = { enumValueConfig: valueMap[enumValue], enumValue, type: candidate.type, subschema: candidate.subschema, transformedSubschema: candidate.transformedSubschema, }; if (enumValue in enumValueConfigCandidatesMap) { enumValueConfigCandidatesMap[enumValue].push(enumValueConfigCandidate); } else { enumValueConfigCandidatesMap[enumValue] = [enumValueConfigCandidate]; } } } const enumValueConfigMap = Object.create(null); for (const enumValue in enumValueConfigCandidatesMap) { const enumValueConfigMerger = typeMergingOptions?.enumValueConfigMerger ?? defaultEnumValueConfigMerger; enumValueConfigMap[enumValue] = enumValueConfigMerger(enumValueConfigCandidatesMap[enumValue]); } return enumValueConfigMap; } function defaultEnumValueConfigMerger(candidates) { const preferred = candidates.find(({ type, transformedSubschema }) => isSubschemaConfig(transformedSubschema) && transformedSubschema.merge?.[type.name]?.canonical); return (preferred || candidates[candidates.length - 1]).enumValueConfig; } function mergeScalarTypeCandidates(typeName, candidates, typeMergingOptions) { candidates = orderedTypeCandidates(candidates, typeMergingOptions); const description = mergeTypeDescriptions(candidates, typeMergingOptions); const serializeFns = pluck('serialize', candidates); const serialize = serializeFns[serializeFns.length - 1]; const parseValueFns = pluck('parseValue', candidates); const parseValue = parseValueFns[parseValueFns.length - 1]; const parseLiteralFns = pluck('parseLiteral', candidates); const parseLiteral = parseLiteralFns[parseLiteralFns.length - 1]; const astNodes = pluck('astNode', candidates); const astNode = astNodes .slice(1) .reduce((acc, astNode) => mergeScalar(astNode, acc), astNodes[0]); const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck('extensions', candidates)); let specifiedByURL; for (const candidate of candidates) { if ('specifiedByURL' in candidate.type && candidate.type.specifiedByURL) { specifiedByURL = candidate.type.specifiedByURL; break; } } const typeConfig = { name: typeName, description, serialize, parseValue, parseLiteral, astNode, extensionASTNodes, extensions, specifiedByURL, }; return new GraphQLScalarType(typeConfig); } function orderedTypeCandidates(candidates, typeMergingOptions) { const typeCandidateMerger = typeMergingOptions?.typeCandidateMerger ?? defaultTypeCandidateMerger; const candidate = typeCandidateMerger(candidates); return candidates.filter(c => c !== candidate).concat([candidate]); } function defaultTypeCandidateMerger(candidates) { const canonical = candidates.filter(({ type, transformedSubschema }) => isSubschemaConfig(transformedSubschema) ? transformedSubschema.merge?.[type.name]?.canonical : false); if (canonical.length > 1) { throw new Error(`Multiple canonical definitions for "${canonical[0].type.name}"`); } else if (canonical.length) { return canonical[0]; } return candidates[candidates.length - 1]; } function mergeTypeDescriptions(candidates, typeMergingOptions) { const typeDescriptionsMerger = typeMergingOptions?.typeDescriptionsMerger ?? defaultTypeDescriptionMerger; return typeDescriptionsMerger(candidates); } function defaultTypeDescriptionMerger(candidates) { return candidates[candidates.length - 1].type.description; } function fieldConfigMapFromTypeCandidates(candidates, typeMergingOptions) { const fieldConfigCandidatesMap = Object.create(null); for (const candidate of candidates) { const typeConfig = candidate.type.toConfig(); const fieldConfigMap = typeConfig.fields; for (const fieldName in fieldConfigMap) { const fieldConfig = fieldConfigMap[fieldName]; const fieldConfigCandidate = { fieldConfig, fieldName, type: candidate.type, subschema: candidate.subschema, transformedSubschema: candidate.transformedSubschema, }; if (fieldName in fieldConfigCandidatesMap) { fieldConfigCandidatesMap[fieldName].push(fieldConfigCandidate); } else { fieldConfigCandidatesMap[fieldName] = [fieldConfigCandidate]; } } } const fieldConfigMap = Object.create(null); for (const fieldName in fieldConfigCandidatesMap) { fieldConfigMap[fieldName] = mergeFieldConfigs(fieldConfigCandidatesMap[fieldName], typeMergingOptions); } return fieldConfigMap; } function mergeFieldConfigs(candidates, typeMergingOptions) { const fieldConfigMerger = typeMergingOptions?.fieldConfigMerger ?? getDefaultFieldConfigMerger(typeMergingOptions?.useNonNullableFieldOnConflict); const finalFieldConfig = fieldConfigMerger(candidates); validateFieldConsistency(finalFieldConfig, candidates, typeMergingOptions); return finalFieldConfig; } export function getDefaultFieldConfigMerger(useNonNullableFieldOnConflict = false) { return function defaultFieldConfigMerger(candidates) { const nullables = []; const nonNullables = []; const canonicalByField = []; const canonicalByType = []; for (const { type, fieldName, fieldConfig, transformedSubschema } of candidates) { if (!isSubschemaConfig(transformedSubschema)) continue; if (transformedSubschema.merge?.[type.name]?.fields?.[fieldName]?.canonical) { canonicalByField.push(fieldConfig); } else if (transformedSubschema.merge?.[type.name]?.canonical) { canonicalByType.push(fieldConfig); } if (isNullableType(fieldConfig.type)) { nullables.push(fieldConfig); } else { nonNullables.push(fieldConfig); } } const nonNullableFinalField = nonNullables.length > 0 && nullables.length > 0 && useNonNullableFieldOnConflict; if (canonicalByField.length > 1) { throw new Error(`Multiple canonical definitions for "${candidates[0].type.name}.${candidates[0].fieldName}"`); } else if (canonicalByField.length) { const finalField = canonicalByField[0]; if (nonNullableFinalField) { return { ...finalField, type: getNullableType(finalField.type), }; } return finalField; } else if (canonicalByType.length) { const finalField = canonicalByType[0]; if (nonNullableFinalField) { return { ...finalField, type: getNullableType(finalField.type), }; } return finalField; } const finalField = candidates[candidates.length - 1].fieldConfig; if (nonNullableFinalField) { return { ...finalField, type: getNullableType(finalField.type), }; } return finalField; }; } function inputFieldConfigMapFromTypeCandidates(candidates, typeMergingOptions) { const inputFieldConfigCandidatesMap = Object.create(null); const fieldInclusionMap = Object.create(null); for (const candidate of candidates) { const typeConfig = candidate.type.toConfig(); const inputFieldConfigMap = typeConfig.fields; for (const fieldName in inputFieldConfigMap) { const inputFieldConfig = inputFieldConfigMap[fieldName]; fieldInclusionMap[fieldName] = fieldInclusionMap[fieldName] || 0; fieldInclusionMap[fieldName] += 1; const inputFieldConfigCandidate = { inputFieldConfig, fieldName, type: candidate.type, subschema: candidate.subschema, transformedSubschema: candidate.transformedSubschema, }; if (fieldName in inputFieldConfigCandidatesMap) { inputFieldConfigCandidatesMap[fieldName].push(inputFieldConfigCandidate); } else { inputFieldConfigCandidatesMap[fieldName] = [inputFieldConfigCandidate]; } } } validateInputObjectConsistency(fieldInclusionMap, candidates, typeMergingOptions); const inputFieldConfigMap = Object.create(null); for (const fieldName in inputFieldConfigCandidatesMap) { const inputFieldConfigMerger = typeMergingOptions?.inputFieldConfigMerger ?? defaultInputFieldConfigMerger; inputFieldConfigMap[fieldName] = inputFieldConfigMerger(inputFieldConfigCandidatesMap[fieldName]); validateInputFieldConsistency(inputFieldConfigMap[fieldName], inputFieldConfigCandidatesMap[fieldName], typeMergingOptions); } return inputFieldConfigMap; } function defaultInputFieldConfigMerger(candidates) { const canonicalByField = []; const canonicalByType = []; for (const { type, fieldName, inputFieldConfig, transformedSubschema } of candidates) { if (!isSubschemaConfig(transformedSubschema)) continue; if (transformedSubschema.merge?.[type.name]?.fields?.[fieldName]?.canonical) { canonicalByField.push(inputFieldConfig); } else if (transformedSubschema.merge?.[type.name]?.canonical) { canonicalByType.push(inputFieldConfig); } } if (canonicalByField.length > 1) { throw new Error(`Multiple canonical definitions for "${candidates[0].type.name}.${candidates[0].fieldName}"`); } else if (canonicalByField.length) { return canonicalByField[0]; } else if (canonicalByType.length) { return canonicalByType[0]; } return candidates[candidates.length - 1].inputFieldConfig; } function canonicalFieldNamesForType(candidates) { const canonicalFieldNames = Object.create(null); for (const { type, transformedSubschema } of candidates) { if (!isSubschemaConfig(transformedSubschema)) continue; const mergeConfig = transformedSubschema.merge?.[type.name]; if (mergeConfig != null && mergeConfig.fields != null && !mergeConfig.canonical) { for (const fieldName in mergeConfig.fields) { const mergedFieldConfig = mergeConfig.fields[fieldName]; if (mergedFieldConfig.canonical) { canonicalFieldNames[fieldName] = true; } } } } return Object.keys(canonicalFieldNames); }