Skip to content

Commit 402cb8a

Browse files
authored
[typescript-resolvers] Fix resolversNonOptionalTypename for union members in mapper cases (#9231)
* Handle resolversNonOptionalTypename override for all member unions * Add tests for mapper and placeholder cases * Add changeset
1 parent ee63489 commit 402cb8a

File tree

3 files changed

+149
-42
lines changed

3 files changed

+149
-42
lines changed

.changeset/strange-years-hope.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-codegen/visitor-plugin-common': patch
3+
'@graphql-codegen/typescript-resolvers': patch
4+
---
5+
6+
Implement resolversNonOptionalTypename for mapper cases

packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts

+45-34
Original file line numberDiff line numberDiff line change
@@ -898,46 +898,57 @@ export class BaseResolversVisitor<
898898
const schemaType = allSchemaTypes[typeName];
899899

900900
if (isUnionType(schemaType)) {
901-
const referencedTypes = schemaType.getTypes().map(unionMemberType => {
902-
const isUnionMemberMapped = this.config.mappers[unionMemberType.name];
903-
904-
// 1. If mapped without placehoder, just use it without doing extra checks
905-
if (isUnionMemberMapped && !hasPlaceholder(isUnionMemberMapped.type)) {
906-
return isUnionMemberMapped.type;
907-
}
901+
const referencedTypes = schemaType
902+
.getTypes()
903+
.map(unionMemberType => {
904+
const isUnionMemberMapped = this.config.mappers[unionMemberType.name];
905+
906+
// 1. If mapped without placehoder, just use it without doing extra checks
907+
if (isUnionMemberMapped && !hasPlaceholder(isUnionMemberMapped.type)) {
908+
return { typename: unionMemberType.name, unionMemberValue: isUnionMemberMapped.type };
909+
}
908910

909-
// 2. Work out value for union member type
910-
// 2a. By default, use the typescript type
911-
let unionMemberValue = this.convertName(unionMemberType.name, {}, true);
911+
// 2. Work out value for union member type
912+
// 2a. By default, use the typescript type
913+
let unionMemberValue = this.convertName(unionMemberType.name, {}, true);
912914

913-
// 2b. Find fields to Omit if needed.
914-
// - If no field to Omit, "type with maybe Omit" is typescript type i.e. no Omit
915-
// - If there are fields to Omit, keep track of these "type with maybe Omit" to replace in original unionMemberValue
916-
const fieldsToOmit = this.getRelevantFieldsToOmit({ schemaType: unionMemberType, getTypeToUse });
917-
if (fieldsToOmit.length > 0) {
918-
unionMemberValue = this.replaceFieldsInType(unionMemberValue, fieldsToOmit);
919-
}
915+
// 2b. Find fields to Omit if needed.
916+
// - If no field to Omit, "type with maybe Omit" is typescript type i.e. no Omit
917+
// - If there are fields to Omit, keep track of these "type with maybe Omit" to replace in original unionMemberValue
918+
const fieldsToOmit = this.getRelevantFieldsToOmit({ schemaType: unionMemberType, getTypeToUse });
919+
if (fieldsToOmit.length > 0) {
920+
unionMemberValue = this.replaceFieldsInType(unionMemberValue, fieldsToOmit);
921+
}
920922

921-
// 2c. If union member is mapped with placeholder, use the "type with maybe Omit" as {T}
922-
if (isUnionMemberMapped && hasPlaceholder(isUnionMemberMapped.type)) {
923-
return replacePlaceholder(isUnionMemberMapped.type, unionMemberValue);
924-
}
923+
// 2c. If union member is mapped with placeholder, use the "type with maybe Omit" as {T}
924+
if (isUnionMemberMapped && hasPlaceholder(isUnionMemberMapped.type)) {
925+
return {
926+
typename: unionMemberType.name,
927+
unionMemberValue: replacePlaceholder(isUnionMemberMapped.type, unionMemberValue),
928+
};
929+
}
925930

926-
// 2d. If has default mapper with placeholder, use the "type with maybe Omit" as {T}
927-
const hasDefaultMapper = !!this.config.defaultMapper?.type;
928-
const isScalar = this.config.scalars[typeName];
929-
if (hasDefaultMapper && hasPlaceholder(this.config.defaultMapper.type)) {
930-
const finalTypename = isScalar ? this._getScalar(typeName) : unionMemberValue;
931-
return replacePlaceholder(this.config.defaultMapper.type, finalTypename);
932-
}
931+
// 2d. If has default mapper with placeholder, use the "type with maybe Omit" as {T}
932+
const hasDefaultMapper = !!this.config.defaultMapper?.type;
933+
const isScalar = this.config.scalars[typeName];
934+
if (hasDefaultMapper && hasPlaceholder(this.config.defaultMapper.type)) {
935+
const finalTypename = isScalar ? this._getScalar(typeName) : unionMemberValue;
936+
return {
937+
typename: unionMemberType.name,
938+
unionMemberValue: replacePlaceholder(this.config.defaultMapper.type, finalTypename),
939+
};
940+
}
933941

934-
const nonOptionalTypenameModifier = this.config.resolversNonOptionalTypename.unionMember
935-
? ` & { __typename: "${unionMemberType}" }`
936-
: '';
942+
return { typename: unionMemberType.name, unionMemberValue };
943+
})
944+
.map(({ typename, unionMemberValue }) => {
945+
const nonOptionalTypenameModifier = this.config.resolversNonOptionalTypename.unionMember
946+
? ` & { __typename: '${typename}' }`
947+
: '';
937948

938-
return `${unionMemberValue}${nonOptionalTypenameModifier}`;
939-
});
940-
res[typeName] = referencedTypes.map(type => `( ${type} )`).join(' | '); // Must wrap every union member in explicit "( )" to separate the members
949+
return `( ${unionMemberValue}${nonOptionalTypenameModifier} )`; // Must wrap every union member in explicit "( )" to separate the members
950+
});
951+
res[typeName] = referencedTypes.join(' | ');
941952
}
942953
return res;
943954
}, {});

packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts

+98-8
Original file line numberDiff line numberDiff line change
@@ -235,14 +235,14 @@ export type MyTypeResolvers<ContextType = any, ParentType extends ResolversParen
235235

236236
expect(result.content).toBeSimilarStringTo(`
237237
export type ResolversUnionTypes = {
238-
ChildUnion: ( Child & { __typename: "Child" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
239-
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> } & { __typename: "MyType" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
238+
ChildUnion: ( Child & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
239+
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> } & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
240240
};
241241
`);
242242
expect(result.content).toBeSimilarStringTo(`
243243
export type ResolversUnionParentTypes = {
244-
ChildUnion: ( Child & { __typename: "Child" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
245-
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> } & { __typename: "MyType" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
244+
ChildUnion: ( Child & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
245+
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> } & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
246246
};
247247
`);
248248
});
@@ -257,17 +257,107 @@ export type MyTypeResolvers<ContextType = any, ParentType extends ResolversParen
257257

258258
expect(result.content).toBeSimilarStringTo(`
259259
export type ResolversUnionTypes = {
260-
ChildUnion: ( Child & { __typename: "Child" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
261-
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> } & { __typename: "MyType" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
260+
ChildUnion: ( Child & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
261+
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> } & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
262262
};
263263
`);
264264
expect(result.content).toBeSimilarStringTo(`
265265
export type ResolversUnionParentTypes = {
266-
ChildUnion: ( Child & { __typename: "Child" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
267-
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> } & { __typename: "MyType" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
266+
ChildUnion: ( Child & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
267+
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> } & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
268268
};
269269
`);
270270
});
271+
272+
it('resolversNonOptionalTypename - adds non-optional typenames to ResolversUnionTypes for mappers with no placeholder', async () => {
273+
const result = await plugin(
274+
resolversTestingSchema,
275+
[],
276+
{
277+
resolversNonOptionalTypename: { unionMember: true },
278+
mappers: { Child: 'ChildMapper', MyType: 'MyTypeMapper' },
279+
},
280+
{ outputFile: '' }
281+
);
282+
283+
expect(result.content).toBeSimilarStringTo(`
284+
export type ResolversUnionTypes = {
285+
ChildUnion: ( ChildMapper & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
286+
MyUnion: ( MyTypeMapper & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
287+
};
288+
`);
289+
expect(result.content).toBeSimilarStringTo(`
290+
export type ResolversUnionParentTypes = {
291+
ChildUnion: ( ChildMapper & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
292+
MyUnion: ( MyTypeMapper & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
293+
};
294+
`);
295+
});
296+
297+
it('resolversNonOptionalTypename - adds non-optional typenames to ResolversUnionTypes for mappers with placeholder', async () => {
298+
const result = await plugin(
299+
resolversTestingSchema,
300+
[],
301+
{
302+
resolversNonOptionalTypename: { unionMember: true },
303+
mappers: { Child: 'Wrapper<{T}>', MyType: 'MyWrapper<{T}>' },
304+
},
305+
{ outputFile: '' }
306+
);
307+
308+
expect(result.content).toBeSimilarStringTo(`
309+
export type ResolversUnionTypes = {
310+
ChildUnion: ( Wrapper<Omit<Child, 'parent'> & { parent?: Maybe<ResolversTypes['MyType']> }> & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
311+
MyUnion: ( MyWrapper<Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> }> & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
312+
};
313+
`);
314+
expect(result.content).toBeSimilarStringTo(`
315+
export type ResolversUnionParentTypes = {
316+
ChildUnion: ( Wrapper<Omit<Child, 'parent'> & { parent?: Maybe<ResolversParentTypes['MyType']> }> & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
317+
MyUnion: ( MyWrapper<Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> }> & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
318+
};
319+
`);
320+
});
321+
322+
it('resolversNonOptionalTypename - adds non-optional typenames to ResolversUnionTypes for default mappers with placeholder', async () => {
323+
const result = await plugin(
324+
resolversTestingSchema,
325+
[],
326+
{
327+
resolversNonOptionalTypename: { unionMember: true },
328+
defaultMapper: 'Partial<{T}>',
329+
},
330+
{ outputFile: '' }
331+
);
332+
333+
expect(result.content).toBeSimilarStringTo(`
334+
export type ResolversUnionTypes = {
335+
ChildUnion: ( Partial<Child> & { __typename: 'Child' } ) | ( Partial<MyOtherType> & { __typename: 'MyOtherType' } );
336+
MyUnion: ( Partial<Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> }> & { __typename: 'MyType' } ) | ( Partial<MyOtherType> & { __typename: 'MyOtherType' } );
337+
};
338+
`);
339+
expect(result.content).toBeSimilarStringTo(`
340+
export type ResolversUnionParentTypes = {
341+
ChildUnion: ( Partial<Child> & { __typename: 'Child' } ) | ( Partial<MyOtherType> & { __typename: 'MyOtherType' } );
342+
MyUnion: ( Partial<Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> }> & { __typename: 'MyType' } ) | ( Partial<MyOtherType> & { __typename: 'MyOtherType' } );
343+
};
344+
`);
345+
});
346+
347+
it('resolversNonOptionalTypename - does not create ResolversUnionTypes for default mappers with no placeholder', async () => {
348+
const result = await plugin(
349+
resolversTestingSchema,
350+
[],
351+
{
352+
resolversNonOptionalTypename: { unionMember: true },
353+
defaultMapper: '{}',
354+
},
355+
{ outputFile: '' }
356+
);
357+
358+
expect(result.content).not.toBeSimilarStringTo('export type ResolversUnionTypes');
359+
expect(result.content).not.toBeSimilarStringTo('export type ResolversUnionParentTypes');
360+
});
271361
});
272362

273363
it('directiveResolverMappings - should generate correct types (import definition)', async () => {

0 commit comments

Comments
 (0)