Skip to content

Commit

Permalink
[resolvers] Add avoidCheckingAbstractTypesRecursively option to sup…
Browse files Browse the repository at this point in the history
…port nested defaultMappers (#10141)

* Add avoidCheckingAbstractTypesRecursively to control whether to recursively checks and generates abstract nested types

* Add changeset

* Add avoidCheckingAbstractTypesRecursively to defaultMapper
  • Loading branch information
eddeee888 authored Oct 14, 2024
1 parent c7af639 commit a235051
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .changeset/grumpy-moose-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@graphql-codegen/visitor-plugin-common': minor
'@graphql-codegen/typescript-resolvers': minor
---

Add avoidCheckingAbstractTypesRecursively to avoid checking and generating abstract types recursively

For users that already sets recursive default mappers e.g. `Partial<{T}>` or `DeepPartial<{T}>`, having both options on will cause a nested loop which eventually crashes Codegen. In such case, setting `avoidCheckingAbstractTypesRecursively: true` allows users to continue to use recursive default mappers as before.
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export interface ParsedResolversConfig extends ParsedConfig {
onlyResolveTypeForInterfaces: boolean;
directiveResolverMappings: Record<string, string>;
resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig;
avoidCheckingAbstractTypesRecursively: boolean;
}

type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null;
Expand Down Expand Up @@ -394,6 +395,7 @@ export interface RawResolversConfig extends RawConfig {
* plugins: ['typescript', 'typescript-resolver', { add: { content: "import { DeepPartial } from 'utility-types';" } }],
* config: {
* defaultMapper: 'DeepPartial<{T}>',
* avoidCheckingAbstractTypesRecursively: true // required if you have complex nested abstract types
* },
* },
* },
Expand Down Expand Up @@ -644,6 +646,13 @@ export interface RawResolversConfig extends RawConfig {
* ```
*/
resolversNonOptionalTypename?: boolean | ResolversNonOptionalTypenameConfig;
/**
* @type boolean
* @default false
* @description If true, recursively goes through all object type's fields, checks if they have abstract types and generates expected types correctly.
* This may not work for cases where provided default mapper types are also nested e.g. `defaultMapper: DeepPartial<{T}>` or `defaultMapper: Partial<{T}>`.
*/
avoidCheckingAbstractTypesRecursively?: boolean;
/**
* @ignore
*/
Expand Down Expand Up @@ -726,6 +735,7 @@ export class BaseResolversVisitor<
resolversNonOptionalTypename: normalizeResolversNonOptionalTypename(
getConfigValue(rawConfig.resolversNonOptionalTypename, false)
),
avoidCheckingAbstractTypesRecursively: getConfigValue(rawConfig.avoidCheckingAbstractTypesRecursively, false),
...additionalConfig,
} as TPluginConfig);

Expand Down Expand Up @@ -1851,7 +1861,7 @@ export class BaseResolversVisitor<
const isObject = isObjectType(baseType);
let isObjectWithAbstractType = false;

if (isObject) {
if (isObject && !this.config.avoidCheckingAbstractTypesRecursively) {
isObjectWithAbstractType = checkIfObjectTypeHasAbstractTypesRecursively(baseType, {
isObjectWithAbstractType,
checkedTypesWithNestedAbstractTypes: this._checkedTypesWithNestedAbstractTypes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,4 +406,59 @@ describe('TypeScript Resolvers Plugin - Interfaces', () => {
};
`);
});

it('does not generate nested types when avoidCheckingAbstractTypesRecursively=true', async () => {
const schema = buildSchema(/* GraphQL */ `
interface I_Node {
id: ID!
}
type T_WithNode {
node: I_Node!
}
type T_Type1 {
id: ID!
type2: T_Type2!
withNode: T_WithNode! # abstract type is in T_Type1
}
type T_Type2 {
id: ID!
type1: T_Type1!
}
`);

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

expect(result.content).toBeSimilarStringTo(`
export type ResolversInterfaceTypes<_RefType extends Record<string, unknown>> = {
I_Node: never;
};
`);

expect(result.content).toBeSimilarStringTo(`
export type ResolversTypes = {
I_Node: ResolverTypeWrapper<ResolversInterfaceTypes<ResolversTypes>['I_Node']>;
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
T_WithNode: ResolverTypeWrapper<Omit<T_WithNode, 'node'> & { node: ResolversTypes['I_Node'] }>;
T_Type1: ResolverTypeWrapper<T_Type1>;
T_Type2: ResolverTypeWrapper<T_Type2>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
String: ResolverTypeWrapper<Scalars['String']['output']>;
};
`);

expect(result.content).toBeSimilarStringTo(`
export type ResolversParentTypes = {
I_Node: ResolversInterfaceTypes<ResolversParentTypes>['I_Node'];
ID: Scalars['ID']['output'];
T_WithNode: Omit<T_WithNode, 'node'> & { node: ResolversParentTypes['I_Node'] };
T_Type1: T_Type1;
T_Type2: T_Type2;
Boolean: Scalars['Boolean']['output'];
String: Scalars['String']['output'];
};
`);
});
});
142 changes: 142 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,146 @@ describe('TypeScript Resolvers Plugin - Union', () => {
expect(content.content).not.toBeSimilarStringTo(`export type ResolversUnionTypes`);
expect(content.content).not.toBeSimilarStringTo(`export type ResolversUnionParentTypes`);
});

it('generates nested types when avoidCheckingAbstractTypesRecursively=false (default)', async () => {
const schema = buildSchema(/* GraphQL */ `
type StandardError {
error: String!
}
type User {
id: ID!
fullName: String!
posts: PostsPayload!
}
type UserResult {
result: User
recommendedPosts: PostsPayload!
}
union UserPayload = UserResult | StandardError
type Post {
author: String
comment: String
}
type PostsResult {
results: [Post!]!
}
union PostsPayload = PostsResult | StandardError
`);

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

expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionTypes<_RefType extends Record<string, unknown>> = {
UserPayload: ( Omit<UserResult, 'result' | 'recommendedPosts'> & { result?: Maybe<_RefType['User']>, recommendedPosts: _RefType['PostsPayload'] } ) | ( StandardError );
PostsPayload: ( PostsResult ) | ( StandardError );
};
`);

expect(result.content).toBeSimilarStringTo(`
export type ResolversTypes = {
StandardError: ResolverTypeWrapper<StandardError>;
String: ResolverTypeWrapper<Scalars['String']['output']>;
User: ResolverTypeWrapper<Omit<User, 'posts'> & { posts: ResolversTypes['PostsPayload'] }>;
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
UserResult: ResolverTypeWrapper<Omit<UserResult, 'result' | 'recommendedPosts'> & { result?: Maybe<ResolversTypes['User']>, recommendedPosts: ResolversTypes['PostsPayload'] }>;
UserPayload: ResolverTypeWrapper<ResolversUnionTypes<ResolversTypes>['UserPayload']>;
Post: ResolverTypeWrapper<Post>;
PostsResult: ResolverTypeWrapper<PostsResult>;
PostsPayload: ResolverTypeWrapper<ResolversUnionTypes<ResolversTypes>['PostsPayload']>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
};
`);

expect(result.content).toBeSimilarStringTo(`
export type ResolversParentTypes = {
StandardError: StandardError;
String: Scalars['String']['output'];
User: Omit<User, 'posts'> & { posts: ResolversParentTypes['PostsPayload'] };
ID: Scalars['ID']['output'];
UserResult: Omit<UserResult, 'result' | 'recommendedPosts'> & { result?: Maybe<ResolversParentTypes['User']>, recommendedPosts: ResolversParentTypes['PostsPayload'] };
UserPayload: ResolversUnionTypes<ResolversParentTypes>['UserPayload'];
Post: Post;
PostsResult: PostsResult;
PostsPayload: ResolversUnionTypes<ResolversParentTypes>['PostsPayload'];
Boolean: Scalars['Boolean']['output'];
};
`);
});

it('does not generate nested types when avoidCheckingAbstractTypesRecursively=true', async () => {
const schema = buildSchema(/* GraphQL */ `
type StandardError {
error: String!
}
type User {
id: ID!
fullName: String!
posts: PostsPayload!
}
type UserResult {
result: User
recommendedPosts: PostsPayload!
}
union UserPayload = UserResult | StandardError
type Post {
author: String
comment: String
}
type PostsResult {
results: [Post!]!
}
union PostsPayload = PostsResult | StandardError
`);

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

expect(result.content).toBeSimilarStringTo(`
export type ResolversUnionTypes<_RefType extends Record<string, unknown>> = {
UserPayload: ( Omit<UserResult, 'recommendedPosts'> & { recommendedPosts: _RefType['PostsPayload'] } ) | ( StandardError );
PostsPayload: ( PostsResult ) | ( StandardError );
};
`);

expect(result.content).toBeSimilarStringTo(`
export type ResolversTypes = {
StandardError: ResolverTypeWrapper<StandardError>;
String: ResolverTypeWrapper<Scalars['String']['output']>;
User: ResolverTypeWrapper<Omit<User, 'posts'> & { posts: ResolversTypes['PostsPayload'] }>;
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
UserResult: ResolverTypeWrapper<Omit<UserResult, 'recommendedPosts'> & { recommendedPosts: ResolversTypes['PostsPayload'] }>;
UserPayload: ResolverTypeWrapper<ResolversUnionTypes<ResolversTypes>['UserPayload']>;
Post: ResolverTypeWrapper<Post>;
PostsResult: ResolverTypeWrapper<PostsResult>;
PostsPayload: ResolverTypeWrapper<ResolversUnionTypes<ResolversTypes>['PostsPayload']>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
};
`);

expect(result.content).toBeSimilarStringTo(`
export type ResolversParentTypes = {
StandardError: StandardError;
String: Scalars['String']['output'];
User: Omit<User, 'posts'> & { posts: ResolversParentTypes['PostsPayload'] };
ID: Scalars['ID']['output'];
UserResult: Omit<UserResult, 'recommendedPosts'> & { recommendedPosts: ResolversParentTypes['PostsPayload'] };
UserPayload: ResolversUnionTypes<ResolversParentTypes>['UserPayload'];
Post: Post;
PostsResult: PostsResult;
PostsPayload: ResolversUnionTypes<ResolversParentTypes>['PostsPayload'];
Boolean: Scalars['Boolean']['output'];
};
`);
});
});

0 comments on commit a235051

Please sign in to comment.