From a23505180ac2f275a55ece27162ec9bfcdc52e03 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 15 Oct 2024 10:42:10 +1100 Subject: [PATCH] [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 --- .changeset/grumpy-moose-bake.md | 8 + .../src/base-resolvers-visitor.ts | 12 +- .../tests/ts-resolvers.interface.spec.ts | 55 +++++++ .../tests/ts-resolvers.union.spec.ts | 142 ++++++++++++++++++ 4 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 .changeset/grumpy-moose-bake.md diff --git a/.changeset/grumpy-moose-bake.md b/.changeset/grumpy-moose-bake.md new file mode 100644 index 00000000000..f3f05d28f6c --- /dev/null +++ b/.changeset/grumpy-moose-bake.md @@ -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. diff --git a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts index cf3f6386c2d..a5132c3a54a 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts @@ -81,6 +81,7 @@ export interface ParsedResolversConfig extends ParsedConfig { onlyResolveTypeForInterfaces: boolean; directiveResolverMappings: Record; resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig; + avoidCheckingAbstractTypesRecursively: boolean; } type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null; @@ -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 * }, * }, * }, @@ -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 */ @@ -726,6 +735,7 @@ export class BaseResolversVisitor< resolversNonOptionalTypename: normalizeResolversNonOptionalTypename( getConfigValue(rawConfig.resolversNonOptionalTypename, false) ), + avoidCheckingAbstractTypesRecursively: getConfigValue(rawConfig.avoidCheckingAbstractTypesRecursively, false), ...additionalConfig, } as TPluginConfig); @@ -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, diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts index 2066de7574c..332216111f6 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts @@ -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> = { + I_Node: never; + }; + `); + + expect(result.content).toBeSimilarStringTo(` + export type ResolversTypes = { + I_Node: ResolverTypeWrapper['I_Node']>; + ID: ResolverTypeWrapper; + T_WithNode: ResolverTypeWrapper & { node: ResolversTypes['I_Node'] }>; + T_Type1: ResolverTypeWrapper; + T_Type2: ResolverTypeWrapper; + Boolean: ResolverTypeWrapper; + String: ResolverTypeWrapper; + }; + `); + + expect(result.content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + I_Node: ResolversInterfaceTypes['I_Node']; + ID: Scalars['ID']['output']; + T_WithNode: Omit & { node: ResolversParentTypes['I_Node'] }; + T_Type1: T_Type1; + T_Type2: T_Type2; + Boolean: Scalars['Boolean']['output']; + String: Scalars['String']['output']; + }; + `); + }); }); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.union.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.union.spec.ts index 4df11c5f4f0..72580745c32 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.union.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.union.spec.ts @@ -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> = { + UserPayload: ( Omit & { result?: Maybe<_RefType['User']>, recommendedPosts: _RefType['PostsPayload'] } ) | ( StandardError ); + PostsPayload: ( PostsResult ) | ( StandardError ); + }; + `); + + expect(result.content).toBeSimilarStringTo(` + export type ResolversTypes = { + StandardError: ResolverTypeWrapper; + String: ResolverTypeWrapper; + User: ResolverTypeWrapper & { posts: ResolversTypes['PostsPayload'] }>; + ID: ResolverTypeWrapper; + UserResult: ResolverTypeWrapper & { result?: Maybe, recommendedPosts: ResolversTypes['PostsPayload'] }>; + UserPayload: ResolverTypeWrapper['UserPayload']>; + Post: ResolverTypeWrapper; + PostsResult: ResolverTypeWrapper; + PostsPayload: ResolverTypeWrapper['PostsPayload']>; + Boolean: ResolverTypeWrapper; + }; + `); + + expect(result.content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + StandardError: StandardError; + String: Scalars['String']['output']; + User: Omit & { posts: ResolversParentTypes['PostsPayload'] }; + ID: Scalars['ID']['output']; + UserResult: Omit & { result?: Maybe, recommendedPosts: ResolversParentTypes['PostsPayload'] }; + UserPayload: ResolversUnionTypes['UserPayload']; + Post: Post; + PostsResult: PostsResult; + PostsPayload: ResolversUnionTypes['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> = { + UserPayload: ( Omit & { recommendedPosts: _RefType['PostsPayload'] } ) | ( StandardError ); + PostsPayload: ( PostsResult ) | ( StandardError ); + }; + `); + + expect(result.content).toBeSimilarStringTo(` + export type ResolversTypes = { + StandardError: ResolverTypeWrapper; + String: ResolverTypeWrapper; + User: ResolverTypeWrapper & { posts: ResolversTypes['PostsPayload'] }>; + ID: ResolverTypeWrapper; + UserResult: ResolverTypeWrapper & { recommendedPosts: ResolversTypes['PostsPayload'] }>; + UserPayload: ResolverTypeWrapper['UserPayload']>; + Post: ResolverTypeWrapper; + PostsResult: ResolverTypeWrapper; + PostsPayload: ResolverTypeWrapper['PostsPayload']>; + Boolean: ResolverTypeWrapper; + }; + `); + + expect(result.content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + StandardError: StandardError; + String: Scalars['String']['output']; + User: Omit & { posts: ResolversParentTypes['PostsPayload'] }; + ID: Scalars['ID']['output']; + UserResult: Omit & { recommendedPosts: ResolversParentTypes['PostsPayload'] }; + UserPayload: ResolversUnionTypes['UserPayload']; + Post: Post; + PostsResult: PostsResult; + PostsPayload: ResolversUnionTypes['PostsPayload']; + Boolean: Scalars['Boolean']['output']; + }; + `); + }); });