Skip to content

Commit b446e84

Browse files
authored
[resolvers][federation] Fix fields or types being wrong generated when marked with @external (#10287)
* 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 * Format and minor text updates * Add comments * Fix __resolveReference not getting generated * Fix test with __isTypeOf when not needed * Fix type cast that results in wrong type * Revert unncessary changes to FieldDefinitionPrintFn * Re-format * Convert to use AST Node instead of GraphQL Type * Update test template * Cache field nodes to generate for processed objects * Put FIXME on base-resolvers-visitor to remove Name method
1 parent 30ac893 commit b446e84

File tree

6 files changed

+294
-152
lines changed

6 files changed

+294
-152
lines changed

.changeset/twenty-planets-complain.md

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

dev-test/test-schema/resolvers-federation.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,6 @@ export type UserResolvers<
210210
GraphQLRecursivePick<FederationType, { address: { city: true; lines: { line2: true } } }>,
211211
ContextType
212212
>;
213-
214213
email?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
215214
};
216215

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

Lines changed: 145 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,13 @@ export interface ParsedResolversConfig extends ParsedConfig {
8282
avoidCheckingAbstractTypesRecursively: boolean;
8383
}
8484

85+
export interface FieldDefinitionResult {
86+
node: FieldDefinitionNode;
87+
printContent: FieldDefinitionPrintFn;
88+
}
89+
8590
type FieldDefinitionPrintFn = (
86-
parentName: string,
91+
parentNode: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode,
8792
avoidResolverOptionals: boolean
8893
) => { value: string | null; meta: { federation?: { isResolveReference: boolean } } };
8994
export interface RootResolver {
@@ -1463,6 +1468,9 @@ export class BaseResolversVisitor<
14631468
return '';
14641469
}
14651470

1471+
// FIXME: this Name method causes a lot of type inconsistencies
1472+
// because the type of nodes no longer matches the `graphql-js` types
1473+
// So, we should update this and remove any relevant `as any as string` or `as unknown as string`
14661474
Name(node: NameNode): string {
14671475
return node.value;
14681476
}
@@ -1519,122 +1527,133 @@ export class BaseResolversVisitor<
15191527
return `ParentType extends ${parentType} = ${parentType}`;
15201528
}
15211529

1522-
FieldDefinition(node: FieldDefinitionNode, key: string | number, parent: any): FieldDefinitionPrintFn {
1530+
FieldDefinition(node: FieldDefinitionNode, key: string | number, parent: any): FieldDefinitionResult {
15231531
const hasArguments = node.arguments && node.arguments.length > 0;
15241532
const declarationKind = 'type';
15251533

1526-
return (parentName, avoidResolverOptionals) => {
1527-
const original: FieldDefinitionNode = parent[key];
1528-
const parentType = this.schema.getType(parentName);
1529-
const meta: ReturnType<FieldDefinitionPrintFn>['meta'] = {};
1534+
const original: FieldDefinitionNode = parent[key];
15301535

1531-
if (this._federation.skipField({ fieldNode: original, parentType })) {
1532-
return { value: null, meta };
1533-
}
1536+
return {
1537+
node: original,
1538+
printContent: (parentNode, avoidResolverOptionals) => {
1539+
const parentName = parentNode.name as unknown as string;
1540+
const parentType = this.schema.getType(parentName);
1541+
const meta: ReturnType<FieldDefinitionPrintFn>['meta'] = {};
1542+
const typeName = node.name as unknown as string;
1543+
1544+
const fieldsToGenerate = this._federation.findFieldNodesToGenerate({ node: parentNode });
1545+
const shouldGenerateField =
1546+
fieldsToGenerate.some(field => field.name.value === typeName) ||
1547+
this._federation.isResolveReferenceField(node);
1548+
1549+
if (!shouldGenerateField) {
1550+
return { value: null, meta };
1551+
}
15341552

1535-
const contextType = this.getContextType(parentName, node);
1536-
1537-
let argsType = hasArguments
1538-
? this.convertName(
1539-
parentName +
1540-
(this.config.addUnderscoreToArgsType ? '_' : '') +
1541-
this.convertName(node.name, {
1542-
useTypesPrefix: false,
1543-
useTypesSuffix: false,
1544-
}) +
1545-
'Args',
1546-
{
1547-
useTypesPrefix: true,
1548-
},
1549-
true
1550-
)
1551-
: null;
1553+
const contextType = this.getContextType(parentName, node);
1554+
1555+
let argsType = hasArguments
1556+
? this.convertName(
1557+
parentName +
1558+
(this.config.addUnderscoreToArgsType ? '_' : '') +
1559+
this.convertName(typeName, {
1560+
useTypesPrefix: false,
1561+
useTypesSuffix: false,
1562+
}) +
1563+
'Args',
1564+
{
1565+
useTypesPrefix: true,
1566+
},
1567+
true
1568+
)
1569+
: null;
1570+
1571+
const avoidInputsOptionals = this.config.avoidOptionals.inputValue;
1572+
1573+
if (argsType !== null) {
1574+
const argsToForceRequire = original.arguments.filter(
1575+
arg => !!arg.defaultValue || arg.type.kind === 'NonNullType'
1576+
);
1577+
1578+
if (argsToForceRequire.length > 0) {
1579+
argsType = this.applyRequireFields(argsType, argsToForceRequire);
1580+
} else if (original.arguments.length > 0 && avoidInputsOptionals !== true) {
1581+
argsType = this.applyOptionalFields(argsType, original.arguments);
1582+
}
1583+
}
15521584

1553-
const avoidInputsOptionals = this.config.avoidOptionals.inputValue;
1585+
const parentTypeSignature = this._federation.transformFieldParentType({
1586+
fieldNode: original,
1587+
parentType,
1588+
parentTypeSignature: this.getParentTypeForSignature(node),
1589+
federationTypeSignature: 'FederationType',
1590+
});
15541591

1555-
if (argsType !== null) {
1556-
const argsToForceRequire = original.arguments.filter(
1557-
arg => !!arg.defaultValue || arg.type.kind === 'NonNullType'
1558-
);
1592+
const { mappedTypeKey, resolverType } = ((): { mappedTypeKey: string; resolverType: string } => {
1593+
const baseType = getBaseTypeNode(original.type);
1594+
const realType = baseType.name.value;
1595+
const typeToUse = this.getTypeToUse(realType);
1596+
/**
1597+
* Turns GraphQL type to TypeScript types (`mappedType`) e.g.
1598+
* - String! -> ResolversTypes['String']>
1599+
* - String -> Maybe<ResolversTypes['String']>
1600+
* - [String] -> Maybe<Array<Maybe<ResolversTypes['String']>>>
1601+
* - [String!]! -> Array<ResolversTypes['String']>
1602+
*/
1603+
const mappedType = this._variablesTransformer.wrapAstTypeWithModifiers(typeToUse, original.type);
1604+
1605+
const subscriptionType = this._schema.getSubscriptionType();
1606+
const isSubscriptionType = subscriptionType && subscriptionType.name === parentName;
1607+
1608+
if (isSubscriptionType) {
1609+
return {
1610+
mappedTypeKey: `${mappedType}, "${typeName}"`,
1611+
resolverType: 'SubscriptionResolver',
1612+
};
1613+
}
15591614

1560-
if (argsToForceRequire.length > 0) {
1561-
argsType = this.applyRequireFields(argsType, argsToForceRequire);
1562-
} else if (original.arguments.length > 0 && avoidInputsOptionals !== true) {
1563-
argsType = this.applyOptionalFields(argsType, original.arguments);
1564-
}
1565-
}
1615+
const directiveMappings =
1616+
node.directives
1617+
?.map(directive => this._directiveResolverMappings[directive.name as any])
1618+
.filter(Boolean)
1619+
.reverse() ?? [];
15661620

1567-
const parentTypeSignature = this._federation.transformFieldParentType({
1568-
fieldNode: original,
1569-
parentType,
1570-
parentTypeSignature: this.getParentTypeForSignature(node),
1571-
federationTypeSignature: 'FederationType',
1572-
});
1573-
1574-
const { mappedTypeKey, resolverType } = ((): { mappedTypeKey: string; resolverType: string } => {
1575-
const baseType = getBaseTypeNode(original.type);
1576-
const realType = baseType.name.value;
1577-
const typeToUse = this.getTypeToUse(realType);
1578-
/**
1579-
* Turns GraphQL type to TypeScript types (`mappedType`) e.g.
1580-
* - String! -> ResolversTypes['String']>
1581-
* - String -> Maybe<ResolversTypes['String']>
1582-
* - [String] -> Maybe<Array<Maybe<ResolversTypes['String']>>>
1583-
* - [String!]! -> Array<ResolversTypes['String']>
1584-
*/
1585-
const mappedType = this._variablesTransformer.wrapAstTypeWithModifiers(typeToUse, original.type);
1586-
1587-
const subscriptionType = this._schema.getSubscriptionType();
1588-
const isSubscriptionType = subscriptionType && subscriptionType.name === parentName;
1589-
1590-
if (isSubscriptionType) {
15911621
return {
1592-
mappedTypeKey: `${mappedType}, "${node.name}"`,
1593-
resolverType: 'SubscriptionResolver',
1622+
mappedTypeKey: mappedType,
1623+
resolverType: directiveMappings[0] ?? 'Resolver',
15941624
};
1595-
}
1625+
})();
1626+
1627+
const signature: {
1628+
name: string;
1629+
modifier: string;
1630+
type: string;
1631+
genericTypes: string[];
1632+
} = {
1633+
name: typeName,
1634+
modifier: avoidResolverOptionals ? '' : '?',
1635+
type: resolverType,
1636+
genericTypes: [mappedTypeKey, parentTypeSignature, contextType, argsType].filter(f => f),
1637+
};
15961638

1597-
const directiveMappings =
1598-
node.directives
1599-
?.map(directive => this._directiveResolverMappings[directive.name as any])
1600-
.filter(Boolean)
1601-
.reverse() ?? [];
1639+
if (this._federation.isResolveReferenceField(node)) {
1640+
if (!this._federation.getMeta()[parentType.name].hasResolveReference) {
1641+
return { value: '', meta };
1642+
}
1643+
signature.type = 'ReferenceResolver';
1644+
signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType];
1645+
meta.federation = { isResolveReference: true };
1646+
}
16021647

16031648
return {
1604-
mappedTypeKey: mappedType,
1605-
resolverType: directiveMappings[0] ?? 'Resolver',
1649+
value: indent(
1650+
`${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join(
1651+
', '
1652+
)}>${this.getPunctuation(declarationKind)}`
1653+
),
1654+
meta,
16061655
};
1607-
})();
1608-
1609-
const signature: {
1610-
name: string;
1611-
modifier: string;
1612-
type: string;
1613-
genericTypes: string[];
1614-
} = {
1615-
name: node.name as any,
1616-
modifier: avoidResolverOptionals ? '' : '?',
1617-
type: resolverType,
1618-
genericTypes: [mappedTypeKey, parentTypeSignature, contextType, argsType].filter(f => f),
1619-
};
1620-
1621-
if (this._federation.isResolveReferenceField(node)) {
1622-
if (!this._federation.getMeta()[parentType.name].hasResolveReference) {
1623-
return { value: '', meta };
1624-
}
1625-
signature.type = 'ReferenceResolver';
1626-
signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType];
1627-
meta.federation = { isResolveReference: true };
1628-
}
1629-
1630-
return {
1631-
value: indent(
1632-
`${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join(
1633-
', '
1634-
)}>${this.getPunctuation(declarationKind)}`
1635-
),
1636-
meta,
1637-
};
1656+
},
16381657
};
16391658
}
16401659

@@ -1707,12 +1726,17 @@ export class BaseResolversVisitor<
17071726
return `Partial<${argsType}>`;
17081727
}
17091728

1710-
ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string {
1729+
ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string | null {
1730+
const typeName = node.name as unknown as string;
1731+
const fieldsToGenerate = this._federation.findFieldNodesToGenerate({ node });
1732+
if (fieldsToGenerate.length === 0) {
1733+
return null;
1734+
}
1735+
17111736
const declarationKind = 'type';
17121737
const name = this.convertName(node, {
17131738
suffix: this.config.resolverTypeSuffix,
17141739
});
1715-
const typeName = node.name as any as string;
17161740
const parentType = this.getParentTypeToUse(typeName);
17171741

17181742
const rootType = ((): false | 'query' | 'mutation' | 'subscription' => {
@@ -1728,15 +1752,17 @@ export class BaseResolversVisitor<
17281752
return false;
17291753
})();
17301754

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-
});
1755+
const fieldsContent = (node.fields as unknown as FieldDefinitionResult[])
1756+
.map(({ printContent }) => {
1757+
return printContent(
1758+
node,
1759+
(rootType === 'query' && this.config.avoidOptionals.query) ||
1760+
(rootType === 'mutation' && this.config.avoidOptionals.mutation) ||
1761+
(rootType === 'subscription' && this.config.avoidOptionals.subscription) ||
1762+
(rootType === false && this.config.avoidOptionals.resolvers)
1763+
).value;
1764+
})
1765+
.filter(v => v);
17401766

17411767
if (!rootType && this._parsedSchemaMeta.typesWithIsTypeOf[typeName]) {
17421768
fieldsContent.push(
@@ -1748,6 +1774,10 @@ export class BaseResolversVisitor<
17481774
);
17491775
}
17501776

1777+
if (fieldsContent.length === 0) {
1778+
return null;
1779+
}
1780+
17511781
const genericTypes: string[] = [
17521782
`ContextType = ${this.config.contextType.type}`,
17531783
this.transformParentGenericType(parentType),
@@ -1958,8 +1988,8 @@ export class BaseResolversVisitor<
19581988

19591989
// An Interface in Federation may have the additional __resolveReference resolver, if resolvable.
19601990
// So, we filter out the normal fields declared on the Interface and add the __resolveReference resolver.
1961-
const fields = (node.fields as unknown as FieldDefinitionPrintFn[]).map(f =>
1962-
f(typeName, this.config.avoidOptionals.resolvers)
1991+
const fields = (node.fields as unknown as FieldDefinitionResult[]).map(({ printContent }) =>
1992+
printContent(node, this.config.avoidOptionals.resolvers)
19631993
);
19641994
for (const field of fields) {
19651995
if (field.meta.federation?.isResolveReference) {

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ describe('customDirectives.sematicNonNull', () => {
6262
nonNullableListWithNonNullableItemLevel0?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
6363
nonNullableListWithNonNullableItemLevel1?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
6464
nonNullableListWithNonNullableItemBothLevels?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
65-
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
6665
};
6766
`);
6867
});

0 commit comments

Comments
 (0)