Skip to content

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: c1ee9bc

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
@eddeee888 eddeee888 changed the title [typescript-resolvers][federation] Fix federation @requires type [resolvers][federation] Fix federation @requires type Jul 9, 2025
@eddeee888 eddeee888 force-pushed the fix-federation-requires branch from 61475de to c1ee9bc Compare August 3, 2025 13:44
@eddeee888
Copy link
Collaborator Author

I'm merging this first so we can start testing the next major release 🙂

@eddeee888 eddeee888 merged commit ba3d434 into federation-fixes Aug 3, 2025
15 checks passed
@eddeee888 eddeee888 deleted the fix-federation-requires branch August 3, 2025 13:56
eddeee888 added a commit that referenced this pull request Aug 5, 2025
* Update test setup

* Implement @requires combination

* Add changeset

* Force release alpha

* Fix issue with empty array, set up tests

* Generate FederationReferenceTypes once

* Update tests related to FederationReferenceTypes

* Update dev-tests

* Revert force release

* Update test related to mapper
eddeee888 added a commit that referenced this pull request Aug 27, 2025
* Update test setup

* Implement @requires combination

* Add changeset

* Force release alpha

* Fix issue with empty array, set up tests

* Generate FederationReferenceTypes once

* Update tests related to FederationReferenceTypes

* Update dev-tests

* Revert force release

* Update test related to mapper
eddeee888 added a commit that referenced this pull request Sep 4, 2025
* Update test setup

* Implement @requires combination

* Add changeset

* Force release alpha

* Fix issue with empty array, set up tests

* Generate FederationReferenceTypes once

* Update tests related to FederationReferenceTypes

* Update dev-tests

* Revert force release

* Update test related to mapper
eddeee888 added a commit that referenced this pull request Sep 7, 2025
* [resolvers][federation] Fix mapper being incorrectly used as the base type for reference (#10216)

* [resolvers][federation] Add `__resolveReference` to applicable `Interface` entities, fix Interface types having non-meta resolver fields (#10221)

* Add __resolveReference for applicable Interfaces

- Deprecate generateInternalResolversIfNeeded.__resolveReference
- Fix tests
- Deprecate onlyResolveTypeForInterfaces
- Add changeset
- Cleanup
- Handle __resolveReference generation in Interface
- Let FieldDefinition decide whether to generate __resolveReference by checking whether parent has resolvable key

* Fix test

* chore(dependencies): updated changesets for modified dependencies

* chore(dependencies): updated changesets for modified dependencies

* [resolvers] Ensure `__isTypeof` is only generated for implementing types (of Interfaces) or Union members (#10283)

* Implement logic to only generate __isTypeOf for implementing types OR union members

* Remove unused types

* Add changeset

* Remove generateInternalResolversIfNeeded

* Fix dev tests

* Refactor to use parsedSchemaMeta

* [resolvers][federation] Bring Federation reference selection set to ResolversParentTypes instead of each resolver (#10297)

* Bring reference selection set to ResolversParentTypes

* Put back old types to extractReferenceSelectionSet

* Update tests for TDD

* Handle parent type consistently for __resolveReference and subsequent resolvers

* Update tests

* chore(dependencies): updated changesets for modified dependencies

* Add missing changeset

* chore(dependencies): updated changesets for modified dependencies

* [resolvers][federation] Fix fields or types being wrong generated when marked with @external (#10287)

* Ensure @external does not generate resolver types

- Handle external directive when part or whole type is marked
- Add changeset
- Add test cases for @provides and @external

* Format and minor text updates

* Add comments

* Fix __resolveReference not getting generated

* Fix test with __isTypeOf when not needed

* Fix type cast that results in wrong type

* Revert unncessary changes to FieldDefinitionPrintFn

* Re-format

* Convert to use AST Node instead of GraphQL Type

* Update test template

* Cache field nodes to generate for processed objects

* Put FIXME on base-resolvers-visitor to remove Name method

* Fix unit test

* [resolvers][federation] Fix federation @requires type (#10366)

* Update test setup

* Implement @requires combination

* Add changeset

* Force release alpha

* Fix issue with empty array, set up tests

* Generate FederationReferenceTypes once

* Update tests related to FederationReferenceTypes

* Update dev-tests

* Revert force release

* Update test related to mapper

* [resolvers] Refactor to remove NameNode override and simplify federation functions (#10377)

* Remove NameNode override and refactor relevant references

* Simplify federation utils by making functions handle nodes

* Fix lint issue

* CODEGEN-834 - [cli] Handle partial generation success (#10376)

* Add writeOnPartialSuccess flag to partially write successful generateiong

* Update config name from writeOnPartialSuccess to allowPartialOutputs

* Ensure consistent experience on complete failure, update tests

* Restructure code and comment

* Update website schema

* Update doc

* Drop Node 18 support (#10392)

* Drop Node 18 support

* Add changeset

* Drop graphql tools prisma loader (#10400)

* Drop @graphql-tools/prisma-loader

* Add changeset

* Update yarn.lock

* Update tsconfig for Node 20 (#10403)

* Update tsconfig to Node 20 recommended version

* Make babel allow declare TS field

* Bump packages (#10404)

* Bump dependency-graph to ^1.0.0

* Bump nock to 14.0.0

* Bump debounce to v2 and remove types package

* Bump ESM packages

- chalk
- detect-indent
- log-symbols
- auto-bind

* Revert "Bump ESM packages"

This reverts commit 7b79aaa.

* Migrate jitit to v2

* Bump cosmiconfig to v9

* Add changesets

* Use min. jiti v2.3.0 to handle default import like before

* [CLI] Bump deps for next major version (#10405)

* Bump listr2

* Migrate implementation and update tests for listr2

* Migrate inquirer to inquirer/prompts

* Remove old resolutions & update lock file (#10407)

* Remove unnecessary resolutions

* Remove deprecated config options for next major version (#10408)

* Remove deprecated config

- watchConfig
- dedupeFragments
- noGraphQLTag

* Add changesets

* Regen website schema

* CODEGEN-840 - Handle empty object type better (#10409)

* Update ts-resolvers to handle {} correctly

* Refactor types

* Update ts-documents to handle {} better

* Add changeset

* Update tests

* Migrate to Vitest (#10410)

* Install vitest

* Set up vitest config

* Install tsconfigPaths

* Set up root vite config

* Set up vitest project for typescript-resolvers

* Set up vitest for plugins-helpers and fix ESM test

* Set up client preset vitest project

* Prepare for vitest.setup.ts

* Set up vitest for graphql-modules-preset

* Set up vitest for typescript plugin

* Set up vitest for typed-document-node

* Set up vitest for typescript-operations

* Set up vitest for gql-tag-operations

* Set up vitest in typescript-document-nodes

* Set up vitest for visitor-plugin-common

* Set up vitest for time plugin

* Set up vitest for schema-ast plugin

* Set up vitest for introspection plugin

* Set up vitest for fragment-matcher plugin

* Set up vitest for add

* Set up vitest for graphql-codegen-core

* Install memfs to support fs testing

* Set up test config for cli

* Fix init tests

* Set up vitest for graphql-codegen-testing

* Update linting rules for test files

* Fix cli-* specs

* Handle cwd mocking for CLI command

* Fix cjs vs esm issues in codegen.spec

* Fix config.spec

* Fix watcher.spec and trim down the mocks

* Migrate rest of examples to vitest

* By jest things. Not even jesting 🤡

* Fix cache jest -> vitest

* Fix lint issues and unncessary disables

* Fix vitest spec

* Fix test and generated things in examples

* Fix vitest things

* Bump ESM packages (#10415)

* Bump ESM packages

* Add changeset

* [typescript] Remove NameNode overrides (#10416)

* Avoid NameNode and StringValue string conversion

* Add changeset

* Remove CI config used for dev

* [resolvers] Report if an object can have `__isTypeOf` resolver to output meta (#10417)

* Report hasIsTypeOf in meta

* Add changeset

* Revert "Remove CI config used for dev"

This reverts commit 6d2c6e4.

* Revert "Bump ESM packages (#10415)" (#10423)

This reverts commit 939752c.

* Remove branch debug

* Update test setup: test once by default, and add watch flag to watch

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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