Skip to content

Commit e33eead

Browse files
implement execution for fragment arguments syntax
Co-authored-by: mjmahone <[email protected]>
1 parent e2d8d2d commit e33eead

File tree

4 files changed

+379
-9
lines changed

4 files changed

+379
-9
lines changed

src/execution/__tests__/variables-test.ts

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

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)