Skip to content

Commit 0432956

Browse files
Support returning async iterables from resolver functions (graphql#2712)
Co-authored-by: Ivan Goncharov <[email protected]>
1 parent 7e79bbe commit 0432956

File tree

2 files changed

+102
-1
lines changed

2 files changed

+102
-1
lines changed

src/execution/__tests__/lists-test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,47 @@ describe('Execute: Accepts any iterable as list value', () => {
6464
});
6565
});
6666

67+
describe('Execute: Accepts async iterables as list value', () => {
68+
function complete(rootValue: mixed) {
69+
return execute({
70+
schema: buildSchema('type Query { listField: [String] }'),
71+
document: parse('{ listField }'),
72+
rootValue,
73+
});
74+
}
75+
76+
it('Accepts an AsyncGenerator function as a List value', async () => {
77+
async function* listField() {
78+
yield await 'two';
79+
yield await 4;
80+
yield await false;
81+
}
82+
83+
expect(await complete({ listField })).to.deep.equal({
84+
data: { listField: ['two', '4', 'false'] },
85+
});
86+
});
87+
88+
it('Handles an AsyncGenerator function that throws', async () => {
89+
async function* listField() {
90+
yield await 'two';
91+
yield await 4;
92+
throw new Error('bad');
93+
}
94+
95+
expect(await complete({ listField })).to.deep.equal({
96+
data: { listField: ['two', '4', null] },
97+
errors: [
98+
{
99+
message: 'bad',
100+
locations: [{ line: 1, column: 3 }],
101+
path: ['listField', 2],
102+
},
103+
],
104+
});
105+
});
106+
});
107+
67108
describe('Execute: Handles list nullability', () => {
68109
async function complete(args: {| listField: mixed, as: string |}) {
69110
const { listField, as } = args;

src/execution/execute.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import arrayFrom from '../polyfills/arrayFrom';
2+
import { SYMBOL_ASYNC_ITERATOR } from '../polyfills/symbols';
23

34
import type { Path } from '../jsutils/Path';
45
import type { ObjMap } from '../jsutils/ObjMap';
@@ -8,6 +9,7 @@ import memoize3 from '../jsutils/memoize3';
89
import invariant from '../jsutils/invariant';
910
import devAssert from '../jsutils/devAssert';
1011
import isPromise from '../jsutils/isPromise';
12+
import isAsyncIterable from '../jsutils/isAsyncIterable';
1113
import isObjectLike from '../jsutils/isObjectLike';
1214
import isCollection from '../jsutils/isCollection';
1315
import promiseReduce from '../jsutils/promiseReduce';
@@ -855,6 +857,48 @@ function completeValue(
855857
);
856858
}
857859

860+
/**
861+
* Complete a async iterator value by completing the result and calling
862+
* recursively until all the results are completed.
863+
*/
864+
function completeAsyncIteratorValue(
865+
exeContext: ExecutionContext,
866+
itemType: GraphQLOutputType,
867+
fieldNodes: $ReadOnlyArray<FieldNode>,
868+
info: GraphQLResolveInfo,
869+
path: Path,
870+
index: number,
871+
completedResults: Array<mixed>,
872+
iterator: AsyncIterator<mixed>,
873+
): Promise<$ReadOnlyArray<mixed>> {
874+
const fieldPath = addPath(path, index, undefined);
875+
return iterator.next().then(
876+
({ value, done }) => {
877+
if (done) {
878+
return completedResults;
879+
}
880+
completedResults.push(
881+
completeValue(exeContext, itemType, fieldNodes, info, fieldPath, value),
882+
);
883+
return completeAsyncIteratorValue(
884+
exeContext,
885+
itemType,
886+
fieldNodes,
887+
info,
888+
path,
889+
index + 1,
890+
completedResults,
891+
iterator,
892+
);
893+
},
894+
(error) => {
895+
completedResults.push(null);
896+
handleFieldError(error, fieldNodes, fieldPath, itemType, exeContext);
897+
return completedResults;
898+
},
899+
);
900+
}
901+
858902
/**
859903
* Complete a list value by completing each item in the list with the
860904
* inner type
@@ -867,6 +911,23 @@ function completeListValue(
867911
path: Path,
868912
result: mixed,
869913
): PromiseOrValue<$ReadOnlyArray<mixed>> {
914+
const itemType = returnType.ofType;
915+
916+
if (isAsyncIterable(result)) {
917+
const iterator = result[SYMBOL_ASYNC_ITERATOR]();
918+
919+
return completeAsyncIteratorValue(
920+
exeContext,
921+
itemType,
922+
fieldNodes,
923+
info,
924+
path,
925+
0,
926+
[],
927+
iterator,
928+
);
929+
}
930+
870931
if (!isCollection(result)) {
871932
throw new GraphQLError(
872933
`Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`,
@@ -875,7 +936,6 @@ function completeListValue(
875936

876937
// This is specified as a simple map, however we're optimizing the path
877938
// where the list contains no Promises by avoiding creating another Promise.
878-
const itemType = returnType.ofType;
879939
let containsPromise = false;
880940
const completedResults = arrayFrom(result, (item, index) => {
881941
// No need to modify the info object containing the path,

0 commit comments

Comments
 (0)