Skip to content

Commit

Permalink
fix(stitching): avoid duplicate directives (#1665)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
yaacovCR committed Jun 21, 2020
1 parent acf93e4 commit bff7610
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 23 deletions.
55 changes: 36 additions & 19 deletions packages/stitch/src/stitchSchemas.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -58,29 +51,51 @@ 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<GraphQLSchema | SubschemaConfig | DocumentNode | GraphQLNamedType> = [...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');
}
});

const transformedSchemas: Map<GraphQLSchema | SubschemaConfig, GraphQLSchema> = new Map();
const typeCandidates: Record<string, Array<MergeTypeCandidate>> = Object.create(null);
const extensions: Array<DocumentNode> = [];
const directives: Array<GraphQLDirective> = [];
const directiveMap: Record<string, GraphQLDirective> = specifiedDirectives.reduce((acc, directive) => {
acc[directive.name] = directive;
return acc;
}, Object.create(null));
const schemaDefs = Object.create(null);
const operationTypeNames = {
query: 'Query',
Expand All @@ -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);
Expand All @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions packages/stitch/src/typeCandidates.ts
Expand Up @@ -39,7 +39,7 @@ export function buildTypeCandidates({
transformedSchemas,
typeCandidates,
extensions,
directives,
directiveMap,
schemaDefs,
operationTypeNames,
mergeDirectives,
Expand All @@ -48,7 +48,7 @@ export function buildTypeCandidates({
transformedSchemas: Map<GraphQLSchema | SubschemaConfig, GraphQLSchema>;
typeCandidates: Record<string, Array<MergeTypeCandidate>>;
extensions: Array<DocumentNode>;
directives: Array<GraphQLDirective>;
directiveMap: Record<string, GraphQLDirective>;
schemaDefs: {
schemaDef: SchemaDefinitionNode;
schemaExtensions: Array<SchemaExtensionNode>;
Expand Down Expand Up @@ -96,7 +96,7 @@ export function buildTypeCandidates({

if (mergeDirectives) {
schema.getDirectives().forEach(directive => {
directives.push(directive);
directiveMap[directive.name] = directive;
});
}

Expand Down Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions packages/utils/src/rewire.ts
Expand Up @@ -23,6 +23,7 @@ import {
isScalarType,
isUnionType,
isSpecifiedScalarType,
isSpecifiedDirective,
} from 'graphql';

import { getBuiltInForStub, isNamedStub } from './stub';
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit bff7610

Please sign in to comment.