Skip to content

Commit 0365115

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 44531e5 commit 0365115

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,
@@ -1392,7 +1389,9 @@ export class BaseResolversVisitor<
13921389

13931390
const federationMeta = this._federation.getMeta()[schemaTypeName];
13941391
if (federationMeta) {
1395-
userDefinedTypes[schemaTypeName].federation = federationMeta;
1392+
userDefinedTypes[schemaTypeName].federation = {
1393+
hasResolveReference: federationMeta.hasResolveReference,
1394+
};
13961395
}
13971396
}
13981397

@@ -1506,9 +1505,10 @@ export class BaseResolversVisitor<
15061505
return (parentName, avoidResolverOptionals) => {
15071506
const original: FieldDefinitionNode = parent[key];
15081507
const parentType = this.schema.getType(parentName);
1508+
const meta: ReturnType<FieldDefinitionPrintFn>['meta'] = {};
15091509

15101510
if (this._federation.skipField({ fieldNode: original, parentType })) {
1511-
return null;
1511+
return { value: null, meta };
15121512
}
15131513

15141514
const contextType = this.getContextType(parentName, node);
@@ -1543,7 +1543,7 @@ export class BaseResolversVisitor<
15431543
}
15441544
}
15451545

1546-
const parentTypeSignature = this._federation.transformParentType({
1546+
const parentTypeSignature = this._federation.transformFieldParentType({
15471547
fieldNode: original,
15481548
parentType,
15491549
parentTypeSignature: this.getParentTypeForSignature(node),
@@ -1598,29 +1598,22 @@ export class BaseResolversVisitor<
15981598
};
15991599

16001600
if (this._federation.isResolveReferenceField(node)) {
1601-
if (this.config.generateInternalResolversIfNeeded.__resolveReference) {
1602-
const federationDetails = checkObjectTypeFederationDetails(
1603-
parentType.astNode as ObjectTypeDefinitionNode,
1604-
this._schema
1605-
);
1606-
1607-
if (!federationDetails || federationDetails.resolvableKeyDirectives.length === 0) {
1608-
return '';
1609-
}
1601+
if (!this._federation.getMeta()[parentType.name].hasResolveReference) {
1602+
return { value: '', meta };
16101603
}
1611-
1612-
this._federation.setMeta(parentType.name, { hasResolveReference: true });
16131604
signature.type = 'ReferenceResolver';
1614-
if (signature.genericTypes.length >= 3) {
1615-
signature.genericTypes = signature.genericTypes.slice(0, 3);
1616-
}
1605+
signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType];
1606+
meta.federation = { isResolveReference: true };
16171607
}
16181608

1619-
return indent(
1620-
`${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join(
1621-
', '
1622-
)}>${this.getPunctuation(declarationKind)}`
1623-
);
1609+
return {
1610+
value: indent(
1611+
`${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join(
1612+
', '
1613+
)}>${this.getPunctuation(declarationKind)}`
1614+
),
1615+
meta,
1616+
};
16241617
};
16251618
}
16261619

@@ -1681,7 +1674,7 @@ export class BaseResolversVisitor<
16811674
(rootType === 'mutation' && this.config.avoidOptionals.mutation) ||
16821675
(rootType === 'subscription' && this.config.avoidOptionals.subscription) ||
16831676
(rootType === false && this.config.avoidOptionals.resolvers)
1684-
);
1677+
).value;
16851678
});
16861679

16871680
if (!rootType) {
@@ -1698,10 +1691,11 @@ export class BaseResolversVisitor<
16981691
`ContextType = ${this.config.contextType.type}`,
16991692
this.transformParentGenericType(parentType),
17001693
];
1701-
if (this._federation.getMeta()[typeName]) {
1702-
const typeRef = `${this.convertName('FederationTypes')}['${typeName}']`;
1703-
genericTypes.push(`FederationType extends ${typeRef} = ${typeRef}`);
1704-
}
1694+
this._federation.addFederationTypeGenericIfApplicable({
1695+
genericTypes,
1696+
federationTypesType: this.convertName('FederationTypes'),
1697+
typeName,
1698+
});
17051699

17061700
const block = new DeclarationBlock(this._declarationBlockConfig)
17071701
.export()
@@ -1890,25 +1884,44 @@ export class BaseResolversVisitor<
18901884
}
18911885

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

18961920
return new DeclarationBlock(this._declarationBlockConfig)
18971921
.export()
18981922
.asKind(declarationKind)
1899-
.withName(name, `<ContextType = ${this.config.contextType.type}, ${this.transformParentGenericType(parentType)}>`)
1900-
.withBlock(
1901-
[
1902-
indent(
1903-
`${this.config.internalResolversPrefix}resolveType${
1904-
this.config.optionalResolveType ? '?' : ''
1905-
}: TypeResolveFn<${possibleTypes}, ParentType, ContextType>${this.getPunctuation(declarationKind)}`
1906-
),
1907-
...(fields as unknown as FieldDefinitionPrintFn[]).map(f =>
1908-
f(typeName, this.config.avoidOptionals.resolvers)
1909-
),
1910-
].join('\n')
1911-
).string;
1923+
.withName(name, `<${genericTypes.join(', ')}>`)
1924+
.withBlock(blockFields.join('\n')).string;
19121925
}
19131926

19141927
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)