Skip to content

Commit 74ca7dd

Browse files
Fix __typename rebasing for interface objects (#2886)
We should never query `__typename` from a subgraph for an object type marked `@interfaceObject`, as the value we get back will always be wrong. Normally, we prevent this by not having a `__typename` edge in the federated query graph. However, during the optimization where we try to reuse existing named fragments, we rebase them from the API schema onto the subgraph schemas. We usually ignore selections where this leads to invalidity (e.g. fields or types that don't exist in the subgraph schema), but the code doesn't currently view the `__typename` field being rebased onto an interface object as invalid. This has led to bugs where named fragments containing `__typename` accidentally cause it to be queried on interface objects in subgraph queries. This PR changes `Field.rebaseOn()` such that `__typename` being rebased onto a parent type that is an interface object is considered invalid. Additionally, if the parent type is an abstract type, and that abstract type could possibly be an interface object type at runtime, then this is additionally considered invalid.
1 parent 7b5b836 commit 74ca7dd

3 files changed

Lines changed: 73 additions & 2 deletions

File tree

.changeset/dry-seahorses-grin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/federation-internals": patch
3+
---
4+
5+
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))

internals-js/src/__tests__/operations.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
SchemaRootKind,
66
} from '../../dist/definitions';
77
import { buildSchema } from '../../dist/buildSchema';
8+
import { FederationBlueprint } from '../../dist/federation';
89
import { FragmentRestrictionAtType, MutableSelectionSet, NamedFragmentDefinition, Operation, operationFromDocument, parseOperation } from '../../dist/operations';
910
import './matchers';
1011
import { DocumentNode, FieldNode, GraphQLError, Kind, OperationDefinitionNode, OperationTypeNode, parse, SelectionNode, SelectionSetNode, validate } from 'graphql';
@@ -2798,7 +2799,7 @@ describe('basic operations', () => {
27982799
`);
27992800
});
28002801
});
2801-
2802+
28022803
describe('same fragment merging', () => {
28032804
test('do merge when same fragment and no directive', () => {
28042805
const operation = operationFromDocument(schema, gql`
@@ -3514,6 +3515,63 @@ describe('named fragment rebasing on subgraphs', () => {
35143515
`);
35153516
});
35163517

3518+
test('it skips __typename field for types that are potentially interface objects at runtime', () => {
3519+
const schema = parseSchema(`
3520+
type Query {
3521+
i: I
3522+
}
3523+
3524+
interface I {
3525+
id: ID!
3526+
x: String!
3527+
}
3528+
`);
3529+
3530+
const operation = parseOperation(schema, `
3531+
query {
3532+
i {
3533+
...FragOnI
3534+
}
3535+
}
3536+
3537+
fragment FragOnI on I {
3538+
__typename
3539+
id
3540+
x
3541+
}
3542+
`);
3543+
3544+
const fragments = operation.fragments;
3545+
assert(fragments, 'Should have some fragments');
3546+
3547+
const subgraph = buildSchema(`
3548+
extend schema
3549+
@link(
3550+
url: "https://specs.apollo.dev/federation/v2.5",
3551+
import: [{ name: "@interfaceObject" }, { name: "@key" }]
3552+
)
3553+
3554+
type Query {
3555+
i: I
3556+
}
3557+
3558+
type I @interfaceObject @key(fields: "id") {
3559+
id: ID!
3560+
x: String!
3561+
}
3562+
`,
3563+
{ blueprint: new FederationBlueprint(true) },
3564+
);
3565+
3566+
const rebased = fragments.rebaseOn(subgraph);
3567+
expect(rebased?.toString('')).toMatchString(`
3568+
fragment FragOnI on I {
3569+
id
3570+
x
3571+
}
3572+
`);
3573+
});
3574+
35173575
test('it skips fragments with no selection or trivial ones applying', () => {
35183576
const schema = parseSchema(`
35193577
type Query {

internals-js/src/operations.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,15 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
305305
}
306306

307307
if (this.name === typenameFieldName) {
308-
return this.withUpdatedDefinition(parentType.typenameField()!);
308+
if (possibleRuntimeTypes(parentType).some((runtimeType) => isInterfaceObjectType(runtimeType))) {
309+
validate(
310+
!errorIfCannotRebase,
311+
() => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}" that is potentially an interface object type at runtime`
312+
);
313+
return undefined;
314+
} else {
315+
return this.withUpdatedDefinition(parentType.typenameField()!);
316+
}
309317
}
310318

311319
const fieldDef = parentType.field(this.name);

0 commit comments

Comments
 (0)