Skip to content

Commit

Permalink
fix pruning of directive arg type (#4694)
Browse files Browse the repository at this point in the history
* fix pruning of directive arg type

* add changeset
  • Loading branch information
dimatill committed Sep 4, 2022
1 parent 63fc7f6 commit 71cb4fa
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .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
44 changes: 41 additions & 3 deletions packages/utils/src/prune.ts
Expand Up @@ -8,6 +8,8 @@ import {
GraphQLFieldMap,
isSpecifiedScalarType,
isScalarType,
isEnumType,
ASTNode,
} from 'graphql';

import { PruneSchemaOptions } from './types.js';
Expand Down Expand Up @@ -138,12 +140,23 @@ function visitQueue(queue: string[], schema: GraphQLSchema, visited: Set<string>
// 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<any, any>;
const entries = Object.entries(fields);
Expand All @@ -154,23 +167,48 @@ function visitQueue(queue: string[], schema: GraphQLSchema, visited: Set<string>

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;
}
}
}

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<ASTNode, { readonly directives?: any }>
) {
return (astNode.directives ?? []).flatMap(
directive => schema.getDirective(directive.name.value)?.args.map(arg => getNamedType(arg.type).name) ?? []
);
}
85 changes: 85 additions & 0 deletions packages/utils/tests/prune.test.ts
Expand Up @@ -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();
});
});

1 comment on commit 71cb4fa

@vercel
Copy link

@vercel vercel bot commented on 71cb4fa Sep 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.