Skip to content

[typescript-resolvers][federation] Fix federation @requires type #10366

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

Open
wants to merge 10 commits into
base: federation-fixes
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions .changeset/small-fans-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphql-codegen/visitor-plugin-common': patch
'@graphql-codegen/typescript-resolvers': patch
'@graphql-codegen/plugin-helpers': patch
---

Update @requires type
27 changes: 13 additions & 14 deletions dev-test/test-schema/resolvers-federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ export type FederationTypes = {
User: User;
};

/** Mapping of federation reference types */
export type FederationReferenceTypes = {
User: { __typename: 'User' } & (
| GraphQLRecursivePick<FederationTypes['User'], { id: true }>
| GraphQLRecursivePick<FederationTypes['User'], { name: true }>
) &
({} | GraphQLRecursivePick<FederationTypes['User'], { address: { city: true; lines: { line2: true } } }>);
};

/** Mapping between all available schema types and the resolvers types */
export type ResolversTypes = {
Address: ResolverTypeWrapper<Address>;
Expand All @@ -154,13 +163,7 @@ export type ResolversParentTypes = {
ID: Scalars['ID']['output'];
Lines: Lines;
Query: {};
User:
| User
| ({ __typename: 'User' } & (
| GraphQLRecursivePick<FederationTypes['User'], { id: true }>
| GraphQLRecursivePick<FederationTypes['User'], { name: true }>
) &
GraphQLRecursivePick<FederationTypes['User'], { address: { city: true; lines: { line2: true } } }>);
User: User | FederationReferenceTypes['User'];
Int: Scalars['Int']['output'];
Boolean: Scalars['Boolean']['output'];
};
Expand Down Expand Up @@ -199,15 +202,11 @@ export type QueryResolvers<
export type UserResolvers<
ContextType = any,
ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'],
FederationType extends FederationTypes['User'] = FederationTypes['User']
FederationReferenceType extends FederationReferenceTypes['User'] = FederationReferenceTypes['User']
> = {
__resolveReference?: ReferenceResolver<
Maybe<ResolversTypes['User']>,
{ __typename: 'User' } & (
| GraphQLRecursivePick<FederationType, { id: true }>
| GraphQLRecursivePick<FederationType, { name: true }>
) &
GraphQLRecursivePick<FederationType, { address: { city: true; lines: { line2: true } } }>,
Maybe<ResolversTypes['User']> | FederationReferenceType,
FederationReferenceType,
ContextType
>;
email?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -818,12 +818,8 @@ export class BaseResolversVisitor<
shouldInclude: namedType => !isEnumType(namedType),
onNotMappedObjectType: ({ typeName, initialType }) => {
let result = initialType;
const federationReferenceTypes = this._federation.printReferenceSelectionSets({
typeName,
baseFederationType: `${this.convertName('FederationTypes')}['${typeName}']`,
});
if (federationReferenceTypes) {
result += ` | ${federationReferenceTypes}`;
if (this._federation.getMeta()[typeName]?.referenceSelectionSetsString) {
result += ` | ${this.convertName('FederationReferenceTypes')}['${typeName}']`;
}
return result;
},
Expand Down Expand Up @@ -1306,6 +1302,33 @@ export class BaseResolversVisitor<
).string;
}

public buildFederationReferenceTypes(): 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('FederationReferenceTypes'))
.withComment('Mapping of federation reference types')
.withBlock(
Object.entries(federationMeta)
.map(([typeName, { referenceSelectionSetsString }]) => {
if (!referenceSelectionSetsString) {
return undefined;
}

return indent(`${typeName}: ${referenceSelectionSetsString}${this.getPunctuation(declarationKind)}`);
})
.filter(v => v)
.join('\n')
).string;
}
Comment on lines +1305 to +1330
Copy link
Collaborator Author

@eddeee888 eddeee888 Jun 9, 2025

Choose a reason for hiding this comment

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

Having buildFederationReferenceTypes to build standalone FederationReferenceTypes allows us to easily refer to the complex reference param types in different scenarios:

  1. When using mappers:
import type { FederationReferenceTypes } from "./types.generated";
export type UserMapper = DatabaseUser | FederationReferenceTypes["User"]

This allows us to return reference as-is without type errors:

export const User: UserResolvers = {
  __resolveReference: (ref) => {
    return ref;
  },
}

  1. When not using mappers:
export type ResolversParentTypes = {
  User: User | FederationReferenceTypes["User"];
};

Similar to the mapper case, the parent of each normal resolver can receive a normal type or reference type


  1. And for object resolvers:
export type UserResolvers<
  ContextType = ServerContext,
  ParentType extends
    ResolversParentTypes["User"] = ResolversParentTypes["User"],
  FederationReferenceType extends
    FederationReferenceTypes["User"] = FederationReferenceTypes["User"],
> = {
  __resolveReference?: ReferenceResolver<
    Maybe<ResolversTypes["User"]> | FederationReferenceType,
    FederationReferenceType,
    ContextType
  >;
};

Again, FederationReferenceTypes["User"] is used:

  • As reference type (2nd generic param)
  • and __resolveReference must return: (1) User value normally, or (2) FederationReferenceType


public get schema(): GraphQLSchema {
return this._schema;
}
Expand Down Expand Up @@ -1586,13 +1609,6 @@ export class BaseResolversVisitor<
}
}

const parentTypeSignature = this._federation.transformFieldParentType({
fieldNode: original,
parentType,
parentTypeSignature: this.getParentTypeForSignature(node),
federationTypeSignature: 'FederationType',
});
Comment on lines -1589 to -1594
Copy link
Collaborator Author

@eddeee888 eddeee888 Jun 9, 2025

Choose a reason for hiding this comment

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

Since federation reference type is extracted to FederationReferenceTypes, we don't need complex, inline reference types.
So, transformFieldParentType can be completely removed 🎉


const { mappedTypeKey, resolverType } = ((): { mappedTypeKey: string; resolverType: string } => {
const baseType = getBaseTypeNode(original.type);
const realType = baseType.name.value;
Expand Down Expand Up @@ -1637,15 +1653,18 @@ export class BaseResolversVisitor<
name: typeName,
modifier: avoidResolverOptionals ? '' : '?',
type: resolverType,
genericTypes: [mappedTypeKey, parentTypeSignature, contextType, argsType].filter(f => f),
genericTypes: [mappedTypeKey, this.getParentTypeForSignature(node), contextType, argsType].filter(f => f),
};

if (this._federation.isResolveReferenceField(node)) {
if (!this._federation.getMeta()[parentType.name].hasResolveReference) {
return { value: '', meta };
}
const resultType = `${mappedTypeKey} | FederationReferenceType`;
const referenceType = 'FederationReferenceType';

signature.type = 'ReferenceResolver';
signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType];
signature.genericTypes = [resultType, referenceType, contextType];
meta.federation = { isResolveReference: true };
}

Expand Down Expand Up @@ -1788,7 +1807,7 @@ export class BaseResolversVisitor<
];
this._federation.addFederationTypeGenericIfApplicable({
genericTypes,
federationTypesType: this.convertName('FederationTypes'),
federationTypesType: this.convertName('FederationReferenceTypes'),
typeName,
});

Expand Down Expand Up @@ -1975,7 +1994,7 @@ export class BaseResolversVisitor<
];
this._federation.addFederationTypeGenericIfApplicable({
genericTypes,
federationTypesType: this.convertName('FederationTypes'),
federationTypesType: this.convertName('FederationReferenceTypes'),
typeName,
});

Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/typescript/resolvers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
`;

const federationTypes = visitor.buildFederationTypes();
const federationReferenceTypes = visitor.buildFederationReferenceTypes();
const resolversTypeMapping = visitor.buildResolversTypes();
const resolversParentTypeMapping = visitor.buildResolversParentTypes();
const resolversUnionTypesMapping = visitor.buildResolversUnionTypes();
Expand Down Expand Up @@ -294,6 +295,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
content: [
header,
federationTypes,
federationReferenceTypes,
resolversUnionTypesMapping,
resolversInterfaceTypesMapping,
resolversTypeMapping,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
) => TResult | Promise<TResult>;



/** Mapping of union types */
export type ResolversUnionTypes<_RefType extends Record<string, unknown>> = ResolversObject<{
ChildUnion:
Expand Down Expand Up @@ -433,6 +434,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
) => TResult | Promise<TResult>;



/** Mapping of union types */
export type ResolversUnionTypes<_RefType extends Record<string, unknown>> = ResolversObject<{
ChildUnion:
Expand Down Expand Up @@ -785,6 +787,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
) => TResult | Promise<TResult>;



/** Mapping of union types */
export type ResolversUnionTypes<_RefType extends Record<string, unknown>> = ResolversObject<{
ChildUnion:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => {
Admin: Admin;
};

/** Mapping of federation reference types */
export type FederationReferenceTypes = {
Person:
( { __typename: 'Person' }
& GraphQLRecursivePick<FederationTypes['Person'], {"id":true}> );
User:
( { __typename: 'User' }
& GraphQLRecursivePick<FederationTypes['User'], {"id":true}> );
Admin:
( { __typename: 'Admin' }
& GraphQLRecursivePick<FederationTypes['Admin'], {"id":true}> );
};


/** Mapping of interface types */
export type ResolversInterfaceTypes<_RefType extends Record<string, unknown>> = {
Expand All @@ -150,12 +163,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => {
Query: {};
Person: ResolversInterfaceTypes<ResolversParentTypes>['Person'];
ID: Scalars['ID']['output'];
User: User |
( { __typename: 'User' }
& GraphQLRecursivePick<FederationTypes['User'], {"id":true}> );
Admin: Admin |
( { __typename: 'Admin' }
& GraphQLRecursivePick<FederationTypes['Admin'], {"id":true}> );
User: User | FederationReferenceTypes['User'];
Admin: Admin | FederationReferenceTypes['Admin'];
Boolean: Scalars['Boolean']['output'];
PersonName: PersonName;
String: Scalars['String']['output'];
Expand All @@ -165,26 +174,20 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => {
me?: Resolver<Maybe<ResolversTypes['Person']>, ParentType, ContextType>;
};

export type PersonResolvers<ContextType = any, ParentType extends ResolversParentTypes['Person'] = ResolversParentTypes['Person'], FederationType extends FederationTypes['Person'] = FederationTypes['Person']> = {
export type PersonResolvers<ContextType = any, ParentType extends ResolversParentTypes['Person'] = ResolversParentTypes['Person'], FederationReferenceType extends FederationReferenceTypes['Person'] = FederationReferenceTypes['Person']> = {
__resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>;
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['Person']>,
( { __typename: 'Person' }
& GraphQLRecursivePick<FederationType, {"id":true}> ), ContextType>;
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['Person']> | FederationReferenceType, FederationReferenceType, ContextType>;
};

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

export type AdminResolvers<ContextType = any, ParentType extends ResolversParentTypes['Admin'] = ResolversParentTypes['Admin'], FederationType extends FederationTypes['Admin'] = FederationTypes['Admin']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['Admin']>,
( { __typename: 'Admin' }
& GraphQLRecursivePick<FederationType, {"id":true}> ), ContextType>;
export type AdminResolvers<ContextType = any, ParentType extends ResolversParentTypes['Admin'] = ResolversParentTypes['Admin'], FederationReferenceType extends FederationReferenceTypes['Admin'] = FederationReferenceTypes['Admin']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['Admin']> | FederationReferenceType, FederationReferenceType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['PersonName'], ParentType, ContextType>;
canImpersonate?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => {
id: ID!
user: User!
}

type Account @key(fields: "id") {
id: ID!
name: String! @external
displayName: String! @requires(fields: "name")
}
`;

const content = await generate({
Expand All @@ -25,14 +31,15 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => {
federation: true,
mappers: {
User: './mappers#UserMapper',
Account: './mappers#AccountMapper',
},
},
});

// User should have it
expect(content).toMatchInlineSnapshot(`
"import { GraphQLResolveInfo } from 'graphql';
import { UserMapper } from './mappers';
import { UserMapper, AccountMapper } from './mappers';
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;


Expand Down Expand Up @@ -115,6 +122,19 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => {
/** Mapping of federation types */
export type FederationTypes = {
User: User;
Account: Account;
};

/** Mapping of federation reference types */
export type FederationReferenceTypes = {
User:
( { __typename: 'User' }
& GraphQLRecursivePick<FederationTypes['User'], {"id":true}> );
Account:
( { __typename: 'Account' }
& GraphQLRecursivePick<FederationTypes['Account'], {"id":true}>
& ( {}
| GraphQLRecursivePick<FederationTypes['Account'], {"name":true}> ) );
};


Expand All @@ -126,6 +146,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => {
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
String: ResolverTypeWrapper<Scalars['String']['output']>;
UserProfile: ResolverTypeWrapper<Omit<UserProfile, 'user'> & { user: ResolversTypes['User'] }>;
Account: ResolverTypeWrapper<AccountMapper>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
};

Expand All @@ -136,17 +157,16 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => {
ID: Scalars['ID']['output'];
String: Scalars['String']['output'];
UserProfile: Omit<UserProfile, 'user'> & { user: ResolversParentTypes['User'] };
Account: AccountMapper;
Boolean: Scalars['Boolean']['output'];
};

export type QueryResolvers<ContextType = any, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = {
me?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>;
};

export type UserResolvers<ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'], FederationType extends FederationTypes['User'] = FederationTypes['User']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>,
( { __typename: 'User' }
& GraphQLRecursivePick<FederationType, {"id":true}> ), ContextType>;
export type UserResolvers<ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'], FederationReferenceType extends FederationReferenceTypes['User'] = FederationReferenceTypes['User']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']> | FederationReferenceType, FederationReferenceType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
};
Expand All @@ -156,10 +176,17 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => {
user?: Resolver<ResolversTypes['User'], ParentType, ContextType>;
};

export type AccountResolvers<ContextType = any, ParentType extends ResolversParentTypes['Account'] = ResolversParentTypes['Account'], FederationReferenceType extends FederationReferenceTypes['Account'] = FederationReferenceTypes['Account']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['Account']> | FederationReferenceType, FederationReferenceType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
displayName?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
};

export type Resolvers<ContextType = any> = {
Query?: QueryResolvers<ContextType>;
User?: UserResolvers<ContextType>;
UserProfile?: UserProfileResolvers<ContextType>;
Account?: AccountResolvers<ContextType>;
};

"
Expand Down
Loading
Loading