diff --git a/.changeset/breezy-results-travel.md b/.changeset/breezy-results-travel.md new file mode 100644 index 00000000..5c7bc8b8 --- /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 83c55100..ff054def 100644 --- a/__tests__/composition.spec.ts +++ b/__tests__/composition.spec.ts @@ -4182,6 +4182,86 @@ testImplementations((api) => { `); }); + 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"]) + + 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: "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")}`, + ); + + 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", () => { const subgraphs = [ { diff --git a/src/supergraph/composition/object-type.ts b/src/supergraph/composition/object-type.ts index bddc11ea..9bd8397e 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;