Skip to content

Commit

Permalink
Add __resolveReference for applicable Interfaces
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
eddeee888 committed Jan 18, 2025
1 parent e39b555 commit bbf0cf7
Show file tree
Hide file tree
Showing 13 changed files with 506 additions and 468 deletions.
10 changes: 10 additions & 0 deletions .changeset/loud-suits-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@graphql-codegen/visitor-plugin-common': major
'@graphql-codegen/typescript-resolvers': major
'@graphql-codegen/plugin-helpers': major
---

Ensure Federation Interfaces have `__resolveReference` if they are resolvable entities

BREAKING CHANGES: Deprecate `onlyResolveTypeForInterfaces` because majority of use cases cannot implement resolvers in Interfaces.
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.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApolloFederation, checkObjectTypeFederationDetails, getBaseType } from '@graphql-codegen/plugin-helpers';
import { ApolloFederation, type FederationMeta, getBaseType } from '@graphql-codegen/plugin-helpers';
import { getRootTypeNames } from '@graphql-tools/utils';
import autoBind from 'auto-bind';
import {
Expand Down Expand Up @@ -78,13 +78,15 @@ export interface ParsedResolversConfig extends ParsedConfig {
allResolversTypeName: string;
internalResolversPrefix: string;
generateInternalResolversIfNeeded: NormalizedGenerateInternalResolversIfNeededConfig;
onlyResolveTypeForInterfaces: boolean;
directiveResolverMappings: Record<string, string>;
resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig;
avoidCheckingAbstractTypesRecursively: boolean;
}

type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null;
type FieldDefinitionPrintFn = (
parentName: string,
avoidResolverOptionals: boolean
) => { value: string | null; meta: { federation?: { isResolveReference: boolean } } };
export interface RootResolver {
content: string;
generatedResolverTypes: {
Expand Down Expand Up @@ -584,20 +586,13 @@ export interface RawResolversConfig extends RawConfig {
internalResolversPrefix?: string;
/**
* @type object
* @default { __resolveReference: false }
* @default {}
* @description If relevant internal resolvers are set to `true`, the resolver type will only be generated if the right conditions are met.
* Enabling this allows a more correct type generation for the resolvers.
* For example:
* - `__isTypeOf` is generated for implementing types and union members
* - `__resolveReference` is generated for federation types that have at least one resolvable `@key` directive
*/
generateInternalResolversIfNeeded?: GenerateInternalResolversIfNeededConfig;
/**
* @type boolean
* @default false
* @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.
*/
onlyResolveTypeForInterfaces?: boolean;
/**
* @description Makes `__typename` of resolver mappings non-optional without affecting the base types.
* @default false
Expand Down Expand Up @@ -700,7 +695,8 @@ export class BaseResolversVisitor<
rawConfig: TRawConfig,
additionalConfig: TPluginConfig,
private _schema: GraphQLSchema,
defaultScalars: NormalizedScalarsMap = DEFAULT_SCALARS
defaultScalars: NormalizedScalarsMap = DEFAULT_SCALARS,
federationMeta: FederationMeta = {}
) {
super(rawConfig, {
immutableTypes: getConfigValue(rawConfig.immutableTypes, false),
Expand All @@ -714,7 +710,6 @@ export class BaseResolversVisitor<
mapOrStr: rawConfig.enumValues,
}),
addUnderscoreToArgsType: getConfigValue(rawConfig.addUnderscoreToArgsType, false),
onlyResolveTypeForInterfaces: getConfigValue(rawConfig.onlyResolveTypeForInterfaces, false),
contextType: parseMapper(rawConfig.contextType || 'any', 'ContextType'),
fieldContextTypes: getConfigValue(rawConfig.fieldContextTypes, []),
directiveContextTypes: getConfigValue(rawConfig.directiveContextTypes, []),
Expand All @@ -729,9 +724,7 @@ export class BaseResolversVisitor<
mappers: transformMappers(rawConfig.mappers || {}, rawConfig.mapperTypeSuffix),
scalars: buildScalarsFromConfig(_schema, rawConfig, defaultScalars),
internalResolversPrefix: getConfigValue(rawConfig.internalResolversPrefix, '__'),
generateInternalResolversIfNeeded: {
__resolveReference: rawConfig.generateInternalResolversIfNeeded?.__resolveReference ?? false,
},
generateInternalResolversIfNeeded: {},
resolversNonOptionalTypename: normalizeResolversNonOptionalTypename(
getConfigValue(rawConfig.resolversNonOptionalTypename, false)
),
Expand All @@ -740,7 +733,11 @@ export class BaseResolversVisitor<
} as TPluginConfig);

autoBind(this);
this._federation = new ApolloFederation({ enabled: this.config.federation, schema: this.schema });
this._federation = new ApolloFederation({
enabled: this.config.federation,
schema: this.schema,
meta: federationMeta,
});
this._rootTypeNames = getRootTypeNames(_schema);
this._variablesTransformer = new OperationVariablesToObject(
this.scalars,
Expand Down Expand Up @@ -1358,7 +1355,9 @@ export class BaseResolversVisitor<

const federationMeta = this._federation.getMeta()[schemaTypeName];
if (federationMeta) {
userDefinedTypes[schemaTypeName].federation = federationMeta;
userDefinedTypes[schemaTypeName].federation = {
hasResolveReference: federationMeta.hasResolveReference,
};
}
}

Expand Down Expand Up @@ -1474,9 +1473,10 @@ export class BaseResolversVisitor<
const baseType = getBaseTypeNode(original.type);
const realType = baseType.name.value;
const parentType = this.schema.getType(parentName);
const meta: ReturnType<FieldDefinitionPrintFn>['meta'] = {};

if (this._federation.skipField({ fieldNode: original, parentType })) {
return null;
return { value: null, meta };
}

const contextType = this.getContextType(parentName, node);
Expand Down Expand Up @@ -1516,7 +1516,7 @@ export class BaseResolversVisitor<
}
}

const parentTypeSignature = this._federation.transformParentType({
const parentTypeSignature = this._federation.transformFieldParentType({
fieldNode: original,
parentType,
parentTypeSignature: this.getParentTypeForSignature(node),
Expand Down Expand Up @@ -1545,29 +1545,22 @@ export class BaseResolversVisitor<
};

if (this._federation.isResolveReferenceField(node)) {
if (this.config.generateInternalResolversIfNeeded.__resolveReference) {
const federationDetails = checkObjectTypeFederationDetails(
parentType.astNode as ObjectTypeDefinitionNode,
this._schema
);

if (!federationDetails || federationDetails.resolvableKeyDirectives.length === 0) {
return '';
}
if (!this._federation.getMeta()[parentType.name].hasResolveReference) {
return { value: '', meta };
}

this._federation.setMeta(parentType.name, { hasResolveReference: true });
signature.type = 'ReferenceResolver';
if (signature.genericTypes.length >= 3) {
signature.genericTypes = signature.genericTypes.slice(0, 3);
}
signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType];
meta.federation = { isResolveReference: true };
}

return indent(
`${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join(
', '
)}>${this.getPunctuation(declarationKind)}`
);
return {
value: indent(
`${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join(
', '
)}>${this.getPunctuation(declarationKind)}`
),
meta,
};
};
}

Expand Down Expand Up @@ -1628,7 +1621,7 @@ export class BaseResolversVisitor<
(rootType === 'mutation' && this.config.avoidOptionals.mutation) ||
(rootType === 'subscription' && this.config.avoidOptionals.subscription) ||
(rootType === false && this.config.avoidOptionals.resolvers)
);
).value;
});

if (!rootType) {
Expand All @@ -1645,10 +1638,11 @@ export class BaseResolversVisitor<
`ContextType = ${this.config.contextType.type}`,
this.transformParentGenericType(parentType),
];
if (this._federation.getMeta()[typeName]) {
const typeRef = `${this.convertName('FederationTypes')}['${typeName}']`;
genericTypes.push(`FederationType extends ${typeRef} = ${typeRef}`);
}
this._federation.addFederationTypeGenericIfApplicable({
genericTypes,
federationTypesType: this.convertName('FederationTypes'),
typeName,
});

const block = new DeclarationBlock(this._declarationBlockConfig)
.export()
Expand Down Expand Up @@ -1837,25 +1831,44 @@ export class BaseResolversVisitor<
}

const parentType = this.getParentTypeToUse(typeName);

const genericTypes: string[] = [
`ContextType = ${this.config.contextType.type}`,
this.transformParentGenericType(parentType),
];
this._federation.addFederationTypeGenericIfApplicable({
genericTypes,
federationTypesType: this.convertName('FederationTypes'),
typeName,
});

const possibleTypes = implementingTypes.map(name => `'${name}'`).join(' | ') || 'null';
const fields = this.config.onlyResolveTypeForInterfaces ? [] : node.fields || [];

// An Interface has __resolveType resolver, and no other fields.
const blockFields: string[] = [
indent(
`${this.config.internalResolversPrefix}resolveType${
this.config.optionalResolveType ? '?' : ''
}: TypeResolveFn<${possibleTypes}, ParentType, ContextType>${this.getPunctuation(declarationKind)}`
),
];

// An Interface in Federation may have the additional __resolveReference resolver, if resolvable.
// So, we filter out the normal fields declared on the Interface and add the __resolveReference resolver.
const fields = (node.fields as unknown as FieldDefinitionPrintFn[]).map(f =>
f(typeName, this.config.avoidOptionals.resolvers)
);
for (const field of fields) {
if (field.meta.federation?.isResolveReference) {
blockFields.push(field.value);
}
}

return new DeclarationBlock(this._declarationBlockConfig)
.export()
.asKind(declarationKind)
.withName(name, `<ContextType = ${this.config.contextType.type}, ${this.transformParentGenericType(parentType)}>`)
.withBlock(
[
indent(
`${this.config.internalResolversPrefix}resolveType${
this.config.optionalResolveType ? '?' : ''
}: TypeResolveFn<${possibleTypes}, ParentType, ContextType>${this.getPunctuation(declarationKind)}`
),
...(fields as unknown as FieldDefinitionPrintFn[]).map(f =>
f(typeName, this.config.avoidOptionals.resolvers)
),
].join('\n')
).string;
.withName(name, `<${genericTypes.join(', ')}>`)
.withBlock(blockFields.join('\n')).string;
}

SchemaDefinition() {
Expand Down
4 changes: 1 addition & 3 deletions packages/plugins/other/visitor-plugin-common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,5 @@ export interface CustomDirectivesConfig {
apolloUnmask?: boolean;
}

export interface GenerateInternalResolversIfNeededConfig {
__resolveReference?: boolean;
}
export interface GenerateInternalResolversIfNeededConfig {}
export type NormalizedGenerateInternalResolversIfNeededConfig = Required<GenerateInternalResolversIfNeededConfig>;
10 changes: 8 additions & 2 deletions packages/plugins/typescript/resolvers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,14 @@ export type Resolver${capitalizedDirectiveName}WithResolve<TResult, TParent, TCo
}
}

const transformedSchema = config.federation ? addFederationReferencesToSchema(schema) : schema;
const visitor = new TypeScriptResolversVisitor({ ...config, directiveResolverMappings }, transformedSchema);
const { transformedSchema, federationMeta } = config.federation
? addFederationReferencesToSchema(schema)
: { transformedSchema: schema, federationMeta: {} };
const visitor = new TypeScriptResolversVisitor(
{ ...config, directiveResolverMappings },
transformedSchema,
federationMeta
);
const namespacedImportPrefix = visitor.config.namespacedImportName ? `${visitor.config.namespacedImportName}.` : '';

const astNode = getCachedDocumentNodeFromSchema(transformedSchema);
Expand Down
8 changes: 6 additions & 2 deletions packages/plugins/typescript/resolvers/src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { TypeScriptOperationVariablesToObject } from '@graphql-codegen/typescrip
import {
BaseResolversVisitor,
DeclarationKind,
DEFAULT_SCALARS,
getConfigValue,
normalizeAvoidOptionals,
ParsedResolversConfig,
} from '@graphql-codegen/visitor-plugin-common';
import type { FederationMeta } from '@graphql-codegen/plugin-helpers';
import autoBind from 'auto-bind';
import { EnumTypeDefinitionNode, GraphQLSchema, ListTypeNode, NamedTypeNode, NonNullTypeNode } from 'graphql';
import { TypeScriptResolversPluginConfig } from './config.js';
Expand All @@ -24,7 +26,7 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor<
TypeScriptResolversPluginConfig,
ParsedTypeScriptResolversConfig
> {
constructor(pluginConfig: TypeScriptResolversPluginConfig, schema: GraphQLSchema) {
constructor(pluginConfig: TypeScriptResolversPluginConfig, schema: GraphQLSchema, federationMeta: FederationMeta) {
super(
pluginConfig,
{
Expand All @@ -34,7 +36,9 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor<
allowParentTypeOverride: getConfigValue(pluginConfig.allowParentTypeOverride, false),
optionalInfoArgument: getConfigValue(pluginConfig.optionalInfoArgument, false),
} as ParsedTypeScriptResolversConfig,
schema
schema,
DEFAULT_SCALARS,
federationMeta
);
autoBind(this);
this.setVariablesTransformer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@ export type SubscriptionResolvers<ContextType = any, ParentType = ResolversParen

export type NodeResolvers<ContextType = any, ParentType = ResolversParentTypes['Node']> = ResolversObject<{
__resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
}>;

export type SomeNodeResolvers<ContextType = any, ParentType = ResolversParentTypes['SomeNode']> = ResolversObject<{
Expand All @@ -282,19 +281,14 @@ export type SomeNodeResolvers<ContextType = any, ParentType = ResolversParentTyp

export type AnotherNodeResolvers<ContextType = any, ParentType = ResolversParentTypes['AnotherNode']> = ResolversObject<{
__resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
}>;

export type WithChildResolvers<ContextType = any, ParentType = ResolversParentTypes['WithChild']> = ResolversObject<{
__resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>;
unionChild?: Resolver<Maybe<ResolversTypes['ChildUnion']>, ParentType, ContextType>;
node?: Resolver<Maybe<ResolversTypes['AnotherNode']>, ParentType, ContextType>;
}>;

export type WithChildrenResolvers<ContextType = any, ParentType = ResolversParentTypes['WithChildren']> = ResolversObject<{
__resolveType: TypeResolveFn<'AnotherNodeWithAll', ParentType, ContextType>;
unionChildren?: Resolver<Array<ResolversTypes['ChildUnion']>, ParentType, ContextType>;
nodes?: Resolver<Array<ResolversTypes['AnotherNode']>, ParentType, ContextType>;
}>;

export type AnotherNodeWithChildResolvers<ContextType = any, ParentType = ResolversParentTypes['AnotherNodeWithChild']> = ResolversObject<{
Expand Down Expand Up @@ -532,7 +526,6 @@ export type SubscriptionResolvers<ContextType = any, ParentType extends Resolver

export type NodeResolvers<ContextType = any, ParentType extends ResolversParentTypes['Node'] = ResolversParentTypes['Node']> = ResolversObject<{
__resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
}>;

export type SomeNodeResolvers<ContextType = any, ParentType extends ResolversParentTypes['SomeNode'] = ResolversParentTypes['SomeNode']> = ResolversObject<{
Expand All @@ -542,19 +535,14 @@ export type SomeNodeResolvers<ContextType = any, ParentType extends ResolversPar

export type AnotherNodeResolvers<ContextType = any, ParentType extends ResolversParentTypes['AnotherNode'] = ResolversParentTypes['AnotherNode']> = ResolversObject<{
__resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
}>;

export type WithChildResolvers<ContextType = any, ParentType extends ResolversParentTypes['WithChild'] = ResolversParentTypes['WithChild']> = ResolversObject<{
__resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>;
unionChild?: Resolver<Types.Maybe<ResolversTypes['ChildUnion']>, ParentType, ContextType>;
node?: Resolver<Types.Maybe<ResolversTypes['AnotherNode']>, ParentType, ContextType>;
}>;

export type WithChildrenResolvers<ContextType = any, ParentType extends ResolversParentTypes['WithChildren'] = ResolversParentTypes['WithChildren']> = ResolversObject<{
__resolveType: TypeResolveFn<'AnotherNodeWithAll', ParentType, ContextType>;
unionChildren?: Resolver<Array<ResolversTypes['ChildUnion']>, ParentType, ContextType>;
nodes?: Resolver<Array<ResolversTypes['AnotherNode']>, ParentType, ContextType>;
}>;

export type AnotherNodeWithChildResolvers<ContextType = any, ParentType extends ResolversParentTypes['AnotherNodeWithChild'] = ResolversParentTypes['AnotherNodeWithChild']> = ResolversObject<{
Expand Down Expand Up @@ -878,7 +866,6 @@ export type SubscriptionResolvers<ContextType = any, ParentType extends Resolver

export type NodeResolvers<ContextType = any, ParentType extends ResolversParentTypes['Node'] = ResolversParentTypes['Node']> = ResolversObject<{
__resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
}>;

export type SomeNodeResolvers<ContextType = any, ParentType extends ResolversParentTypes['SomeNode'] = ResolversParentTypes['SomeNode']> = ResolversObject<{
Expand All @@ -888,19 +875,14 @@ export type SomeNodeResolvers<ContextType = any, ParentType extends ResolversPar

export type AnotherNodeResolvers<ContextType = any, ParentType extends ResolversParentTypes['AnotherNode'] = ResolversParentTypes['AnotherNode']> = ResolversObject<{
__resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
}>;

export type WithChildResolvers<ContextType = any, ParentType extends ResolversParentTypes['WithChild'] = ResolversParentTypes['WithChild']> = ResolversObject<{
__resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>;
unionChild?: Resolver<Maybe<ResolversTypes['ChildUnion']>, ParentType, ContextType>;
node?: Resolver<Maybe<ResolversTypes['AnotherNode']>, ParentType, ContextType>;
}>;

export type WithChildrenResolvers<ContextType = any, ParentType extends ResolversParentTypes['WithChildren'] = ResolversParentTypes['WithChildren']> = ResolversObject<{
__resolveType: TypeResolveFn<'AnotherNodeWithAll', ParentType, ContextType>;
unionChildren?: Resolver<Array<ResolversTypes['ChildUnion']>, ParentType, ContextType>;
nodes?: Resolver<Array<ResolversTypes['AnotherNode']>, ParentType, ContextType>;
}>;

export type AnotherNodeWithChildResolvers<ContextType = any, ParentType extends ResolversParentTypes['AnotherNodeWithChild'] = ResolversParentTypes['AnotherNodeWithChild']> = ResolversObject<{
Expand Down
Loading

0 comments on commit bbf0cf7

Please sign in to comment.