From 71cb4faeb0833a228520a7bc2beed8ac7274443f Mon Sep 17 00:00:00 2001 From: Dmitry Til Date: Sun, 4 Sep 2022 16:32:05 +0200 Subject: [PATCH] fix pruning of directive arg type (#4694) * fix pruning of directive arg type * add changeset --- .changeset/early-camels-listen.md | 5 ++ packages/utils/src/prune.ts | 44 ++++++++++++++-- packages/utils/tests/prune.test.ts | 85 ++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 .changeset/early-camels-listen.md diff --git a/.changeset/early-camels-listen.md b/.changeset/early-camels-listen.md new file mode 100644 index 00000000000..e11d52e1987 --- /dev/null +++ b/.changeset/early-camels-listen.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/utils': patch +--- + +Fix pruneSchema to not remove type that is used only as a directive argument type diff --git a/packages/utils/src/prune.ts b/packages/utils/src/prune.ts index 7c2bfa931b4..ab88396ce5c 100644 --- a/packages/utils/src/prune.ts +++ b/packages/utils/src/prune.ts @@ -8,6 +8,8 @@ import { GraphQLFieldMap, isSpecifiedScalarType, isScalarType, + isEnumType, + ASTNode, } from 'graphql'; import { PruneSchemaOptions } from './types.js'; @@ -138,12 +140,23 @@ function visitQueue(queue: string[], schema: GraphQLSchema, visited: Set // No need to revisit this interface again revisit[typeName] = false; } + if (isEnumType(type)) { + // Visit enum values directives argument types + queue.push( + ...type.getValues().flatMap(value => { + if (value.astNode) { + return getDirectivesArgumentsTypeNames(schema, value.astNode); + } + return []; + }) + ); + } // Visit interfaces this type is implementing if they haven't been visited yet if ('getInterfaces' in type) { // Only pushes to queue to visit but not return types queue.push(...type.getInterfaces().map(iface => iface.name)); } - // If the type has files visit those field types + // If the type has fields visit those field types if ('getFields' in type) { const fields = type.getFields() as GraphQLFieldMap; const entries = Object.entries(fields); @@ -154,14 +167,26 @@ function visitQueue(queue: string[], schema: GraphQLSchema, visited: Set for (const [, field] of entries) { if (isObjectType(type)) { - // Visit arg types - queue.push(...field.args.map(arg => getNamedType(arg.type).name)); + // Visit arg types and arg directives arguments types + queue.push( + ...field.args.flatMap(arg => { + const typeNames = [getNamedType(arg.type).name]; + if (arg.astNode) { + typeNames.push(...getDirectivesArgumentsTypeNames(schema, arg.astNode)); + } + return typeNames; + }) + ); } const namedType = getNamedType(field.type); queue.push(namedType.name); + if (field.astNode) { + queue.push(...getDirectivesArgumentsTypeNames(schema, field.astNode)); + } + // Interfaces returned on fields need to be revisited to add their implementations if (isInterfaceType(namedType) && !(namedType.name in revisit)) { revisit[namedType.name] = true; @@ -169,8 +194,21 @@ function visitQueue(queue: string[], schema: GraphQLSchema, visited: Set } } + if (type.astNode) { + queue.push(...getDirectivesArgumentsTypeNames(schema, type.astNode)); + } + visited.add(typeName); // Mark as visited (and therefore it is used and should be kept) } } return visited; } + +function getDirectivesArgumentsTypeNames( + schema: GraphQLSchema, + astNode: Extract +) { + return (astNode.directives ?? []).flatMap( + directive => schema.getDirective(directive.name.value)?.args.map(arg => getNamedType(arg.type).name) ?? [] + ); +} diff --git a/packages/utils/tests/prune.test.ts b/packages/utils/tests/prune.test.ts index bffe12a21cd..d1ec2ed2110 100644 --- a/packages/utils/tests/prune.test.ts +++ b/packages/utils/tests/prune.test.ts @@ -405,4 +405,89 @@ describe('pruneSchema', () => { expect(result.getType('CustomType')).toBeDefined(); expect(result.getType('SomeInterface')).toBeDefined(); }); + + test('does not remove type used in object type directive argument', () => { + const schema = buildSchema(/* GraphQL */ ` + directive @bar(arg: DirectiveArg) on OBJECT + + enum DirectiveArg { + VALUE1 + VALUE2 + } + + type CustomType @bar(arg: VALUE1) { + value: String + } + + type Query { + foo: CustomType + } + `); + + const result = pruneSchema(schema); + expect(result.getType('DirectiveArg')).toBeDefined(); + }); + + test('does not remove type used in field definition directive argument', () => { + const schema = buildSchema(/* GraphQL */ ` + directive @bar(arg: DirectiveArg) on FIELD_DEFINITION + + enum DirectiveArg { + VALUE1 + VALUE2 + } + + type CustomType { + value: String @bar(arg: VALUE1) + } + + type Query { + foo: CustomType + } + `); + + const result = pruneSchema(schema); + expect(result.getType('DirectiveArg')).toBeDefined(); + }); + + test('does not remove type used in enum value directive argument', () => { + const schema = buildSchema(/* GraphQL */ ` + directive @bar(arg: DirectiveArg) on ENUM_VALUE + + enum DirectiveArg { + VALUE1 + VALUE2 + } + + enum MyEnum { + VALUE3 @bar(arg: VALUE1) + VALUE4 + } + + type Query { + foo: MyEnum + } + `); + + const result = pruneSchema(schema); + expect(result.getType('DirectiveArg')).toBeDefined(); + }); + + test('does not remove type used in argument definition directive argument', () => { + const schema = buildSchema(/* GraphQL */ ` + directive @bar(arg: DirectiveArg) on ARGUMENT_DEFINITION + + enum DirectiveArg { + VALUE1 + VALUE2 + } + + type Query { + foo(arg: String @bar(arg: VALUE1)): Boolean + } + `); + + const result = pruneSchema(schema); + expect(result.getType('DirectiveArg')).toBeDefined(); + }); });