diff --git a/.changeset/dry-seahorses-grin.md b/.changeset/dry-seahorses-grin.md new file mode 100644 index 000000000..18fc3bdca --- /dev/null +++ b/.changeset/dry-seahorses-grin.md @@ -0,0 +1,5 @@ +--- +"@apollo/federation-internals": patch +--- + +Fix query planning bug where `__typename` on interface object types in named fragments can cause query plan execution to fail. ([#2886](https://github.com/apollographql/federation/issues/2886)) diff --git a/internals-js/src/__tests__/operations.test.ts b/internals-js/src/__tests__/operations.test.ts index 7022225ca..9657c4f46 100644 --- a/internals-js/src/__tests__/operations.test.ts +++ b/internals-js/src/__tests__/operations.test.ts @@ -5,6 +5,7 @@ import { SchemaRootKind, } from '../../dist/definitions'; import { buildSchema } from '../../dist/buildSchema'; +import { FederationBlueprint } from '../../dist/federation'; import { FragmentRestrictionAtType, MutableSelectionSet, NamedFragmentDefinition, Operation, operationFromDocument, parseOperation } from '../../dist/operations'; import './matchers'; import { DocumentNode, FieldNode, GraphQLError, Kind, OperationDefinitionNode, OperationTypeNode, parse, SelectionNode, SelectionSetNode, validate } from 'graphql'; @@ -2798,7 +2799,7 @@ describe('basic operations', () => { `); }); }); - + describe('same fragment merging', () => { test('do merge when same fragment and no directive', () => { const operation = operationFromDocument(schema, gql` @@ -3514,6 +3515,63 @@ describe('named fragment rebasing on subgraphs', () => { `); }); + test('it skips __typename field for types that are potentially interface objects at runtime', () => { + const schema = parseSchema(` + type Query { + i: I + } + + interface I { + id: ID! + x: String! + } + `); + + const operation = parseOperation(schema, ` + query { + i { + ...FragOnI + } + } + + fragment FragOnI on I { + __typename + id + x + } + `); + + const fragments = operation.fragments; + assert(fragments, 'Should have some fragments'); + + const subgraph = buildSchema(` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.5", + import: [{ name: "@interfaceObject" }, { name: "@key" }] + ) + + type Query { + i: I + } + + type I @interfaceObject @key(fields: "id") { + id: ID! + x: String! + } + `, + { blueprint: new FederationBlueprint(true) }, + ); + + const rebased = fragments.rebaseOn(subgraph); + expect(rebased?.toString('')).toMatchString(` + fragment FragOnI on I { + id + x + } + `); + }); + test('it skips fragments with no selection or trivial ones applying', () => { const schema = parseSchema(` type Query { diff --git a/internals-js/src/operations.ts b/internals-js/src/operations.ts index b00f2664d..c98573a06 100644 --- a/internals-js/src/operations.ts +++ b/internals-js/src/operations.ts @@ -305,7 +305,15 @@ export class Field ex } if (this.name === typenameFieldName) { - return this.withUpdatedDefinition(parentType.typenameField()!); + if (possibleRuntimeTypes(parentType).some((runtimeType) => isInterfaceObjectType(runtimeType))) { + validate( + !errorIfCannotRebase, + () => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}" that is potentially an interface object type at runtime` + ); + return undefined; + } else { + return this.withUpdatedDefinition(parentType.typenameField()!); + } } const fieldDef = parentType.field(this.name);