Skip to content

Commit 40ddde8

Browse files
committed
[resolvers][federation] Add __resolveReference to applicable Interface entities, fix Interface types having non-meta resolver fields (#10221)
* Add __resolveReference for applicable Interfaces - Deprecate generateInternalResolversIfNeeded.__resolveReference - Fix tests - Deprecate onlyResolveTypeForInterfaces - Add changeset - Cleanup - Handle __resolveReference generation in Interface - Let FieldDefinition decide whether to generate __resolveReference by checking whether parent has resolvable key * Fix test
1 parent de55668 commit 40ddde8

File tree

13 files changed

+506
-467
lines changed

13 files changed

+506
-467
lines changed

.changeset/loud-suits-admire.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@graphql-codegen/visitor-plugin-common': major
3+
'@graphql-codegen/typescript-resolvers': major
4+
'@graphql-codegen/plugin-helpers': major
5+
---
6+
7+
Ensure Federation Interfaces have `__resolveReference` if they are resolvable entities
8+
9+
BREAKING CHANGES: Deprecate `onlyResolveTypeForInterfaces` because majority of use cases cannot implement resolvers in Interfaces.
10+
BREAKING CHANGES: Deprecate `generateInternalResolversIfNeeded.__resolveReference` because types do not have `__resolveReference` if they are not Federation entities or are not resolvable. Users should not have to manually set this option. This option was put in to wait for this major version.

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

Lines changed: 71 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ApolloFederation, checkObjectTypeFederationDetails, getBaseType } from '@graphql-codegen/plugin-helpers';
1+
import { ApolloFederation, type FederationMeta, getBaseType } from '@graphql-codegen/plugin-helpers';
22
import { getRootTypeNames } from '@graphql-tools/utils';
33
import autoBind from 'auto-bind';
44
import {
@@ -78,13 +78,15 @@ export interface ParsedResolversConfig extends ParsedConfig {
7878
allResolversTypeName: string;
7979
internalResolversPrefix: string;
8080
generateInternalResolversIfNeeded: NormalizedGenerateInternalResolversIfNeededConfig;
81-
onlyResolveTypeForInterfaces: boolean;
8281
directiveResolverMappings: Record<string, string>;
8382
resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig;
8483
avoidCheckingAbstractTypesRecursively: boolean;
8584
}
8685

87-
type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null;
86+
type FieldDefinitionPrintFn = (
87+
parentName: string,
88+
avoidResolverOptionals: boolean
89+
) => { value: string | null; meta: { federation?: { isResolveReference: boolean } } };
8890
export interface RootResolver {
8991
content: string;
9092
generatedResolverTypes: {
@@ -618,20 +620,13 @@ export interface RawResolversConfig extends RawConfig {
618620
internalResolversPrefix?: string;
619621
/**
620622
* @type object
621-
* @default { __resolveReference: false }
623+
* @default {}
622624
* @description If relevant internal resolvers are set to `true`, the resolver type will only be generated if the right conditions are met.
623625
* Enabling this allows a more correct type generation for the resolvers.
624626
* For example:
625627
* - `__isTypeOf` is generated for implementing types and union members
626-
* - `__resolveReference` is generated for federation types that have at least one resolvable `@key` directive
627628
*/
628629
generateInternalResolversIfNeeded?: GenerateInternalResolversIfNeededConfig;
629-
/**
630-
* @type boolean
631-
* @default false
632-
* @description Turning this flag to `true` will generate resolver signature that has only `resolveType` for interfaces, forcing developers to write inherited type resolvers in the type itself.
633-
*/
634-
onlyResolveTypeForInterfaces?: boolean;
635630
/**
636631
* @description Makes `__typename` of resolver mappings non-optional without affecting the base types.
637632
* @default false
@@ -734,7 +729,8 @@ export class BaseResolversVisitor<
734729
rawConfig: TRawConfig,
735730
additionalConfig: TPluginConfig,
736731
private _schema: GraphQLSchema,
737-
defaultScalars: NormalizedScalarsMap = DEFAULT_SCALARS
732+
defaultScalars: NormalizedScalarsMap = DEFAULT_SCALARS,
733+
federationMeta: FederationMeta = {}
738734
) {
739735
super(rawConfig, {
740736
immutableTypes: getConfigValue(rawConfig.immutableTypes, false),
@@ -748,7 +744,6 @@ export class BaseResolversVisitor<
748744
mapOrStr: rawConfig.enumValues,
749745
}),
750746
addUnderscoreToArgsType: getConfigValue(rawConfig.addUnderscoreToArgsType, false),
751-
onlyResolveTypeForInterfaces: getConfigValue(rawConfig.onlyResolveTypeForInterfaces, false),
752747
contextType: parseMapper(rawConfig.contextType || 'any', 'ContextType'),
753748
fieldContextTypes: getConfigValue(rawConfig.fieldContextTypes, []),
754749
directiveContextTypes: getConfigValue(rawConfig.directiveContextTypes, []),
@@ -763,9 +758,7 @@ export class BaseResolversVisitor<
763758
mappers: transformMappers(rawConfig.mappers || {}, rawConfig.mapperTypeSuffix),
764759
scalars: buildScalarsFromConfig(_schema, rawConfig, defaultScalars),
765760
internalResolversPrefix: getConfigValue(rawConfig.internalResolversPrefix, '__'),
766-
generateInternalResolversIfNeeded: {
767-
__resolveReference: rawConfig.generateInternalResolversIfNeeded?.__resolveReference ?? false,
768-
},
761+
generateInternalResolversIfNeeded: {},
769762
resolversNonOptionalTypename: normalizeResolversNonOptionalTypename(
770763
getConfigValue(rawConfig.resolversNonOptionalTypename, false)
771764
),
@@ -774,7 +767,11 @@ export class BaseResolversVisitor<
774767
} as TPluginConfig);
775768

776769
autoBind(this);
777-
this._federation = new ApolloFederation({ enabled: this.config.federation, schema: this.schema });
770+
this._federation = new ApolloFederation({
771+
enabled: this.config.federation,
772+
schema: this.schema,
773+
meta: federationMeta,
774+
});
778775
this._rootTypeNames = getRootTypeNames(_schema);
779776
this._variablesTransformer = new OperationVariablesToObject(
780777
this.scalars,
@@ -1396,7 +1393,9 @@ export class BaseResolversVisitor<
13961393

13971394
const federationMeta = this._federation.getMeta()[schemaTypeName];
13981395
if (federationMeta) {
1399-
userDefinedTypes[schemaTypeName].federation = federationMeta;
1396+
userDefinedTypes[schemaTypeName].federation = {
1397+
hasResolveReference: federationMeta.hasResolveReference,
1398+
};
14001399
}
14011400
}
14021401

@@ -1510,9 +1509,10 @@ export class BaseResolversVisitor<
15101509
return (parentName, avoidResolverOptionals) => {
15111510
const original: FieldDefinitionNode = parent[key];
15121511
const parentType = this.schema.getType(parentName);
1512+
const meta: ReturnType<FieldDefinitionPrintFn>['meta'] = {};
15131513

15141514
if (this._federation.skipField({ fieldNode: original, parentType })) {
1515-
return null;
1515+
return { value: null, meta };
15161516
}
15171517

15181518
const contextType = this.getContextType(parentName, node);
@@ -1547,7 +1547,7 @@ export class BaseResolversVisitor<
15471547
}
15481548
}
15491549

1550-
const parentTypeSignature = this._federation.transformParentType({
1550+
const parentTypeSignature = this._federation.transformFieldParentType({
15511551
fieldNode: original,
15521552
parentType,
15531553
parentTypeSignature: this.getParentTypeForSignature(node),
@@ -1602,29 +1602,22 @@ export class BaseResolversVisitor<
16021602
};
16031603

16041604
if (this._federation.isResolveReferenceField(node)) {
1605-
if (this.config.generateInternalResolversIfNeeded.__resolveReference) {
1606-
const federationDetails = checkObjectTypeFederationDetails(
1607-
parentType.astNode as ObjectTypeDefinitionNode,
1608-
this._schema
1609-
);
1610-
1611-
if (!federationDetails || federationDetails.resolvableKeyDirectives.length === 0) {
1612-
return '';
1613-
}
1605+
if (!this._federation.getMeta()[parentType.name].hasResolveReference) {
1606+
return { value: '', meta };
16141607
}
1615-
1616-
this._federation.setMeta(parentType.name, { hasResolveReference: true });
16171608
signature.type = 'ReferenceResolver';
1618-
if (signature.genericTypes.length >= 3) {
1619-
signature.genericTypes = signature.genericTypes.slice(0, 3);
1620-
}
1609+
signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType];
1610+
meta.federation = { isResolveReference: true };
16211611
}
16221612

1623-
return indent(
1624-
`${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join(
1625-
', '
1626-
)}>${this.getPunctuation(declarationKind)}`
1627-
);
1613+
return {
1614+
value: indent(
1615+
`${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join(
1616+
', '
1617+
)}>${this.getPunctuation(declarationKind)}`
1618+
),
1619+
meta,
1620+
};
16281621
};
16291622
}
16301623

@@ -1685,7 +1678,7 @@ export class BaseResolversVisitor<
16851678
(rootType === 'mutation' && this.config.avoidOptionals.mutation) ||
16861679
(rootType === 'subscription' && this.config.avoidOptionals.subscription) ||
16871680
(rootType === false && this.config.avoidOptionals.resolvers)
1688-
);
1681+
).value;
16891682
});
16901683

16911684
if (!rootType) {
@@ -1702,10 +1695,11 @@ export class BaseResolversVisitor<
17021695
`ContextType = ${this.config.contextType.type}`,
17031696
this.transformParentGenericType(parentType),
17041697
];
1705-
if (this._federation.getMeta()[typeName]) {
1706-
const typeRef = `${this.convertName('FederationTypes')}['${typeName}']`;
1707-
genericTypes.push(`FederationType extends ${typeRef} = ${typeRef}`);
1708-
}
1698+
this._federation.addFederationTypeGenericIfApplicable({
1699+
genericTypes,
1700+
federationTypesType: this.convertName('FederationTypes'),
1701+
typeName,
1702+
});
17091703

17101704
const block = new DeclarationBlock(this._declarationBlockConfig)
17111705
.export()
@@ -1894,25 +1888,44 @@ export class BaseResolversVisitor<
18941888
}
18951889

18961890
const parentType = this.getParentTypeToUse(typeName);
1891+
1892+
const genericTypes: string[] = [
1893+
`ContextType = ${this.config.contextType.type}`,
1894+
this.transformParentGenericType(parentType),
1895+
];
1896+
this._federation.addFederationTypeGenericIfApplicable({
1897+
genericTypes,
1898+
federationTypesType: this.convertName('FederationTypes'),
1899+
typeName,
1900+
});
1901+
18971902
const possibleTypes = implementingTypes.map(name => `'${name}'`).join(' | ') || 'null';
1898-
const fields = this.config.onlyResolveTypeForInterfaces ? [] : node.fields || [];
1903+
1904+
// An Interface has __resolveType resolver, and no other fields.
1905+
const blockFields: string[] = [
1906+
indent(
1907+
`${this.config.internalResolversPrefix}resolveType${
1908+
this.config.optionalResolveType ? '?' : ''
1909+
}: TypeResolveFn<${possibleTypes}, ParentType, ContextType>${this.getPunctuation(declarationKind)}`
1910+
),
1911+
];
1912+
1913+
// An Interface in Federation may have the additional __resolveReference resolver, if resolvable.
1914+
// So, we filter out the normal fields declared on the Interface and add the __resolveReference resolver.
1915+
const fields = (node.fields as unknown as FieldDefinitionPrintFn[]).map(f =>
1916+
f(typeName, this.config.avoidOptionals.resolvers)
1917+
);
1918+
for (const field of fields) {
1919+
if (field.meta.federation?.isResolveReference) {
1920+
blockFields.push(field.value);
1921+
}
1922+
}
18991923

19001924
return new DeclarationBlock(this._declarationBlockConfig)
19011925
.export()
19021926
.asKind(declarationKind)
1903-
.withName(name, `<ContextType = ${this.config.contextType.type}, ${this.transformParentGenericType(parentType)}>`)
1904-
.withBlock(
1905-
[
1906-
indent(
1907-
`${this.config.internalResolversPrefix}resolveType${
1908-
this.config.optionalResolveType ? '?' : ''
1909-
}: TypeResolveFn<${possibleTypes}, ParentType, ContextType>${this.getPunctuation(declarationKind)}`
1910-
),
1911-
...(fields as unknown as FieldDefinitionPrintFn[]).map(f =>
1912-
f(typeName, this.config.avoidOptionals.resolvers)
1913-
),
1914-
].join('\n')
1915-
).string;
1927+
.withName(name, `<${genericTypes.join(', ')}>`)
1928+
.withBlock(blockFields.join('\n')).string;
19161929
}
19171930

19181931
SchemaDefinition() {

packages/plugins/other/visitor-plugin-common/src/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,5 @@ export interface CustomDirectivesConfig {
139139
apolloUnmask?: boolean;
140140
}
141141

142-
export interface GenerateInternalResolversIfNeededConfig {
143-
__resolveReference?: boolean;
144-
}
142+
export interface GenerateInternalResolversIfNeededConfig {}
145143
export type NormalizedGenerateInternalResolversIfNeededConfig = Required<GenerateInternalResolversIfNeededConfig>;

packages/plugins/typescript/resolvers/src/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,20 @@ export type Resolver${capitalizedDirectiveName}WithResolve<TResult, TParent, TCo
7575
}
7676
}
7777

78-
let transformedSchema = config.federation ? addFederationReferencesToSchema(schema) : schema;
78+
let { transformedSchema, federationMeta } = config.federation
79+
? addFederationReferencesToSchema(schema)
80+
: { transformedSchema: schema, federationMeta: {} };
81+
7982
transformedSchema = config.customDirectives?.semanticNonNull
8083
? await semanticToStrict(transformedSchema)
8184
: transformedSchema;
8285

83-
const visitor = new TypeScriptResolversVisitor({ ...config, directiveResolverMappings }, transformedSchema);
86+
const visitor = new TypeScriptResolversVisitor(
87+
{ ...config, directiveResolverMappings },
88+
transformedSchema,
89+
federationMeta
90+
);
91+
8492
const namespacedImportPrefix = visitor.config.namespacedImportName ? `${visitor.config.namespacedImportName}.` : '';
8593

8694
const astNode = getCachedDocumentNodeFromSchema(transformedSchema);

packages/plugins/typescript/resolvers/src/visitor.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { TypeScriptOperationVariablesToObject } from '@graphql-codegen/typescrip
22
import {
33
BaseResolversVisitor,
44
DeclarationKind,
5+
DEFAULT_SCALARS,
56
getConfigValue,
67
normalizeAvoidOptionals,
78
ParsedResolversConfig,
89
} from '@graphql-codegen/visitor-plugin-common';
10+
import type { FederationMeta } from '@graphql-codegen/plugin-helpers';
911
import autoBind from 'auto-bind';
1012
import { EnumTypeDefinitionNode, GraphQLSchema, ListTypeNode, NamedTypeNode, NonNullTypeNode } from 'graphql';
1113
import { TypeScriptResolversPluginConfig } from './config.js';
@@ -24,7 +26,7 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor<
2426
TypeScriptResolversPluginConfig,
2527
ParsedTypeScriptResolversConfig
2628
> {
27-
constructor(pluginConfig: TypeScriptResolversPluginConfig, schema: GraphQLSchema) {
29+
constructor(pluginConfig: TypeScriptResolversPluginConfig, schema: GraphQLSchema, federationMeta: FederationMeta) {
2830
super(
2931
pluginConfig,
3032
{
@@ -34,7 +36,9 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor<
3436
allowParentTypeOverride: getConfigValue(pluginConfig.allowParentTypeOverride, false),
3537
optionalInfoArgument: getConfigValue(pluginConfig.optionalInfoArgument, false),
3638
} as ParsedTypeScriptResolversConfig,
37-
schema
39+
schema,
40+
DEFAULT_SCALARS,
41+
federationMeta
3842
);
3943
autoBind(this);
4044
this.setVariablesTransformer(

0 commit comments

Comments
 (0)