Skip to content

Commit 914fd83

Browse files
committed
Execution works with new AST node
1 parent 94fef5d commit 914fd83

File tree

3 files changed

+118
-11
lines changed

3 files changed

+118
-11
lines changed

src/execution/collectFields.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
} from '../type/directives.js';
2323
import type { GraphQLSchema } from '../type/schema.js';
2424

25+
import { keyForFragmentSpread } from '../utilities/keyForFragmentSpread.js';
26+
import { substituteFragmentArguments } from '../utilities/substituteFragmentArguments.js';
2527
import { typeFromAST } from '../utilities/typeFromAST.js';
2628

2729
import { getDirectiveValues } from './values.js';
@@ -124,7 +126,7 @@ function collectFieldsImpl(
124126
selectionSet: SelectionSetNode,
125127
fields: AccumulatorMap<string, FieldNode>,
126128
patches: Array<PatchFields>,
127-
visitedFragmentNames: Set<string>,
129+
visitedFragmentKeys: Set<string>,
128130
): void {
129131
for (const selection of selectionSet.selections) {
130132
switch (selection.kind) {
@@ -156,7 +158,7 @@ function collectFieldsImpl(
156158
selection.selectionSet,
157159
patchFields,
158160
patches,
159-
visitedFragmentNames,
161+
visitedFragmentKeys,
160162
);
161163
patches.push({
162164
label: defer.label,
@@ -172,24 +174,24 @@ function collectFieldsImpl(
172174
selection.selectionSet,
173175
fields,
174176
patches,
175-
visitedFragmentNames,
177+
visitedFragmentKeys,
176178
);
177179
}
178180
break;
179181
}
180182
case Kind.FRAGMENT_SPREAD: {
181-
const fragName = selection.name.value;
183+
const fragmentKey = keyForFragmentSpread(selection);
182184

183185
if (!shouldIncludeNode(variableValues, selection)) {
184186
continue;
185187
}
186188

187189
const defer = getDeferValues(operation, variableValues, selection);
188-
if (visitedFragmentNames.has(fragName) && !defer) {
190+
if (visitedFragmentKeys.has(fragmentKey) && !defer) {
189191
continue;
190192
}
191193

192-
const fragment = fragments[fragName];
194+
const fragment = fragments[selection.name.value];
193195
if (
194196
!fragment ||
195197
!doesFragmentConditionMatch(schema, fragment, runtimeType)
@@ -198,9 +200,13 @@ function collectFieldsImpl(
198200
}
199201

200202
if (!defer) {
201-
visitedFragmentNames.add(fragName);
203+
visitedFragmentKeys.add(fragmentKey);
202204
}
203205

206+
const fragmentSelectionSet = substituteFragmentArguments(
207+
fragment,
208+
selection,
209+
);
204210
if (defer) {
205211
const patchFields = new AccumulatorMap<string, FieldNode>();
206212
collectFieldsImpl(
@@ -209,10 +215,10 @@ function collectFieldsImpl(
209215
variableValues,
210216
operation,
211217
runtimeType,
212-
fragment.selectionSet,
218+
fragmentSelectionSet,
213219
patchFields,
214220
patches,
215-
visitedFragmentNames,
221+
visitedFragmentKeys,
216222
);
217223
patches.push({
218224
label: defer.label,
@@ -225,10 +231,10 @@ function collectFieldsImpl(
225231
variableValues,
226232
operation,
227233
runtimeType,
228-
fragment.selectionSet,
234+
fragmentSelectionSet,
229235
fields,
230236
patches,
231-
visitedFragmentNames,
237+
visitedFragmentKeys,
232238
);
233239
}
234240
break;

src/utilities/keyForFragmentSpread.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { FragmentSpreadNode } from '../language/ast.js';
2+
import { print } from '../language/printer.js';
3+
4+
/**
5+
* Create a key that uniquely identifies common fragment spreads.
6+
* Treats the fragment spread as the source of truth for the key: it
7+
* does not bother to look up the argument definitions to de-duplicate default-variable args.
8+
*
9+
* Using the fragment definition to more accurately de-duplicate common spreads
10+
* is a potential performance win, but in practice it seems unlikely to be common.
11+
*/
12+
export function keyForFragmentSpread(fragmentSpread: FragmentSpreadNode) {
13+
const fragmentName = fragmentSpread.name.value;
14+
const fragmentArguments = fragmentSpread.arguments;
15+
if (fragmentArguments == null || fragmentArguments.length === 0) {
16+
return fragmentName;
17+
}
18+
19+
const printedArguments: Array<string> = fragmentArguments
20+
.map(print)
21+
.sort((a, b) => a.localeCompare(b));
22+
return fragmentName + '(' + printedArguments.join(',') + ')';
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { Maybe } from '../jsutils/Maybe.js';
2+
import type { ObjMap } from '../jsutils/ObjMap.js';
3+
4+
import type {
5+
ArgumentNode,
6+
FragmentArgumentDefinitionNode,
7+
FragmentDefinitionNode,
8+
FragmentSpreadNode,
9+
SelectionSetNode,
10+
ValueNode,
11+
} from '../language/ast.js';
12+
import { Kind } from '../language/kinds.js';
13+
import { visit } from '../language/visitor.js';
14+
15+
/**
16+
* Replaces all fragment argument values with non-fragment-scoped values.
17+
*
18+
* NOTE: fragment arguments are scoped to the fragment they're defined on.
19+
* Therefore, after we apply the passed-in arguments, all remaining variables
20+
* must be either operation defined variables or explicitly unset.
21+
*/
22+
export function substituteFragmentArguments(
23+
def: FragmentDefinitionNode,
24+
fragmentSpread: FragmentSpreadNode,
25+
): SelectionSetNode {
26+
const argumentDefinitions = def.arguments;
27+
if (argumentDefinitions == null || argumentDefinitions.length === 0) {
28+
return def.selectionSet;
29+
}
30+
const argumentValues = fragmentArgumentSubstitutions(
31+
argumentDefinitions,
32+
fragmentSpread.arguments,
33+
);
34+
return visit(def.selectionSet, {
35+
Variable(node) {
36+
return argumentValues[node.name.value];
37+
},
38+
});
39+
}
40+
41+
export function fragmentArgumentSubstitutions(
42+
argumentDefinitions: ReadonlyArray<FragmentArgumentDefinitionNode>,
43+
argumentValues: Maybe<ReadonlyArray<ArgumentNode>>,
44+
): ObjMap<ValueNode> {
45+
const substitutions: ObjMap<ValueNode> = {};
46+
if (argumentValues) {
47+
for (const argument of argumentValues) {
48+
substitutions[argument.name.value] = argument.value;
49+
}
50+
}
51+
52+
for (const argumentDefinition of argumentDefinitions) {
53+
const argumentName = argumentDefinition.variable.name.value;
54+
if (substitutions[argumentName]) {
55+
continue;
56+
}
57+
58+
const defaultValue = argumentDefinition.defaultValue;
59+
if (defaultValue) {
60+
substitutions[argumentName] = defaultValue;
61+
} else {
62+
// We need a way to allow unset arguments without accidentally
63+
// replacing an unset fragment argument with an operation
64+
// variable value. Fragment arguments must always have LOCAL scope.
65+
//
66+
// To remove this hack, we need to either:
67+
// - include fragment argument scope when evaluating fields
68+
// - make unset fragment arguments invalid
69+
// Requiring the spread to pass all non-default-defined arguments is nice,
70+
// but makes field argument default values impossible to use.
71+
substitutions[argumentName] = {
72+
kind: Kind.VARIABLE,
73+
name: { kind: Kind.NAME, value: '__UNSET' },
74+
};
75+
}
76+
}
77+
return substitutions;
78+
}

0 commit comments

Comments
 (0)