Skip to content

Commit 8642016

Browse files
authored
avoid re-visiting deferred fragment spreads (#4462)
This PR changes the fragment spread visiting logic to avoid re-visiting fragment spreads in additional cases. Previously, fragment spreads that have been visited in a non-deferred capacity were not revisited, but fragment spreads that had been visited in a deferred capacity could be revisited. This PR updates the logic so that fragment spreads that had been visited in a deferred capacity are not revisited in a deferred capacity. See: graphql/graphql-spec#1045 (comment)
1 parent e28da52 commit 8642016

File tree

2 files changed

+142
-25
lines changed

2 files changed

+142
-25
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { invariant } from '../../jsutils/invariant.js';
5+
6+
import { parse } from '../../language/parser.js';
7+
8+
import { buildSchema } from '../../utilities/buildASTSchema.js';
9+
10+
import { collectFields } from '../collectFields.js';
11+
import { validateExecutionArgs } from '../execute.js';
12+
13+
const schema = buildSchema(`
14+
type Query {
15+
field: String
16+
}
17+
`);
18+
19+
function collectRootFields(query: string) {
20+
const validatedExecutionArgs = validateExecutionArgs({
21+
schema,
22+
document: parse(query),
23+
});
24+
25+
invariant('operation' in validatedExecutionArgs);
26+
27+
const { operation, fragments, variableValues } = validatedExecutionArgs;
28+
29+
const queryType = schema.getQueryType();
30+
31+
invariant(queryType != null);
32+
33+
return collectFields(
34+
schema,
35+
fragments,
36+
variableValues,
37+
queryType,
38+
operation.selectionSet,
39+
false,
40+
);
41+
}
42+
43+
describe('collectFields', () => {
44+
describe('overlapping fragment spreads', () => {
45+
it('should not collect a deferred spread after a non-deferred spread has been collected', () => {
46+
const { newDeferUsages } = collectRootFields(`
47+
query {
48+
...FragmentName
49+
...FragmentName @defer
50+
}
51+
fragment FragmentName on Query {
52+
field
53+
}
54+
`);
55+
56+
expect(newDeferUsages).to.have.lengthOf(0);
57+
});
58+
59+
it('should not collect a deferred spread after a deferred spread has been collected', () => {
60+
const { newDeferUsages } = collectRootFields(`
61+
query {
62+
...FragmentName @defer
63+
...FragmentName @defer
64+
}
65+
fragment FragmentName on Query {
66+
field
67+
}
68+
`);
69+
70+
expect(newDeferUsages).to.have.lengthOf(1);
71+
});
72+
73+
it('should collect a non-deferred spread after a deferred spread has been collected', () => {
74+
const { groupedFieldSet } = collectRootFields(`
75+
query {
76+
...FragmentName @defer
77+
...FragmentName
78+
}
79+
fragment FragmentName on Query {
80+
field
81+
}
82+
`);
83+
84+
const fieldDetailsList = groupedFieldSet.get('field');
85+
86+
invariant(fieldDetailsList != null);
87+
88+
expect(fieldDetailsList).to.have.lengthOf(2);
89+
});
90+
91+
it('should not collect a non-deferred spread after a non-deferred spread has been collected', () => {
92+
const { groupedFieldSet } = collectRootFields(`
93+
query {
94+
...FragmentName
95+
...FragmentName
96+
}
97+
fragment FragmentName on Query {
98+
field
99+
}
100+
`);
101+
102+
const fieldDetailsList = groupedFieldSet.get('field');
103+
104+
invariant(fieldDetailsList != null);
105+
106+
expect(fieldDetailsList).to.have.lengthOf(1);
107+
});
108+
});
109+
});

src/execution/collectFields.ts

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ interface CollectFieldsContext {
6767
fragments: ObjMap<FragmentDetails>;
6868
variableValues: VariableValues;
6969
runtimeType: GraphQLObjectType;
70-
visitedFragmentNames: Set<string>;
70+
visitedFragmentNames: Map<string, boolean>;
7171
hideSuggestions: boolean;
7272
forbiddenDirectiveInstances: Array<DirectiveNode>;
7373
forbidSkipAndInclude: boolean;
@@ -103,7 +103,7 @@ export function collectFields(
103103
fragments,
104104
variableValues,
105105
runtimeType,
106-
visitedFragmentNames: new Set(),
106+
visitedFragmentNames: new Map(),
107107
hideSuggestions,
108108
forbiddenDirectiveInstances: [],
109109
forbidSkipAndInclude,
@@ -144,7 +144,7 @@ export function collectSubfields(
144144
fragments,
145145
variableValues,
146146
runtimeType: returnType,
147-
visitedFragmentNames: new Set(),
147+
visitedFragmentNames: new Map(),
148148
hideSuggestions,
149149
forbiddenDirectiveInstances: [],
150150
forbidSkipAndInclude: false,
@@ -258,7 +258,6 @@ function collectFieldsImpl(
258258
const fragName = selection.name.value;
259259

260260
if (
261-
visitedFragmentNames.has(fragName) ||
262261
!shouldIncludeNode(
263262
context,
264263
selection,
@@ -284,6 +283,28 @@ function collectFieldsImpl(
284283
deferUsage,
285284
);
286285

286+
const visitedAsDeferred = visitedFragmentNames.get(fragName);
287+
288+
let maybeNewDeferUsage: DeferUsage | undefined;
289+
if (!newDeferUsage) {
290+
// If this spread is not deferred, it may be skipped when already visited
291+
// as a non-deferred spread. If it was previously visited as a deferred spread,
292+
// it must be revisited.
293+
if (visitedAsDeferred === false) {
294+
continue;
295+
}
296+
visitedFragmentNames.set(fragName, false);
297+
maybeNewDeferUsage = deferUsage;
298+
} else {
299+
// If this spread is deferred, it can be skipped if it has already been visited.
300+
if (visitedAsDeferred !== undefined) {
301+
continue;
302+
}
303+
visitedFragmentNames.set(fragName, true);
304+
newDeferUsages.push(newDeferUsage);
305+
maybeNewDeferUsage = newDeferUsage;
306+
}
307+
287308
const fragmentVariableSignatures = fragment.variableSignatures;
288309
let newFragmentVariableValues: FragmentVariableValues | undefined;
289310
if (fragmentVariableSignatures) {
@@ -296,27 +317,14 @@ function collectFieldsImpl(
296317
);
297318
}
298319

299-
if (!newDeferUsage) {
300-
visitedFragmentNames.add(fragName);
301-
collectFieldsImpl(
302-
context,
303-
fragment.definition.selectionSet,
304-
groupedFieldSet,
305-
newDeferUsages,
306-
deferUsage,
307-
newFragmentVariableValues,
308-
);
309-
} else {
310-
newDeferUsages.push(newDeferUsage);
311-
collectFieldsImpl(
312-
context,
313-
fragment.definition.selectionSet,
314-
groupedFieldSet,
315-
newDeferUsages,
316-
newDeferUsage,
317-
newFragmentVariableValues,
318-
);
319-
}
320+
collectFieldsImpl(
321+
context,
322+
fragment.definition.selectionSet,
323+
groupedFieldSet,
324+
newDeferUsages,
325+
maybeNewDeferUsage,
326+
newFragmentVariableValues,
327+
);
320328
break;
321329
}
322330
}

0 commit comments

Comments
 (0)