From bff76102b227929b608fb30da617ecf1a630456c Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 21 Jun 2020 05:58:35 -0400 Subject: [PATCH] fix(stitching): avoid duplicate directives (#1665) Fixes #1656. Later directives with the same name should override earlier directives rather than causing an error. Directives with the same names from a later subschema will override an earlier subschema. Directives from typedefs will override both. Mixing of legacy schemas argument and newer subschemas, typeDefs, and types arguments should respect this same order. We can consider adding onDirectiveConflict option to match onTypeConflict for customization. --- packages/stitch/src/stitchSchemas.ts | 55 ++++++++++++++++++--------- packages/stitch/src/typeCandidates.ts | 9 +++-- packages/utils/src/rewire.ts | 4 ++ 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/packages/stitch/src/stitchSchemas.ts b/packages/stitch/src/stitchSchemas.ts index 12fbd92a4ac..cd0d2b21957 100644 --- a/packages/stitch/src/stitchSchemas.ts +++ b/packages/stitch/src/stitchSchemas.ts @@ -10,14 +10,7 @@ import { GraphQLNamedType, } from 'graphql'; -import { - SchemaDirectiveVisitor, - cloneDirective, - mergeDeep, - IResolvers, - rewireTypes, - pruneSchema, -} from '@graphql-tools/utils'; +import { SchemaDirectiveVisitor, mergeDeep, IResolvers, rewireTypes, pruneSchema } from '@graphql-tools/utils'; import { addResolversToSchema, @@ -58,22 +51,40 @@ export function stitchSchemas({ throw new Error('Expected `resolverValidationOptions` to be an object'); } + schemas.forEach(schemaLikeObject => { + if ( + !isSchema(schemaLikeObject) && + !isSubschemaConfig(schemaLikeObject) && + typeof schemaLikeObject !== 'string' && + !isDocumentNode(schemaLikeObject) && + !Array.isArray(schemaLikeObject) + ) { + throw new Error('Invalid schema passed'); + } + }); + let schemaLikeObjects: Array = [...subschemas]; + schemas.forEach(schemaLikeObject => { + if (isSchema(schemaLikeObject) || isSubschemaConfig(schemaLikeObject)) { + schemaLikeObjects.push(schemaLikeObject); + } + }); + if ((typeDefs && !Array.isArray(typeDefs)) || (Array.isArray(typeDefs) && typeDefs.length)) { schemaLikeObjects.push(buildDocumentFromTypeDefinitions(typeDefs, parseOptions)); } + schemas.forEach(schemaLikeObject => { + if (typeof schemaLikeObject === 'string' || isDocumentNode(schemaLikeObject)) { + schemaLikeObjects.push(buildDocumentFromTypeDefinitions(schemaLikeObject, parseOptions)); + } + }); + if (types != null) { schemaLikeObjects = schemaLikeObjects.concat(types); } schemas.forEach(schemaLikeObject => { - if (isSchema(schemaLikeObject) || isSubschemaConfig(schemaLikeObject)) { - schemaLikeObjects.push(schemaLikeObject); - } else if (typeof schemaLikeObject === 'string' || isDocumentNode(schemaLikeObject)) { - schemaLikeObjects.push(buildDocumentFromTypeDefinitions(schemaLikeObject, parseOptions)); - } else if (Array.isArray(schemaLikeObject)) { + if (Array.isArray(schemaLikeObject)) { schemaLikeObjects = schemaLikeObjects.concat(schemaLikeObject); - } else { - throw new Error('Invalid schema passed'); } }); @@ -81,6 +92,10 @@ export function stitchSchemas({ const typeCandidates: Record> = Object.create(null); const extensions: Array = []; const directives: Array = []; + const directiveMap: Record = specifiedDirectives.reduce((acc, directive) => { + acc[directive.name] = directive; + return acc; + }, Object.create(null)); const schemaDefs = Object.create(null); const operationTypeNames = { query: 'Query', @@ -93,12 +108,16 @@ export function stitchSchemas({ transformedSchemas, typeCandidates, extensions, - directives, + directiveMap, schemaDefs, operationTypeNames, mergeDirectives, }); + Object.keys(directiveMap).forEach(directiveName => { + directives.push(directiveMap[directiveName]); + }); + let stitchingInfo: StitchingInfo; stitchingInfo = createStitchingInfo(transformedSchemas, typeCandidates, mergeTypes); @@ -118,9 +137,7 @@ export function stitchSchemas({ mutation: newTypeMap[operationTypeNames.mutation] as GraphQLObjectType, subscription: newTypeMap[operationTypeNames.subscription] as GraphQLObjectType, types: Object.keys(newTypeMap).map(key => newTypeMap[key]), - directives: newDirectives.length - ? specifiedDirectives.slice().concat(newDirectives.map(directive => cloneDirective(directive))) - : undefined, + directives: newDirectives, astNode: schemaDefs.schemaDef, extensionASTNodes: schemaDefs.schemaExtensions, extensions: null, diff --git a/packages/stitch/src/typeCandidates.ts b/packages/stitch/src/typeCandidates.ts index 1e107577e3a..6b228318bcd 100644 --- a/packages/stitch/src/typeCandidates.ts +++ b/packages/stitch/src/typeCandidates.ts @@ -39,7 +39,7 @@ export function buildTypeCandidates({ transformedSchemas, typeCandidates, extensions, - directives, + directiveMap, schemaDefs, operationTypeNames, mergeDirectives, @@ -48,7 +48,7 @@ export function buildTypeCandidates({ transformedSchemas: Map; typeCandidates: Record>; extensions: Array; - directives: Array; + directiveMap: Record; schemaDefs: { schemaDef: SchemaDefinitionNode; schemaExtensions: Array; @@ -96,7 +96,7 @@ export function buildTypeCandidates({ if (mergeDirectives) { schema.getDirectives().forEach(directive => { - directives.push(directive); + directiveMap[directive.name] = directive; }); } @@ -131,7 +131,8 @@ export function buildTypeCandidates({ const directivesDocument = extractDirectiveDefinitions(schemaLikeObject); directivesDocument.definitions.forEach(def => { - directives.push(typeFromAST(def) as GraphQLDirective); + const directive = typeFromAST(def) as GraphQLDirective; + directiveMap[directive.name] = directive; }); const extensionsDocument = extractTypeExtensionDefinitions(schemaLikeObject); diff --git a/packages/utils/src/rewire.ts b/packages/utils/src/rewire.ts index 1994bb6461a..0b45b19aef0 100644 --- a/packages/utils/src/rewire.ts +++ b/packages/utils/src/rewire.ts @@ -23,6 +23,7 @@ import { isScalarType, isUnionType, isSpecifiedScalarType, + isSpecifiedDirective, } from 'graphql'; import { getBuiltInForStub, isNamedStub } from './stub'; @@ -78,6 +79,9 @@ export function rewireTypes( : pruneTypes(newTypeMap, newDirectives); function rewireDirective(directive: GraphQLDirective): GraphQLDirective { + if (isSpecifiedDirective(directive)) { + return directive; + } const directiveConfig = directive.toConfig(); directiveConfig.args = rewireArgs(directiveConfig.args); return new GraphQLDirective(directiveConfig);