From 0fc510cb5fef81b0f5d7a5bd3fb9636b03e8100b Mon Sep 17 00:00:00 2001 From: Trevor Livingston Date: Mon, 9 May 2022 14:00:14 -0500 Subject: [PATCH] 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 --- .changeset/dry-tools-attend.md | 5 +++ packages/utils/src/prune.ts | 39 +++++++++++++------ packages/utils/tests/prune.test.ts | 62 ++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 .changeset/dry-tools-attend.md diff --git a/.changeset/dry-tools-attend.md b/.changeset/dry-tools-attend.md new file mode 100644 index 0000000000..ccf8841ee7 --- /dev/null +++ b/.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. diff --git a/packages/utils/src/prune.ts b/packages/utils/src/prune.ts index d774564e5c..bd047234b4 100644 --- a/packages/utils/src/prune.ts +++ b/packages/utils/src/prune.ts @@ -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 @@ -112,12 +113,15 @@ function visitSchema(schema: GraphQLSchema): Set { } function visitQueue(queue: string[], schema: GraphQLSchema, visited: Set = new Set()): Set { + // Interfaces encountered that are field return types need to be revisited to add their implementations + const revisit: Map = new Map(); + // 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; } @@ -128,7 +132,17 @@ function visitQueue(queue: string[], schema: GraphQLSchema, visited: Set 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; @@ -140,18 +154,19 @@ function visitQueue(queue: string[], schema: GraphQLSchema, visited: Set 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) diff --git a/packages/utils/tests/prune.test.ts b/packages/utils/tests/prune.test.ts index 07c7f8542a..f1d20dd338 100644 --- a/packages/utils/tests/prune.test.ts +++ b/packages/utils/tests/prune.test.ts @@ -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 {