Skip to content

Commit 2e4a9bc

Browse files
committed
Executor changes with helpers needed for executor and validation
1 parent 6858a1f commit 2e4a9bc

File tree

4 files changed

+345
-11
lines changed

4 files changed

+345
-11
lines changed

src/execution/__tests__/variables-test.ts

+227
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ function fieldWithInputArg(
8282
};
8383
}
8484

85+
const NestedType: GraphQLObjectType = new GraphQLObjectType({
86+
name: 'NestedType',
87+
fields: {
88+
echo: fieldWithInputArg({ type: GraphQLString }),
89+
},
90+
});
91+
8592
const TestType = new GraphQLObjectType({
8693
name: 'TestType',
8794
fields: {
@@ -107,6 +114,10 @@ const TestType = new GraphQLObjectType({
107114
defaultValue: 'Hello World',
108115
}),
109116
list: fieldWithInputArg({ type: new GraphQLList(GraphQLString) }),
117+
nested: {
118+
type: NestedType,
119+
resolve: () => ({}),
120+
},
110121
nnList: fieldWithInputArg({
111122
type: new GraphQLNonNull(new GraphQLList(GraphQLString)),
112123
}),
@@ -1006,6 +1017,222 @@ describe('Execute: Handles inputs', () => {
10061017
});
10071018
});
10081019

1020+
describe('using fragment arguments', () => {
1021+
it('when there are no fragment arguments', () => {
1022+
const result = executeQuery(`
1023+
query {
1024+
...a
1025+
}
1026+
fragment a on TestType {
1027+
fieldWithNonNullableStringInput(input: "A")
1028+
}
1029+
`);
1030+
expect(result).to.deep.equal({
1031+
data: {
1032+
fieldWithNonNullableStringInput: '"A"',
1033+
},
1034+
});
1035+
});
1036+
1037+
it('when a value is required and provided', () => {
1038+
const result = executeQuery(`
1039+
query {
1040+
...a(value: "A")
1041+
}
1042+
fragment a($value: String!) on TestType {
1043+
fieldWithNonNullableStringInput(input: $value)
1044+
}
1045+
`);
1046+
expect(result).to.deep.equal({
1047+
data: {
1048+
fieldWithNonNullableStringInput: '"A"',
1049+
},
1050+
});
1051+
});
1052+
1053+
it('when a value is required and not provided', () => {
1054+
const result = executeQuery(`
1055+
query {
1056+
...a
1057+
}
1058+
fragment a($value: String!) on TestType {
1059+
fieldWithNullableStringInput(input: $value)
1060+
}
1061+
`);
1062+
expect(result).to.deep.equal({
1063+
data: {
1064+
fieldWithNullableStringInput: null,
1065+
},
1066+
});
1067+
});
1068+
1069+
it('when the definition has a default and is provided', () => {
1070+
const result = executeQuery(`
1071+
query {
1072+
...a(value: "A")
1073+
}
1074+
fragment a($value: String! = "B") on TestType {
1075+
fieldWithNonNullableStringInput(input: $value)
1076+
}
1077+
`);
1078+
expect(result).to.deep.equal({
1079+
data: {
1080+
fieldWithNonNullableStringInput: '"A"',
1081+
},
1082+
});
1083+
});
1084+
1085+
it('when the definition has a default and is not provided', () => {
1086+
const result = executeQuery(`
1087+
query {
1088+
...a
1089+
}
1090+
fragment a($value: String! = "B") on TestType {
1091+
fieldWithNonNullableStringInput(input: $value)
1092+
}
1093+
`);
1094+
expect(result).to.deep.equal({
1095+
data: {
1096+
fieldWithNonNullableStringInput: '"B"',
1097+
},
1098+
});
1099+
});
1100+
1101+
it('when the definition has a non-nullable default and is provided null', () => {
1102+
const result = executeQuery(`
1103+
query {
1104+
...a(value: null)
1105+
}
1106+
fragment a($value: String! = "B") on TestType {
1107+
fieldWithNullableStringInput(input: $value)
1108+
}
1109+
`);
1110+
expect(result).to.deep.equal({
1111+
data: {
1112+
fieldWithNullableStringInput: 'null',
1113+
},
1114+
});
1115+
});
1116+
1117+
it('when the definition has no default and is not provided', () => {
1118+
const result = executeQuery(`
1119+
query {
1120+
...a
1121+
}
1122+
fragment a($value: String) on TestType {
1123+
fieldWithNonNullableStringInputAndDefaultArgumentValue(input: $value)
1124+
}
1125+
`);
1126+
expect(result).to.deep.equal({
1127+
data: {
1128+
fieldWithNonNullableStringInputAndDefaultArgumentValue:
1129+
'"Hello World"',
1130+
},
1131+
});
1132+
});
1133+
1134+
it('when an argument is shadowed by an operation variable', () => {
1135+
const result = executeQuery(`
1136+
query($x: String! = "A") {
1137+
...a(x: "B")
1138+
}
1139+
fragment a($x: String) on TestType {
1140+
fieldWithNullableStringInput(input: $x)
1141+
}
1142+
`);
1143+
expect(result).to.deep.equal({
1144+
data: {
1145+
fieldWithNullableStringInput: '"B"',
1146+
},
1147+
});
1148+
});
1149+
1150+
it('when a nullable argument with a field default is not provided and shadowed by an operation variable', () => {
1151+
const result = executeQuery(`
1152+
query($x: String = "A") {
1153+
...a
1154+
}
1155+
fragment a($x: String) on TestType {
1156+
fieldWithNonNullableStringInputAndDefaultArgumentValue(input: $x)
1157+
}
1158+
`);
1159+
expect(result).to.deep.equal({
1160+
data: {
1161+
fieldWithNonNullableStringInputAndDefaultArgumentValue:
1162+
'"Hello World"',
1163+
},
1164+
});
1165+
});
1166+
1167+
it('when a fragment is used with different args', () => {
1168+
const result = executeQuery(`
1169+
query($x: String = "Hello") {
1170+
a: nested {
1171+
...a(x: "a")
1172+
}
1173+
b: nested {
1174+
...a(x: "b", b: true)
1175+
}
1176+
hello: nested {
1177+
...a(x: $x)
1178+
}
1179+
}
1180+
fragment a($x: String, $b: Boolean = false) on NestedType {
1181+
a: echo(input: $x) @skip(if: $b)
1182+
b: echo(input: $x) @include(if: $b)
1183+
}
1184+
`);
1185+
expect(result).to.deep.equal({
1186+
data: {
1187+
a: {
1188+
a: '"a"',
1189+
},
1190+
b: {
1191+
b: '"b"',
1192+
},
1193+
hello: {
1194+
a: '"Hello"',
1195+
},
1196+
},
1197+
});
1198+
});
1199+
1200+
it('when the argument variable is nested in a complex type', () => {
1201+
const result = executeQuery(`
1202+
query {
1203+
...a(value: "C")
1204+
}
1205+
fragment a($value: String) on TestType {
1206+
list(input: ["A", "B", $value, "D"])
1207+
}
1208+
`);
1209+
expect(result).to.deep.equal({
1210+
data: {
1211+
list: '["A", "B", "C", "D"]',
1212+
},
1213+
});
1214+
});
1215+
1216+
it('when argument variables are used recursively', () => {
1217+
const result = executeQuery(`
1218+
query {
1219+
...a(aValue: "C")
1220+
}
1221+
fragment a($aValue: String) on TestType {
1222+
...b(bValue: $aValue)
1223+
}
1224+
fragment b($bValue: String) on TestType {
1225+
list(input: ["A", "B", $bValue, "D"])
1226+
}
1227+
`);
1228+
expect(result).to.deep.equal({
1229+
data: {
1230+
list: '["A", "B", "C", "D"]',
1231+
},
1232+
});
1233+
});
1234+
});
1235+
10091236
describe('getVariableValues: limit maximum number of coercion errors', () => {
10101237
const doc = parse(`
10111238
query ($input: [String!]) {

src/execution/collectFields.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
} from '../type/directives.js';
2020
import type { GraphQLSchema } from '../type/schema.js';
2121

22+
import { keyForFragmentSpread } from '../utilities/keyForFragmentSpread.js';
23+
import { substituteFragmentArguments } from '../utilities/substituteFragmentArguments.js';
2224
import { typeFromAST } from '../utilities/typeFromAST.js';
2325

2426
import { getDirectiveValues } from './values.js';
@@ -116,7 +118,7 @@ function collectFieldsImpl(
116118
selectionSet: SelectionSetNode,
117119
fields: AccumulatorMap<string, FieldNode>,
118120
patches: Array<PatchFields>,
119-
visitedFragmentNames: Set<string>,
121+
visitedFragmentKeys: Set<string>,
120122
): void {
121123
for (const selection of selectionSet.selections) {
122124
switch (selection.kind) {
@@ -147,7 +149,7 @@ function collectFieldsImpl(
147149
selection.selectionSet,
148150
patchFields,
149151
patches,
150-
visitedFragmentNames,
152+
visitedFragmentKeys,
151153
);
152154
patches.push({
153155
label: defer.label,
@@ -162,24 +164,24 @@ function collectFieldsImpl(
162164
selection.selectionSet,
163165
fields,
164166
patches,
165-
visitedFragmentNames,
167+
visitedFragmentKeys,
166168
);
167169
}
168170
break;
169171
}
170172
case Kind.FRAGMENT_SPREAD: {
171-
const fragName = selection.name.value;
173+
const fragmentKey = keyForFragmentSpread(selection);
172174

173175
if (!shouldIncludeNode(variableValues, selection)) {
174176
continue;
175177
}
176178

177179
const defer = getDeferValues(variableValues, selection);
178-
if (visitedFragmentNames.has(fragName) && !defer) {
180+
if (visitedFragmentKeys.has(fragmentKey) && !defer) {
179181
continue;
180182
}
181183

182-
const fragment = fragments[fragName];
184+
const fragment = fragments[selection.name.value];
183185
if (
184186
!fragment ||
185187
!doesFragmentConditionMatch(schema, fragment, runtimeType)
@@ -188,20 +190,24 @@ function collectFieldsImpl(
188190
}
189191

190192
if (!defer) {
191-
visitedFragmentNames.add(fragName);
193+
visitedFragmentKeys.add(fragmentKey);
192194
}
193195

196+
const fragmentSelectionSet = substituteFragmentArguments(
197+
fragment,
198+
selection,
199+
);
194200
if (defer) {
195201
const patchFields = new AccumulatorMap<string, FieldNode>();
196202
collectFieldsImpl(
197203
schema,
198204
fragments,
199205
variableValues,
200206
runtimeType,
201-
fragment.selectionSet,
207+
fragmentSelectionSet,
202208
patchFields,
203209
patches,
204-
visitedFragmentNames,
210+
visitedFragmentKeys,
205211
);
206212
patches.push({
207213
label: defer.label,
@@ -213,10 +219,10 @@ function collectFieldsImpl(
213219
fragments,
214220
variableValues,
215221
runtimeType,
216-
fragment.selectionSet,
222+
fragmentSelectionSet,
217223
fields,
218224
patches,
219-
visitedFragmentNames,
225+
visitedFragmentKeys,
220226
);
221227
}
222228
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+
}

0 commit comments

Comments
 (0)