Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lemon-insects-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-codegen/graphql-modules-preset': patch
---

Fix \_\_isTypeOf wrongly picked on objects that are not implementing types or union members
19 changes: 18 additions & 1 deletion packages/presets/graphql-modules/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type RegistryKeys = 'objects' | 'inputs' | 'interfaces' | 'scalars' | 'unions' |
type Registry = Record<RegistryKeys, string[]>;
const registryKeys: RegistryKeys[] = ['objects', 'inputs', 'interfaces', 'scalars', 'unions', 'enums'];
const resolverKeys: Array<Extract<RegistryKeys, 'objects' | 'enums' | 'scalars'>> = ['scalars', 'objects', 'enums'];
const withIsTypeOfKeys: Array<'objects'> = ['objects'];

export function buildModule(
name: string,
Expand Down Expand Up @@ -65,6 +66,7 @@ export function buildModule(
const picks: Record<RegistryKeys, Record<string, string[]>> = createObject(registryKeys, () => ({}));
const defined: Registry = createObject(registryKeys, () => []);
const extended: Registry = createObject(registryKeys, () => []);
const withIsTypeOf: { objects: string[] } = createObject(withIsTypeOfKeys, () => []);

// List of types used in objects, fields, arguments etc
const usedTypes = collectUsedTypes(doc);
Expand Down Expand Up @@ -216,7 +218,9 @@ export function buildModule(
'DefinedFields',
// In case of enabled `requireRootResolvers` flag, the preset has to produce a non-optional properties.
requireRootResolvers && rootTypes.includes(name),
!rootTypes.includes(name) && defined.objects.includes(name) ? ` | '__isTypeOf'` : ''
!rootTypes.includes(name) && defined.objects.includes(name) && withIsTypeOf.objects.includes(name)
? ` | '__isTypeOf'`
: ''
)
)
.join('\n'),
Expand Down Expand Up @@ -405,6 +409,11 @@ export function buildModule(
case Kind.OBJECT_TYPE_DEFINITION: {
defined.objects.push(name);
collectFields(node, picks.objects);

if (node.interfaces?.length > 0) {
withIsTypeOf.objects.push(name);
}

break;
}

Expand Down Expand Up @@ -433,6 +442,10 @@ export function buildModule(

case Kind.UNION_TYPE_DEFINITION: {
defined.unions.push(name);

for (const namedType of node.types || []) {
pushUnique(withIsTypeOf.objects, namedType.name.value);
Copy link
Collaborator Author

@eddeee888 eddeee888 Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using pushUnique for union type here (unlike .push on other type definition) because a union may contain a union member that already exists on another union declaration.

}
break;
}
}
Expand All @@ -453,6 +466,10 @@ export function buildModule(

pushUnique(extended.objects, name);

if (node.interfaces?.length > 0) {
pushUnique(withIsTypeOf.objects, name);
}

break;
}

Expand Down
87 changes: 87 additions & 0 deletions packages/presets/graphql-modules/tests/builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,90 @@ test('should generate a signature for ResolveMiddleware (with widlcards)', () =>
};
`);
});

test('only picks __isTypeOf from implementing types (of Interfaces) and union members', () => {
const output = buildModule(
'test',
parse(/* GraphQL */ `
type Query {
me: User
pet: Pet
offer: Offer
}

type User {
id: ID!
username: String!
}

interface Pet {
id: ID!
name: String!
}
type Cat implements Pet {
id: ID!
name: String!
canScratch: Boolean!
}
type Dog implements Pet {
id: ID!
name: String!
canBark: Boolean!
}
type Elephant {
id: ID!
}
extend type Elephant implements Pet {
name: String!
hasTrunk: Boolean!
}

union Offer = Discount | Coupon
type Discount {
id: ID!
name: String!
}
type Coupon {
id: ID!
name: String!
}
`),
{
importPath: '../types',
importNamespace: 'core',
encapsulate: 'none',
requireRootResolvers: false,
shouldDeclare: false,
rootTypes: ROOT_TYPES,
baseVisitor,
useGraphQLModules: true,
}
);

// User does not pick `__isTypeOf` because it is not a union member, or implementing types
expect(output).toBeSimilarStringTo(`
export type UserResolvers = Pick<core.UserResolvers, DefinedFields['User']>;
`);

// Cat picks `__isTypeOf` because it is an implementing type of Pet
expect(output).toBeSimilarStringTo(`
export type CatResolvers = Pick<core.CatResolvers, DefinedFields['Cat'] | '__isTypeOf'>;
`);
// Dog picks `__isTypeOf` because it is an implementing type of Pet
expect(output).toBeSimilarStringTo(`
export type DogResolvers = Pick<core.DogResolvers, DefinedFields['Dog'] | '__isTypeOf'>;
`);
// Elephant picks `__isTypeOf` because it is an implementing type of Pet, via `extend type `
expect(output).toBeSimilarStringTo(`
export type ElephantResolvers = Pick<core.ElephantResolvers, DefinedFields['Elephant'] | '__isTypeOf'>;
`);

// Discount picks `__isTypeOf` because it is a union member
expect(output).toBeSimilarStringTo(`
export type DiscountResolvers = Pick<core.DiscountResolvers, DefinedFields['Discount'] | '__isTypeOf'>;
`);
// Coupon picks `__isTypeOf` because it is a union member
expect(output).toBeSimilarStringTo(`
export type CouponResolvers = Pick<core.CouponResolvers, DefinedFields['Coupon'] | '__isTypeOf'>;
`);
});
Loading