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

[resolvers] Add generateInternalResolversIfNeeded.__isTypeOf #10138

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 0 additions & 2 deletions .changeset/fifty-dodos-marry.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@
Add `generateInternalResolversIfNeeded` option

This option can be used to generate more correct types for internal resolvers. For example, only generate `__resolveReference` if the federation object has a resolvable `@key`.

In the future, this option can be extended to support other internal resolvers e.g. `__isTypeOf` is only generated for implementing types and union members.
9 changes: 9 additions & 0 deletions .changeset/pink-rice-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@graphql-codegen/visitor-plugin-common': minor
'@graphql-codegen/typescript-resolvers': minor
'@graphql-codegen/plugin-helpers': minor
---

Implement `generateInternalResolversIfNeeded.__isTypeOf` option

When set to `true`, `__isTypeOf` resolver will only be generated for implementing types and union members.
Original file line number Diff line number Diff line change
Expand Up @@ -582,10 +582,9 @@ export interface RawResolversConfig extends RawConfig {
internalResolversPrefix?: string;
/**
* @type object
* @default { __resolveReference: false }
* @default { __resolveReference: false, __isTypeOf: false }
* @description If relevant internal resolvers are set to `true`, the resolver type will only be generated if the right conditions are met.
* Enabling this allows a more correct type generation for the resolvers.
* For example:
* Enabling this allows a more correct type generation for the resolvers:
* - `__isTypeOf` is generated for implementing types and union members
* - `__resolveReference` is generated for federation types that have at least one resolvable `@key` directive
*/
Expand Down Expand Up @@ -675,8 +674,8 @@ export class BaseResolversVisitor<
protected _hasReferencedResolversUnionTypes = false;
protected _hasReferencedResolversInterfaceTypes = false;
protected _resolversUnionTypes: Record<string, string> = {};
protected _resolversUnionParentTypes: Record<string, string> = {};
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

_resolversUnionParentTypes was used previously but not needed anymore. Removing in this PR as a drive-by

protected _resolversInterfaceTypes: Record<string, string> = {};
protected _objectTypesWithIsTypeOf: Record<string, true> = {};
protected _rootTypeNames = new Set<string>();
protected _globalDeclarations = new Set<string>();
protected _federation: ApolloFederation;
Expand Down Expand Up @@ -722,6 +721,7 @@ export class BaseResolversVisitor<
internalResolversPrefix: getConfigValue(rawConfig.internalResolversPrefix, '__'),
generateInternalResolversIfNeeded: {
__resolveReference: rawConfig.generateInternalResolversIfNeeded?.__resolveReference ?? false,
__isTypeOf: rawConfig.generateInternalResolversIfNeeded?.__isTypeOf ?? false,
},
resolversNonOptionalTypename: normalizeResolversNonOptionalTypename(
getConfigValue(rawConfig.resolversNonOptionalTypename, false)
Expand Down Expand Up @@ -993,9 +993,16 @@ export class BaseResolversVisitor<

if (isUnionType(schemaType)) {
const { unionMember, excludeTypes } = this.config.resolversNonOptionalTypename;

const memberTypes = schemaType.getTypes();

for (const type of memberTypes) {
this._objectTypesWithIsTypeOf[type.name] = true;
}

res[typeName] = this.getAbstractMembersType({
typeName,
memberTypes: schemaType.getTypes(),
memberTypes,
isTypenameNonOptional: unionMember && !excludeTypes?.includes(typeName),
});
}
Expand Down Expand Up @@ -1032,6 +1039,10 @@ export class BaseResolversVisitor<

const { interfaceImplementingType, excludeTypes } = this.config.resolversNonOptionalTypename;

for (const type of implementingTypes) {
this._objectTypesWithIsTypeOf[type.name] = true;
}

res[typeName] = this.getAbstractMembersType({
typeName,
memberTypes: implementingTypes,
Expand Down Expand Up @@ -1595,7 +1606,11 @@ export class BaseResolversVisitor<
);
});

if (!rootType) {
if (
!rootType &&
(!this.config.generateInternalResolversIfNeeded.__isTypeOf ||
(this.config.generateInternalResolversIfNeeded.__isTypeOf && this._objectTypesWithIsTypeOf[typeName]))
) {
fieldsContent.push(
indent(
`${
Expand Down
1 change: 1 addition & 0 deletions packages/plugins/other/visitor-plugin-common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,6 @@ export interface ResolversNonOptionalTypenameConfig {

export interface GenerateInternalResolversIfNeededConfig {
__resolveReference?: boolean;
__isTypeOf?: boolean;
}
export type NormalizedGenerateInternalResolversIfNeededConfig = Required<GenerateInternalResolversIfNeededConfig>;
Original file line number Diff line number Diff line change
Expand Up @@ -406,4 +406,102 @@ describe('TypeScript Resolvers Plugin - Interfaces', () => {
};
`);
});

it('if generateInternalResolversIfNeeded.__isTypeOf = false (default), generates __isTypeOf for all object types', async () => {
const schema = buildSchema(/* GraphQL */ `
interface Node {
id: ID!
}

type Cat implements Node {
id: ID!
name: String!
}

type Dog implements Node {
id: ID!
isGoodBoy: Boolean!
}

type Human {
_id: ID!
}
`);

const result = await plugin(schema, [], {}, { outputFile: '' });

expect(result.content).toBeSimilarStringTo(`
export type CatResolvers<ContextType = any, ParentType extends ResolversParentTypes['Cat'] = ResolversParentTypes['Cat']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}
`);

expect(result.content).toBeSimilarStringTo(`
export type DogResolvers<ContextType = any, ParentType extends ResolversParentTypes['Dog'] = ResolversParentTypes['Dog']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
isGoodBoy?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

expect(result.content).toBeSimilarStringTo(`
export type HumanResolvers<ContextType = any, ParentType extends ResolversParentTypes['Human'] = ResolversParentTypes['Human']> = {
_id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);
});

it('if generateInternalResolversIfNeeded.__isTypeOf = true, generates __isTypeOf for only implementing object types', async () => {
const schema = buildSchema(/* GraphQL */ `
interface Node {
id: ID!
}

type Cat implements Node {
id: ID!
name: String!
}

type Dog implements Node {
id: ID!
isGoodBoy: Boolean!
}

type Human {
_id: ID!
}
`);

const result = await plugin(
schema,
[],
{ generateInternalResolversIfNeeded: { __isTypeOf: true } },
{ outputFile: '' }
);

expect(result.content).toBeSimilarStringTo(`
export type CatResolvers<ContextType = any, ParentType extends ResolversParentTypes['Cat'] = ResolversParentTypes['Cat']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}
`);

expect(result.content).toBeSimilarStringTo(`
export type DogResolvers<ContextType = any, ParentType extends ResolversParentTypes['Dog'] = ResolversParentTypes['Dog']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
isGoodBoy?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

expect(result.content).toBeSimilarStringTo(`
export type HumanResolvers<ContextType = any, ParentType extends ResolversParentTypes['Human'] = ResolversParentTypes['Human']> = {
_id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
};
`);
});
});
114 changes: 114 additions & 0 deletions packages/plugins/typescript/resolvers/tests/ts-resolvers.union.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,118 @@ describe('TypeScript Resolvers Plugin - Union', () => {
expect(content.content).not.toBeSimilarStringTo(`export type ResolversUnionTypes`);
expect(content.content).not.toBeSimilarStringTo(`export type ResolversUnionParentTypes`);
});

it('if generateInternalResolversIfNeeded.__isTypeOf = false (default), generates __isTypeOf for all object types', async () => {
const schema = buildSchema(/* GraphQL */ `
type MemberOne {
id: ID!
}

type MemberTwo {
id: ID!
name: String!
}

type MemberThree {
id: ID!
isMember: Boolean!
}

type Normal {
id: ID!
}
`);

const result = await plugin(schema, [], {}, { outputFile: '' });

expect(result.content).toBeSimilarStringTo(`
export type MemberOneResolvers<ContextType = any, ParentType extends ResolversParentTypes['MemberOne'] = ResolversParentTypes['MemberOne']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}
`);

expect(result.content).toBeSimilarStringTo(`
export type MemberTwoResolvers<ContextType = any, ParentType extends ResolversParentTypes['MemberTwo'] = ResolversParentTypes['MemberTwo']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

expect(result.content).toBeSimilarStringTo(`
export type MemberThreeResolvers<ContextType = any, ParentType extends ResolversParentTypes['MemberThree'] = ResolversParentTypes['MemberThree']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
isMember?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

expect(result.content).toBeSimilarStringTo(`
export type NormalResolvers<ContextType = any, ParentType extends ResolversParentTypes['Normal'] = ResolversParentTypes['Normal']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);
});

it('if generateInternalResolversIfNeeded.__isTypeOf = true, generates __isTypeOf for only union members', async () => {
const schema = buildSchema(/* GraphQL */ `
type MemberOne {
id: ID!
}

type MemberTwo {
id: ID!
name: String!
}

type MemberThree {
id: ID!
isMember: Boolean!
}

union Union = MemberOne | MemberTwo | MemberThree

type Normal {
id: ID!
}
`);

const result = await plugin(
schema,
[],
{ generateInternalResolversIfNeeded: { __isTypeOf: true } },
{ outputFile: '' }
);

expect(result.content).toBeSimilarStringTo(`
export type MemberOneResolvers<ContextType = any, ParentType extends ResolversParentTypes['MemberOne'] = ResolversParentTypes['MemberOne']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}
`);

expect(result.content).toBeSimilarStringTo(`
export type MemberTwoResolvers<ContextType = any, ParentType extends ResolversParentTypes['MemberTwo'] = ResolversParentTypes['MemberTwo']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

expect(result.content).toBeSimilarStringTo(`
export type MemberThreeResolvers<ContextType = any, ParentType extends ResolversParentTypes['MemberThree'] = ResolversParentTypes['MemberThree']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
isMember?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

expect(result.content).toBeSimilarStringTo(`
export type NormalResolvers<ContextType = any, ParentType extends ResolversParentTypes['Normal'] = ResolversParentTypes['Normal']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
};
`);
});
});
Loading