From 036f8d593b0321c3181fbc29107a50f85bb352c0 Mon Sep 17 00:00:00 2001 From: jdolle <1841898+jdolle@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:16:54 -0700 Subject: [PATCH 1/3] Reproduce external directive bug --- __tests__/composition.spec.ts | 102 ++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/__tests__/composition.spec.ts b/__tests__/composition.spec.ts index 8b4324b..89fc270 100644 --- a/__tests__/composition.spec.ts +++ b/__tests__/composition.spec.ts @@ -4182,6 +4182,107 @@ testImplementations((api) => { `); }); + test("@external + @shareable", () => { + const result = composeServices([ + { + name: "a", + typeDefs: parse(/* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/${version}", import: ["@key", "@shareable"]) + + type Product @key(fields: "id") @shareable{ + id: ID! + productId: String! + } + + type Query { + products: [Product] + } + `), + }, + { + name: "b", + typeDefs: parse(/* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/${version}", import: ["@key"]) + + type Product @key(fields: "id", resolvable: false) { + id: ID! + } + `), + }, + { + name: "c", + typeDefs: parse(/* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/${version}", import: ["@shareable", "@external", "@key", "@requires"]) + + extend type Product @key(fields: "id") @shareable { + id: ID! + productId: String! @external + bField: Boolean @requires(fields: "productId") + } + `), + }, + { + // FIX ME: this is the one that is causing issues + name: "d", + typeDefs: parse(/* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/${version}", import: ["@shareable", "@external", "@key", "@requires"]) + + extend type Product @shareable @key(fields: "id productId") { + id: ID! @external + productId: String! @external + bField: Boolean + } + `), + }, + { + name: "e", + typeDefs: parse(/* GraphQL */ ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable", "@external", "@requires"]) + + extend type Product @shareable @key(fields: "id") { + id: ID! @external + productId: String! @external + bField: Boolean @requires(fields: "productId") + } + `), + }, + ]); + + assertCompositionSuccess(result, `Composition failures:\n${result.errors?.map(e => `- ${e.message}`).join('\n')}`); + + api.runIf('guild', () => { + expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` + type Product + @join__type(graph: A, key: "id") + @join__type(graph: B, key: "id", resolvable: false) + @join__type(graph: C, key: "id", extension: true) + @join__type(graph: D, key: "id productId", extension: true) + @join__type(graph: E, key: "id", extension: true) + { + id: ID! + productId: String! @join__field(graph: A) @join__field(graph: C) @join__field(graph: D) @join__field(graph: E) + bField: Boolean @join__field(graph: C, requires: "productId") @join__field(graph: D) @join__field(graph: E, requires: "productId") + } + `); + }) + + api.runIf('apollo', () => { + expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` + type Product + @join__type(graph: A, key: "id") + @join__type(graph: B, key: "id", resolvable: false) + @join__type(graph: C, key: "id", extension: true) + @join__type(graph: D, key: "id productId", extension: true) + @join__type(graph: E, key: "id", extension: true) + { + id: ID! + productId: String! @join__field(external: true, graph: C) @join__field(external: true, graph: E) @join__field(graph: A) @join__field(graph: D) + bField: Boolean @join__field(graph: C, requires: "productId") @join__field(graph: D) @join__field(graph: E, requires: "productId") + } + `); + }) + }); + test("@external + @tag", () => { const subgraphs = [ { @@ -4236,6 +4337,7 @@ testImplementations((api) => { @join__field(graph: B, requires: "inStock") @tag(name: "public") } + `); }); From 7fe6afd281a13a7a9f0c47749fdd830f873c8d2b Mon Sep 17 00:00:00 2001 From: jdolle <1841898+jdolle@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:18:26 -0700 Subject: [PATCH 2/3] remove whitespace --- __tests__/composition.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/__tests__/composition.spec.ts b/__tests__/composition.spec.ts index 89fc270..b381597 100644 --- a/__tests__/composition.spec.ts +++ b/__tests__/composition.spec.ts @@ -4337,7 +4337,6 @@ testImplementations((api) => { @join__field(graph: B, requires: "inStock") @tag(name: "public") } - `); }); From 169caaf95603afb15b36c1913f2ede40b3a73463 Mon Sep 17 00:00:00 2001 From: jdolle <1841898+jdolle@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:43:00 -0700 Subject: [PATCH 3/3] Fix external join__field for external fields that are used in keys in other subgraphs --- .changeset/breezy-results-travel.md | 5 ++ __tests__/composition.spec.ts | 75 ++++++++--------------- src/supergraph/composition/object-type.ts | 2 +- 3 files changed, 33 insertions(+), 49 deletions(-) create mode 100644 .changeset/breezy-results-travel.md diff --git a/.changeset/breezy-results-travel.md b/.changeset/breezy-results-travel.md new file mode 100644 index 0000000..5c7bc8b --- /dev/null +++ b/.changeset/breezy-results-travel.md @@ -0,0 +1,5 @@ +--- +"@theguild/federation-composition": patch +--- + +Set external to true for external fields that are not in the key, but are used in keys for other graphs diff --git a/__tests__/composition.spec.ts b/__tests__/composition.spec.ts index b381597..975762b 100644 --- a/__tests__/composition.spec.ts +++ b/__tests__/composition.spec.ts @@ -4182,9 +4182,9 @@ testImplementations((api) => { `); }); - test("@external + @shareable", () => { + test("@external and not a @key in its graph, but is @key in other graph", () => { const result = composeServices([ - { + { name: "a", typeDefs: parse(/* GraphQL */ ` extend schema @link(url: "https://specs.apollo.dev/federation/${version}", import: ["@key", "@shareable"]) @@ -4210,19 +4210,6 @@ testImplementations((api) => { `), }, { - name: "c", - typeDefs: parse(/* GraphQL */ ` - extend schema @link(url: "https://specs.apollo.dev/federation/${version}", import: ["@shareable", "@external", "@key", "@requires"]) - - extend type Product @key(fields: "id") @shareable { - id: ID! - productId: String! @external - bField: Boolean @requires(fields: "productId") - } - `), - }, - { - // FIX ME: this is the one that is causing issues name: "d", typeDefs: parse(/* GraphQL */ ` extend schema @link(url: "https://specs.apollo.dev/federation/${version}", import: ["@shareable", "@external", "@key", "@requires"]) @@ -4237,7 +4224,11 @@ testImplementations((api) => { { name: "e", typeDefs: parse(/* GraphQL */ ` - extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable", "@external", "@requires"]) + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@shareable", "@external", "@requires"] + ) extend type Product @shareable @key(fields: "id") { id: ID! @external @@ -4248,39 +4239,27 @@ testImplementations((api) => { }, ]); - assertCompositionSuccess(result, `Composition failures:\n${result.errors?.map(e => `- ${e.message}`).join('\n')}`); - - api.runIf('guild', () => { - expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` - type Product - @join__type(graph: A, key: "id") - @join__type(graph: B, key: "id", resolvable: false) - @join__type(graph: C, key: "id", extension: true) - @join__type(graph: D, key: "id productId", extension: true) - @join__type(graph: E, key: "id", extension: true) - { - id: ID! - productId: String! @join__field(graph: A) @join__field(graph: C) @join__field(graph: D) @join__field(graph: E) - bField: Boolean @join__field(graph: C, requires: "productId") @join__field(graph: D) @join__field(graph: E, requires: "productId") - } - `); - }) + assertCompositionSuccess( + result, + `Composition failures:\n${result.errors?.map((e) => `- ${e.message}`).join("\n")}`, + ); - api.runIf('apollo', () => { - expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` - type Product - @join__type(graph: A, key: "id") - @join__type(graph: B, key: "id", resolvable: false) - @join__type(graph: C, key: "id", extension: true) - @join__type(graph: D, key: "id productId", extension: true) - @join__type(graph: E, key: "id", extension: true) - { - id: ID! - productId: String! @join__field(external: true, graph: C) @join__field(external: true, graph: E) @join__field(graph: A) @join__field(graph: D) - bField: Boolean @join__field(graph: C, requires: "productId") @join__field(graph: D) @join__field(graph: E, requires: "productId") - } - `); - }) + expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` + type Product + @join__type(graph: A, key: "id") + @join__type(graph: B, key: "id", resolvable: false) + @join__type(graph: D, key: "id productId", extension: true) + @join__type(graph: E, key: "id", extension: true) { + id: ID! + productId: String! + @join__field(external: true, graph: E) + @join__field(graph: A) + @join__field(graph: D) + bField: Boolean + @join__field(graph: D) + @join__field(graph: E, requires: "productId") + } + `); }); test("@external + @tag", () => { diff --git a/src/supergraph/composition/object-type.ts b/src/supergraph/composition/object-type.ts index bddc11e..9bd8397 100644 --- a/src/supergraph/composition/object-type.ts +++ b/src/supergraph/composition/object-type.ts @@ -534,7 +534,7 @@ export function objectTypeBuilder(): TypeBuilder { // mark field as external if it's annotated with @external, but it's not used as a key on the extension type if ( - fieldState.usedAsKey && + fieldStateInGraph.usedAsKey && objectType.byGraph.get(graphId)!.extension === true ) { return false;