Skip to content

Commit

Permalink
[federation][resolvers] Add `generateInternalResolversIfNeeded. __res…
Browse files Browse the repository at this point in the history
…olveReference` to generate `__resolveReference` only when resolvable (#9989)
  • Loading branch information
eddeee888 authored Oct 8, 2024
1 parent 3fd4486 commit 55a1e9e
Show file tree
Hide file tree
Showing 6 changed files with 529 additions and 44 deletions.
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;
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

0 comments on commit 55a1e9e

Please sign in to comment.