Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[federation][resolvers] Add generateInternalResolversIfNeeded. __resolveReference to generate __resolveReference only when resolvable #9989

Merged
merged 11 commits into from
Oct 8, 2024
11 changes: 11 additions & 0 deletions .changeset/fifty-dodos-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@graphql-codegen/visitor-plugin-common': minor
'@graphql-codegen/typescript-resolvers': minor
'@graphql-codegen/plugin-helpers': minor
---

Add `generateInternalResolversIfNeeded` option

This option can be used to generate more correct types for internal resolvers. For example, only generate `__resolveReference` if the federation object has a resolvable `@key`.

In the future, this option can be extended to support other internal resolvers e.g. `__isTypeOf` is only generated for implementing types and union members.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApolloFederation, getBaseType } from '@graphql-codegen/plugin-helpers';
import { ApolloFederation, checkObjectTypeFederationDetails, getBaseType } from '@graphql-codegen/plugin-helpers';
import { getRootTypeNames } from '@graphql-tools/utils';
import autoBind from 'auto-bind';
import {
Expand Down Expand Up @@ -33,6 +33,8 @@ import {
ConvertOptions,
DeclarationKind,
EnumValuesMap,
type NormalizedGenerateInternalResolversIfNeededConfig,
type GenerateInternalResolversIfNeededConfig,
NormalizedAvoidOptionalsConfig,
NormalizedScalarsMap,
ParsedEnumValuesMap,
Expand Down Expand Up @@ -75,12 +77,20 @@ export interface ParsedResolversConfig extends ParsedConfig {
resolverTypeSuffix: string;
allResolversTypeName: string;
internalResolversPrefix: string;
generateInternalResolversIfNeeded: NormalizedGenerateInternalResolversIfNeededConfig;
onlyResolveTypeForInterfaces: boolean;
directiveResolverMappings: Record<string, string>;
resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig;
}

type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null;
export interface RootResolver {
content: string;
generatedResolverTypes: {
resolversMap: { name: string };
userDefined: Record<string, { name: string; federation?: { hasResolveReference: boolean } }>;
};
}

export interface RawResolversConfig extends RawConfig {
/**
Expand Down Expand Up @@ -570,6 +580,16 @@ export interface RawResolversConfig extends RawConfig {
* If you are using `mercurius-js`, please set this field to empty string for better compatibility.
*/
internalResolversPrefix?: string;
/**
* @type object
* @default { __resolveReference: false }
* @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
Expand Down Expand Up @@ -641,7 +661,12 @@ export class BaseResolversVisitor<
> extends BaseVisitor<TRawConfig, TPluginConfig> {
protected _parsedConfig: TPluginConfig;
protected _declarationBlockConfig: DeclarationBlockConfig = {};
protected _collectedResolvers: { [key: string]: { typename: string; baseGeneratedTypename?: string } } = {};
protected _collectedResolvers: {
[key: string]: {
typename: string;
baseGeneratedTypename?: string;
};
} = {};
protected _collectedDirectiveResolvers: { [key: string]: string } = {};
protected _variablesTransformer: OperationVariablesToObject;
protected _usedMappers: { [key: string]: boolean } = {};
Expand All @@ -656,7 +681,6 @@ export class BaseResolversVisitor<
protected _globalDeclarations = new Set<string>();
protected _federation: ApolloFederation;
protected _hasScalars = false;
protected _hasFederation = false;
protected _fieldContextTypeMap: FieldContextTypeMap;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Previously, whenever there's a type with __resolveReference, this will be set to true.

Instead of a boolean, we now want to track all the types with generated __resolveReference types as output meta, so downstream consumers (e.g. Server Preset) can easily use it.
The ApolloFederation class now tracks this metadata.

I could just put it here as a property of the plugin e.g. _federationMeta but it seems like ApolloFederation is supposed to handle federation concerns. Happy to discuss if needed.

protected _directiveContextTypesMap: FieldContextTypeMap;
protected _checkedTypesWithNestedAbstractTypes: Record<string, { checkStatus: 'yes' | 'no' | 'checking' }> = {};
Expand Down Expand Up @@ -696,6 +720,9 @@ export class BaseResolversVisitor<
mappers: transformMappers(rawConfig.mappers || {}, rawConfig.mapperTypeSuffix),
scalars: buildScalarsFromConfig(_schema, rawConfig, defaultScalars),
internalResolversPrefix: getConfigValue(rawConfig.internalResolversPrefix, '__'),
generateInternalResolversIfNeeded: {
__resolveReference: rawConfig.generateInternalResolversIfNeeded?.__resolveReference ?? false,
},
resolversNonOptionalTypename: normalizeResolversNonOptionalTypename(
getConfigValue(rawConfig.resolversNonOptionalTypename, false)
),
Expand Down Expand Up @@ -1269,21 +1296,15 @@ export class BaseResolversVisitor<
}

public hasFederation(): boolean {
return this._hasFederation;
return Object.keys(this._federation.getMeta()).length > 0;
}

public getRootResolver(): {
content: string;
generatedResolverTypes: {
resolversMap: { name: string };
userDefined: Record<string, { name: string }>;
};
} {
public getRootResolver(): RootResolver {
const name = this.convertName(this.config.allResolversTypeName);
const declarationKind = 'type';
const contextType = `<ContextType = ${this.config.contextType.type}>`;

const userDefinedTypes: Record<string, { name: string }> = {};
const userDefinedTypes: RootResolver['generatedResolverTypes']['userDefined'] = {};
const content = [
new DeclarationBlock(this._declarationBlockConfig)
.export()
Expand All @@ -1295,7 +1316,14 @@ export class BaseResolversVisitor<
const resolverType = this._collectedResolvers[schemaTypeName];

if (resolverType.baseGeneratedTypename) {
userDefinedTypes[schemaTypeName] = { name: resolverType.baseGeneratedTypename };
userDefinedTypes[schemaTypeName] = {
name: resolverType.baseGeneratedTypename,
};

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

return indent(this.formatRootResolver(schemaTypeName, resolverType.typename, declarationKind));
Expand Down Expand Up @@ -1480,9 +1508,20 @@ export class BaseResolversVisitor<
};

if (this._federation.isResolveReferenceField(node)) {
this._hasFederation = true;
signature.type = 'ReferenceResolver';
if (this.config.generateInternalResolversIfNeeded.__resolveReference) {
const federationDetails = checkObjectTypeFederationDetails(
parentType.astNode as ObjectTypeDefinitionNode,
this._schema
);

if (!federationDetails || federationDetails.resolvableKeyDirectives.length === 0) {
return '';
}
signature.modifier = ''; // if a federation type has resolvable @key, then it should be required
}

this._federation.setMeta(parentType.name, { hasResolveReference: true });
signature.type = 'ReferenceResolver';
if (signature.genericTypes.length >= 3) {
signature.genericTypes = signature.genericTypes.slice(0, 3);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/plugins/other/visitor-plugin-common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,8 @@ export interface ResolversNonOptionalTypenameConfig {
interfaceImplementingType?: boolean;
excludeTypes?: string[];
}

export interface GenerateInternalResolversIfNeededConfig {
__resolveReference?: boolean;
}
export type NormalizedGenerateInternalResolversIfNeededConfig = Required<GenerateInternalResolversIfNeededConfig>;
7 changes: 2 additions & 5 deletions packages/plugins/typescript/resolvers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
PluginFunction,
Types,
} from '@graphql-codegen/plugin-helpers';
import { parseMapper } from '@graphql-codegen/visitor-plugin-common';
import { parseMapper, type RootResolver } from '@graphql-codegen/visitor-plugin-common';
import { GraphQLSchema } from 'graphql';
import { TypeScriptResolversPluginConfig } from './config.js';
import { TypeScriptResolversVisitor } from './visitor.js';
Expand All @@ -15,10 +15,7 @@ const capitalize = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1)
export const plugin: PluginFunction<
TypeScriptResolversPluginConfig,
Types.ComplexPluginOutput<{
generatedResolverTypes: {
resolversMap: { name: string };
userDefined: Record<string, { name: string }>;
};
generatedResolverTypes: RootResolver['generatedResolverTypes'];
}>
> = (schema: GraphQLSchema, documents: Types.DocumentFile[], config: TypeScriptResolversPluginConfig) => {
const imports = [];
Expand Down
Loading
Loading