Skip to content

Commit e9fbda2

Browse files
committed
RFC: Fragment Arguments
1 parent 4b1ae03 commit e9fbda2

24 files changed

+1547
-209
lines changed

src/execution/__tests__/variables-test.ts

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ function fieldWithInputArg(
104104
};
105105
}
106106

107+
const NestedType: GraphQLObjectType = new GraphQLObjectType({
108+
name: 'NestedType',
109+
fields: {
110+
echo: fieldWithInputArg({ type: GraphQLString }),
111+
},
112+
});
113+
107114
const TestType = new GraphQLObjectType({
108115
name: 'TestType',
109116
fields: {
@@ -129,6 +136,10 @@ const TestType = new GraphQLObjectType({
129136
defaultValue: 'Hello World',
130137
}),
131138
list: fieldWithInputArg({ type: new GraphQLList(GraphQLString) }),
139+
nested: {
140+
type: NestedType,
141+
resolve: () => ({}),
142+
},
132143
nnList: fieldWithInputArg({
133144
type: new GraphQLNonNull(new GraphQLList(GraphQLString)),
134145
}),
@@ -1066,6 +1077,245 @@ describe('Execute: Handles inputs', () => {
10661077
});
10671078
});
10681079

1080+
describe('using fragment arguments', () => {
1081+
it('when there are no fragment arguments', () => {
1082+
const result = executeQuery(`
1083+
query {
1084+
...a
1085+
}
1086+
fragment a on TestType {
1087+
fieldWithNonNullableStringInput(input: "A")
1088+
}
1089+
`);
1090+
expect(result).to.deep.equal({
1091+
data: {
1092+
fieldWithNonNullableStringInput: '"A"',
1093+
},
1094+
});
1095+
});
1096+
1097+
it('when a value is required and provided', () => {
1098+
const result = executeQuery(`
1099+
query {
1100+
...a(value: "A")
1101+
}
1102+
fragment a($value: String!) on TestType {
1103+
fieldWithNonNullableStringInput(input: $value)
1104+
}
1105+
`);
1106+
expect(result).to.deep.equal({
1107+
data: {
1108+
fieldWithNonNullableStringInput: '"A"',
1109+
},
1110+
});
1111+
});
1112+
1113+
it('when a value is required and not provided', () => {
1114+
const result = executeQuery(`
1115+
query {
1116+
...a
1117+
}
1118+
fragment a($value: String!) on TestType {
1119+
fieldWithNullableStringInput(input: $value)
1120+
}
1121+
`);
1122+
expect(result).to.deep.equal({
1123+
data: {
1124+
fieldWithNullableStringInput: null,
1125+
},
1126+
});
1127+
});
1128+
1129+
it('when the definition has a default and is provided', () => {
1130+
const result = executeQuery(`
1131+
query {
1132+
...a(value: "A")
1133+
}
1134+
fragment a($value: String! = "B") on TestType {
1135+
fieldWithNonNullableStringInput(input: $value)
1136+
}
1137+
`);
1138+
expect(result).to.deep.equal({
1139+
data: {
1140+
fieldWithNonNullableStringInput: '"A"',
1141+
},
1142+
});
1143+
});
1144+
1145+
it('when the definition has a default and is not provided', () => {
1146+
const result = executeQuery(`
1147+
query {
1148+
...a
1149+
}
1150+
fragment a($value: String! = "B") on TestType {
1151+
fieldWithNonNullableStringInput(input: $value)
1152+
}
1153+
`);
1154+
expect(result).to.deep.equal({
1155+
data: {
1156+
fieldWithNonNullableStringInput: '"B"',
1157+
},
1158+
});
1159+
});
1160+
1161+
it('when the definition has a non-nullable default and is provided null', () => {
1162+
const result = executeQuery(`
1163+
query {
1164+
...a(value: null)
1165+
}
1166+
fragment a($value: String! = "B") on TestType {
1167+
fieldWithNullableStringInput(input: $value)
1168+
}
1169+
`);
1170+
expect(result).to.deep.equal({
1171+
data: {
1172+
fieldWithNullableStringInput: 'null',
1173+
},
1174+
});
1175+
});
1176+
1177+
it('when the definition has no default and is not provided', () => {
1178+
const result = executeQuery(`
1179+
query {
1180+
...a
1181+
}
1182+
fragment a($value: String) on TestType {
1183+
fieldWithNonNullableStringInputAndDefaultArgumentValue(input: $value)
1184+
}
1185+
`);
1186+
expect(result).to.deep.equal({
1187+
data: {
1188+
fieldWithNonNullableStringInputAndDefaultArgumentValue:
1189+
'"Hello World"',
1190+
},
1191+
});
1192+
});
1193+
1194+
it('when an argument is shadowed by an operation variable', () => {
1195+
const result = executeQuery(`
1196+
query($x: String! = "A") {
1197+
...a(x: "B")
1198+
}
1199+
fragment a($x: String) on TestType {
1200+
fieldWithNullableStringInput(input: $x)
1201+
}
1202+
`);
1203+
expect(result).to.deep.equal({
1204+
data: {
1205+
fieldWithNullableStringInput: '"B"',
1206+
},
1207+
});
1208+
});
1209+
1210+
it('when a nullable argument with a field default is not provided and shadowed by an operation variable', () => {
1211+
const result = executeQuery(`
1212+
query($x: String = "A") {
1213+
...a
1214+
}
1215+
fragment a($x: String) on TestType {
1216+
fieldWithNonNullableStringInputAndDefaultArgumentValue(input: $x)
1217+
}
1218+
`);
1219+
expect(result).to.deep.equal({
1220+
data: {
1221+
fieldWithNonNullableStringInputAndDefaultArgumentValue:
1222+
'"Hello World"',
1223+
},
1224+
});
1225+
});
1226+
1227+
it('when a fragment is used with different args', () => {
1228+
const result = executeQuery(`
1229+
query($x: String = "Hello") {
1230+
a: nested {
1231+
...a(x: "a")
1232+
}
1233+
b: nested {
1234+
...a(x: "b", b: true)
1235+
}
1236+
hello: nested {
1237+
...a(x: $x)
1238+
}
1239+
}
1240+
fragment a($x: String, $b: Boolean = false) on NestedType {
1241+
a: echo(input: $x) @skip(if: $b)
1242+
b: echo(input: $x) @include(if: $b)
1243+
}
1244+
`);
1245+
expect(result).to.deep.equal({
1246+
data: {
1247+
a: {
1248+
a: '"a"',
1249+
},
1250+
b: {
1251+
b: '"b"',
1252+
},
1253+
hello: {
1254+
a: '"Hello"',
1255+
},
1256+
},
1257+
});
1258+
});
1259+
1260+
it('when the argument variable is nested in a complex type', () => {
1261+
const result = executeQuery(`
1262+
query {
1263+
...a(value: "C")
1264+
}
1265+
fragment a($value: String) on TestType {
1266+
list(input: ["A", "B", $value, "D"])
1267+
}
1268+
`);
1269+
expect(result).to.deep.equal({
1270+
data: {
1271+
list: '["A", "B", "C", "D"]',
1272+
},
1273+
});
1274+
});
1275+
1276+
it('when argument variables are used recursively', () => {
1277+
const result = executeQuery(`
1278+
query {
1279+
...a(aValue: "C")
1280+
}
1281+
fragment a($aValue: String) on TestType {
1282+
...b(bValue: $aValue)
1283+
}
1284+
fragment b($bValue: String) on TestType {
1285+
list(input: ["A", "B", $bValue, "D"])
1286+
}
1287+
`);
1288+
expect(result).to.deep.equal({
1289+
data: {
1290+
list: '["A", "B", "C", "D"]',
1291+
},
1292+
});
1293+
});
1294+
1295+
it('when argument passed in as list', () => {
1296+
const result = executeQuery(`
1297+
query Q($opValue: String = "op") {
1298+
...a(aValue: "A")
1299+
}
1300+
fragment a($aValue: String, $bValue: String) on TestType {
1301+
...b(aValue: [$aValue, "B"], bValue: [$bValue, $opValue])
1302+
}
1303+
fragment b($aValue: [String], $bValue: [String], $cValue: String) on TestType {
1304+
aList: list(input: $aValue)
1305+
bList: list(input: $bValue)
1306+
cList: list(input: [$cValue])
1307+
}
1308+
`);
1309+
expect(result).to.deep.equal({
1310+
data: {
1311+
aList: '["A", "B"]',
1312+
bList: '[null, "op"]',
1313+
cList: '[null]',
1314+
},
1315+
});
1316+
});
1317+
});
1318+
10691319
describe('getVariableValues: limit maximum number of coercion errors', () => {
10701320
const doc = parse(`
10711321
query ($input: [String!]) {

src/execution/collectFields.ts

Lines changed: 17 additions & 11 deletions
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;

0 commit comments

Comments
 (0)