Skip to content

Commit 8cc6c93

Browse files
Implement collectFields for fragment-arguments
Co-authored-by: mjmahone <[email protected]>
1 parent 5931263 commit 8cc6c93

File tree

4 files changed

+384
-9
lines changed

4 files changed

+384
-9
lines changed

src/execution/__tests__/variables-test.ts

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ const TestComplexScalar = new GraphQLScalarType({
5959
},
6060
});
6161

62+
const NestedType: GraphQLObjectType = new GraphQLObjectType({
63+
name: 'NestedType',
64+
fields: {
65+
echo: fieldWithInputArg({ type: GraphQLString }),
66+
},
67+
});
68+
6269
const TestInputObject = new GraphQLInputObjectType({
6370
name: 'TestInputObject',
6471
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
}),
@@ -153,6 +164,15 @@ function executeQuery(
153164
return executeSync({ schema, document, variableValues });
154165
}
155166

167+
function executeQueryWithFragmentArguments(
168+
query: string,
169+
variableValues?: { [variable: string]: unknown },
170+
) {
171+
const document = parse(query, { experimentalFragmentArguments: true });
172+
return executeSync({ schema, document, variableValues });
173+
}
174+
175+
156176
describe('Execute: Handles inputs', () => {
157177
describe('Handles objects and nullability', () => {
158178
describe('using inline structs', () => {
@@ -1136,4 +1156,243 @@ describe('Execute: Handles inputs', () => {
11361156
});
11371157
});
11381158
});
1159+
1160+
describe('using fragment arguments', () => {
1161+
it('when there are no fragment arguments', () => {
1162+
const result = executeQueryWithFragmentArguments(`
1163+
query {
1164+
...a
1165+
}
1166+
fragment a on TestType {
1167+
fieldWithNonNullableStringInput(input: "A")
1168+
}
1169+
`);
1170+
expect(result).to.deep.equal({
1171+
data: {
1172+
fieldWithNonNullableStringInput: '"A"',
1173+
},
1174+
});
1175+
});
1176+
1177+
it('when a value is required and provided', () => {
1178+
const result = executeQueryWithFragmentArguments(`
1179+
query {
1180+
...a(value: "A")
1181+
}
1182+
fragment a($value: String!) on TestType {
1183+
fieldWithNonNullableStringInput(input: $value)
1184+
}
1185+
`);
1186+
expect(result).to.deep.equal({
1187+
data: {
1188+
fieldWithNonNullableStringInput: '"A"',
1189+
},
1190+
});
1191+
});
1192+
1193+
it('when a value is required and not provided', () => {
1194+
const result = executeQueryWithFragmentArguments(`
1195+
query {
1196+
...a
1197+
}
1198+
fragment a($value: String!) on TestType {
1199+
fieldWithNullableStringInput(input: $value)
1200+
}
1201+
`);
1202+
expect(result).to.deep.equal({
1203+
data: {
1204+
fieldWithNullableStringInput: null,
1205+
},
1206+
});
1207+
});
1208+
1209+
it('when the definition has a default and is provided', () => {
1210+
const result = executeQueryWithFragmentArguments(`
1211+
query {
1212+
...a(value: "A")
1213+
}
1214+
fragment a($value: String! = "B") on TestType {
1215+
fieldWithNonNullableStringInput(input: $value)
1216+
}
1217+
`);
1218+
expect(result).to.deep.equal({
1219+
data: {
1220+
fieldWithNonNullableStringInput: '"A"',
1221+
},
1222+
});
1223+
});
1224+
1225+
it('when the definition has a default and is not provided', () => {
1226+
const result = executeQueryWithFragmentArguments(`
1227+
query {
1228+
...a
1229+
}
1230+
fragment a($value: String! = "B") on TestType {
1231+
fieldWithNonNullableStringInput(input: $value)
1232+
}
1233+
`);
1234+
expect(result).to.deep.equal({
1235+
data: {
1236+
fieldWithNonNullableStringInput: '"B"',
1237+
},
1238+
});
1239+
});
1240+
1241+
it('when the definition has a non-nullable default and is provided null', () => {
1242+
const result = executeQueryWithFragmentArguments(`
1243+
query {
1244+
...a(value: null)
1245+
}
1246+
fragment a($value: String! = "B") on TestType {
1247+
fieldWithNullableStringInput(input: $value)
1248+
}
1249+
`);
1250+
expect(result).to.deep.equal({
1251+
data: {
1252+
fieldWithNullableStringInput: 'null',
1253+
},
1254+
});
1255+
});
1256+
1257+
it('when the definition has no default and is not provided', () => {
1258+
const result = executeQueryWithFragmentArguments(`
1259+
query {
1260+
...a
1261+
}
1262+
fragment a($value: String) on TestType {
1263+
fieldWithNonNullableStringInputAndDefaultArgumentValue(input: $value)
1264+
}
1265+
`);
1266+
expect(result).to.deep.equal({
1267+
data: {
1268+
fieldWithNonNullableStringInputAndDefaultArgumentValue:
1269+
'"Hello World"',
1270+
},
1271+
});
1272+
});
1273+
1274+
it('when an argument is shadowed by an operation variable', () => {
1275+
const result = executeQueryWithFragmentArguments(`
1276+
query($x: String! = "A") {
1277+
...a(x: "B")
1278+
}
1279+
fragment a($x: String) on TestType {
1280+
fieldWithNullableStringInput(input: $x)
1281+
}
1282+
`);
1283+
expect(result).to.deep.equal({
1284+
data: {
1285+
fieldWithNullableStringInput: '"B"',
1286+
},
1287+
});
1288+
});
1289+
1290+
it('when a nullable argument with a field default is not provided and shadowed by an operation variable', () => {
1291+
const result = executeQueryWithFragmentArguments(`
1292+
query($x: String = "A") {
1293+
...a
1294+
}
1295+
fragment a($x: String) on TestType {
1296+
fieldWithNonNullableStringInputAndDefaultArgumentValue(input: $x)
1297+
}
1298+
`);
1299+
expect(result).to.deep.equal({
1300+
data: {
1301+
fieldWithNonNullableStringInputAndDefaultArgumentValue:
1302+
'"Hello World"',
1303+
},
1304+
});
1305+
});
1306+
1307+
it('when a fragment is used with different args', () => {
1308+
const result = executeQueryWithFragmentArguments(`
1309+
query($x: String = "Hello") {
1310+
a: nested {
1311+
...a(x: "a")
1312+
}
1313+
b: nested {
1314+
...a(x: "b", b: true)
1315+
}
1316+
hello: nested {
1317+
...a(x: $x)
1318+
}
1319+
}
1320+
fragment a($x: String, $b: Boolean = false) on NestedType {
1321+
a: echo(input: $x) @skip(if: $b)
1322+
b: echo(input: $x) @include(if: $b)
1323+
}
1324+
`);
1325+
expect(result).to.deep.equal({
1326+
data: {
1327+
a: {
1328+
a: '"a"',
1329+
},
1330+
b: {
1331+
b: '"b"',
1332+
},
1333+
hello: {
1334+
a: '"Hello"',
1335+
},
1336+
},
1337+
});
1338+
});
1339+
1340+
it('when the argument variable is nested in a complex type', () => {
1341+
const result = executeQueryWithFragmentArguments(`
1342+
query {
1343+
...a(value: "C")
1344+
}
1345+
fragment a($value: String) on TestType {
1346+
list(input: ["A", "B", $value, "D"])
1347+
}
1348+
`);
1349+
expect(result).to.deep.equal({
1350+
data: {
1351+
list: '["A", "B", "C", "D"]',
1352+
},
1353+
});
1354+
});
1355+
1356+
it('when argument variables are used recursively', () => {
1357+
const result = executeQueryWithFragmentArguments(`
1358+
query {
1359+
...a(aValue: "C")
1360+
}
1361+
fragment a($aValue: String) on TestType {
1362+
...b(bValue: $aValue)
1363+
}
1364+
fragment b($bValue: String) on TestType {
1365+
list(input: ["A", "B", $bValue, "D"])
1366+
}
1367+
`);
1368+
expect(result).to.deep.equal({
1369+
data: {
1370+
list: '["A", "B", "C", "D"]',
1371+
},
1372+
});
1373+
});
1374+
1375+
it('when argument passed in as list', () => {
1376+
const result = executeQueryWithFragmentArguments(`
1377+
query Q($opValue: String = "op") {
1378+
...a(aValue: "A")
1379+
}
1380+
fragment a($aValue: String, $bValue: String) on TestType {
1381+
...b(aValue: [$aValue, "B"], bValue: [$bValue, $opValue])
1382+
}
1383+
fragment b($aValue: [String], $bValue: [String], $cValue: String) on TestType {
1384+
aList: list(input: $aValue)
1385+
bList: list(input: $bValue)
1386+
cList: list(input: [$cValue])
1387+
}
1388+
`);
1389+
expect(result).to.deep.equal({
1390+
data: {
1391+
aList: '["A", "B"]',
1392+
bList: '[null, "op"]',
1393+
cList: '[null]',
1394+
},
1395+
});
1396+
});
1397+
});
11391398
});

src/execution/collectFields.ts

Lines changed: 17 additions & 9 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';
@@ -42,7 +44,7 @@ interface CollectFieldsContext {
4244
variableValues: { [variable: string]: unknown };
4345
operation: OperationDefinitionNode;
4446
runtimeType: GraphQLObjectType;
45-
visitedFragmentNames: Set<string>;
47+
visitedFragmentKeys: Set<string>,
4648
}
4749

4850
/**
@@ -68,7 +70,7 @@ export function collectFields(
6870
variableValues,
6971
runtimeType,
7072
operation,
71-
visitedFragmentNames: new Set(),
73+
visitedFragmentKeys: new Set(),
7274
};
7375

7476
collectFieldsImpl(context, operation.selectionSet, groupedFieldSet);
@@ -100,7 +102,7 @@ export function collectSubfields(
100102
variableValues,
101103
runtimeType: returnType,
102104
operation,
103-
visitedFragmentNames: new Set(),
105+
visitedFragmentKeys: new Set(),
104106
};
105107
const subGroupedFieldSet = new AccumulatorMap<string, FieldDetails>();
106108

@@ -132,7 +134,7 @@ function collectFieldsImpl(
132134
variableValues,
133135
runtimeType,
134136
operation,
135-
visitedFragmentNames,
137+
visitedFragmentKeys,
136138
} = context;
137139

138140
for (const selection of selectionSet.selections) {
@@ -173,7 +175,7 @@ function collectFieldsImpl(
173175
break;
174176
}
175177
case Kind.FRAGMENT_SPREAD: {
176-
const fragName = selection.name.value;
178+
const fragmentKey = keyForFragmentSpread(selection);
177179

178180
const newDeferUsage = getDeferUsage(
179181
operation,
@@ -184,26 +186,32 @@ function collectFieldsImpl(
184186

185187
if (
186188
!newDeferUsage &&
187-
(visitedFragmentNames.has(fragName) ||
189+
(visitedFragmentKeys.has(fragmentKey) ||
188190
!shouldIncludeNode(variableValues, selection))
189191
) {
190192
continue;
191193
}
192194

193-
const fragment = fragments[fragName];
195+
const fragment = fragments[selection.name.value];
194196
if (
195197
fragment == null ||
196198
!doesFragmentConditionMatch(schema, fragment, runtimeType)
197199
) {
198200
continue;
199201
}
202+
200203
if (!newDeferUsage) {
201-
visitedFragmentNames.add(fragName);
204+
visitedFragmentKeys.add(fragmentKey);
202205
}
203206

207+
const fragmentSelectionSet = substituteFragmentArguments(
208+
fragment,
209+
selection,
210+
);
211+
204212
collectFieldsImpl(
205213
context,
206-
fragment.selectionSet,
214+
fragmentSelectionSet,
207215
groupedFieldSet,
208216
parentDeferUsage,
209217
newDeferUsage ?? deferUsage,

0 commit comments

Comments
 (0)