Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 } } };
Comment on lines +86 to +89
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, we don't know if a field is __resolveReference or not until we have generated it. This is because we are injecting __resolveReference as real fields in addFederationReferencesToSchema. Therefore, we need to add meta to have more info about the generated field

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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateInternalResolversIfNeeded might be deprecated in this major version, it'll come in one of the next few PRs.
Since we are bumping a major version, it's worth generating these meta types only when required

/**
* @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;
Comment on lines -595 to -600
Copy link
Collaborator Author

@eddeee888 eddeee888 Dec 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related #5648

onlyResolveTypeForInterfaces is only applicable for some rare use cases mentioned here. This option's implementation blocks __resolveReference being created for Interfaces when the option is turned on.

I think it's simpler just to deprecate this option as we are moving to a new major version. If anyone needs it, we can put it back as a minor version

/**
* @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);
}
Comment on lines -1561 to -1563
Copy link
Collaborator Author

@eddeee888 eddeee888 Jan 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit odd to run .slice to limit generics to 3, may as well be explicit here!

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
Loading