Skip to content

Commit

Permalink
[typescript-resolvers] Fix resolversNonOptionalTypename for union mem…
Browse files Browse the repository at this point in the history
…bers in mapper cases (#9231)

* Handle resolversNonOptionalTypename override for all member unions

* Add tests for mapper and placeholder cases

* Add changeset
  • Loading branch information
eddeee888 committed Apr 4, 2023
1 parent ee63489 commit 402cb8a
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 42 deletions.
6 changes: 6 additions & 0 deletions .changeset/strange-years-hope.md
@@ -0,0 +1,6 @@
---
'@graphql-codegen/visitor-plugin-common': patch
'@graphql-codegen/typescript-resolvers': patch
---

Implement resolversNonOptionalTypename for mapper cases
Expand Up @@ -898,46 +898,57 @@ export class BaseResolversVisitor<
const schemaType = allSchemaTypes[typeName];

if (isUnionType(schemaType)) {
const referencedTypes = schemaType.getTypes().map(unionMemberType => {
const isUnionMemberMapped = this.config.mappers[unionMemberType.name];

// 1. If mapped without placehoder, just use it without doing extra checks
if (isUnionMemberMapped && !hasPlaceholder(isUnionMemberMapped.type)) {
return isUnionMemberMapped.type;
}
const referencedTypes = schemaType
.getTypes()
.map(unionMemberType => {
const isUnionMemberMapped = this.config.mappers[unionMemberType.name];

// 1. If mapped without placehoder, just use it without doing extra checks
if (isUnionMemberMapped && !hasPlaceholder(isUnionMemberMapped.type)) {
return { typename: unionMemberType.name, unionMemberValue: isUnionMemberMapped.type };
}

// 2. Work out value for union member type
// 2a. By default, use the typescript type
let unionMemberValue = this.convertName(unionMemberType.name, {}, true);
// 2. Work out value for union member type
// 2a. By default, use the typescript type
let unionMemberValue = this.convertName(unionMemberType.name, {}, true);

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

// 2c. If union member is mapped with placeholder, use the "type with maybe Omit" as {T}
if (isUnionMemberMapped && hasPlaceholder(isUnionMemberMapped.type)) {
return replacePlaceholder(isUnionMemberMapped.type, unionMemberValue);
}
// 2c. If union member is mapped with placeholder, use the "type with maybe Omit" as {T}
if (isUnionMemberMapped && hasPlaceholder(isUnionMemberMapped.type)) {
return {
typename: unionMemberType.name,
unionMemberValue: replacePlaceholder(isUnionMemberMapped.type, unionMemberValue),
};
}

// 2d. If has default mapper with placeholder, use the "type with maybe Omit" as {T}
const hasDefaultMapper = !!this.config.defaultMapper?.type;
const isScalar = this.config.scalars[typeName];
if (hasDefaultMapper && hasPlaceholder(this.config.defaultMapper.type)) {
const finalTypename = isScalar ? this._getScalar(typeName) : unionMemberValue;
return replacePlaceholder(this.config.defaultMapper.type, finalTypename);
}
// 2d. If has default mapper with placeholder, use the "type with maybe Omit" as {T}
const hasDefaultMapper = !!this.config.defaultMapper?.type;
const isScalar = this.config.scalars[typeName];
if (hasDefaultMapper && hasPlaceholder(this.config.defaultMapper.type)) {
const finalTypename = isScalar ? this._getScalar(typeName) : unionMemberValue;
return {
typename: unionMemberType.name,
unionMemberValue: replacePlaceholder(this.config.defaultMapper.type, finalTypename),
};
}

const nonOptionalTypenameModifier = this.config.resolversNonOptionalTypename.unionMember
? ` & { __typename: "${unionMemberType}" }`
: '';
return { typename: unionMemberType.name, unionMemberValue };
})
.map(({ typename, unionMemberValue }) => {
const nonOptionalTypenameModifier = this.config.resolversNonOptionalTypename.unionMember
? ` & { __typename: '${typename}' }`
: '';

return `${unionMemberValue}${nonOptionalTypenameModifier}`;
});
res[typeName] = referencedTypes.map(type => `( ${type} )`).join(' | '); // Must wrap every union member in explicit "( )" to separate the members
return `( ${unionMemberValue}${nonOptionalTypenameModifier} )`; // Must wrap every union member in explicit "( )" to separate the members
});
res[typeName] = referencedTypes.join(' | ');
}
return res;
}, {});
Expand Down
106 changes: 98 additions & 8 deletions packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts
Expand Up @@ -235,14 +235,14 @@ export type MyTypeResolvers<ContextType = any, ParentType extends ResolversParen

expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionTypes = {
ChildUnion: ( Child & { __typename: "Child" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> } & { __typename: "MyType" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
ChildUnion: ( Child & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> } & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
};
`);
expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionParentTypes = {
ChildUnion: ( Child & { __typename: "Child" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> } & { __typename: "MyType" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
ChildUnion: ( Child & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> } & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
};
`);
});
Expand All @@ -257,17 +257,107 @@ export type MyTypeResolvers<ContextType = any, ParentType extends ResolversParen

expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionTypes = {
ChildUnion: ( Child & { __typename: "Child" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> } & { __typename: "MyType" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
ChildUnion: ( Child & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> } & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
};
`);
expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionParentTypes = {
ChildUnion: ( Child & { __typename: "Child" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> } & { __typename: "MyType" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
ChildUnion: ( Child & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> } & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
};
`);
});

it('resolversNonOptionalTypename - adds non-optional typenames to ResolversUnionTypes for mappers with no placeholder', async () => {
const result = await plugin(
resolversTestingSchema,
[],
{
resolversNonOptionalTypename: { unionMember: true },
mappers: { Child: 'ChildMapper', MyType: 'MyTypeMapper' },
},
{ outputFile: '' }
);

expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionTypes = {
ChildUnion: ( ChildMapper & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
MyUnion: ( MyTypeMapper & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
};
`);
expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionParentTypes = {
ChildUnion: ( ChildMapper & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
MyUnion: ( MyTypeMapper & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
};
`);
});

it('resolversNonOptionalTypename - adds non-optional typenames to ResolversUnionTypes for mappers with placeholder', async () => {
const result = await plugin(
resolversTestingSchema,
[],
{
resolversNonOptionalTypename: { unionMember: true },
mappers: { Child: 'Wrapper<{T}>', MyType: 'MyWrapper<{T}>' },
},
{ outputFile: '' }
);

expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionTypes = {
ChildUnion: ( Wrapper<Omit<Child, 'parent'> & { parent?: Maybe<ResolversTypes['MyType']> }> & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
MyUnion: ( MyWrapper<Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> }> & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
};
`);
expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionParentTypes = {
ChildUnion: ( Wrapper<Omit<Child, 'parent'> & { parent?: Maybe<ResolversParentTypes['MyType']> }> & { __typename: 'Child' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
MyUnion: ( MyWrapper<Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> }> & { __typename: 'MyType' } ) | ( MyOtherType & { __typename: 'MyOtherType' } );
};
`);
});

it('resolversNonOptionalTypename - adds non-optional typenames to ResolversUnionTypes for default mappers with placeholder', async () => {
const result = await plugin(
resolversTestingSchema,
[],
{
resolversNonOptionalTypename: { unionMember: true },
defaultMapper: 'Partial<{T}>',
},
{ outputFile: '' }
);

expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionTypes = {
ChildUnion: ( Partial<Child> & { __typename: 'Child' } ) | ( Partial<MyOtherType> & { __typename: 'MyOtherType' } );
MyUnion: ( Partial<Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> }> & { __typename: 'MyType' } ) | ( Partial<MyOtherType> & { __typename: 'MyOtherType' } );
};
`);
expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionParentTypes = {
ChildUnion: ( Partial<Child> & { __typename: 'Child' } ) | ( Partial<MyOtherType> & { __typename: 'MyOtherType' } );
MyUnion: ( Partial<Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> }> & { __typename: 'MyType' } ) | ( Partial<MyOtherType> & { __typename: 'MyOtherType' } );
};
`);
});

it('resolversNonOptionalTypename - does not create ResolversUnionTypes for default mappers with no placeholder', async () => {
const result = await plugin(
resolversTestingSchema,
[],
{
resolversNonOptionalTypename: { unionMember: true },
defaultMapper: '{}',
},
{ outputFile: '' }
);

expect(result.content).not.toBeSimilarStringTo('export type ResolversUnionTypes');
expect(result.content).not.toBeSimilarStringTo('export type ResolversUnionParentTypes');
});
});

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

0 comments on commit 402cb8a

Please sign in to comment.