From d2ba24705919e89db8953124305485d79c3ff340 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 11 Jun 2020 10:48:40 -0400 Subject: [PATCH] improve type merging (#1636) = merge astNodes, extensions, etc, when available. = with conflicts, merge last type candidate, to allow overriding on gateway = add scalar merging --- packages/stitch/package.json | 5 +- packages/stitch/src/mergeCandidates.ts | 304 +++++++++++++++++++++++++ packages/stitch/src/typeCandidates.ts | 114 +--------- 3 files changed, 309 insertions(+), 114 deletions(-) create mode 100644 packages/stitch/src/mergeCandidates.ts diff --git a/packages/stitch/package.json b/packages/stitch/package.json index 03101bf0f3f..75e90077fdc 100644 --- a/packages/stitch/package.json +++ b/packages/stitch/package.json @@ -22,13 +22,14 @@ }, "dependencies": { "@graphql-tools/delegate": "6.0.9", + "@graphql-tools/merge": "6.0.9", "@graphql-tools/schema": "6.0.9", - "@graphql-tools/wrap": "6.0.9", "@graphql-tools/utils": "6.0.9", + "@graphql-tools/wrap": "6.0.9", "tslib": "~2.0.0" }, "publishConfig": { "access": "public", "directory": "dist" } -} \ No newline at end of file +} diff --git a/packages/stitch/src/mergeCandidates.ts b/packages/stitch/src/mergeCandidates.ts new file mode 100644 index 00000000000..5d20f2d0afe --- /dev/null +++ b/packages/stitch/src/mergeCandidates.ts @@ -0,0 +1,304 @@ +import { + GraphQLNamedType, + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLUnionType, + GraphQLEnumType, + isScalarType, + isObjectType, + isInterfaceType, + isUnionType, + isEnumType, + isInputObjectType, + GraphQLFieldConfigMap, + GraphQLInputObjectType, + GraphQLInputFieldConfigMap, + ObjectTypeDefinitionNode, + InputObjectTypeDefinitionNode, + InterfaceTypeDefinitionNode, + UnionTypeDefinitionNode, + EnumTypeDefinitionNode, + GraphQLEnumValueConfigMap, + ScalarTypeDefinitionNode, + GraphQLScalarType, + GraphQLScalarSerializer, + GraphQLScalarValueParser, + GraphQLScalarLiteralParser, +} from 'graphql'; + +import { mergeType, mergeInputType, mergeInterface, mergeUnion, mergeEnum } from '@graphql-tools/merge'; + +import { MergeTypeCandidate } from './types'; + +export function mergeCandidates(typeName: string, candidates: Array): GraphQLNamedType { + 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); + } else if (isInputObjectType(initialCandidateType)) { + return mergeInputObjectTypeCandidates(typeName, candidates); + } else if (isInterfaceType(initialCandidateType)) { + return mergeInterfaceTypeCandidates(typeName, candidates); + } else if (isUnionType(initialCandidateType)) { + return mergeUnionTypeCandidates(typeName, candidates); + } else if (isEnumType(initialCandidateType)) { + return mergeEnumTypeCandidates(typeName, candidates); + } else if (isScalarType(initialCandidateType)) { + return mergeScalarTypeCandidates(typeName, candidates); + } else { + // not reachable. + throw new Error(`Type ${typeName} has unknown GraphQL type.`); + } +} + +function mergeObjectTypeCandidates( + typeName: string, + candidates: Array +): GraphQLObjectType { + const descriptions = pluck('description', candidates); + const description = descriptions[descriptions.length - 1]; + + const configs = candidates.map(candidate => (candidate.type as GraphQLObjectType).toConfig()); + const fields = configs.reduce>( + (acc, config) => ({ + ...acc, + ...config.fields, + }), + {} + ); + + const interfaces = configs + .map(config => config.interfaces) + .reduce((acc, interfaces) => { + return interfaces != null ? acc.concat(interfaces) : acc; + }, []); + + const astNodes = pluck('astNode', candidates); + const astNode = astNodes + .slice(1) + .reduce( + (acc, astNode) => mergeType(astNode, acc as ObjectTypeDefinitionNode) as ObjectTypeDefinitionNode, + astNodes[0] + ); + + const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + + const extensions = Object.assign({}, ...pluck>('extensions', candidates)); + + const config = { + name: typeName, + description, + fields, + interfaces, + astNode, + extensionASTNodes, + extensions, + }; + + return new GraphQLObjectType(config); +} + +function mergeInputObjectTypeCandidates( + typeName: string, + candidates: Array +): GraphQLInputObjectType { + const descriptions = pluck('description', candidates); + const description = descriptions[descriptions.length - 1]; + + const configs = candidates.map(candidate => (candidate.type as GraphQLInputObjectType).toConfig()); + const fields = configs.reduce( + (acc, config) => ({ + ...acc, + ...config.fields, + }), + {} + ); + + const astNodes = pluck('astNode', candidates); + const astNode = astNodes + .slice(1) + .reduce( + (acc, astNode) => mergeInputType(astNode, acc as InputObjectTypeDefinitionNode) as InputObjectTypeDefinitionNode, + astNodes[0] + ); + + const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + + const extensions = Object.assign({}, ...pluck>('extensions', candidates)); + + const config = { + name: typeName, + description, + fields, + astNode, + extensionASTNodes, + extensions, + }; + + return new GraphQLInputObjectType(config); +} + +function pluck(typeProperty: string, candidates: Array): Array { + return candidates.map(candidate => candidate.type[typeProperty]).filter(value => value != null) as Array; +} + +function mergeInterfaceTypeCandidates(typeName: string, candidates: Array): GraphQLInterfaceType { + const descriptions = pluck('description', candidates); + const description = descriptions[descriptions.length - 1]; + + const configs = candidates.map(candidate => (candidate.type as GraphQLInterfaceType).toConfig()); + const fields = configs.reduce>( + (acc, config) => ({ + ...acc, + ...config.fields, + }), + {} + ); + + const interfaces = + 'interfaces' in candidates[0].type.toConfig() + ? configs + .map(config => ((config as unknown) as { interfaces: Array }).interfaces) + .reduce((acc, interfaces) => { + return interfaces != null ? acc.concat(interfaces) : acc; + }, []) + : undefined; + + const astNodes = pluck('astNode', candidates); + const astNode = astNodes + .slice(1) + .reduce( + (acc, astNode) => mergeInterface(astNode, acc as InterfaceTypeDefinitionNode, {}) as InterfaceTypeDefinitionNode, + astNodes[0] + ); + + const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + + const extensions = Object.assign({}, ...pluck>('extensions', candidates)); + + const config = { + name: typeName, + description, + fields, + interfaces, + astNode, + extensionASTNodes, + extensions, + }; + + return new GraphQLInterfaceType(config); +} + +function mergeUnionTypeCandidates(typeName: string, candidates: Array): GraphQLUnionType { + const descriptions = pluck('description', candidates); + const description = descriptions[descriptions.length - 1]; + + const configs = candidates.map(candidate => (candidate.type as GraphQLUnionType).toConfig()); + const types = configs.reduce((acc, config) => acc.concat(config.types), []); + + const astNodes = pluck('astNode', candidates); + const astNode = astNodes + .slice(1) + .reduce( + (acc, astNode) => mergeUnion(astNode, acc as UnionTypeDefinitionNode) as UnionTypeDefinitionNode, + astNodes[0] + ); + + const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + + const extensions = Object.assign({}, ...pluck>('extensions', candidates)); + + const config = { + name: typeName, + description, + types, + astNode, + extensionASTNodes, + extensions, + }; + + return new GraphQLUnionType(config); +} + +function mergeEnumTypeCandidates(typeName: string, candidates: Array): GraphQLEnumType { + const descriptions = pluck('description', candidates); + const description = descriptions[descriptions.length - 1]; + + const configs = candidates.map(candidate => (candidate.type as GraphQLEnumType).toConfig()); + const values = configs.reduce( + (acc, config) => ({ + ...acc, + ...config.values, + }), + {} + ); + + const astNodes = pluck('astNode', candidates); + const astNode = astNodes + .slice(1) + .reduce((acc, astNode) => mergeEnum(astNode, acc as EnumTypeDefinitionNode) as EnumTypeDefinitionNode, astNodes[0]); + + const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + + const extensions = Object.assign({}, ...pluck>('extensions', candidates)); + + const config = { + name: typeName, + description, + values, + astNode, + extensionASTNodes, + extensions, + }; + + return new GraphQLEnumType(config); +} + +function mergeScalarTypeCandidates(typeName: string, candidates: Array): GraphQLScalarType { + const descriptions = pluck('description', candidates); + const description = descriptions[descriptions.length - 1]; + + const serializeFns = pluck>('serialize', candidates); + const serialize = serializeFns[serializeFns.length - 1]; + + const parseValueFns = pluck>('parseValue', candidates); + const parseValue = parseValueFns[descriptions.length - 1]; + + const parseLiteralFns = pluck>('parseLiteral', candidates); + const parseLiteral = parseLiteralFns[descriptions.length - 1]; + + const astNodes = pluck('astNode', candidates); + const astNode = astNodes + .slice(1) + .reduce((acc, astNode) => mergeScalarTypeDefinitionNodes(acc as ScalarTypeDefinitionNode, astNode), astNodes[0]); + + const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + + const extensions = Object.assign({}, ...pluck>('extensions', candidates)); + + const config = { + name: typeName, + description, + serialize, + parseValue, + parseLiteral, + astNode, + extensionASTNodes, + extensions, + }; + + return new GraphQLScalarType(config); +} + +function mergeScalarTypeDefinitionNodes( + targetNode: ScalarTypeDefinitionNode, + sourceNode: ScalarTypeDefinitionNode +): ScalarTypeDefinitionNode { + return { + ...targetNode, + description: sourceNode.description ?? targetNode.description, + directives: (targetNode.directives ?? []).concat(sourceNode.directives ?? []), + }; +} diff --git a/packages/stitch/src/typeCandidates.ts b/packages/stitch/src/typeCandidates.ts index d76a4c33b4c..1e107577e3a 100644 --- a/packages/stitch/src/typeCandidates.ts +++ b/packages/stitch/src/typeCandidates.ts @@ -1,27 +1,15 @@ import { DocumentNode, GraphQLNamedType, - GraphQLObjectType, GraphQLSchema, getNamedType, isNamedType, GraphQLDirective, - GraphQLInterfaceType, - GraphQLUnionType, - GraphQLEnumType, ASTNode, isSchema, isScalarType, - isObjectType, - isInterfaceType, - isUnionType, - isEnumType, - isInputObjectType, SchemaDefinitionNode, SchemaExtensionNode, - GraphQLFieldConfigMap, - GraphQLInputObjectType, - GraphQLInputFieldConfigMap, } from 'graphql'; import { wrapSchema } from '@graphql-tools/wrap'; @@ -38,6 +26,7 @@ import { import typeFromAST from './typeFromAST'; import { MergeTypeCandidate, MergeTypeFilter, OnTypeConflict, StitchingInfo } from './types'; import { TypeMap } from '@graphql-tools/utils'; +import { mergeCandidates } from './mergeCandidates'; type CandidateSelector = (candidates: Array) => MergeTypeCandidate; @@ -219,7 +208,7 @@ export function buildTypeMap({ (Array.isArray(mergeTypes) && mergeTypes.includes(typeName)) || (stitchingInfo != null && typeName in stitchingInfo.mergedTypes) ) { - typeMap[typeName] = merge(typeName, typeCandidates[typeName]); + typeMap[typeName] = mergeCandidates(typeName, typeCandidates[typeName]); } else { const candidateSelector = onTypeConflict != null @@ -254,102 +243,3 @@ function onTypeConflictToCandidateSelector(onTypeConflict: OnTypeConflict): Cand }; }); } - -function merge(typeName: string, candidates: Array): GraphQLNamedType { - 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)) { - const config = { - name: typeName, - fields: candidates.reduce>( - (acc, candidate) => ({ - ...acc, - ...(candidate.type as GraphQLObjectType).toConfig().fields, - }), - {} - ), - interfaces: candidates.reduce((acc, candidate) => { - const interfaces = (candidate.type as GraphQLObjectType).toConfig().interfaces; - return interfaces != null ? acc.concat(interfaces) : acc; - }, []), - description: initialCandidateType.description, - extensions: initialCandidateType.extensions, - astNode: initialCandidateType.astNode, - extensionASTNodes: initialCandidateType.extensionASTNodes, - }; - return new GraphQLObjectType(config); - } else if (isInputObjectType(initialCandidateType)) { - const config = { - name: typeName, - fields: candidates.reduce( - (acc, candidate) => ({ - ...acc, - ...(candidate.type as GraphQLInputObjectType).toConfig().fields, - }), - {} - ), - description: initialCandidateType.description, - extensions: initialCandidateType.extensions, - astNode: initialCandidateType.astNode, - extensionASTNodes: initialCandidateType.extensionASTNodes, - }; - return new GraphQLInputObjectType(config); - } else if (isInterfaceType(initialCandidateType)) { - const config = { - name: typeName, - fields: candidates.reduce>( - (acc, candidate) => ({ - ...acc, - ...(candidate.type as GraphQLInterfaceType).toConfig().fields, - }), - {} - ), - interfaces: candidates.reduce((acc, candidate) => { - const candidateConfig = candidate.type.toConfig(); - if ('interfaces' in candidateConfig) { - return acc.concat(candidateConfig.interfaces); - } - return acc; - }, []), - description: initialCandidateType.description, - extensions: initialCandidateType.extensions, - astNode: initialCandidateType.astNode, - extensionASTNodes: initialCandidateType.extensionASTNodes, - }; - return new GraphQLInterfaceType(config); - } else if (isUnionType(initialCandidateType)) { - return new GraphQLUnionType({ - name: typeName, - types: candidates.reduce( - (acc, candidate) => acc.concat((candidate.type as GraphQLUnionType).toConfig().types), - [] - ), - description: initialCandidateType.description, - extensions: initialCandidateType.extensions, - astNode: initialCandidateType.astNode, - extensionASTNodes: initialCandidateType.extensionASTNodes, - }); - } else if (isEnumType(initialCandidateType)) { - return new GraphQLEnumType({ - name: typeName, - values: candidates.reduce( - (acc, candidate) => ({ - ...acc, - ...(candidate.type as GraphQLEnumType).toConfig().values, - }), - {} - ), - description: initialCandidateType.description, - extensions: initialCandidateType.extensions, - astNode: initialCandidateType.astNode, - extensionASTNodes: initialCandidateType.extensionASTNodes, - }); - } else if (isScalarType(initialCandidateType)) { - throw new Error(`Cannot merge type ${typeName}. Merging not supported for GraphQLScalarType.`); - } else { - // not reachable. - throw new Error(`Type ${typeName} has unknown GraphQL type.`); - } -}