Skip to content

Commit c614f75

Browse files
authored
separateOperations - a utility function for splitting an AST (#456)
* separateOperations - a utility function for splitting one AST into one per operation. A typical task using GraphQL at Facebook looks something like: 1. Load and parse all .graphql files which may contain operations or fragments. 2. Use `concatAST` to produce one AST that contains all operations and fragments. 3. Separate this all-encompasing AST into individual ASTs that represent each operation which could be sent to the server in isolation. `separateOperations` fulfills this third step. * add flow types, more tests
1 parent 7e8e57b commit c614f75

File tree

4 files changed

+276
-0
lines changed

4 files changed

+276
-0
lines changed

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ export {
192192
// Concatenates multiple AST together.
193193
concatAST,
194194

195+
// Separates an AST into an AST per Operation.
196+
separateOperations,
197+
195198
// Comparators for types
196199
isEqualType,
197200
isTypeSubTypeOf,
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* Copyright (c) 2015, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
import { describe, it } from 'mocha';
11+
import { expect } from 'chai';
12+
import { separateOperations } from '../separateOperations';
13+
import { parse, print } from '../../language';
14+
15+
16+
describe('separateOperations', () => {
17+
18+
it('separates one AST into multiple, maintaining document order', () => {
19+
20+
const ast = parse(`
21+
{
22+
...Y
23+
...X
24+
}
25+
26+
query One {
27+
foo
28+
bar
29+
...A
30+
...X
31+
}
32+
33+
fragment A on T {
34+
field
35+
...B
36+
}
37+
38+
fragment X on T {
39+
fieldX
40+
}
41+
42+
query Two {
43+
...A
44+
...Y
45+
baz
46+
}
47+
48+
fragment Y on T {
49+
fieldY
50+
}
51+
52+
fragment B on T {
53+
something
54+
}
55+
`);
56+
57+
const separatedASTs = separateOperations(ast);
58+
59+
expect(Object.keys(separatedASTs)).to.deep.equal([ '', 'One', 'Two' ]);
60+
61+
expect(print(separatedASTs[''])).to.equal(
62+
`{
63+
...Y
64+
...X
65+
}
66+
67+
fragment X on T {
68+
fieldX
69+
}
70+
71+
fragment Y on T {
72+
fieldY
73+
}
74+
`
75+
);
76+
77+
expect(print(separatedASTs.One)).to.equal(
78+
`query One {
79+
foo
80+
bar
81+
...A
82+
...X
83+
}
84+
85+
fragment A on T {
86+
field
87+
...B
88+
}
89+
90+
fragment X on T {
91+
fieldX
92+
}
93+
94+
fragment B on T {
95+
something
96+
}
97+
`
98+
);
99+
100+
expect(print(separatedASTs.Two)).to.equal(
101+
`fragment A on T {
102+
field
103+
...B
104+
}
105+
106+
query Two {
107+
...A
108+
...Y
109+
baz
110+
}
111+
112+
fragment Y on T {
113+
fieldY
114+
}
115+
116+
fragment B on T {
117+
something
118+
}
119+
`
120+
);
121+
122+
});
123+
124+
it('survives circular dependencies', () => {
125+
126+
const ast = parse(`
127+
query One {
128+
...A
129+
}
130+
131+
fragment A on T {
132+
...B
133+
}
134+
135+
fragment B on T {
136+
...A
137+
}
138+
139+
query Two {
140+
...B
141+
}
142+
`);
143+
144+
const separatedASTs = separateOperations(ast);
145+
146+
expect(Object.keys(separatedASTs)).to.deep.equal([ 'One', 'Two' ]);
147+
148+
expect(print(separatedASTs.One)).to.equal(
149+
`query One {
150+
...A
151+
}
152+
153+
fragment A on T {
154+
...B
155+
}
156+
157+
fragment B on T {
158+
...A
159+
}
160+
`
161+
);
162+
163+
expect(print(separatedASTs.Two)).to.equal(
164+
`fragment A on T {
165+
...B
166+
}
167+
168+
fragment B on T {
169+
...A
170+
}
171+
172+
query Two {
173+
...B
174+
}
175+
`
176+
);
177+
178+
});
179+
180+
});

src/utilities/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export { isValidLiteralValue } from './isValidLiteralValue';
4848
// Concatenates multiple AST together.
4949
export { concatAST } from './concatAST';
5050

51+
// Separates an AST into an AST per Operation.
52+
export { separateOperations } from './separateOperations';
53+
5154
// Comparators for types
5255
export {
5356
isEqualType,
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/* @flow */
2+
/**
3+
* Copyright (c) 2015, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree. An additional grant
8+
* of patent rights can be found in the PATENTS file in the same directory.
9+
*/
10+
11+
import { visit } from '../language/visitor';
12+
import type {
13+
Document,
14+
OperationDefinition,
15+
} from '../language/ast';
16+
17+
/**
18+
* separateOperations accepts a single AST document which may contain many
19+
* operations and fragments and returns a collection of AST documents each of
20+
* which contains a single operation as well the fragment definitions it
21+
* refers to.
22+
*/
23+
export function separateOperations(
24+
documentAST: Document
25+
): { [operationName: string]: Document } {
26+
27+
const operations = [];
28+
const depGraph: DepGraph = Object.create(null);
29+
let fromName;
30+
31+
// Populate the list of operations and build a dependency graph.
32+
visit(documentAST, {
33+
OperationDefinition(node) {
34+
operations.push(node);
35+
fromName = opName(node);
36+
},
37+
FragmentDefinition(node) {
38+
fromName = node.name.value;
39+
},
40+
FragmentSpread(node) {
41+
const toName = node.name.value;
42+
(depGraph[fromName] ||
43+
(depGraph[fromName] = Object.create(null)))[toName] = true;
44+
}
45+
});
46+
47+
// For each operation, produce a new synthesized AST which includes only what
48+
// is necessary for completing that operation.
49+
const separatedDocumentASTs = Object.create(null);
50+
operations.forEach(operation => {
51+
const operationName = opName(operation);
52+
const dependencies = Object.create(null);
53+
collectTransitiveDependencies(dependencies, depGraph, operationName);
54+
55+
separatedDocumentASTs[operationName] = {
56+
kind: 'Document',
57+
definitions: documentAST.definitions.filter(def =>
58+
def === operation ||
59+
def.kind === 'FragmentDefinition' && dependencies[def.name.value]
60+
)
61+
};
62+
});
63+
64+
return separatedDocumentASTs;
65+
}
66+
67+
type DepGraph = {[from: string]: {[to: string]: boolean}};
68+
69+
// Provides the empty string for anonymous operations.
70+
function opName(operation: OperationDefinition): string {
71+
return operation.name ? operation.name.value : '';
72+
}
73+
74+
// From a dependency graph, collects a list of transitive dependencies by
75+
// recursing through a dependency graph.
76+
function collectTransitiveDependencies(
77+
collected: {[key: string]: boolean},
78+
depGraph: DepGraph,
79+
fromName: string
80+
): void {
81+
const immediateDeps = depGraph[fromName];
82+
if (immediateDeps) {
83+
Object.keys(immediateDeps).forEach(toName => {
84+
if (!collected[toName]) {
85+
collected[toName] = true;
86+
collectTransitiveDependencies(collected, depGraph, toName);
87+
}
88+
});
89+
}
90+
}

0 commit comments

Comments
 (0)