Skip to content

Commit 8258f1f

Browse files
authored
[typescript-resolvers] Add addInterfaceFieldResolverTypes option (#10449)
* Add addInterfaceFieldResolverTypes option to support custom Interface shared field inherit behaviour * Add changeset * Fix typos
1 parent 9f6ccb1 commit 8258f1f

File tree

4 files changed

+105
-1
lines changed

4 files changed

+105
-1
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-codegen/visitor-plugin-common': minor
3+
'@graphql-codegen/typescript-resolvers': minor
4+
---
5+
6+
Add addInterfaceFieldResolverTypes option to support custom Interface resolver inheritance

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export interface ParsedResolversConfig extends ParsedConfig {
6565
defaultMapper: ParsedMapper | null;
6666
avoidOptionals: NormalizedAvoidOptionalsConfig;
6767
addUnderscoreToArgsType: boolean;
68+
addInterfaceFieldResolverTypes: boolean;
6869
enumValues: ParsedEnumValuesMap;
6970
resolverTypeWrapperSignature: string;
7071
federation: boolean;
@@ -683,6 +684,40 @@ export interface RawResolversConfig extends RawConfig {
683684
* This may not work for cases where provided default mapper types are also nested e.g. `defaultMapper: DeepPartial<{T}>` or `defaultMapper: Partial<{T}>`.
684685
*/
685686
avoidCheckingAbstractTypesRecursively?: boolean;
687+
/**
688+
* @description If true, add field resolver types to Interfaces.
689+
* By default, GraphQL Interfaces do not trigger any field resolvers,
690+
* meaning every implementing type must implement the same resolver for the shared fields.
691+
*
692+
* Some tools provide a way to change the default behaviour by making GraphQL Objects inherit
693+
* missing resolvers from their Interface types. In these cases, it is fine to turn this option to true.
694+
*
695+
* For example, if you are using `@graphql-tools/schema#makeExecutableSchema` with `inheritResolversFromInterfaces: true`,
696+
* you can make `addInterfaceFieldResolverTypes: true` as well
697+
* https://the-guild.dev/graphql/tools/docs/generate-schema#makeexecutableschema
698+
*
699+
* @exampleMarkdown
700+
* ```ts filename="codegen.ts"
701+
* import type { CodegenConfig } from '@graphql-codegen/cli';
702+
*
703+
* const config: CodegenConfig = {
704+
* // ...
705+
* generates: {
706+
* 'path/to/file': {
707+
* plugins: ['typescript', 'typescript-resolver'],
708+
* config: {
709+
* addInterfaceFieldResolverTypes: true,
710+
* },
711+
* },
712+
* },
713+
* };
714+
* export default config;
715+
* ```
716+
*
717+
* @type boolean
718+
* @default false
719+
*/
720+
addInterfaceFieldResolverTypes?: boolean;
686721
/**
687722
* @ignore
688723
*/
@@ -769,6 +804,7 @@ export class BaseResolversVisitor<
769804
mapOrStr: rawConfig.enumValues,
770805
}),
771806
addUnderscoreToArgsType: getConfigValue(rawConfig.addUnderscoreToArgsType, false),
807+
addInterfaceFieldResolverTypes: getConfigValue(rawConfig.addInterfaceFieldResolverTypes, false),
772808
contextType: parseMapper(rawConfig.contextType || 'any', 'ContextType'),
773809
fieldContextTypes: getConfigValue(rawConfig.fieldContextTypes, []),
774810
directiveContextTypes: getConfigValue(rawConfig.directiveContextTypes, []),
@@ -2015,7 +2051,7 @@ export class BaseResolversVisitor<
20152051
printContent(node, this.config.avoidOptionals.resolvers)
20162052
);
20172053
for (const field of fields) {
2018-
if (field.meta.federation?.isResolveReference) {
2054+
if (field.meta.federation?.isResolveReference || this.config.addInterfaceFieldResolverTypes) {
20192055
blockFields.push(field.value);
20202056
}
20212057
}

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,4 +210,50 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => {
210210
"
211211
`);
212212
});
213+
214+
it('generates normal Interface fields with addInterfaceFieldResolverTypes:true', async () => {
215+
const federatedSchema = /* GraphQL */ `
216+
type Query {
217+
me: Person
218+
}
219+
220+
interface Person @key(fields: "id") {
221+
id: ID!
222+
name: PersonName!
223+
}
224+
225+
type User implements Person @key(fields: "id") {
226+
id: ID!
227+
name: PersonName!
228+
}
229+
230+
type Admin implements Person @key(fields: "id") {
231+
id: ID!
232+
name: PersonName!
233+
canImpersonate: Boolean!
234+
}
235+
236+
type PersonName {
237+
first: String!
238+
last: String!
239+
}
240+
`;
241+
242+
const content = await generate({
243+
schema: federatedSchema,
244+
config: {
245+
federation: true,
246+
addInterfaceFieldResolverTypes: true,
247+
},
248+
});
249+
250+
expect(content).toBeSimilarStringTo(`
251+
export type PersonResolvers<ContextType = any, ParentType extends ResolversParentTypes['Person'] = ResolversParentTypes['Person'], FederationReferenceType extends FederationReferenceTypes['Person'] = FederationReferenceTypes['Person']> = {
252+
__resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>;
253+
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['Person']> | FederationReferenceType, FederationReferenceType, ContextType>;
254+
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
255+
name?: Resolver<ResolversTypes['PersonName'], ParentType, ContextType>;
256+
};
257+
`);
258+
});
213259
});

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,22 @@ describe('TypeScript Resolvers Plugin', () => {
8585
});
8686

8787
describe('Config', () => {
88+
it('addInterfaceFieldResolverTypes - should allow to have only resolveType for interfaces', async () => {
89+
const config = {
90+
addInterfaceFieldResolverTypes: true,
91+
};
92+
const result = await plugin(resolversTestingSchema, [], config, { outputFile: '' });
93+
const content = await resolversTestingValidate(result, config, resolversTestingSchema);
94+
95+
expect(content).toBeSimilarStringTo(`
96+
export type WithChildrenResolvers<ContextType = any, ParentType extends ResolversParentTypes['WithChildren'] = ResolversParentTypes['WithChildren']> = {
97+
__resolveType: TypeResolveFn<'AnotherNodeWithAll', ParentType, ContextType>;
98+
unionChildren?: Resolver<Array<ResolversTypes['ChildUnion']>, ParentType, ContextType>;
99+
nodes?: Resolver<Array<ResolversTypes['AnotherNode']>, ParentType, ContextType>;
100+
};
101+
`);
102+
});
103+
88104
it('optionalInfoArgument - should allow to have optional info argument', async () => {
89105
const config = {
90106
noSchemaStitching: true,

0 commit comments

Comments
 (0)