Skip to content

Commit a235051

Browse files
authored
[resolvers] Add avoidCheckingAbstractTypesRecursively option to support nested defaultMappers (#10141)
* Add avoidCheckingAbstractTypesRecursively to control whether to recursively checks and generates abstract nested types * Add changeset * Add avoidCheckingAbstractTypesRecursively to defaultMapper
1 parent c7af639 commit a235051

File tree

4 files changed

+216
-1
lines changed

4 files changed

+216
-1
lines changed

.changeset/grumpy-moose-bake.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@graphql-codegen/visitor-plugin-common': minor
3+
'@graphql-codegen/typescript-resolvers': minor
4+
---
5+
6+
Add avoidCheckingAbstractTypesRecursively to avoid checking and generating abstract types recursively
7+
8+
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.

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export interface ParsedResolversConfig extends ParsedConfig {
8181
onlyResolveTypeForInterfaces: boolean;
8282
directiveResolverMappings: Record<string, string>;
8383
resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig;
84+
avoidCheckingAbstractTypesRecursively: boolean;
8485
}
8586

8687
type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null;
@@ -394,6 +395,7 @@ export interface RawResolversConfig extends RawConfig {
394395
* plugins: ['typescript', 'typescript-resolver', { add: { content: "import { DeepPartial } from 'utility-types';" } }],
395396
* config: {
396397
* defaultMapper: 'DeepPartial<{T}>',
398+
* avoidCheckingAbstractTypesRecursively: true // required if you have complex nested abstract types
397399
* },
398400
* },
399401
* },
@@ -644,6 +646,13 @@ export interface RawResolversConfig extends RawConfig {
644646
* ```
645647
*/
646648
resolversNonOptionalTypename?: boolean | ResolversNonOptionalTypenameConfig;
649+
/**
650+
* @type boolean
651+
* @default false
652+
* @description If true, recursively goes through all object type's fields, checks if they have abstract types and generates expected types correctly.
653+
* This may not work for cases where provided default mapper types are also nested e.g. `defaultMapper: DeepPartial<{T}>` or `defaultMapper: Partial<{T}>`.
654+
*/
655+
avoidCheckingAbstractTypesRecursively?: boolean;
647656
/**
648657
* @ignore
649658
*/
@@ -726,6 +735,7 @@ export class BaseResolversVisitor<
726735
resolversNonOptionalTypename: normalizeResolversNonOptionalTypename(
727736
getConfigValue(rawConfig.resolversNonOptionalTypename, false)
728737
),
738+
avoidCheckingAbstractTypesRecursively: getConfigValue(rawConfig.avoidCheckingAbstractTypesRecursively, false),
729739
...additionalConfig,
730740
} as TPluginConfig);
731741

@@ -1851,7 +1861,7 @@ export class BaseResolversVisitor<
18511861
const isObject = isObjectType(baseType);
18521862
let isObjectWithAbstractType = false;
18531863

1854-
if (isObject) {
1864+
if (isObject && !this.config.avoidCheckingAbstractTypesRecursively) {
18551865
isObjectWithAbstractType = checkIfObjectTypeHasAbstractTypesRecursively(baseType, {
18561866
isObjectWithAbstractType,
18571867
checkedTypesWithNestedAbstractTypes: this._checkedTypesWithNestedAbstractTypes,

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,4 +406,59 @@ describe('TypeScript Resolvers Plugin - Interfaces', () => {
406406
};
407407
`);
408408
});
409+
410+
it('does not generate nested types when avoidCheckingAbstractTypesRecursively=true', async () => {
411+
const schema = buildSchema(/* GraphQL */ `
412+
interface I_Node {
413+
id: ID!
414+
}
415+
416+
type T_WithNode {
417+
node: I_Node!
418+
}
419+
420+
type T_Type1 {
421+
id: ID!
422+
type2: T_Type2!
423+
withNode: T_WithNode! # abstract type is in T_Type1
424+
}
425+
426+
type T_Type2 {
427+
id: ID!
428+
type1: T_Type1!
429+
}
430+
`);
431+
432+
const result = await plugin(schema, [], { avoidCheckingAbstractTypesRecursively: true }, { outputFile: '' });
433+
434+
expect(result.content).toBeSimilarStringTo(`
435+
export type ResolversInterfaceTypes<_RefType extends Record<string, unknown>> = {
436+
I_Node: never;
437+
};
438+
`);
439+
440+
expect(result.content).toBeSimilarStringTo(`
441+
export type ResolversTypes = {
442+
I_Node: ResolverTypeWrapper<ResolversInterfaceTypes<ResolversTypes>['I_Node']>;
443+
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
444+
T_WithNode: ResolverTypeWrapper<Omit<T_WithNode, 'node'> & { node: ResolversTypes['I_Node'] }>;
445+
T_Type1: ResolverTypeWrapper<T_Type1>;
446+
T_Type2: ResolverTypeWrapper<T_Type2>;
447+
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
448+
String: ResolverTypeWrapper<Scalars['String']['output']>;
449+
};
450+
`);
451+
452+
expect(result.content).toBeSimilarStringTo(`
453+
export type ResolversParentTypes = {
454+
I_Node: ResolversInterfaceTypes<ResolversParentTypes>['I_Node'];
455+
ID: Scalars['ID']['output'];
456+
T_WithNode: Omit<T_WithNode, 'node'> & { node: ResolversParentTypes['I_Node'] };
457+
T_Type1: T_Type1;
458+
T_Type2: T_Type2;
459+
Boolean: Scalars['Boolean']['output'];
460+
String: Scalars['String']['output'];
461+
};
462+
`);
463+
});
409464
});

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

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,146 @@ describe('TypeScript Resolvers Plugin - Union', () => {
9494
expect(content.content).not.toBeSimilarStringTo(`export type ResolversUnionTypes`);
9595
expect(content.content).not.toBeSimilarStringTo(`export type ResolversUnionParentTypes`);
9696
});
97+
98+
it('generates nested types when avoidCheckingAbstractTypesRecursively=false (default)', async () => {
99+
const schema = buildSchema(/* GraphQL */ `
100+
type StandardError {
101+
error: String!
102+
}
103+
104+
type User {
105+
id: ID!
106+
fullName: String!
107+
posts: PostsPayload!
108+
}
109+
110+
type UserResult {
111+
result: User
112+
recommendedPosts: PostsPayload!
113+
}
114+
115+
union UserPayload = UserResult | StandardError
116+
117+
type Post {
118+
author: String
119+
comment: String
120+
}
121+
122+
type PostsResult {
123+
results: [Post!]!
124+
}
125+
126+
union PostsPayload = PostsResult | StandardError
127+
`);
128+
129+
const result = await plugin(schema, [], {}, { outputFile: '' });
130+
131+
expect(result.content).toBeSimilarStringTo(`
132+
export type ResolversUnionTypes<_RefType extends Record<string, unknown>> = {
133+
UserPayload: ( Omit<UserResult, 'result' | 'recommendedPosts'> & { result?: Maybe<_RefType['User']>, recommendedPosts: _RefType['PostsPayload'] } ) | ( StandardError );
134+
PostsPayload: ( PostsResult ) | ( StandardError );
135+
};
136+
`);
137+
138+
expect(result.content).toBeSimilarStringTo(`
139+
export type ResolversTypes = {
140+
StandardError: ResolverTypeWrapper<StandardError>;
141+
String: ResolverTypeWrapper<Scalars['String']['output']>;
142+
User: ResolverTypeWrapper<Omit<User, 'posts'> & { posts: ResolversTypes['PostsPayload'] }>;
143+
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
144+
UserResult: ResolverTypeWrapper<Omit<UserResult, 'result' | 'recommendedPosts'> & { result?: Maybe<ResolversTypes['User']>, recommendedPosts: ResolversTypes['PostsPayload'] }>;
145+
UserPayload: ResolverTypeWrapper<ResolversUnionTypes<ResolversTypes>['UserPayload']>;
146+
Post: ResolverTypeWrapper<Post>;
147+
PostsResult: ResolverTypeWrapper<PostsResult>;
148+
PostsPayload: ResolverTypeWrapper<ResolversUnionTypes<ResolversTypes>['PostsPayload']>;
149+
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
150+
};
151+
`);
152+
153+
expect(result.content).toBeSimilarStringTo(`
154+
export type ResolversParentTypes = {
155+
StandardError: StandardError;
156+
String: Scalars['String']['output'];
157+
User: Omit<User, 'posts'> & { posts: ResolversParentTypes['PostsPayload'] };
158+
ID: Scalars['ID']['output'];
159+
UserResult: Omit<UserResult, 'result' | 'recommendedPosts'> & { result?: Maybe<ResolversParentTypes['User']>, recommendedPosts: ResolversParentTypes['PostsPayload'] };
160+
UserPayload: ResolversUnionTypes<ResolversParentTypes>['UserPayload'];
161+
Post: Post;
162+
PostsResult: PostsResult;
163+
PostsPayload: ResolversUnionTypes<ResolversParentTypes>['PostsPayload'];
164+
Boolean: Scalars['Boolean']['output'];
165+
};
166+
`);
167+
});
168+
169+
it('does not generate nested types when avoidCheckingAbstractTypesRecursively=true', async () => {
170+
const schema = buildSchema(/* GraphQL */ `
171+
type StandardError {
172+
error: String!
173+
}
174+
175+
type User {
176+
id: ID!
177+
fullName: String!
178+
posts: PostsPayload!
179+
}
180+
181+
type UserResult {
182+
result: User
183+
recommendedPosts: PostsPayload!
184+
}
185+
186+
union UserPayload = UserResult | StandardError
187+
188+
type Post {
189+
author: String
190+
comment: String
191+
}
192+
193+
type PostsResult {
194+
results: [Post!]!
195+
}
196+
197+
union PostsPayload = PostsResult | StandardError
198+
`);
199+
200+
const result = await plugin(schema, [], { avoidCheckingAbstractTypesRecursively: true }, { outputFile: '' });
201+
202+
expect(result.content).toBeSimilarStringTo(`
203+
export type ResolversUnionTypes<_RefType extends Record<string, unknown>> = {
204+
UserPayload: ( Omit<UserResult, 'recommendedPosts'> & { recommendedPosts: _RefType['PostsPayload'] } ) | ( StandardError );
205+
PostsPayload: ( PostsResult ) | ( StandardError );
206+
};
207+
`);
208+
209+
expect(result.content).toBeSimilarStringTo(`
210+
export type ResolversTypes = {
211+
StandardError: ResolverTypeWrapper<StandardError>;
212+
String: ResolverTypeWrapper<Scalars['String']['output']>;
213+
User: ResolverTypeWrapper<Omit<User, 'posts'> & { posts: ResolversTypes['PostsPayload'] }>;
214+
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
215+
UserResult: ResolverTypeWrapper<Omit<UserResult, 'recommendedPosts'> & { recommendedPosts: ResolversTypes['PostsPayload'] }>;
216+
UserPayload: ResolverTypeWrapper<ResolversUnionTypes<ResolversTypes>['UserPayload']>;
217+
Post: ResolverTypeWrapper<Post>;
218+
PostsResult: ResolverTypeWrapper<PostsResult>;
219+
PostsPayload: ResolverTypeWrapper<ResolversUnionTypes<ResolversTypes>['PostsPayload']>;
220+
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
221+
};
222+
`);
223+
224+
expect(result.content).toBeSimilarStringTo(`
225+
export type ResolversParentTypes = {
226+
StandardError: StandardError;
227+
String: Scalars['String']['output'];
228+
User: Omit<User, 'posts'> & { posts: ResolversParentTypes['PostsPayload'] };
229+
ID: Scalars['ID']['output'];
230+
UserResult: Omit<UserResult, 'recommendedPosts'> & { recommendedPosts: ResolversParentTypes['PostsPayload'] };
231+
UserPayload: ResolversUnionTypes<ResolversParentTypes>['UserPayload'];
232+
Post: Post;
233+
PostsResult: PostsResult;
234+
PostsPayload: ResolversUnionTypes<ResolversParentTypes>['PostsPayload'];
235+
Boolean: Scalars['Boolean']['output'];
236+
};
237+
`);
238+
});
97239
});

0 commit comments

Comments
 (0)