Skip to content

Commit 6038634

Browse files
authored
Fragment interface name edge case (#10580)
* fix: extracted typenames concrete name conflict * added changeset
1 parent 62c7618 commit 6038634

3 files changed

Lines changed: 138 additions & 6 deletions

File tree

.changeset/icy-hotels-flash.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-codegen/visitor-plugin-common': patch
3+
'@graphql-codegen/typescript-operations': patch
4+
---
5+
6+
fixed invalid extracted concrete type name on shared interface

packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -964,17 +964,36 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
964964

965965
const schemaType = this._schema.getType(typeName);
966966

967-
// Check if current selection set has fragments (e.g., "... AppNotificationFragment" or "... on AppNotification")
968-
const hasFragment =
969-
this._selectionSet?.selections?.some(
970-
selection => selection.kind === Kind.INLINE_FRAGMENT || selection.kind === Kind.FRAGMENT_SPREAD
971-
) ?? false;
967+
// Check if current selection set has type-narrowing fragments.
968+
// - Inline fragments are always type-narrowing
969+
// - Fragment spreads are only type-narrowing if they are on a different type than the current parent schema type
970+
// (e.g. spreading `fragment Foo on Pet` while processing `Pet` is not type-narrowing).
971+
const hasTypeNarrowingFragments =
972+
this._selectionSet?.selections?.some(selection => {
973+
if (selection.kind === Kind.INLINE_FRAGMENT) {
974+
return true;
975+
}
976+
977+
if (selection.kind === Kind.FRAGMENT_SPREAD) {
978+
const spreadFragment = this._loadedFragments.find(lf => lf.name === selection.name.value);
979+
// If we can't resolve fragment metadata (or the current parent type), treat it as type-narrowing.
980+
// This avoids incorrectly using interface-rooted names in cases that are actually concrete-targeting.
981+
return !spreadFragment || !this._parentSchemaType || spreadFragment.onType !== this._parentSchemaType.name;
982+
}
983+
984+
return false;
985+
}) ?? false;
972986

973987
// When the parent schema type is an interface:
974988
// - If we're processing inline fragments, use the concrete type name
975989
// - If we're processing the interface directly, use the interface name
976990
// - If we're in a named fragment, always use the concrete type name
977-
if (isObjectType(schemaType) && this._parentSchemaType && isInterfaceType(this._parentSchemaType) && !hasFragment) {
991+
if (
992+
isObjectType(schemaType) &&
993+
this._parentSchemaType &&
994+
isInterfaceType(this._parentSchemaType) &&
995+
!hasTypeNarrowingFragments
996+
) {
978997
return `${parentName}_${this._parentSchemaType.name}`;
979998
}
980999

packages/plugins/typescript/operations/tests/extract-all-types.spec.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,113 @@ describe('extractAllFieldsToTypes: true', () => {
15561556
await validate(content, config, nestedInterfacesSchema);
15571557
});
15581558

1559+
it('fragment spreads on the same interface should not force concrete parent type names (regression #10502)', async () => {
1560+
const interfaceFragmentSchema = buildSchema(/* GraphQL */ `
1561+
schema {
1562+
query: Query
1563+
}
1564+
1565+
type Query {
1566+
pet(petId: ID!): Pet
1567+
}
1568+
1569+
interface Pet {
1570+
id: ID!
1571+
home: Home
1572+
}
1573+
1574+
type Dog implements Pet {
1575+
id: ID!
1576+
home: Home
1577+
}
1578+
1579+
type Cat implements Pet {
1580+
id: ID!
1581+
home: Home
1582+
}
1583+
1584+
interface Home {
1585+
id: ID!
1586+
}
1587+
1588+
type House implements Home {
1589+
id: ID!
1590+
}
1591+
`);
1592+
1593+
const interfaceFragmentDoc = parse(/* GraphQL */ `
1594+
fragment GetFragmentPet on Pet {
1595+
id
1596+
home {
1597+
id
1598+
}
1599+
}
1600+
1601+
query GetPetData($petId: ID!) {
1602+
pet(petId: $petId) {
1603+
id
1604+
home {
1605+
id
1606+
}
1607+
...GetFragmentPet
1608+
}
1609+
}
1610+
`);
1611+
1612+
const config: TypeScriptDocumentsPluginConfig = {
1613+
preResolveTypes: true,
1614+
extractAllFieldsToTypes: true,
1615+
nonOptionalTypename: true,
1616+
dedupeOperationSuffix: true,
1617+
};
1618+
1619+
const { content } = await plugin(
1620+
interfaceFragmentSchema,
1621+
[{ location: 'test-file.ts', document: interfaceFragmentDoc }],
1622+
config,
1623+
{ outputFile: '' }
1624+
);
1625+
1626+
// Edge case: a fragment spread on the same interface should not cause extracted types
1627+
// for interface fields to be rooted to the first concrete parent (e.g. Cat).
1628+
expect(content).toMatchInlineSnapshot(`
1629+
"export type GetFragmentPetFragment_Pet_home_House = { __typename: 'House', id: string };
1630+
1631+
type GetFragmentPet_Dog_Fragment = { __typename: 'Dog', id: string, home?: GetFragmentPetFragment_Pet_home_House | null };
1632+
1633+
type GetFragmentPet_Cat_Fragment = { __typename: 'Cat', id: string, home?: GetFragmentPetFragment_Pet_home_House | null };
1634+
1635+
export type GetFragmentPetFragment =
1636+
| GetFragmentPet_Dog_Fragment
1637+
| GetFragmentPet_Cat_Fragment
1638+
;
1639+
1640+
export type GetPetDataQuery_pet_Pet_home_House = { __typename: 'House', id: string };
1641+
1642+
export type GetPetDataQuery_pet_Dog = { __typename: 'Dog', id: string, home?: GetPetDataQuery_pet_Pet_home_House | null };
1643+
1644+
export type GetPetDataQuery_pet_Cat = { __typename: 'Cat', id: string, home?: GetPetDataQuery_pet_Pet_home_House | null };
1645+
1646+
export type GetPetDataQuery_pet =
1647+
| GetPetDataQuery_pet_Dog
1648+
| GetPetDataQuery_pet_Cat
1649+
;
1650+
1651+
export type GetPetDataQuery_Query = { __typename: 'Query', pet?: GetPetDataQuery_pet | null };
1652+
1653+
1654+
export type GetPetDataQueryVariables = Exact<{
1655+
petId: Scalars['ID']['input'];
1656+
}>;
1657+
1658+
1659+
export type GetPetDataQuery = GetPetDataQuery_Query;
1660+
"
1661+
`);
1662+
1663+
await validate(content, config, interfaceFragmentSchema);
1664+
});
1665+
15591666
// Exception case for Issue #10502 - shared schema for fragment tests
15601667
const notificationSchema = buildSchema(/* GraphQL */ `
15611668
type Query {

0 commit comments

Comments
 (0)