Skip to content

Commit 7e2ca46

Browse files
author
Sylvain Lebresne
authored
Fix QP bug querying some fields with @interfaceObject (#2362)
When an interface field is requested only for some implementation, and the query "transit" through a subgraph using `@interfaceObject`, then the query planning code was not recognizing early enough that the field was queried on an implementation type and not the interface itself, and this lead to a later assertion error. This commit adds a reproduction test, and fix the issue by simply making sure, when we build the query plan "paths", to not use an interface field when it's an implementation field that is queried.
1 parent 1b86de1 commit 7e2ca46

File tree

4 files changed

+103
-6
lines changed

4 files changed

+103
-6
lines changed

.changeset/tidy-cherries-fly.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@apollo/query-graphs": patch
3+
"@apollo/query-planner": patch
4+
---
5+
6+
Fix assertion errors thrown by the query planner when querying fields for a specific interface implementation in some cases where `@interfaceObject` is involved
7+

gateway-js/src/__generated__/graphqlTypes.ts

+40-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

query-graphs-js/src/graphPath.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -2130,6 +2130,12 @@ function advanceWithOperation<V extends Vertex>(
21302130
);
21312131
return { options: pathAsOptions(fieldPath) };
21322132
case 'InterfaceType':
2133+
// Due to @interfaceObject, we could be in a case where the field asked is not on the interface but
2134+
// rather on one of it's implementation. This can happen if we just entered the subgraph on an interface @key
2135+
// and coming from an @interfaceObject. In that case, we'll skip checking for an interface direct edge and
2136+
// simply cast into that implementation below.
2137+
const fieldIsOfAnImplementation = field.parent.name !== currentType.name;
2138+
21332139
// First, we check if there is a direct edge from the interface (which only happens if we're in a subgraph that knows all of the
21342140
// implementations of that interface globally and all of them resolve the field).
21352141
// If there is one, then we have 2 options:
@@ -2141,7 +2147,7 @@ function advanceWithOperation<V extends Vertex>(
21412147
// - either type-exploding cannot work unless taking the interface edge also do (the `anImplementationIsEntityWithFieldShareable`)
21422148
// - or that type-exploding cannot be more efficient than the direct path (when no @provides are involved; if a provide is involved
21432149
// in one of the implementation, then type-exploding may lead to a shorter overall plan thanks to that @provides)
2144-
const itfEdge = nextEdgeForField(path, operation);
2150+
const itfEdge = fieldIsOfAnImplementation ? undefined : nextEdgeForField(path, operation);
21452151
let itfPath: OpGraphPath<V> | undefined = undefined;
21462152
let directPathOverrideTypeExplosion = false;
21472153
if (itfEdge) {
@@ -2174,12 +2180,11 @@ function advanceWithOperation<V extends Vertex>(
21742180
// - the most common is that it's a field of the interface that is queried, and
21752181
// so we should type-explode because either didn't had a direct edge, or @provides
21762182
// makes it potentially worthwile to check with type explosion.
2177-
// - but we could also have the case where the field queried is actually of one
2178-
// of the implementation of the interface: this happens if we just entered the
2179-
// subgraph on an interface @key. In that case, we only want to consider that one
2183+
// - but, as mentionned earlier, we could be in the case where the field queried is actually of one
2184+
// of the implementation of the interface. In that case, we only want to consider that one
21802185
// implementation.
21812186
let implementations: readonly ObjectType[];
2182-
if (field.parent.name !== currentType.name) {
2187+
if (fieldIsOfAnImplementation) {
21832188
assert(
21842189
isObjectType(field.parent) && path.tailPossibleRuntimeTypes().some((t) => t.name === field.parent.name),
21852190
() => `${field.coordinate} requested on ${currentType}, but ${field.parent} is not an implementation`

query-planner-js/src/__tests__/buildPlan.interfaceObject.test.ts

+46
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,52 @@ describe('basic @key on interface/@interfaceObject handling', () => {
312312
expect(rewrite.path).toEqual(['... on A', '__typename']);
313313
expect(rewrite.setValueTo).toBe('I');
314314
});
315+
316+
test('handles query of an interface field (that is not on the `@interfaceObject`) for a specific implementation when query starts on the @interfaceObject', () => {
317+
// Here, we start on S2, but `x` is only in S1. Further, while `x` is on the `I` interface, we only query it for `A`.
318+
const operation = operationFromDocument(api, gql`
319+
{
320+
iFromS2 {
321+
... on A {
322+
x
323+
}
324+
}
325+
}
326+
`);
327+
328+
const plan = queryPlanner.buildQueryPlan(operation);
329+
expect(plan).toMatchInlineSnapshot(`
330+
QueryPlan {
331+
Sequence {
332+
Fetch(service: "S2") {
333+
{
334+
iFromS2 {
335+
__typename
336+
id
337+
}
338+
}
339+
},
340+
Flatten(path: "iFromS2") {
341+
Fetch(service: "S1") {
342+
{
343+
... on I {
344+
__typename
345+
id
346+
}
347+
} =>
348+
{
349+
... on I {
350+
... on A {
351+
x
352+
}
353+
}
354+
}
355+
},
356+
},
357+
},
358+
}
359+
`);
360+
});
315361
});
316362

317363
it('avoids buffering @interfaceObject results that may have to filtered with lists', () => {

0 commit comments

Comments
 (0)