Skip to content

Commit 7949e63

Browse files
committed
Ensure @external does not generate resolver types
- Handle external directive when part or whole type is marked - Add changeset - Add test cases for @provides and @external
1 parent 6a2bca7 commit 7949e63

4 files changed

Lines changed: 94 additions & 19 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@graphql-codegen/visitor-plugin-common': patch
3+
'@graphql-codegen/typescript-resolvers': patch
4+
'@graphql-codegen/plugin-helpers': patch
5+
---
6+
7+
Fix fields or object types marked with @external being wrongly generated

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

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,7 +1707,11 @@ export class BaseResolversVisitor<
17071707
return `Partial<${argsType}>`;
17081708
}
17091709

1710-
ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string {
1710+
ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string | null {
1711+
if (this._federation.skipObjectType({ node })) {
1712+
return null;
1713+
}
1714+
17111715
const declarationKind = 'type';
17121716
const name = this.convertName(node, {
17131717
suffix: this.config.resolverTypeSuffix,
@@ -1728,15 +1732,17 @@ export class BaseResolversVisitor<
17281732
return false;
17291733
})();
17301734

1731-
const fieldsContent = (node.fields as unknown as FieldDefinitionPrintFn[]).map(f => {
1732-
return f(
1733-
typeName,
1734-
(rootType === 'query' && this.config.avoidOptionals.query) ||
1735-
(rootType === 'mutation' && this.config.avoidOptionals.mutation) ||
1736-
(rootType === 'subscription' && this.config.avoidOptionals.subscription) ||
1737-
(rootType === false && this.config.avoidOptionals.resolvers)
1738-
).value;
1739-
});
1735+
const fieldsContent = (node.fields as unknown as FieldDefinitionPrintFn[])
1736+
.map(f => {
1737+
return f(
1738+
typeName,
1739+
(rootType === 'query' && this.config.avoidOptionals.query) ||
1740+
(rootType === 'mutation' && this.config.avoidOptionals.mutation) ||
1741+
(rootType === 'subscription' && this.config.avoidOptionals.subscription) ||
1742+
(rootType === false && this.config.avoidOptionals.resolvers)
1743+
).value;
1744+
})
1745+
.filter(v => v);
17401746

17411747
if (!rootType && this._parsedSchemaMeta.typesWithIsTypeOf[typeName]) {
17421748
fieldsContent.push(
@@ -1748,6 +1754,10 @@ export class BaseResolversVisitor<
17481754
);
17491755
}
17501756

1757+
if (fieldsContent.length === 0) {
1758+
return null;
1759+
}
1760+
17511761
const genericTypes: string[] = [
17521762
`ContextType = ${this.config.contextType.type}`,
17531763
this.transformParentGenericType(parentType),

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

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,20 +496,46 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
496496
`);
497497
});
498498

499-
it('should skip to generate resolvers of fields with @external directive', async () => {
499+
it('should skip to generate resolvers of fields or object types with @external directive', async () => {
500500
const federatedSchema = /* GraphQL */ `
501501
type Query {
502502
users: [User]
503503
}
504504
505505
type Book {
506506
author: User @provides(fields: "name")
507+
editor: User @provides(fields: "company { taxCode }")
507508
}
508509
509510
type User @key(fields: "id") {
510511
id: ID!
511512
name: String @external
512513
username: String @external
514+
address: Address
515+
dateOfBirth: DateOfBirth
516+
placeOfBirth: PlaceOfBirth
517+
company: Company
518+
}
519+
520+
type Address {
521+
street: String! @external
522+
zip: String!
523+
}
524+
525+
type DateOfBirth {
526+
day: Int! @external
527+
month: Int! @external
528+
year: Int! @external
529+
}
530+
531+
type PlaceOfBirth @external {
532+
city: String!
533+
country: String!
534+
}
535+
536+
type Company @external {
537+
name: String!
538+
taxCode: String!
513539
}
514540
`;
515541

@@ -520,7 +546,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
520546
},
521547
});
522548

523-
// UserResolver should not have a resolver function of name field
549+
// UserResolvers should not have `username` resolver because it is marked with `@external`
550+
// UserResolvers should have `name` resolver because whilst it is marked with `@external`, it is provided by `Book.author`
524551
expect(content).toBeSimilarStringTo(`
525552
export type UserResolvers<ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'], FederationType extends FederationTypes['User'] = FederationTypes['User']> = {
526553
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>,
@@ -530,6 +557,26 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
530557
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
531558
};
532559
`);
560+
561+
// AddressResolvers should only have fields not marked with @external
562+
expect(content).toBeSimilarStringTo(`
563+
export type AddressResolvers<ContextType = any, ParentType extends ResolversParentTypes['Address'] = ResolversParentTypes['Address']> = {
564+
zip?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
565+
};
566+
`);
567+
568+
// CompanyResolvers should only have taxCode resolver because it is part of the `@provides` directive in `Book.editor`
569+
expect(content).toBeSimilarStringTo(`
570+
export type CompanyResolvers<ContextType = any, ParentType extends ResolversParentTypes['Company'] = ResolversParentTypes['Company']> = {
571+
taxCode?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
572+
};
573+
`);
574+
575+
// DateOfBirthResolvers should not be generated because there is no field not marked with @external
576+
expect(content).not.toBeSimilarStringTo('export type DateOfBirthResolvers');
577+
578+
// PlaceOfBirthResolvers should not be generated because the type is marked with @external
579+
expect(content).not.toBeSimilarStringTo('export type PlaceOfBirthResolvers');
533580
});
534581

535582
it('should not include _FieldSet scalar', async () => {

packages/utils/plugins-helpers/src/federation.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -265,16 +265,20 @@ export class ApolloFederation {
265265
return this.enabled && name === '_FieldSet';
266266
}
267267

268+
skipObjectType({ node }: { node: ObjectTypeDefinitionNode }): boolean {
269+
if (!this.enabled) {
270+
return false;
271+
}
272+
273+
return this.isExternal(node);
274+
}
275+
268276
/**
269277
* Decides if field should not be generated
270278
* @param data
271279
*/
272280
skipField({ fieldNode, parentType }: { fieldNode: FieldDefinitionNode; parentType: GraphQLNamedType }): boolean {
273-
if (
274-
!this.enabled ||
275-
!(isObjectType(parentType) && !isInterfaceType(parentType)) ||
276-
!checkTypeFederationDetails(parentType, this.schema)
277-
) {
281+
if (!this.enabled || !(isObjectType(parentType) && !isInterfaceType(parentType))) {
278282
return false;
279283
}
280284

@@ -349,7 +353,7 @@ export class ApolloFederation {
349353
return this.isExternal(fieldNode) && !this.hasProvides(objectType, fieldNode);
350354
}
351355

352-
private isExternal(node: FieldDefinitionNode): boolean {
356+
private isExternal(node: FieldDefinitionNode | ObjectTypeDefinitionNode): boolean {
353357
return getDirectivesByName('external', node).length > 0;
354358
}
355359

@@ -481,7 +485,14 @@ function getDirectivesByName(
481485
astNode = node;
482486
}
483487

484-
return astNode?.directives?.filter(d => d.name.value === name) || [];
488+
return (
489+
astNode?.directives?.filter(d => {
490+
// A ObjectTypeDefinitionNode's directive looks like `{ kind: 'Directive', name: 'external', arguments: [] }`
491+
// However, other directives looks like `{ kind: 'Directive', name: { kind: 'Name', value: 'external' }, arguments: [] }`
492+
// Therefore, we need to check for both `d.name.value` and d.name
493+
return d.name.value === name || (d.name as unknown as string) === name;
494+
}) || []
495+
);
485496
}
486497

487498
function extractReferenceSelectionSet(directive: DirectiveNode): ReferenceSelectionSet {

0 commit comments

Comments
 (0)