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

Conversation

eddeee888
Copy link
Collaborator

@eddeee888 eddeee888 commented Jun 4, 2025

Description

  • Creates FederationReferenceTypes to declare the type of references of each object. This is used by all generated types, and can be used in mappers. More details in comment
  • Correctly finds all combinations of selection sets declared by @requires in different fields and put them in FederationReferenceTypes. More details in comment

Related #10206

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

  • Unit test

Copy link

changeset-bot bot commented Jun 4, 2025

🦋 Changeset detected

Latest commit: 39faa7e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@graphql-codegen/visitor-plugin-common Patch
@graphql-codegen/typescript-resolvers Patch
@graphql-codegen/plugin-helpers Patch
@graphql-codegen/typescript-document-nodes Patch
@graphql-codegen/gql-tag-operations Patch
@graphql-codegen/typescript-operations Patch
@graphql-codegen/typed-document-node Patch
@graphql-codegen/typescript Patch
@graphql-codegen/graphql-modules-preset Patch
@graphql-codegen/client-preset Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@eddeee888 eddeee888 changed the base branch from federation-fixes to master June 5, 2025 12:53
@eddeee888 eddeee888 changed the base branch from master to federation-fixes June 5, 2025 12:54
Copy link
Contributor

github-actions bot commented Jun 5, 2025

🚀 Snapshot Release (alpha)

The latest changes of this PR are available as alpha on npm (based on the declared changesets):

Package Version Info
@graphql-codegen/cli 5.0.7-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/core 4.0.3-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/add 5.0.4-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/fragment-matcher 5.1.1-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/introspection 4.0.4-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/schema-ast 4.1.1-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/time 5.0.2-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/visitor-plugin-common 6.0.0-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript-document-nodes 4.0.17-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/gql-tag-operations 4.0.18-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript-operations 4.6.2-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript-resolvers 5.0.0-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/typed-document-node 5.1.2-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript 4.1.7-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/client-preset 4.8.2-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/graphql-modules-preset 4.0.18-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/testing 3.0.5-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎
@graphql-codegen/plugin-helpers 6.0.0-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 npm ↗︎ unpkg ↗︎

@eddeee888 eddeee888 force-pushed the fix-federation-requires branch 2 times, most recently from 016484a to 7295def Compare June 9, 2025 13:23
Comment on lines +1305 to +1330
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;
}
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

Comment on lines -1589 to -1594
const parentTypeSignature = this._federation.transformFieldParentType({
fieldNode: original,
parentType,
parentTypeSignature: this.getParentTypeForSignature(node),
federationTypeSignature: 'FederationType',
});
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 🎉

Comment on lines -415 to -450
printReferenceSelectionSet({
typeName,
referenceSelectionSet,
}: {
typeName: string;
referenceSelectionSet: ReferenceSelectionSet;
}): string {
return `GraphQLRecursivePick<${typeName}, ${JSON.stringify(referenceSelectionSet)}>`;
}

printReferenceSelectionSets({
typeName,
baseFederationType,
}: {
typeName: string;
baseFederationType: string;
}): string | false {
const federationMeta = this.getMeta()[typeName];

if (!federationMeta?.hasResolveReference) {
return false;
}

return `\n ( { __typename: '${typeName}' }\n & ${federationMeta.referenceSelectionSets
.map(referenceSelectionSetArray => {
const result = referenceSelectionSetArray.map(referenceSelectionSet => {
return this.printReferenceSelectionSet({
referenceSelectionSet,
typeName: baseFederationType,
});
});

return result.length > 1 ? `( ${result.join(' | ')} )` : result.join(' | ');
})
.join('\n & ')} )`;
}
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.

This is now moved to addFederationReferencesToSchema because it's only used there.
I'm thinking to create a addFederationReferencesToSchema.ts file to shorten this file.

addFederationReferencesToSchema started as the function to mutate the schema (to re-use a lot of core plugin's functionalities).
However, since it is run once at the start on each interface and object, it is convenient to gather metadata (e.g. reference type) and do stuff with said metadata (e.g. turn reference metadata to TypeScript type)

Comment on lines +42 to +47
interface DirectiveSelectionSet {
name: string;
selection: boolean | ReferenceSelectionSet[];
selection: boolean | DirectiveSelectionSet[];
}

type ReferenceSelectionSet = Record<string, boolean>; // TODO: handle nested
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I now understand extractReferenceSelectionSet better. It is a series of functions that turn GraphQL selection set -> DirectiveSelectionSet -> ReferenceSelectionSet

The previously named ReferenceSelectionSet is now correctly named DirectiveSelectionSet (It's only the immediary step)

@eddeee888 eddeee888 force-pushed the fix-federation-requires branch from 7295def to 44802c0 Compare June 12, 2025 13:26
Comment on lines +127 to +192
/**
* Function to find all combinations of selection sets and push them into the `result`
* This is used for `@requires` directive because depending on the operation selection set, different
* combination of fields are sent from the router.
*
* @example
* Input: [
* { a: true },
* { b: true },
* { c: true },
* { d: true},
* ]
* Output: [
* { a: true },
* { a: true, b: true },
* { a: true, c: true },
* { a: true, d: true },
* { a: true, b: true, c: true },
* { a: true, b: true, d: true },
* { a: true, c: true, d: true },
* { a: true, b: true, c: true, d: true }
*
* { b: true },
* { b: true, c: true },
* { b: true, d: true },
* { b: true, c: true, d: true }
*
* { c: true },
* { c: true, d: true },
*
* { d: true },
* ]
* ```
*/
function findAllSelectionSetCombinations(
selectionSets: ReferenceSelectionSet[],
result: ReferenceSelectionSet[]
): void {
if (selectionSets.length === 0) {
return;
}

for (let baseIndex = 0; baseIndex < selectionSets.length; baseIndex++) {
const base = selectionSets.slice(0, baseIndex + 1);
const rest = selectionSets.slice(baseIndex + 1, selectionSets.length);

const currentSelectionSet = base.reduce((acc, selectionSet) => {
acc = { ...acc, ...selectionSet };
return acc;
}, {});

if (baseIndex === 0) {
result.push(currentSelectionSet);
}

for (const selectionSet of rest) {
result.push({ ...currentSelectionSet, ...selectionSet });
}
}

const next = selectionSets.slice(1, selectionSets.length);

if (next.length > 0) {
findAllSelectionSetCombinations(next, result);
}
}
Copy link
Collaborator Author

@eddeee888 eddeee888 Jun 12, 2025

Choose a reason for hiding this comment

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

findAllSelectionSetCombinations is core to @requires (based on my understanding of this directive, feel free to correct if I misunderstood it):

If a field is marked with @requires:

  • and a client requests for said field, then the reference object contains fields in the selection set declared by @requires
  • otherwise, if the client doesn't request for said field, the declared selection set won't be in the reference object

This means when there are multiple fields with @requires, the selection set fields can appear in any combination.


For example:

type Foo @key(fields: "id") {
  id: ID!

  a: String @external
  aRequires: String @requires(fields: "a")


  b: String @external
  bRequires: String @requires(fields: "b")

  c: String @external
  cRequires: String @requires(fields: "c")
}
  • If a client doesn't request for aRequires, bRequires, or cRequires, then the reference shape is { __typename: 'Foo', id: 'something' }
  • If a client requests for aRequires, then the reference shape is { __typename: 'Foo', id: 'something', a: 'a-value' }
  • If a client requests for aRequires and bRequires, then the reference shape is { __typename: 'Foo', id: 'something', a: 'a-value', b: 'b-value' }
  • If a client requests for aRequires and cRequires, then the reference shape is { __typename: 'Foo', id: 'something', a: 'a-value', c: 'c-value' }
  • etc.

findAllSelectionSetCombinations will find all the combination of selection sets declared for aRequires, bRequires and cRequires

@eddeee888 eddeee888 marked this pull request as ready for review June 12, 2025 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant