Skip to content

Commit b9a2695

Browse files
authored
Filter subsequent payloads when parent field is null (#3720)
1 parent 80c44e5 commit b9a2695

File tree

2 files changed

+401
-43
lines changed

2 files changed

+401
-43
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 319 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,21 @@ const query = new GraphQLObjectType({
5858
scalarField: {
5959
type: GraphQLString,
6060
},
61+
nonNullScalarField: {
62+
type: new GraphQLNonNull(GraphQLString),
63+
},
6164
nestedFriendList: { type: new GraphQLList(friendType) },
65+
deeperNestedObject: {
66+
type: new GraphQLObjectType({
67+
name: 'DeeperNestedObject',
68+
fields: {
69+
nonNullScalarField: {
70+
type: new GraphQLNonNull(GraphQLString),
71+
},
72+
deeperNestedFriendList: { type: new GraphQLList(friendType) },
73+
},
74+
}),
75+
},
6276
},
6377
}),
6478
},
@@ -765,9 +779,15 @@ describe('Execute: stream directive', () => {
765779
`);
766780
const result = await complete(document, {
767781
async *nonNullFriendList() {
768-
yield await Promise.resolve(friends[0]);
769-
yield await Promise.resolve(null);
770-
yield await Promise.resolve(friends[1]);
782+
try {
783+
yield await Promise.resolve(friends[0]);
784+
yield await Promise.resolve(null); /* c8 ignore start */
785+
// Not reachable, early return
786+
} finally {
787+
/* c8 ignore stop */
788+
// eslint-disable-next-line no-unsafe-finally
789+
throw new Error('Oops');
790+
}
771791
},
772792
});
773793
expectJSON(result).toDeepEqual([
@@ -792,18 +812,6 @@ describe('Execute: stream directive', () => {
792812
],
793813
},
794814
],
795-
hasNext: true,
796-
},
797-
{
798-
incremental: [
799-
{
800-
items: [{ name: 'Han' }],
801-
path: ['nonNullFriendList', 2],
802-
},
803-
],
804-
hasNext: true,
805-
},
806-
{
807815
hasNext: false,
808816
},
809817
]);
@@ -886,15 +894,6 @@ describe('Execute: stream directive', () => {
886894
],
887895
},
888896
],
889-
hasNext: true,
890-
},
891-
{
892-
incremental: [
893-
{
894-
items: [{ nonNullName: 'Han' }],
895-
path: ['nonNullFriendList', 2],
896-
},
897-
],
898897
hasNext: false,
899898
},
900899
]);
@@ -953,6 +952,302 @@ describe('Execute: stream directive', () => {
953952
},
954953
]);
955954
});
955+
it('Filters payloads that are nulled', async () => {
956+
const document = parse(`
957+
query {
958+
nestedObject {
959+
nonNullScalarField
960+
nestedFriendList @stream(initialCount: 0) {
961+
name
962+
}
963+
}
964+
}
965+
`);
966+
const result = await complete(document, {
967+
nestedObject: {
968+
nonNullScalarField: () => Promise.resolve(null),
969+
async *nestedFriendList() {
970+
yield await Promise.resolve(friends[0]);
971+
},
972+
},
973+
});
974+
expectJSON(result).toDeepEqual({
975+
errors: [
976+
{
977+
message:
978+
'Cannot return null for non-nullable field NestedObject.nonNullScalarField.',
979+
locations: [{ line: 4, column: 11 }],
980+
path: ['nestedObject', 'nonNullScalarField'],
981+
},
982+
],
983+
data: {
984+
nestedObject: null,
985+
},
986+
});
987+
});
988+
it('Does not filter payloads when null error is in a different path', async () => {
989+
const document = parse(`
990+
query {
991+
otherNestedObject: nestedObject {
992+
... @defer {
993+
scalarField
994+
}
995+
}
996+
nestedObject {
997+
nestedFriendList @stream(initialCount: 0) {
998+
name
999+
}
1000+
}
1001+
}
1002+
`);
1003+
const result = await complete(document, {
1004+
nestedObject: {
1005+
scalarField: () => Promise.reject(new Error('Oops')),
1006+
async *nestedFriendList() {
1007+
yield await Promise.resolve(friends[0]);
1008+
},
1009+
},
1010+
});
1011+
expectJSON(result).toDeepEqual([
1012+
{
1013+
data: {
1014+
otherNestedObject: {},
1015+
nestedObject: { nestedFriendList: [] },
1016+
},
1017+
hasNext: true,
1018+
},
1019+
{
1020+
incremental: [
1021+
{
1022+
data: { scalarField: null },
1023+
path: ['otherNestedObject'],
1024+
errors: [
1025+
{
1026+
message: 'Oops',
1027+
locations: [{ line: 5, column: 13 }],
1028+
path: ['otherNestedObject', 'scalarField'],
1029+
},
1030+
],
1031+
},
1032+
{
1033+
items: [{ name: 'Luke' }],
1034+
path: ['nestedObject', 'nestedFriendList', 0],
1035+
},
1036+
],
1037+
hasNext: true,
1038+
},
1039+
{
1040+
hasNext: false,
1041+
},
1042+
]);
1043+
});
1044+
it('Filters stream payloads that are nulled in a deferred payload', async () => {
1045+
const document = parse(`
1046+
query {
1047+
nestedObject {
1048+
... @defer {
1049+
deeperNestedObject {
1050+
nonNullScalarField
1051+
deeperNestedFriendList @stream(initialCount: 0) {
1052+
name
1053+
}
1054+
}
1055+
}
1056+
}
1057+
}
1058+
`);
1059+
const result = await complete(document, {
1060+
nestedObject: {
1061+
deeperNestedObject: {
1062+
nonNullScalarField: () => Promise.resolve(null),
1063+
async *deeperNestedFriendList() {
1064+
yield await Promise.resolve(friends[0]);
1065+
},
1066+
},
1067+
},
1068+
});
1069+
expectJSON(result).toDeepEqual([
1070+
{
1071+
data: {
1072+
nestedObject: {},
1073+
},
1074+
hasNext: true,
1075+
},
1076+
{
1077+
incremental: [
1078+
{
1079+
data: {
1080+
deeperNestedObject: null,
1081+
},
1082+
path: ['nestedObject'],
1083+
errors: [
1084+
{
1085+
message:
1086+
'Cannot return null for non-nullable field DeeperNestedObject.nonNullScalarField.',
1087+
locations: [{ line: 6, column: 15 }],
1088+
path: [
1089+
'nestedObject',
1090+
'deeperNestedObject',
1091+
'nonNullScalarField',
1092+
],
1093+
},
1094+
],
1095+
},
1096+
],
1097+
hasNext: false,
1098+
},
1099+
]);
1100+
});
1101+
it('Filters defer payloads that are nulled in a stream response', async () => {
1102+
const document = parse(`
1103+
query {
1104+
friendList @stream(initialCount: 0) {
1105+
nonNullName
1106+
... @defer {
1107+
name
1108+
}
1109+
}
1110+
}
1111+
`);
1112+
const result = await complete(document, {
1113+
async *friendList() {
1114+
yield await Promise.resolve({
1115+
name: friends[0].name,
1116+
nonNullName: () => Promise.resolve(null),
1117+
});
1118+
},
1119+
});
1120+
expectJSON(result).toDeepEqual([
1121+
{
1122+
data: {
1123+
friendList: [],
1124+
},
1125+
hasNext: true,
1126+
},
1127+
{
1128+
incremental: [
1129+
{
1130+
items: [null],
1131+
path: ['friendList', 0],
1132+
errors: [
1133+
{
1134+
message:
1135+
'Cannot return null for non-nullable field Friend.nonNullName.',
1136+
locations: [{ line: 4, column: 9 }],
1137+
path: ['friendList', 0, 'nonNullName'],
1138+
},
1139+
],
1140+
},
1141+
],
1142+
hasNext: true,
1143+
},
1144+
{
1145+
hasNext: false,
1146+
},
1147+
]);
1148+
});
1149+
1150+
it('Returns iterator and ignores errors when stream payloads are filtered', async () => {
1151+
let returned = false;
1152+
let index = 0;
1153+
const iterable = {
1154+
[Symbol.asyncIterator]: () => ({
1155+
next: () => {
1156+
const friend = friends[index++];
1157+
if (!friend) {
1158+
return Promise.resolve({ done: true, value: undefined });
1159+
}
1160+
return Promise.resolve({
1161+
done: false,
1162+
value: {
1163+
name: friend.name,
1164+
nonNullName: null,
1165+
},
1166+
});
1167+
},
1168+
return: () => {
1169+
returned = true;
1170+
return Promise.reject(new Error('Oops'));
1171+
},
1172+
}),
1173+
};
1174+
1175+
const document = parse(`
1176+
query {
1177+
nestedObject {
1178+
... @defer {
1179+
deeperNestedObject {
1180+
nonNullScalarField
1181+
deeperNestedFriendList @stream(initialCount: 0) {
1182+
name
1183+
}
1184+
}
1185+
}
1186+
}
1187+
}
1188+
`);
1189+
1190+
const executeResult = await experimentalExecuteIncrementally({
1191+
schema,
1192+
document,
1193+
rootValue: {
1194+
nestedObject: {
1195+
deeperNestedObject: {
1196+
nonNullScalarField: () => Promise.resolve(null),
1197+
deeperNestedFriendList: iterable,
1198+
},
1199+
},
1200+
},
1201+
});
1202+
assert('initialResult' in executeResult);
1203+
const iterator = executeResult.subsequentResults[Symbol.asyncIterator]();
1204+
1205+
const result1 = executeResult.initialResult;
1206+
expectJSON(result1).toDeepEqual({
1207+
data: {
1208+
nestedObject: {},
1209+
},
1210+
hasNext: true,
1211+
});
1212+
1213+
const result2 = await iterator.next();
1214+
expectJSON(result2).toDeepEqual({
1215+
done: false,
1216+
value: {
1217+
incremental: [
1218+
{
1219+
data: {
1220+
deeperNestedObject: null,
1221+
},
1222+
path: ['nestedObject'],
1223+
errors: [
1224+
{
1225+
message:
1226+
'Cannot return null for non-nullable field DeeperNestedObject.nonNullScalarField.',
1227+
locations: [{ line: 6, column: 15 }],
1228+
path: [
1229+
'nestedObject',
1230+
'deeperNestedObject',
1231+
'nonNullScalarField',
1232+
],
1233+
},
1234+
],
1235+
},
1236+
],
1237+
hasNext: true,
1238+
},
1239+
});
1240+
const result3 = await iterator.next();
1241+
expectJSON(result3).toDeepEqual({
1242+
done: false,
1243+
value: { hasNext: false },
1244+
});
1245+
1246+
const result4 = await iterator.next();
1247+
expectJSON(result4).toDeepEqual({ done: true, value: undefined });
1248+
1249+
assert(returned);
1250+
});
9561251
it('Handles promises returned by completeValue after initialCount is reached', async () => {
9571252
const document = parse(`
9581253
query {

0 commit comments

Comments
 (0)