diff --git a/.changeset/popular-colts-remember.md b/.changeset/popular-colts-remember.md new file mode 100644 index 00000000000..73ba18b74ed --- /dev/null +++ b/.changeset/popular-colts-remember.md @@ -0,0 +1,6 @@ +--- +'@graphql-codegen/visitor-plugin-common': minor +'@graphql-codegen/typescript-resolvers': minor +--- + +Add addInterfaceFieldResolverTypes option to support custom Interface resolver inheritance 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 43725b8e367..119db4119dc 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 @@ -65,6 +65,7 @@ export interface ParsedResolversConfig extends ParsedConfig { defaultMapper: ParsedMapper | null; avoidOptionals: NormalizedAvoidOptionalsConfig; addUnderscoreToArgsType: boolean; + addInterfaceFieldResolverTypes: boolean; enumValues: ParsedEnumValuesMap; resolverTypeWrapperSignature: string; federation: boolean; @@ -683,6 +684,40 @@ export interface RawResolversConfig extends RawConfig { * 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; + /** + * @description If true, add field resolver types to Interfaces. + * By default, GraphQL Interfaces do not trigger any field resolvers, + * meaning every implementing type must implement the same resolver for the shared fields. + * + * Some tools provide a way to change the default behaviour by making GraphQL Objects inherit + * missing resolvers from their Interface types. In these cases, it is fine to turn this option to true. + * + * For example, if you are using `@graphql-tools/schema#makeExecutableSchema` with `inheritResolversFromInterfaces: true`, + * you can make `addInterfaceFieldResolverTypes: true` as well + * https://the-guild.dev/graphql/tools/docs/generate-schema#makeexecutableschema + * + * @exampleMarkdown + * ```ts filename="codegen.ts" + * import type { CodegenConfig } from '@graphql-codegen/cli'; + * + * const config: CodegenConfig = { + * // ... + * generates: { + * 'path/to/file': { + * plugins: ['typescript', 'typescript-resolver'], + * config: { + * addInterfaceFieldResolverTypes: true, + * }, + * }, + * }, + * }; + * export default config; + * ``` + * + * @type boolean + * @default false + */ + addInterfaceFieldResolverTypes?: boolean; /** * @ignore */ @@ -769,6 +804,7 @@ export class BaseResolversVisitor< mapOrStr: rawConfig.enumValues, }), addUnderscoreToArgsType: getConfigValue(rawConfig.addUnderscoreToArgsType, false), + addInterfaceFieldResolverTypes: getConfigValue(rawConfig.addInterfaceFieldResolverTypes, false), contextType: parseMapper(rawConfig.contextType || 'any', 'ContextType'), fieldContextTypes: getConfigValue(rawConfig.fieldContextTypes, []), directiveContextTypes: getConfigValue(rawConfig.directiveContextTypes, []), @@ -2015,7 +2051,7 @@ export class BaseResolversVisitor< printContent(node, this.config.avoidOptionals.resolvers) ); for (const field of fields) { - if (field.meta.federation?.isResolveReference) { + if (field.meta.federation?.isResolveReference || this.config.addInterfaceFieldResolverTypes) { blockFields.push(field.value); } } diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts index 8757ecfdd89..cb367cec020 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts @@ -210,4 +210,50 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { " `); }); + + it('generates normal Interface fields with addInterfaceFieldResolverTypes:true', async () => { + const federatedSchema = /* GraphQL */ ` + type Query { + me: Person + } + + interface Person @key(fields: "id") { + id: ID! + name: PersonName! + } + + type User implements Person @key(fields: "id") { + id: ID! + name: PersonName! + } + + type Admin implements Person @key(fields: "id") { + id: ID! + name: PersonName! + canImpersonate: Boolean! + } + + type PersonName { + first: String! + last: String! + } + `; + + const content = await generate({ + schema: federatedSchema, + config: { + federation: true, + addInterfaceFieldResolverTypes: true, + }, + }); + + expect(content).toBeSimilarStringTo(` + export type PersonResolvers = { + __resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>; + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + name?: Resolver; + }; + `); + }); }); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts index b7e2e26b348..b558857820d 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts @@ -85,6 +85,22 @@ describe('TypeScript Resolvers Plugin', () => { }); describe('Config', () => { + it('addInterfaceFieldResolverTypes - should allow to have only resolveType for interfaces', async () => { + const config = { + addInterfaceFieldResolverTypes: true, + }; + const result = await plugin(resolversTestingSchema, [], config, { outputFile: '' }); + const content = await resolversTestingValidate(result, config, resolversTestingSchema); + + expect(content).toBeSimilarStringTo(` + export type WithChildrenResolvers = { + __resolveType: TypeResolveFn<'AnotherNodeWithAll', ParentType, ContextType>; + unionChildren?: Resolver, ParentType, ContextType>; + nodes?: Resolver, ParentType, ContextType>; + }; + `); + }); + it('optionalInfoArgument - should allow to have optional info argument', async () => { const config = { noSchemaStitching: true,