Skip to content

Commit cd273ad

Browse files
separateOperations: distinguish query and fragment names (#2859)
Fixes #2853
1 parent 93e26db commit cd273ad

File tree

2 files changed

+115
-36
lines changed

2 files changed

+115
-36
lines changed

src/utilities/__tests__/separateOperations-test.js

+69
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,73 @@ describe('separateOperations', () => {
158158
`,
159159
});
160160
});
161+
162+
it('distinguish query and fragment names', () => {
163+
const ast = parse(`
164+
{
165+
...NameClash
166+
}
167+
168+
fragment NameClash on T {
169+
oneField
170+
}
171+
172+
query NameClash {
173+
...ShouldBeSkippedInFirstQuery
174+
}
175+
176+
fragment ShouldBeSkippedInFirstQuery on T {
177+
twoField
178+
}
179+
`);
180+
181+
const separatedASTs = mapValue(separateOperations(ast), print);
182+
expect(separatedASTs).to.deep.equal({
183+
'': dedent`
184+
{
185+
...NameClash
186+
}
187+
188+
fragment NameClash on T {
189+
oneField
190+
}
191+
`,
192+
NameClash: dedent`
193+
query NameClash {
194+
...ShouldBeSkippedInFirstQuery
195+
}
196+
197+
fragment ShouldBeSkippedInFirstQuery on T {
198+
twoField
199+
}
200+
`,
201+
});
202+
});
203+
204+
it('handles unknown fragments', () => {
205+
const ast = parse(`
206+
{
207+
...Unknown
208+
...Known
209+
}
210+
211+
fragment Known on T {
212+
someField
213+
}
214+
`);
215+
216+
const separatedASTs = mapValue(separateOperations(ast), print);
217+
expect(separatedASTs).to.deep.equal({
218+
'': dedent`
219+
{
220+
...Unknown
221+
...Known
222+
}
223+
224+
fragment Known on T {
225+
someField
226+
}
227+
`,
228+
});
229+
});
161230
});

src/utilities/separateOperations.js

+46-36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { ObjMap } from '../jsutils/ObjMap';
22

3-
import type { DocumentNode, OperationDefinitionNode } from '../language/ast';
3+
import type {
4+
DocumentNode,
5+
OperationDefinitionNode,
6+
SelectionSetNode,
7+
} from '../language/ast';
48
import { Kind } from '../language/kinds';
59
import { visit } from '../language/visitor';
610

@@ -13,36 +17,35 @@ import { visit } from '../language/visitor';
1317
export function separateOperations(
1418
documentAST: DocumentNode,
1519
): ObjMap<DocumentNode> {
16-
const operations = [];
20+
const operations: Array<OperationDefinitionNode> = [];
1721
const depGraph: DepGraph = Object.create(null);
18-
let fromName;
1922

2023
// Populate metadata and build a dependency graph.
21-
visit(documentAST, {
22-
OperationDefinition(node) {
23-
fromName = opName(node);
24-
operations.push(node);
25-
},
26-
FragmentDefinition(node) {
27-
fromName = node.name.value;
28-
},
29-
FragmentSpread(node) {
30-
const toName = node.name.value;
31-
let dependents = depGraph[fromName];
32-
if (dependents === undefined) {
33-
dependents = depGraph[fromName] = Object.create(null);
34-
}
35-
dependents[toName] = true;
36-
},
37-
});
24+
for (const definitionNode of documentAST.definitions) {
25+
switch (definitionNode.kind) {
26+
case Kind.OPERATION_DEFINITION:
27+
operations.push(definitionNode);
28+
break;
29+
case Kind.FRAGMENT_DEFINITION:
30+
depGraph[definitionNode.name.value] = collectDependencies(
31+
definitionNode.selectionSet,
32+
);
33+
break;
34+
}
35+
}
3836

3937
// For each operation, produce a new synthesized AST which includes only what
4038
// is necessary for completing that operation.
4139
const separatedDocumentASTs = Object.create(null);
4240
for (const operation of operations) {
43-
const operationName = opName(operation);
44-
const dependencies = Object.create(null);
45-
collectTransitiveDependencies(dependencies, depGraph, operationName);
41+
const dependencies = new Set();
42+
43+
for (const fragmentName of collectDependencies(operation.selectionSet)) {
44+
collectTransitiveDependencies(dependencies, depGraph, fragmentName);
45+
}
46+
47+
// Provides the empty string for anonymous operations.
48+
const operationName = operation.name ? operation.name.value : '';
4649

4750
// The list of definition nodes to be included for this operation, sorted
4851
// to retain the same order as the original document.
@@ -52,35 +55,42 @@ export function separateOperations(
5255
(node) =>
5356
node === operation ||
5457
(node.kind === Kind.FRAGMENT_DEFINITION &&
55-
dependencies[node.name.value]),
58+
dependencies.has(node.name.value)),
5659
),
5760
};
5861
}
5962

6063
return separatedDocumentASTs;
6164
}
6265

63-
type DepGraph = ObjMap<ObjMap<boolean>>;
64-
65-
// Provides the empty string for anonymous operations.
66-
function opName(operation: OperationDefinitionNode): string {
67-
return operation.name ? operation.name.value : '';
68-
}
66+
type DepGraph = ObjMap<Array<string>>;
6967

7068
// From a dependency graph, collects a list of transitive dependencies by
7169
// recursing through a dependency graph.
7270
function collectTransitiveDependencies(
73-
collected: ObjMap<boolean>,
71+
collected: Set<string>,
7472
depGraph: DepGraph,
7573
fromName: string,
7674
): void {
77-
const immediateDeps = depGraph[fromName];
78-
if (immediateDeps) {
79-
for (const toName of Object.keys(immediateDeps)) {
80-
if (!collected[toName]) {
81-
collected[toName] = true;
75+
if (!collected.has(fromName)) {
76+
collected.add(fromName);
77+
78+
const immediateDeps = depGraph[fromName];
79+
if (immediateDeps !== undefined) {
80+
for (const toName of immediateDeps) {
8281
collectTransitiveDependencies(collected, depGraph, toName);
8382
}
8483
}
8584
}
8685
}
86+
87+
function collectDependencies(selectionSet: SelectionSetNode): Array<string> {
88+
const dependencies = [];
89+
90+
visit(selectionSet, {
91+
FragmentSpread(node) {
92+
dependencies.push(node.name.value);
93+
},
94+
});
95+
return dependencies;
96+
}

0 commit comments

Comments
 (0)