Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[typescript-resolvers] Fix resolversNonOptionalTypename for union members in mapper cases #9231

Merged
merged 5 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/strange-years-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-codegen/visitor-plugin-common': patch
'@graphql-codegen/typescript-resolvers': patch
---

Implement resolversNonOptionalTypename for mapper cases
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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