Skip to content

Commit 03cc5dc

Browse files
committed
fix(incremental): emit only single completion when multiple deferred grouped field sets error
1 parent 15ab731 commit 03cc5dc

File tree

3 files changed

+162
-8
lines changed

3 files changed

+162
-8
lines changed

src/execution/IncrementalGraph.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,14 +217,16 @@ export class IncrementalGraph {
217217
return reconcilableResults;
218218
}
219219

220-
removeDeferredFragment(deferredFragmentRecord: DeferredFragmentRecord): void {
220+
removeDeferredFragment(
221+
deferredFragmentRecord: DeferredFragmentRecord,
222+
): boolean {
221223
const deferredFragmentNode = this._deferredFragmentNodes.get(
222224
deferredFragmentRecord,
223225
);
224226
// TODO: add test case?
225227
/* c8 ignore next 3 */
226228
if (deferredFragmentNode === undefined) {
227-
return;
229+
return false;
228230
}
229231
this._removePending(deferredFragmentNode);
230232
this._deferredFragmentNodes.delete(deferredFragmentRecord);
@@ -233,6 +235,7 @@ export class IncrementalGraph {
233235
for (const child of deferredFragmentNode.children) {
234236
this.removeDeferredFragment(child.deferredFragmentRecord);
235237
}
238+
return true;
236239
}
237240

238241
removeStream(streamRecord: StreamRecord): void {

src/execution/IncrementalPublisher.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,18 @@ class IncrementalPublisher {
233233
for (const deferredFragmentRecord of deferredGroupedFieldSetResult
234234
.deferredGroupedFieldSetRecord.deferredFragmentRecords) {
235235
const id = deferredFragmentRecord.id;
236-
if (id !== undefined) {
237-
context.completed.push({
238-
id,
239-
errors: deferredGroupedFieldSetResult.errors,
240-
});
241-
this._incrementalGraph.removeDeferredFragment(deferredFragmentRecord);
236+
if (
237+
!this._incrementalGraph.removeDeferredFragment(deferredFragmentRecord)
238+
) {
239+
// This can occur if multiple deferred grouped field sets error for a fragment.
240+
continue;
242241
}
242+
invariant(id !== undefined);
243+
context.completed.push({
244+
id,
245+
errors: deferredGroupedFieldSetResult.errors,
246+
});
247+
this._incrementalGraph.removeDeferredFragment(deferredFragmentRecord);
243248
}
244249
return;
245250
}

src/execution/__tests__/defer-test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,6 +1334,152 @@ describe('Execute: defer directive', () => {
13341334
]);
13351335
});
13361336

1337+
it('Handles multiple erroring deferred grouped field sets', async () => {
1338+
const document = parse(`
1339+
query {
1340+
... @defer {
1341+
a {
1342+
b {
1343+
c {
1344+
someError: nonNullErrorField
1345+
}
1346+
}
1347+
}
1348+
}
1349+
... @defer {
1350+
a {
1351+
b {
1352+
c {
1353+
anotherError: nonNullErrorField
1354+
}
1355+
}
1356+
}
1357+
}
1358+
}
1359+
`);
1360+
const result = await complete(document, {
1361+
a: {
1362+
b: { c: { nonNullErrorField: null } },
1363+
},
1364+
});
1365+
expectJSON(result).toDeepEqual([
1366+
{
1367+
data: {},
1368+
pending: [
1369+
{ id: '0', path: [] },
1370+
{ id: '1', path: [] },
1371+
],
1372+
hasNext: true,
1373+
},
1374+
{
1375+
completed: [
1376+
{
1377+
id: '0',
1378+
errors: [
1379+
{
1380+
message:
1381+
'Cannot return null for non-nullable field c.nonNullErrorField.',
1382+
locations: [{ line: 7, column: 17 }],
1383+
path: ['a', 'b', 'c', 'someError'],
1384+
},
1385+
],
1386+
},
1387+
{
1388+
id: '1',
1389+
errors: [
1390+
{
1391+
message:
1392+
'Cannot return null for non-nullable field c.nonNullErrorField.',
1393+
locations: [{ line: 16, column: 17 }],
1394+
path: ['a', 'b', 'c', 'anotherError'],
1395+
},
1396+
],
1397+
},
1398+
],
1399+
hasNext: false,
1400+
},
1401+
]);
1402+
});
1403+
1404+
it('Handles multiple erroring deferred grouped field sets for the same fragment', async () => {
1405+
const document = parse(`
1406+
query {
1407+
... @defer {
1408+
a {
1409+
b {
1410+
someC: c {
1411+
d: d
1412+
}
1413+
anotherC: c {
1414+
d: d
1415+
}
1416+
}
1417+
}
1418+
}
1419+
... @defer {
1420+
a {
1421+
b {
1422+
someC: c {
1423+
someError: nonNullErrorField
1424+
}
1425+
anotherC: c {
1426+
anotherError: nonNullErrorField
1427+
}
1428+
}
1429+
}
1430+
}
1431+
}
1432+
`);
1433+
const result = await complete(document, {
1434+
a: {
1435+
b: { c: { d: 'd', nonNullErrorField: null } },
1436+
},
1437+
});
1438+
expectJSON(result).toDeepEqual([
1439+
{
1440+
data: {},
1441+
pending: [
1442+
{ id: '0', path: [] },
1443+
{ id: '1', path: [] },
1444+
],
1445+
hasNext: true,
1446+
},
1447+
{
1448+
incremental: [
1449+
{
1450+
data: { a: { b: { someC: {}, anotherC: {} } } },
1451+
id: '0',
1452+
},
1453+
{
1454+
data: { d: 'd' },
1455+
id: '0',
1456+
subPath: ['a', 'b', 'someC'],
1457+
},
1458+
{
1459+
data: { d: 'd' },
1460+
id: '0',
1461+
subPath: ['a', 'b', 'anotherC'],
1462+
},
1463+
],
1464+
completed: [
1465+
{
1466+
id: '1',
1467+
errors: [
1468+
{
1469+
message:
1470+
'Cannot return null for non-nullable field c.nonNullErrorField.',
1471+
locations: [{ line: 19, column: 17 }],
1472+
path: ['a', 'b', 'someC', 'someError'],
1473+
},
1474+
],
1475+
},
1476+
{ id: '0' },
1477+
],
1478+
hasNext: false,
1479+
},
1480+
]);
1481+
});
1482+
13371483
it('filters a payload with a null that cannot be merged', async () => {
13381484
const document = parse(`
13391485
query {

0 commit comments

Comments
 (0)