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

Next major version feature branch #10218

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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.
11 changes: 11 additions & 0 deletions .changeset/thick-pianos-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@graphql-codegen/visitor-plugin-common': major
'@graphql-codegen/typescript-resolvers': major
'@graphql-codegen/plugin-helpers': major
---

Fix `mappers` usage with Federation

`mappers` was previously used as `__resolveReference`'s first param (usually called "reference"). However, this is incorrect because `reference` interface comes directly from `@key` and `@requires` directives. This patch fixes the issue by creating a new `FederationTypes` type and use it as the base for federation entity types when being used to type entity references.

BREAKING CHANGES: No longer generate `UnwrappedObject` utility type, as this was used to support the wrong previously generated type.
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
pull_request:
branches:
- master
- federation-fixes # FIXME: Remove this line after the PR is merged

env:
NODE_OPTIONS: '--max_old_space_size=4096'
Expand Down
18 changes: 12 additions & 6 deletions dev-test/test-schema/resolvers-federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
info: GraphQLResolveInfo
) => TResult | Promise<TResult>;

/** Mapping of federation types */
export type FederationTypes = {
User: User;
};

/** Mapping between all available schema types and the resolvers types */
export type ResolversTypes = {
Address: ResolverTypeWrapper<Address>;
Expand Down Expand Up @@ -190,24 +195,25 @@ export type QueryResolvers<

export type UserResolvers<
ContextType = any,
ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']
ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'],
FederationType extends FederationTypes['User'] = FederationTypes['User']
> = {
__resolveReference?: ReferenceResolver<
Maybe<ResolversTypes['User']>,
{ __typename: 'User' } & (
| GraphQLRecursivePick<ParentType, { id: true }>
| GraphQLRecursivePick<ParentType, { name: true }>
| GraphQLRecursivePick<FederationType, { id: true }>
| GraphQLRecursivePick<FederationType, { name: true }>
),
ContextType
>;

email?: Resolver<
ResolversTypes['String'],
{ __typename: 'User' } & (
| GraphQLRecursivePick<ParentType, { id: true }>
| GraphQLRecursivePick<ParentType, { name: true }>
| GraphQLRecursivePick<FederationType, { id: true }>
| GraphQLRecursivePick<FederationType, { name: true }>
) &
GraphQLRecursivePick<ParentType, { address: { city: true; lines: { line2: true } } }>,
GraphQLRecursivePick<FederationType, { address: { city: true; lines: { line2: true } } }>,
ContextType
>;

Expand Down
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 @@ -1223,6 +1220,28 @@ export class BaseResolversVisitor<
).string;
}

public buildFederationTypes(): string {
const federationMeta = this._federation.getMeta();

if (Object.keys(federationMeta).length === 0) {
return '';
}

const declarationKind = 'type';
return new DeclarationBlock(this._declarationBlockConfig)
.export()
.asKind(declarationKind)
.withName(this.convertName('FederationTypes'))
.withComment('Mapping of federation types')
.withBlock(
Object.keys(federationMeta)
.map(typeName => {
return indent(`${typeName}: ${this.convertName(typeName)}${this.getPunctuation(declarationKind)}`);
})
.join('\n')
).string;
}

public get schema(): GraphQLSchema {
return this._schema;
}
Expand Down Expand Up @@ -1336,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 @@ -1452,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 @@ -1494,10 +1516,11 @@ export class BaseResolversVisitor<
}
}

const parentTypeSignature = this._federation.transformParentType({
const parentTypeSignature = this._federation.transformFieldParentType({
fieldNode: original,
parentType,
parentTypeSignature: this.getParentTypeForSignature(node),
federationTypeSignature: 'FederationType',
});
const mappedTypeKey = isSubscriptionType ? `${mappedType}, "${node.name}"` : mappedType;

Expand All @@ -1522,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 @@ -1605,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 @@ -1618,10 +1634,20 @@ export class BaseResolversVisitor<
);
}

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

const block = new DeclarationBlock(this._declarationBlockConfig)
.export()
.asKind(declarationKind)
.withName(name, `<ContextType = ${this.config.contextType.type}, ${this.transformParentGenericType(parentType)}>`)
.withName(name, `<${genericTypes.join(', ')}>`)
.withBlock(fieldsContent.join('\n'));

this._collectedResolvers[node.name as any] = {
Expand Down Expand Up @@ -1805,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>;
Loading
Loading