From afdd01ee78b21aefb8f265c8077036fbd787a790 Mon Sep 17 00:00:00 2001 From: yaacovCR Date: Sun, 16 Jun 2019 07:41:27 -0400 Subject: [PATCH] feat(stitching): restore onTypeConflict option to mergeSchemas --- docs/source/schema-stitching.md | 6 +- src/stitching/mergeSchemas.ts | 36 ++++++++++- src/test/testAlternateMergeSchemas.ts | 87 +++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 6 deletions(-) diff --git a/docs/source/schema-stitching.md b/docs/source/schema-stitching.md index 3220f4a38c6..936dd532501 100644 --- a/docs/source/schema-stitching.md +++ b/docs/source/schema-stitching.md @@ -391,12 +391,12 @@ type OnTypeConflict = ( The `onTypeConflict` option to `mergeSchemas` allows customization of type resolving logic. -The default behavior of `mergeSchemas` is to take the first encountered type of all the types with the same name. If there are conflicts, `onTypeConflict` enables explicit selection of the winning type. +The default behavior of `mergeSchemas` is to take the *last* encountered type of all the types with the same name, with a warning that type conflicts have been encountered. If specified, `onTypeConflict` enables explicit selection of the winning type. -For example, here's how we could select the last type among multiple types with the same name: +For example, here's how we could select the *first* type among multiple types with the same name: ```js -const onTypeConflict = (left, right) => right; +const onTypeConflict = (left, right) => left; ``` And here's how we might select the type whose schema has the latest `version`: diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index d704262f11c..d936887e3f9 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -56,6 +56,10 @@ export type OnTypeConflict = ( }, ) => GraphQLNamedType; +type CandidateSelector = ( + candidates: Array, +) => MergeTypeCandidate; + export default function mergeSchemas({ schemas, onTypeConflict, @@ -76,6 +80,7 @@ export default function mergeSchemas({ }): GraphQLSchema { return mergeSchemasImplementation({ schemas, + onTypeConflict, resolvers, schemaDirectives, inheritResolversFromInterfaces, @@ -85,6 +90,7 @@ export default function mergeSchemas({ function mergeSchemasImplementation({ schemas, + onTypeConflict, resolvers, schemaDirectives, inheritResolversFromInterfaces, @@ -93,6 +99,7 @@ function mergeSchemasImplementation({ schemas: Array< string | GraphQLSchema | DocumentNode | Array >; + onTypeConflict?: OnTypeConflict; resolvers?: IResolversParameter; schemaDirectives?: { [name: string]: typeof SchemaDirectiveVisitor }; inheritResolversFromInterfaces?: boolean; @@ -225,6 +232,7 @@ function mergeSchemasImplementation({ const resultType: VisitTypeResult = defaultVisitType( typeName, typeCandidates[typeName], + onTypeConflict ? onTypeConflictToCandidateSelector(onTypeConflict) : undefined ); if (resultType === null) { types[typeName] = null; @@ -441,12 +449,34 @@ function addTypeCandidate( typeCandidates[name].push(typeCandidate); } +function onTypeConflictToCandidateSelector(onTypeConflict: OnTypeConflict): CandidateSelector { + return cands => + cands.reduce((prev, next) => { + const type = onTypeConflict(prev.type, next.type, { + left: { + schema: prev.schema, + }, + right: { + schema: next.schema, + }, + }); + if (prev.type === type) { + return prev; + } else if (next.type === type) { + return next; + } else { + return { + schemaName: 'unknown', + type + }; + } + }); +} + function defaultVisitType( name: string, candidates: Array, - candidateSelector?: ( - candidates: Array, - ) => MergeTypeCandidate, + candidateSelector?: CandidateSelector ) { if (!candidateSelector) { candidateSelector = cands => cands[cands.length - 1]; diff --git a/src/test/testAlternateMergeSchemas.ts b/src/test/testAlternateMergeSchemas.ts index 81fb9aaf699..7db04074e04 100644 --- a/src/test/testAlternateMergeSchemas.ts +++ b/src/test/testAlternateMergeSchemas.ts @@ -537,3 +537,90 @@ describe('mergeSchemas', () => { expect(response.errors).to.be.undefined; }); }); + +describe('onTypeConflict', () => { + let schema1: GraphQLSchema; + let schema2: GraphQLSchema; + + beforeEach(() => { + const typeDefs1 = ` + type Query { + test1: Test + } + + type Test { + fieldA: String + fieldB: String + } + `; + + const typeDefs2 = ` + type Query { + test2: Test + } + + type Test { + fieldA: String + fieldC: String + } + `; + + schema1 = makeExecutableSchema({ + typeDefs: typeDefs1, + resolvers: { + Query: { + test1: () => ({}) + }, + Test: { + fieldA: () => 'A', + fieldB: () => 'B' + } + } + }); + + schema2 = makeExecutableSchema({ + typeDefs: typeDefs2, + resolvers: { + Query: { + test2: () => ({}) + }, + Test: { + fieldA: () => 'A', + fieldC: () => 'C' + } + } + }); + }) + + it('by default takes last type', async () => { + const mergedSchema = mergeSchemas({ + schemas: [schema1, schema2] + }); + const result1 = await graphql(mergedSchema, `{ test2 { fieldC } }`); + expect(result1.data.test2.fieldC).to.equal('C'); + const result2 = await graphql(mergedSchema, `{ test2 { fieldB } }`); + expect(result2.data).to.be.undefined; + }); + + it('can use onTypeConflict to select last type', async () => { + const mergedSchema = mergeSchemas({ + schemas: [schema1, schema2], + onTypeConflict: (left, right) => right + }); + const result1 = await graphql(mergedSchema, `{ test2 { fieldC } }`); + expect(result1.data.test2.fieldC).to.equal('C'); + const result2 = await graphql(mergedSchema, `{ test2 { fieldB } }`); + expect(result2.data).to.be.undefined; + }); + + it('can use onTypeConflict to select first type', async () => { + const mergedSchema = mergeSchemas({ + schemas: [schema1, schema2], + onTypeConflict: (left) => left + }); + const result1 = await graphql(mergedSchema, `{ test1 { fieldB } }`); + expect(result1.data.test1.fieldB).to.equal('B'); + const result2 = await graphql(mergedSchema, `{ test1 { fieldC } }`); + expect(result2.data).to.be.undefined; + }); +});