Skip to content

Commit 0005227

Browse files
committed
convert completeValue to completeNonLeafValue
1 parent e40e016 commit 0005227

File tree

2 files changed

+228
-52
lines changed

2 files changed

+228
-52
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,35 @@ describe('Execute: stream directive', () => {
151151
},
152152
]);
153153
});
154+
it('Can stream a list field that returns an async iterable', async () => {
155+
const document = parse('{ scalarList @stream(initialCount: 1) }');
156+
const result = await complete(document, {
157+
async *scalarList() {
158+
yield await Promise.resolve('apple');
159+
yield await Promise.resolve('banana');
160+
yield await Promise.resolve('coconut');
161+
},
162+
});
163+
expectJSON(result).toDeepEqual([
164+
{
165+
data: {
166+
scalarList: ['apple'],
167+
},
168+
hasNext: true,
169+
},
170+
{
171+
incremental: [{ items: ['banana'], path: ['scalarList', 1] }],
172+
hasNext: true,
173+
},
174+
{
175+
incremental: [{ items: ['coconut'], path: ['scalarList', 2] }],
176+
hasNext: true,
177+
},
178+
{
179+
hasNext: false,
180+
},
181+
]);
182+
});
154183
it('Can use default value of initialCount', async () => {
155184
const document = parse('{ scalarList @stream }');
156185
const result = await complete(document, {
@@ -536,7 +565,7 @@ describe('Execute: stream directive', () => {
536565
},
537566
]);
538567
});
539-
it('Can stream a field that returns an async iterable', async () => {
568+
it('Can stream an object field that returns an async iterable', async () => {
540569
const document = parse(`
541570
query {
542571
friendList @stream {
@@ -770,6 +799,71 @@ describe('Execute: stream directive', () => {
770799
},
771800
]);
772801
});
802+
it('Handles null returned in list items after initialCount is reached', async () => {
803+
const document = parse(`
804+
query {
805+
friendList @stream(initialCount: 1) {
806+
name
807+
}
808+
}
809+
`);
810+
const result = await complete(document, {
811+
friendList: () => [friends[0], null],
812+
});
813+
814+
expectJSON(result).toDeepEqual([
815+
{
816+
data: {
817+
friendList: [{ name: 'Luke' }],
818+
},
819+
hasNext: true,
820+
},
821+
{
822+
incremental: [
823+
{
824+
items: [null],
825+
path: ['friendList', 1],
826+
},
827+
],
828+
hasNext: false,
829+
},
830+
]);
831+
});
832+
it('Handles null returned in async iterable list items after initialCount is reached', async () => {
833+
const document = parse(`
834+
query {
835+
friendList @stream(initialCount: 1) {
836+
name
837+
}
838+
}
839+
`);
840+
const result = await complete(document, {
841+
async *friendList() {
842+
yield await Promise.resolve(friends[0]);
843+
yield await Promise.resolve(null);
844+
},
845+
});
846+
expectJSON(result).toDeepEqual([
847+
{
848+
data: {
849+
friendList: [{ name: 'Luke' }],
850+
},
851+
hasNext: true,
852+
},
853+
{
854+
incremental: [
855+
{
856+
items: [null],
857+
path: ['friendList', 1],
858+
},
859+
],
860+
hasNext: true,
861+
},
862+
{
863+
hasNext: false,
864+
},
865+
]);
866+
});
773867
it('Handles null returned in non-null list items after initialCount is reached', async () => {
774868
const document = parse(`
775869
query {

src/execution/execute.ts

Lines changed: 133 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -641,8 +641,9 @@ function executeFields(
641641
/**
642642
* Implements the "Executing fields" section of the spec
643643
* In particular, this function figures out the value that the field returns by
644-
* calling its resolve function, then calls completeValue to complete promises,
645-
* serialize scalars, or execute the sub-selection-set for objects.
644+
* calling its resolve function, checks for promises, and then serializes leaf
645+
* values or calls completeNonLeafValue to execute the sub-selection-set for
646+
* objects and/or complete lists as necessary.
646647
*/
647648
function executeField(
648649
exeContext: ExecutionContext,
@@ -699,9 +700,32 @@ function executeField(
699700
);
700701
}
701702

702-
const completed = completeValue(
703+
if (result instanceof Error) {
704+
throw result;
705+
}
706+
707+
let nullableType: GraphQLNullableOutputType;
708+
if (isNonNullType(returnType)) {
709+
if (result == null) {
710+
throw new Error(
711+
`Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`,
712+
);
713+
}
714+
nullableType = returnType.ofType;
715+
} else {
716+
if (result == null) {
717+
return null;
718+
}
719+
nullableType = returnType;
720+
}
721+
722+
if (isLeafType(nullableType)) {
723+
return completeLeafValue(nullableType, result);
724+
}
725+
726+
const completed = completeNonLeafValue(
703727
exeContext,
704-
returnType,
728+
nullableType,
705729
fieldGroup,
706730
info,
707731
path,
@@ -789,57 +813,27 @@ function handleFieldError(
789813
}
790814

791815
/**
792-
* Implements the instructions for completeValue as defined in the
816+
* Implements the instructions for completing non-leaf values as defined in the
793817
* "Value Completion" section of the spec.
794818
*
795-
* If the field type is Non-Null, then this recursively completes the value
796-
* for the inner type. It throws a field error if that completion returns null,
797-
* as per the "Nullability" section of the spec.
798-
*
799819
* If the field type is a List, then this recursively completes the value
800820
* for the inner type on each item in the list.
801821
*
802-
* If the field type is a Scalar or Enum, ensures the completed value is a legal
803-
* value of the type by calling the `serialize` method of GraphQL type
804-
* definition.
805-
*
806822
* If the field is an abstract type, determine the runtime type of the value
807823
* and then complete based on that type
808824
*
809825
* Otherwise, the field type expects a sub-selection set, and will complete the
810826
* value by executing all sub-selections.
811827
*/
812-
function completeValue(
828+
function completeNonLeafValue(
813829
exeContext: ExecutionContext,
814-
returnType: GraphQLOutputType,
830+
nullableType: GraphQLNullableOutputType,
815831
fieldGroup: FieldGroup,
816832
info: GraphQLResolveInfo,
817833
path: Path,
818834
result: unknown,
819835
incrementalDataRecord: IncrementalDataRecord,
820836
): PromiseOrValue<unknown> {
821-
// If result is an Error, throw a located error.
822-
if (result instanceof Error) {
823-
throw result;
824-
}
825-
826-
let nullableType: GraphQLNullableOutputType;
827-
if (isNonNullType(returnType)) {
828-
// If result value is null or undefined then throw an error.
829-
if (result == null) {
830-
throw new Error(
831-
`Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`,
832-
);
833-
}
834-
nullableType = returnType.ofType;
835-
} else {
836-
// If result value is null or undefined then return null.
837-
if (result == null) {
838-
return null;
839-
}
840-
nullableType = returnType;
841-
}
842-
843837
// If field type is List, complete each item in the list with the inner type
844838
if (isListType(nullableType)) {
845839
return completeListValue(
@@ -853,12 +847,6 @@ function completeValue(
853847
);
854848
}
855849

856-
// If field type is a leaf type, Scalar or Enum, serialize to a valid value,
857-
// returning null if serialization is not possible.
858-
if (isLeafType(nullableType)) {
859-
return completeLeafValue(nullableType, result);
860-
}
861-
862850
// If field type is an abstract type, Interface or Union, determine the
863851
// runtime Object type and complete for that type.
864852
if (isAbstractType(nullableType)) {
@@ -904,9 +892,33 @@ async function completePromisedValue(
904892
): Promise<unknown> {
905893
try {
906894
const resolved = await result;
907-
let completed = completeValue(
895+
896+
if (resolved instanceof Error) {
897+
throw resolved;
898+
}
899+
900+
let nullableType: GraphQLNullableOutputType;
901+
if (isNonNullType(returnType)) {
902+
if (resolved == null) {
903+
throw new Error(
904+
`Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`,
905+
);
906+
}
907+
nullableType = returnType.ofType;
908+
} else {
909+
if (resolved == null) {
910+
return null;
911+
}
912+
nullableType = returnType;
913+
}
914+
915+
if (isLeafType(nullableType)) {
916+
return completeLeafValue(nullableType, resolved);
917+
}
918+
919+
let completed = completeNonLeafValue(
908920
exeContext,
909-
returnType,
921+
nullableType,
910922
fieldGroup,
911923
info,
912924
path,
@@ -1179,9 +1191,34 @@ function completeListItemValue(
11791191
}
11801192

11811193
try {
1182-
const completedItem = completeValue(
1194+
if (item instanceof Error) {
1195+
throw item;
1196+
}
1197+
1198+
let nullableType: GraphQLNullableOutputType;
1199+
if (isNonNullType(itemType)) {
1200+
if (item == null) {
1201+
throw new Error(
1202+
`Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`,
1203+
);
1204+
}
1205+
nullableType = itemType.ofType;
1206+
} else {
1207+
if (item == null) {
1208+
completedResults.push(null);
1209+
return false;
1210+
}
1211+
nullableType = itemType;
1212+
}
1213+
1214+
if (isLeafType(nullableType)) {
1215+
completedResults.push(completeLeafValue(nullableType, item));
1216+
return false;
1217+
}
1218+
1219+
const completedItem = completeNonLeafValue(
11831220
exeContext,
1184-
itemType,
1221+
nullableType,
11851222
fieldGroup,
11861223
info,
11871224
itemPath,
@@ -1852,9 +1889,35 @@ function executeStreamField(
18521889
let completedItem: PromiseOrValue<unknown>;
18531890
try {
18541891
try {
1855-
completedItem = completeValue(
1892+
let nullableType: GraphQLNullableOutputType;
1893+
if (isNonNullType(itemType)) {
1894+
if (item == null) {
1895+
throw new Error(
1896+
`Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`,
1897+
);
1898+
}
1899+
nullableType = itemType.ofType;
1900+
} else {
1901+
if (item == null) {
1902+
incrementalPublisher.completeStreamItemsRecord(
1903+
incrementalDataRecord,
1904+
[null],
1905+
);
1906+
return incrementalDataRecord;
1907+
}
1908+
nullableType = itemType;
1909+
}
1910+
1911+
if (isLeafType(nullableType)) {
1912+
incrementalPublisher.completeStreamItemsRecord(incrementalDataRecord, [
1913+
completeLeafValue(nullableType, item),
1914+
]);
1915+
return incrementalDataRecord;
1916+
}
1917+
1918+
completedItem = completeNonLeafValue(
18561919
exeContext,
1857-
itemType,
1920+
nullableType,
18581921
fieldGroup,
18591922
info,
18601923
itemPath,
@@ -1945,9 +2008,28 @@ async function executeStreamAsyncIteratorItem(
19452008
}
19462009
let completedItem;
19472010
try {
1948-
completedItem = completeValue(
2011+
let nullableType: GraphQLNullableOutputType;
2012+
if (isNonNullType(itemType)) {
2013+
if (item == null) {
2014+
throw new Error(
2015+
`Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`,
2016+
);
2017+
}
2018+
nullableType = itemType.ofType;
2019+
} else {
2020+
if (item == null) {
2021+
return { done: false, value: null };
2022+
}
2023+
nullableType = itemType;
2024+
}
2025+
2026+
if (isLeafType(nullableType)) {
2027+
return { done: false, value: completeLeafValue(nullableType, item) };
2028+
}
2029+
2030+
completedItem = completeNonLeafValue(
19492031
exeContext,
1950-
itemType,
2032+
nullableType,
19512033
fieldGroup,
19522034
info,
19532035
itemPath,

0 commit comments

Comments
 (0)