diff --git a/.changeset/hot-panthers-boil.md b/.changeset/hot-panthers-boil.md new file mode 100644 index 0000000..421305b --- /dev/null +++ b/.changeset/hot-panthers-boil.md @@ -0,0 +1,5 @@ +--- +'@theguild/federation-composition': patch +--- + +Fix a missing `@join__field` on a query field where `@override` is used, but not in all subgraphs. diff --git a/__tests__/supergraph-composition.spec.ts b/__tests__/supergraph-composition.spec.ts index 12279c9..90218b2 100644 --- a/__tests__/supergraph-composition.spec.ts +++ b/__tests__/supergraph-composition.spec.ts @@ -769,4 +769,245 @@ testImplementations(api => { expect(result.supergraphSdl).not.toMatch('federation__Policy'); expect(result.supergraphSdl).not.toMatch('federation__Scope'); }); + + test('@override with @shareable in different conditions', () => { + let result = api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + bar: String! @shareable + } + `, + }, + { + name: 'b', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + bar: String! @shareable + } + `, + }, + { + name: 'c', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@shareable", "@override"] + ) + + type Query { + bar: String! @shareable @override(from: "a") + } + `, + }, + ]); + + assertCompositionSuccess(result); + + expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` + type Query @join__type(graph: A) @join__type(graph: B) @join__type(graph: C) { + bar: String! @join__field(graph: B) @join__field(graph: C, override: "a") + } + `); + + result = api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + bar: String! @shareable + } + `, + }, + { + name: 'b', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + bar: String! @shareable + } + `, + }, + { + name: 'c', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@shareable", "@override"] + ) + + type Query { + bar: String! @shareable @override(from: "a") + } + `, + }, + { + name: 'd', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + foo: String! + } + `, + }, + ]); + + assertCompositionSuccess(result); + + expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` + type Query + @join__type(graph: A) + @join__type(graph: B) + @join__type(graph: C) + @join__type(graph: D) { + foo: String! @join__field(graph: D) + bar: String! @join__field(graph: B) @join__field(graph: C, override: "a") + } + `); + + result = api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + bar: String! @shareable + } + `, + }, + { + name: 'b', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + bar: String! @shareable + } + `, + }, + { + name: 'c', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + bar: String! @shareable + } + `, + }, + ]); + + assertCompositionSuccess(result); + + expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` + type Query @join__type(graph: A) @join__type(graph: B) @join__type(graph: C) { + bar: String! + } + `); + + result = api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + bar: String! @shareable + } + `, + }, + { + name: 'b', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + bar: String! @shareable + } + `, + }, + { + name: 'c', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + bar: String! @shareable + } + `, + }, + { + name: 'd', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) + + type Query { + foo: String! @shareable + } + `, + }, + ]); + + assertCompositionSuccess(result); + + expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` + type Query + @join__type(graph: A) + @join__type(graph: B) + @join__type(graph: C) + @join__type(graph: D) { + foo: String! @join__field(graph: D) + bar: String! @join__field(graph: A) @join__field(graph: B) @join__field(graph: C) + } + `); + + result = api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@shareable", "@override"] + ) + + type Query { + bar: String! @shareable @override(from: "non-existent") + } + `, + }, + ]); + + assertCompositionSuccess(result); + + expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` + type Query @join__type(graph: A) { + bar: String! + } + `); + }); }); diff --git a/src/supergraph/composition/object-type.ts b/src/supergraph/composition/object-type.ts index 64777e2..e9d972a 100644 --- a/src/supergraph/composition/object-type.ts +++ b/src/supergraph/composition/object-type.ts @@ -467,20 +467,18 @@ export function objectTypeBuilder(): TypeBuilder { // If it's a Query type, we don't need to emit `@join__field` directives when there's only one graph // We do not have to emit `@join__field` if the field is shareable in every graph as well. - if (differencesBetweenGraphs.override) { - const graphsWithOverride = fieldInGraphs.filter( - ([_, meta]) => - meta.override !== null && - (objectType.byGraph.size > 1 - ? // if there's more than one graph - // we want to emit `@join__field` with override even when it's pointing to a non-existing subgraph - true - : // but if there's only one graph, - // we don't want to emit `@join__field` if the override is pointing to a non-existing subgraph - typeof graphNameToId(meta.override) === 'string'), + if (differencesBetweenGraphs.override && graphs.size > 1) { + const overriddenGraphs = fieldInGraphs + .map(([_, meta]) => (meta.override ? graphNameToId(meta.override) : null)) + .filter((graphId): graphId is string => typeof graphId === 'string'); + + const graphsToPrintJoinField = fieldInGraphs.filter( + ([graphId, meta]) => + meta.override !== null || + (meta.shareable && !overriddenGraphs.includes(graphId)), ); - joinFields = graphsWithOverride.map(([graphId, meta]) => ({ + joinFields = graphsToPrintJoinField.map(([graphId, meta]) => ({ graph: graphId, override: meta.override ?? undefined, usedOverridden: provideUsedOverriddenValue(