Skip to content

fix: support linked, repeatable, federation directives#7249

Merged
jdolle merged 9 commits intomasterfrom
link-repeat-fed-directives
Jul 7, 2025
Merged

fix: support linked, repeatable, federation directives#7249
jdolle merged 9 commits intomasterfrom
link-repeat-fed-directives

Conversation

@jdolle
Copy link
Collaborator

@jdolle jdolle commented Jun 16, 2025

Description

Adds support for known repeatable federation directives without requiring they be defined in the SDL.

Previously, mergeTypeDefs used the directive definition node to check if a directive was repeatable. This is an issue with Federation because @link can be used without specifying what is being imported for the default federation links. Rather than add the definitions, which may become stale, I made note of specifically which directives with federation are repeatable. This list is short and is unlikely to change frequently.

I also adjusted the mergeTypeDefs logic for repeatable directives. If they are repeatable, then they are only merged if all the arguments match.

Related:

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

See test files.

Checklist:

  • I have followed the
    CONTRIBUTING doc and the
    style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests and linter rules pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

@jdolle jdolle requested a review from ardatan June 16, 2025 21:09
@changeset-bot
Copy link

changeset-bot bot commented Jun 16, 2025

🦋 Changeset detected

Latest commit: 1f1afd6

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

This PR includes changesets to release 6 packages
Name Type
@graphql-tools/merge Minor
@graphql-tools/schema Patch
graphql-tools Patch
@graphql-tools/load Patch
@graphql-tools/mock Patch
@graphql-tools/node-require 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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 16, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added support for repeatable federation directives (such as @link and @key) during schema merging, allowing multiple instances to be handled correctly.
  • Bug Fixes
    • Fixed issues with merging non-identical repeatable directives, ensuring distinct directives are preserved without conflict.
  • Tests
    • Expanded test coverage for merging behavior of repeatable directives, including edge cases and federation scenarios.

Summary by CodeRabbit

  • New Features

    • Improved support for repeatable directives imported via federation @link specifications during schema merging.
    • Enhanced handling of merging repeatable directives, ensuring non-identical directives are preserved distinctly.
  • Bug Fixes

    • More accurate deduplication of directives by deeply comparing directive arguments and values.
  • Tests

    • Added and updated test cases to cover merging behaviors for repeatable directives and federation scenarios.
  • Chores

    • Updated dependencies to include support for federation composition.

Walkthrough

This update enhances the handling of repeatable directives in GraphQL schema merging, particularly for federation-related directives imported via @link. It introduces deep argument equality checks for directive deduplication, tracks repeatable directives from external specifications, updates merging logic, and adds comprehensive tests for these scenarios.

Changes

Files / Groups Change Summary
packages/merge/package.json
.changeset/@graphql-tools_merge-7249-dependencies.md
Added @theguild/federation-composition as a dependency in @graphql-tools/merge. Updated dependencies documentation.
packages/merge/src/typedefs-mergers/directives.ts Enhanced repeatable directive detection, deep argument equality for deduplication, and support for repeatable directives from federation/link imports. Updated function signatures accordingly.
packages/merge/src/typedefs-mergers/merge-typedefs.ts Added tracking of repeatable directives from federation/link imports using @theguild/federation-composition. Updated config to include repeatableLinkImports.
packages/merge/src/typedefs-mergers/schema-def.ts Made existingNode parameter optional in mergeSchemaDefs for flexible merging.
packages/merge/tests/merge-typedefs.spec.ts Added and revised tests for merging repeatable directives, especially with varying arguments and on different schema elements. Improved test coverage for federation and repeatable directive cases.
packages/import/tests/schema/import-schema.spec.ts Added/updated tests for importSchema to verify handling of duplicate and repeatable federation directives via @link.
packages/import/tests/schema/fixtures/directive/i.graphql Added new GraphQL schema fixture with @key directives demonstrating repeatable directive usage for federation.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant mergeTypeDefs
    participant mergeDirectives
    participant isRepeatableDirective
    participant federation-composition

    User->>mergeTypeDefs: Merge type sources (with federation/link imports)
    mergeTypeDefs->>federation-composition: extractLinkImplementations
    federation-composition-->>mergeTypeDefs: Return repeatable directive names
    mergeTypeDefs->>mergeDirectives: Pass repeatableLinkImports in config
    mergeDirectives->>isRepeatableDirective: Check if directive is repeatable (including link imports)
    isRepeatableDirective-->>mergeDirectives: Result (repeatable or not)
    mergeDirectives->>mergeDirectives: Deduplicate/merge directives using deep argument equality
    mergeDirectives-->>mergeTypeDefs: Return merged directives
    mergeTypeDefs-->>User: Return merged schema
Loading

Suggested reviewers

  • ardatan

Poem

In the warren where schemas entwine,
Repeatable directives now merge just fine.
With deep checks for each field and name,
Federation links join the merge game!
🥕 Tests abound, bugs hop away—
This rabbit’s code brings a brighter day.

((\
( -.-)
o_(")(")

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/merge/tests/merge-typedefs.spec.ts

Oops! Something went wrong! :(

ESLint: 9.30.0

ESLint couldn't find an eslint.config.(js|mjs|cjs) file.

From ESLint v9.0.0, the default configuration file is now eslint.config.js.
If you are using a .eslintrc.* file, please follow the migration guide
to update your configuration file to the new format:

https://eslint.org/docs/latest/use/configure/migration-guide

If you still have problems after following the migration guide, please stop by
https://eslint.org/chat/help to chat with the team.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f1afd6 and ed5e78c.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (1)
  • packages/merge/tests/merge-typedefs.spec.ts (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/merge/tests/merge-typedefs.spec.ts
⏰ Context from checks skipped due to timeout of 90000ms (10)
  • GitHub Check: Unit Test on Node 24 (ubuntu-latest) and GraphQL v16
  • GitHub Check: Unit Test on Node 18 (windows-latest) and GraphQL v16
  • GitHub Check: Unit Test on Node 22 (ubuntu-latest) and GraphQL v16
  • GitHub Check: Unit Test on Node 18 (ubuntu-latest) and GraphQL v15
  • GitHub Check: Unit Test on Node 18 (ubuntu-latest) and GraphQL v16
  • GitHub Check: Unit Test on Bun
  • GitHub Check: alpha / snapshot
  • GitHub Check: Type Check on GraphQL v15
  • GitHub Check: Full Check on GraphQL v16
  • GitHub Check: deployment
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate Unit Tests
  • Create PR with Unit Tests
  • Post Copyable Unit Tests in Comment
  • Commit Unit Tests in branch link-repeat-fed-directives

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai auto-generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link
Contributor

github-actions bot commented Jun 16, 2025

💻 Website Preview

The latest changes are available as preview in: https://pr-7249.graphql-tools.pages.dev

DirectiveDefinitionNode
>;

config.repeatableLinkImports = repeatableLinkImports;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This may not be how we want to pass this value around. I considered passing another argument to all the merge functions, but adding a config option was much simpler. Let me know if this would be better to do in a different way.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (9)
.changeset/@graphql-tools_merge-7249-dependencies.md (1)

4-5: Fix Markdown list indentation (MD007).

The dependency bullet is indented two spaces, violating the project’s lint rule. Align it with the front-matter header or add a leading dash only one space from the margin.

-  - Added dependency [`@theguild/federation-composition@^0.16.0` ↗︎](...)
+- Added dependency [`@theguild/federation-composition@^0.16.0` ↗︎](...)
packages/import/tests/schema/import-schema.spec.ts (2)

470-474: Typo in comment.

"If this every changes""If this ever changes".

-// If this every changes, then the expectedSDL here can have the duplicate
+// If this ever changes, then the expectedSDL here can have the duplicate

482-486: Double @key directive may mask real deduplication regressions.

Expecting two identical @key(fields: "id") directives bakes current duplication into the gold file. When deduplication gets fixed this test will start failing for the wrong reason. Consider asserting on order-insensitive set equality or removing the duplicate from expectedSDL and using toBeSimilarGqlDoc with a comparison that ignores directive ordering/count.

packages/merge/src/typedefs-mergers/schema-def.ts (1)

35-37: Guarding undefined existingNode is correct, but update type hints for callers.

The signature changed to allow undefined; ensure every call site is re-compiled under strictNullChecks to avoid passing null inadvertently. If any external types exported this function, bump at least a minor version.

packages/merge/src/typedefs-mergers/directives.ts (1)

68-94: matchValues is quadratic and verbose – consider a simpler deep-equality

matchValues performs nested find calls for lists/objects, turning directive-deduplication into an O(n²) walk per comparison. Converting AST values to JS via valueFromASTUntyped (or graphql/jsutils) and then doing a shallow fast-deep-equal style compare would both simplify the code and reduce complexity.

Not critical for small schemas but noticeable in large, heavily-annotated federated graphs.

packages/merge/tests/merge-typedefs.spec.ts (4)

486-498: Directive location is invalid & one MyType definition is redundant

  1. @fields is declared repeatable on INTERFACE, yet it is attached to type definitions (CoreType, MyType).
    The test still parses because validation is skipped, but the schema would be invalid once validated.
    Use OBJECT, or OBJECT | INTERFACE, to avoid false–positive test coverage.

  2. The third array element duplicates the previous type MyType @fields(name: "id") { … } verbatim.
    The duplication is only needed to assert directive de-duplication, but repeating the whole type clutters the test – an extend type (or even type MyType @fields(name: "id") once) is enough.

-        directive @fields(name: String!, args: [String]) repeatable on INTERFACE
+        directive @fields(name: String!, args: [String]) repeatable on OBJECT | INTERFACE
@@
-        `type MyType @fields(name: "id") { id: Int }`,

528-554: Same directive-location mismatch as above

The helper directive in this test is also limited to INTERFACE but is applied to the object type CoreType.
Please adjust the location or the attachment site for consistency with the spec.


610-628: Minor API-consistency nitpick

mergeTypeDefs accepts either a single TypeSource or an array.
All other tests pass an array; here you pass the DocumentNode directly.
Wrapping it in an array keeps the test style uniform and avoids an accidental “options” parameter shift if the call site is edited later.

-      const merged = mergeTypeDefs(
-        parse(`
+      const merged = mergeTypeDefs([
+        parse(`
@@
-          `,
-        ),
-      );
+          `,
+        ),
+      ]);

1796-1817: it.todo comment left in – consider tracking with an issue

Leaving the TODO test stub is fine, but adding a link to an open issue (or creating one) will prevent it from being forgotten.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ddc1283 and 2a1fd9b.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (8)
  • .changeset/@graphql-tools_merge-7249-dependencies.md (1 hunks)
  • packages/import/tests/schema/fixtures/directive/i.graphql (1 hunks)
  • packages/import/tests/schema/import-schema.spec.ts (3 hunks)
  • packages/merge/package.json (1 hunks)
  • packages/merge/src/typedefs-mergers/directives.ts (4 hunks)
  • packages/merge/src/typedefs-mergers/merge-typedefs.ts (6 hunks)
  • packages/merge/src/typedefs-mergers/schema-def.ts (1 hunks)
  • packages/merge/tests/merge-typedefs.spec.ts (7 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/merge/src/typedefs-mergers/merge-typedefs.ts (3)
packages/utils/src/Interfaces.ts (1)
  • TypeSource (270-277)
packages/utils/src/comments.ts (1)
  • resetComments (24-26)
packages/merge/src/typedefs-mergers/merge-nodes.ts (1)
  • mergeGraphQLNodes (32-135)
🪛 markdownlint-cli2 (0.17.2)
.changeset/@graphql-tools_merge-7249-dependencies.md

5-5: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)

🔇 Additional comments (5)
packages/merge/package.json (1)

53-56: [web_search]

According to the semantic versioning specification, what version range does the constraint "^0.16.0" match?
packages/import/tests/schema/fixtures/directive/i.graphql (1)

1-8: LGTM – minimal fixture, covers repeatable @key directive.
No issues detected.

packages/import/tests/schema/import-schema.spec.ts (1)

491-503: Great addition – validates repeatable directives context.

The new test captures federation 2.6 behaviour and should prevent regressions.

packages/merge/tests/merge-typedefs.spec.ts (2)

1766-1779: Looks good – validates new federation behaviour

The test accurately captures the “do not merge unless arguments match” requirement for repeatable federation directives.
No issues spotted.


1781-1794: Good edge-case coverage

Nice addition verifying that @link imports are recognised even when the extend schema follows type definitions.
No changes required.

Comment on lines +258 to +259
config.repeatableLinkImports = repeatableLinkImports;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid mutating the caller’s config object

Assigning to config.repeatableLinkImports breaks referential transparency and leaks internal state:

config.repeatableLinkImports = repeatableLinkImports;

Clone before modification, or return the computed set via the function result instead.

🤖 Prompt for AI Agents
In packages/merge/src/typedefs-mergers/merge-typedefs.ts around lines 258 to
259, avoid directly mutating the caller's config object by assigning to
config.repeatableLinkImports. Instead, create a clone of the config object
before modifying repeatableLinkImports or refactor the function to return the
updated config or the computed repeatableLinkImports value without altering the
original config. This preserves referential transparency and prevents leaking
internal state.

Comment on lines +81 to 86
/**
* Allow directives that are not defined in the schema, but are imported
* through federated @links, to be repeated.
*/
repeatableLinkImports?: Set<string>;
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Config is mutated later – treat repeatableLinkImports as read-only

repeatableLinkImports is declared on the public Config interface but later (line 258) the same object passed by the caller is mutated in-place. Public options are normally considered immutable; doing otherwise can create surprising side-effects for integrators who reuse a single config object across calls.

- config.repeatableLinkImports = repeatableLinkImports;
+ const nextCfg = { ...config, repeatableLinkImports };

and pass nextCfg further down instead of config.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/merge/src/typedefs-mergers/merge-typedefs.ts around lines 81 to 86,
the repeatableLinkImports property on the Config interface is currently mutated
later in the code (around line 258), which breaks the expectation that public
config objects are immutable. To fix this, treat repeatableLinkImports as
read-only by creating a copy of it before any mutation occurs, and ensure that
the copied config object (nextCfg) is passed down in subsequent calls instead of
the original config to avoid side effects on the caller's object.

}

function mergeArguments(a1: readonly ArgumentNode[], a2: readonly ArgumentNode[]): ArgumentNode[] {
const result: ArgumentNode[] = [...a2];
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To merge arguments correctly, even if defined within the same schema, we must not assume a2 is all clear here. Instead, we must process the entire list of arguments to build up the result set.

return result;
}

function deduplicateDirectives(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no need for a separate call to dedupe if we dedupe as we go.

});

it.todo('supports multiple schema extensions');
// , () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is outside the scope of this PR but i think it is something we need in order to better support federation, because extend schema with directives is common

@jdolle jdolle merged commit e5f98c2 into master Jul 7, 2025
14 of 16 checks passed
@jdolle jdolle deleted the link-repeat-fed-directives branch July 7, 2025 15:07
ardatan pushed a commit that referenced this pull request Sep 22, 2025
* fix: support linked, repeatable, federation directives

* chore(dependencies): updated changesets for modified dependencies

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@coderabbitai coderabbitai bot mentioned this pull request Sep 24, 2025
12 tasks
@coderabbitai coderabbitai bot mentioned this pull request Oct 3, 2025
9 tasks
@coderabbitai coderabbitai bot mentioned this pull request Dec 11, 2025
14 tasks
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.

2 participants