From 61605baf5e985bde7048081adc5f1f23982ecfec Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Mon, 28 Oct 2024 12:38:29 -0700 Subject: [PATCH] fix: keep multiple best path options per subgraph - when advancing to a subgraph with multiple overlapping @interfaceObject implementations --- composition-js/src/__tests__/compose.test.ts | 41 ++++++++++++++++++++ query-graphs-js/src/graphPath.ts | 24 +++++++++--- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/composition-js/src/__tests__/compose.test.ts b/composition-js/src/__tests__/compose.test.ts index e0a538fab..dd7399d02 100644 --- a/composition-js/src/__tests__/compose.test.ts +++ b/composition-js/src/__tests__/compose.test.ts @@ -2444,6 +2444,47 @@ describe('composition', () => { }); }); + it('fed-393', () => { + const subgraphA = { + typeDefs: gql` + type I1 @interfaceObject @key(fields: "id") { + id: ID! + f1: Int! + } + + type I2 @interfaceObject @key(fields: "id") { + id: ID! + f2: Int! + } + `, + name: 'subgraphA', + }; + + const subgraphB = { + typeDefs: gql` + type Query { + AFromB: A + } + + interface I1 @key(fields: "id") { + id: ID! + } + + interface I2 @key(fields: "id") { + id: ID! + } + + type A implements I1 & I2 @key(fields: "id") { + id: ID! + } + `, + name: 'subgraphB', + }; + + const result = composeAsFed2Subgraphs([subgraphA, subgraphB]); + expect(result.errors).toBeUndefined(); + }); + it('handles renamed federation directives', () => { const subgraphA = { typeDefs: gql` diff --git a/query-graphs-js/src/graphPath.ts b/query-graphs-js/src/graphPath.ts index ba87dc723..9c2b1799f 100644 --- a/query-graphs-js/src/graphPath.ts +++ b/query-graphs-js/src/graphPath.ts @@ -1431,7 +1431,9 @@ function advancePathWithNonCollectingAndTypePreservingTransitions, number] | null>(); + // Note: There may be multiple "best" paths if the tail implements multiple interfaces and target + // subgraph also has multiple @interfaceObject implementing them. + const bestPathBySource = new Map, number, GraphPath[]] | null>(); const deadEndClosures: UnadvanceableClosure[] = []; const toTry: GraphPath[] = [ path ]; while (toTry.length > 0) { @@ -1503,7 +1505,7 @@ function advancePathWithNonCollectingAndTypePreservingTransitions `Ignored: a better (shorter) path to the same subgraph already added`); continue; } + // New path has the same size as the previous best and they are tied. + // (See the note above about the "same size" rule). + const is_tied = prevForSource && (prevForSource[0].size == toAdvance.size + 1 && prevForSource[1] <= 1 && prevForSource[0].tail != edge.tail); if (isConditionExcluded(edge.conditions, excludedConditions)) { debug.groupEnd(`Ignored: edge condition is excluded`); @@ -1542,7 +1552,7 @@ function advancePathWithNonCollectingAndTypePreservingTransitions `Using edge, advance path: ${updatedPath}`); - bestPathBySource.set(target.source, [updatedPath, conditionResolution.cost]); + if (is_tied) { + prevForSource![2].push(updatedPath); + } else { + bestPathBySource.set(target.source, [updatedPath, conditionResolution.cost, [updatedPath]]); + } // It can be necessary to "chain" keys, because different subgraphs may have different keys exposed, and so we when we took // a key, we want to check if there is new key we can now take that take us to other subgraphs. For other 'non-collecting' // edges ('QueryResolution' and 'SubgraphEnteringTransition') however, chaining never give us additional value. @@ -1652,7 +1666,7 @@ function advancePathWithNonCollectingAndTypePreservingTransitions p !== null).map(b => b![0]), + paths: mapValues(bestPathBySource).filter(p => p !== null).flatMap(b => b![2]), deadEnds: new UnadvanceableClosures(deadEndClosures) as TDeadEnds } }