Skip to content

Commit 03f06e9

Browse files
authored
[RFC] Support iterable values as inputs and outputs (#449)
This adds support for returning any Iterable from a resolver function expecting a List type, rather than only Arrays, by using the `iterall` library. This also adds support for accepting any Iterable as input for arguments or variable values which expect a List type.
1 parent fb8ed67 commit 03f06e9

File tree

7 files changed

+73
-15
lines changed

7 files changed

+73
-15
lines changed

.flowconfig

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
.*/dist/.*
44
.*/coverage/.*
55
.*/resources/.*
6-
.*/node_modules/.*
76

87
[include]
98

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
"preversion": ". ./resources/checkgit.sh && npm test",
3535
"prepublish": ". ./resources/prepublish.sh"
3636
},
37+
"dependencies": {
38+
"iterall": "1.0.2"
39+
},
3740
"devDependencies": {
3841
"babel-cli": "6.10.1",
3942
"babel-eslint": "6.1.0",

src/execution/__tests__/lists-test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { parse } from '../../language';
1818
import {
1919
GraphQLSchema,
2020
GraphQLObjectType,
21+
GraphQLString,
2122
GraphQLInt,
2223
GraphQLList,
2324
GraphQLNonNull
@@ -67,6 +68,48 @@ function check(testType, testData, expected) {
6768
};
6869
}
6970

71+
describe('Execute: Accepts any iterable as list value', () => {
72+
73+
it('Accepts a Set as a List value', check(
74+
new GraphQLList(GraphQLString),
75+
new Set([ 'apple', 'banana', 'apple', 'coconut' ]),
76+
{ data: { nest: { test: [ 'apple', 'banana', 'coconut' ] } } }
77+
));
78+
79+
function *yieldItems() {
80+
yield 'one';
81+
yield 2;
82+
yield true;
83+
}
84+
85+
it('Accepts an Generator function as a List value', check(
86+
new GraphQLList(GraphQLString),
87+
yieldItems(),
88+
{ data: { nest: { test: [ 'one', '2', 'true' ] } } }
89+
));
90+
91+
function getArgs() {
92+
return arguments;
93+
}
94+
95+
it('Accepts function arguments as a List value', check(
96+
new GraphQLList(GraphQLString),
97+
getArgs('one', 'two'),
98+
{ data: { nest: { test: [ 'one', 'two' ] } } }
99+
));
100+
101+
it('Does not accept (Iterable) String-literal as a List value', check(
102+
new GraphQLList(GraphQLString),
103+
'Singluar',
104+
{ data: { nest: { test: null } },
105+
errors: [ {
106+
message: 'Expected Iterable, but did not find one for field DataType.test.',
107+
locations: [ { line: 1, column: 10 } ]
108+
} ] }
109+
));
110+
111+
});
112+
70113
describe('Execute: Handles list nullability', () => {
71114

72115
describe('[T]', () => {

src/execution/execute.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

11+
import { forEach, isCollection } from 'iterall';
12+
1113
import { GraphQLError, locatedError } from '../error';
1214
import find from '../jsutils/find';
1315
import invariant from '../jsutils/invariant';
@@ -829,16 +831,17 @@ function completeListValue(
829831
result: mixed
830832
): mixed {
831833
invariant(
832-
Array.isArray(result),
833-
`User Error: expected iterable, but did not find one for field ${
834+
isCollection(result),
835+
`Expected Iterable, but did not find one for field ${
834836
info.parentType.name}.${info.fieldName}.`
835837
);
836838

837839
// This is specified as a simple map, however we're optimizing the path
838840
// where the list contains no Promises by avoiding creating another Promise.
839841
const itemType = returnType.ofType;
840842
let containsPromise = false;
841-
const completedResults = result.map((item, index) => {
843+
const completedResults = [];
844+
forEach((result: any), (item, index) => {
842845
// No need to modify the info object containing the path,
843846
// since from here on it is not ever accessed by resolver functions.
844847
const fieldPath = path.concat([ index ]);
@@ -854,7 +857,7 @@ function completeListValue(
854857
if (!containsPromise && isThenable(completedItem)) {
855858
containsPromise = true;
856859
}
857-
return completedItem;
860+
completedResults.push(completedItem);
858861
});
859862

860863
return containsPromise ? Promise.all(completedResults) : completedResults;

src/execution/values.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

11+
import { forEach, isCollection } from 'iterall';
12+
1113
import { GraphQLError } from '../error';
1214
import invariant from '../jsutils/invariant';
1315
import isNullish from '../jsutils/isNullish';
@@ -138,9 +140,12 @@ function coerceValue(type: GraphQLInputType, value: mixed): mixed {
138140

139141
if (type instanceof GraphQLList) {
140142
const itemType = type.ofType;
141-
// TODO: support iterable input
142-
if (Array.isArray(_value)) {
143-
return _value.map(item => coerceValue(itemType, item));
143+
if (isCollection(_value)) {
144+
const coercedValues = [];
145+
forEach((_value: any), item => {
146+
coercedValues.push(coerceValue(itemType, item));
147+
});
148+
return coercedValues;
144149
}
145150
return [ coerceValue(itemType, _value) ];
146151
}

src/utilities/astFromValue.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

11+
import { forEach, isCollection } from 'iterall';
12+
1113
import invariant from '../jsutils/invariant';
1214
import isNullish from '../jsutils/isNullish';
1315
import type {
@@ -79,9 +81,9 @@ export function astFromValue(
7981
// the value is not an array, convert the value using the list's item type.
8082
if (type instanceof GraphQLList) {
8183
const itemType = type.ofType;
82-
if (Array.isArray(_value)) {
84+
if (isCollection(_value)) {
8385
const valuesASTs = [];
84-
_value.forEach(item => {
86+
forEach((_value: any), item => {
8587
const itemAST = astFromValue(item, itemType);
8688
if (itemAST) {
8789
valuesASTs.push(itemAST);

src/utilities/isValidJSValue.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

11+
import { forEach, isCollection } from 'iterall';
12+
1113
import invariant from '../jsutils/invariant';
1214
import isNullish from '../jsutils/isNullish';
1315
import {
@@ -44,13 +46,14 @@ export function isValidJSValue(value: mixed, type: GraphQLInputType): [string] {
4446
// Lists accept a non-list value as a list of one.
4547
if (type instanceof GraphQLList) {
4648
const itemType = type.ofType;
47-
if (Array.isArray(value)) {
48-
return value.reduce((acc, item, index) => {
49-
const errors = isValidJSValue(item, itemType);
50-
return acc.concat(errors.map(error =>
49+
if (isCollection(value)) {
50+
const errors = [];
51+
forEach((value: any), (item, index) => {
52+
errors.push.apply(errors, isValidJSValue(item, itemType).map(error =>
5153
`In element #${index}: ${error}`
5254
));
53-
}, []);
55+
});
56+
return errors;
5457
}
5558
return isValidJSValue(value, itemType);
5659
}

0 commit comments

Comments
 (0)