Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Correct interface implementation pruning (#4442)
* Retain interface implementations for return types.

* Handle edge case where interface is encountered first as a type interface, second as a return type.

* Added changeset

* Simplified tracking interfaces to revisit
  • Loading branch information
tlivings committed May 9, 2022
1 parent a2cc38a commit 0fc510c
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-tools-attend.md
@@ -0,0 +1,5 @@
---
'@graphql-tools/utils': patch
---

Interface implementations should be included when a return type is an interface.
39 changes: 27 additions & 12 deletions packages/utils/src/prune.ts
Expand Up @@ -15,6 +15,7 @@ import { PruneSchemaOptions } from './types';
import { mapSchema } from './mapSchema';
import { MapperKind } from './Interfaces';
import { getRootTypes } from './rootTypes';
import { getImplementingTypes } from './get-implementing-types';

/**
* Prunes the provided schema, removing unused and empty types
Expand Down Expand Up @@ -112,12 +113,15 @@ function visitSchema(schema: GraphQLSchema): Set<string> {
}

function visitQueue(queue: string[], schema: GraphQLSchema, visited: Set<string> = new Set<string>()): Set<string> {
// Interfaces encountered that are field return types need to be revisited to add their implementations
const revisit: Map<string, boolean> = new Map<string, boolean>();

// Navigate all types starting with pre-queued types (root types)
while (queue.length) {
const typeName = queue.pop() as string;

// Skip types we already visited
if (visited.has(typeName)) {
// Skip types we already visited unless it is an interface type that needs revisiting
if (visited.has(typeName) && revisit[typeName] !== true) {
continue;
}

Expand All @@ -128,7 +132,17 @@ function visitQueue(queue: string[], schema: GraphQLSchema, visited: Set<string>
if (isUnionType(type)) {
queue.push(...type.getTypes().map(type => type.name));
}

// If it is an interface and it is a returned type, grab all implementations so we can use proper __typename in fragments
if (isInterfaceType(type) && revisit[typeName] === true) {
queue.push(...getImplementingTypes(type.name, schema));
// No need to revisit this interface again
revisit[typeName] = false;
}
// 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 ('getFields' in type) {
const fields = type.getFields() as GraphQLFieldMap<any, any>;
Expand All @@ -140,18 +154,19 @@ function visitQueue(queue: string[], schema: GraphQLSchema, visited: Set<string>

for (const [, field] of entries) {
if (isObjectType(type)) {
for (const arg of field.args) {
queue.push(getNamedType(arg.type).name); // Visit arg types
}
// Visit arg types
queue.push(...field.args.map(arg => getNamedType(arg.type).name));
}

queue.push(getNamedType(field.type).name);
}
}
const namedType = getNamedType(field.type);

// Visit interfaces this type is implementing if they haven't been visited yet
if ('getInterfaces' in type) {
queue.push(...type.getInterfaces().map(iface => iface.name));
queue.push(namedType.name);

// Interfaces returned on fields need to be revisited to add their implementations
if (isInterfaceType(namedType) && !(namedType.name in revisit)) {
revisit[namedType.name] = true;
}
}
}

visited.add(typeName); // Mark as visited (and therefore it is used and should be kept)
Expand Down
62 changes: 62 additions & 0 deletions packages/utils/tests/prune.test.ts
Expand Up @@ -212,6 +212,68 @@ describe('pruneSchema', () => {
expect(result.getType('ShouldPrune')).toBeUndefined();
});

test('does not remove implementations of interfaces used as return', () => {
const schema = buildSchema(/* GraphQL */ `
type Query {
operation: SomeType
}
type SomeType {
# SomeInterface is inline and should have all its implementations kept
field: SomeInterface
}
interface SomeInterface {
field: String
}
type KeepMe implements SomeInterface {
field: String
}
`);

const result = pruneSchema(schema);

expect(result.getType('KeepMe')).toBeDefined();
});

test('does not remove interfaces despite first pass', () => {
const schema = buildSchema(/* GraphQL */ `
type Query {
operation: SomeType
}
type SomeType {
# This will be processed last
afield: AsReturnType
# This will be processed first
bField: AsInterfaceType
}
# SomeInterface is declared as an interface so it should be not a return type but still visited
type AsInterfaceType implements SomeInterface {
field: String
}
# SomeInterface is a return type and should have all its implementations kept
type AsReturnType {
field: SomeInterface
}
interface SomeInterface {
field: String
}
type KeepMe implements SomeInterface {
field: String
}
`);

const result = pruneSchema(schema);

expect(result.getType('KeepMe')).toBeDefined();
});

test('removes top level objects with no fields', () => {
const schema = buildSchema(/* GraphQL */ `
type Query {
Expand Down

1 comment on commit 0fc510c

@vercel
Copy link

@vercel vercel bot commented on 0fc510c May 9, 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.