Skip to content

Commit 31e8484

Browse files
Drop support for non-iteratable ArrayLike objects (#2917)
1 parent 33b14c5 commit 31e8484

File tree

8 files changed

+119
-117
lines changed

8 files changed

+119
-117
lines changed

src/execution/execute.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { invariant } from '../jsutils/invariant';
77
import { devAssert } from '../jsutils/devAssert';
88
import { isPromise } from '../jsutils/isPromise';
99
import { isObjectLike } from '../jsutils/isObjectLike';
10-
import { isCollection } from '../jsutils/isCollection';
1110
import { promiseReduce } from '../jsutils/promiseReduce';
1211
import { promiseForObject } from '../jsutils/promiseForObject';
1312
import { addPath, pathToArray } from '../jsutils/Path';
13+
import { isIteratableObject } from '../jsutils/isIteratableObject';
1414

1515
import type { GraphQLFormattedError } from '../error/formatError';
1616
import { GraphQLError } from '../error/GraphQLError';
@@ -821,7 +821,7 @@ function completeListValue(
821821
path: Path,
822822
result: mixed,
823823
): PromiseOrValue<$ReadOnlyArray<mixed>> {
824-
if (!isCollection(result)) {
824+
if (!isIteratableObject(result)) {
825825
throw new GraphQLError(
826826
`Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`,
827827
);

src/jsutils/__tests__/isCollection-test.js

Lines changed: 0 additions & 71 deletions
This file was deleted.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { identityFunc } from '../identityFunc';
5+
import { isIteratableObject } from '../isIteratableObject';
6+
7+
describe('isIteratableObject', () => {
8+
it('should return `true` for collections', () => {
9+
expect(isIteratableObject([])).to.equal(true);
10+
expect(isIteratableObject(new Int8Array(1))).to.equal(true);
11+
12+
// eslint-disable-next-line no-new-wrappers
13+
expect(isIteratableObject(new String('ABC'))).to.equal(true);
14+
15+
function getArguments() {
16+
return arguments;
17+
}
18+
expect(isIteratableObject(getArguments())).to.equal(true);
19+
20+
const iterator = { [Symbol.iterator]: identityFunc };
21+
expect(isIteratableObject(iterator)).to.equal(true);
22+
23+
// istanbul ignore next (Never called and use just as a placeholder)
24+
function* generatorFunc() {
25+
/* do nothing */
26+
}
27+
expect(isIteratableObject(generatorFunc())).to.equal(true);
28+
29+
// But generator function itself is not iteratable
30+
expect(isIteratableObject(generatorFunc)).to.equal(false);
31+
});
32+
33+
it('should return `false` for non-collections', () => {
34+
expect(isIteratableObject(null)).to.equal(false);
35+
expect(isIteratableObject(undefined)).to.equal(false);
36+
37+
expect(isIteratableObject('ABC')).to.equal(false);
38+
expect(isIteratableObject('0')).to.equal(false);
39+
expect(isIteratableObject('')).to.equal(false);
40+
41+
expect(isIteratableObject(1)).to.equal(false);
42+
expect(isIteratableObject(0)).to.equal(false);
43+
expect(isIteratableObject(NaN)).to.equal(false);
44+
// eslint-disable-next-line no-new-wrappers
45+
expect(isIteratableObject(new Number(123))).to.equal(false);
46+
47+
expect(isIteratableObject(true)).to.equal(false);
48+
expect(isIteratableObject(false)).to.equal(false);
49+
// eslint-disable-next-line no-new-wrappers
50+
expect(isIteratableObject(new Boolean(true))).to.equal(false);
51+
52+
expect(isIteratableObject({})).to.equal(false);
53+
expect(isIteratableObject({ iterable: true })).to.equal(false);
54+
55+
const iteratorWithoutSymbol = { next: identityFunc };
56+
expect(isIteratableObject(iteratorWithoutSymbol)).to.equal(false);
57+
58+
const invalidIteratable = {
59+
[Symbol.iterator]: { next: identityFunc },
60+
};
61+
expect(isIteratableObject(invalidIteratable)).to.equal(false);
62+
63+
const arrayLike = {};
64+
arrayLike[0] = 'Alpha';
65+
arrayLike[1] = 'Bravo';
66+
arrayLike[2] = 'Charlie';
67+
arrayLike.length = 3;
68+
69+
expect(isIteratableObject(arrayLike)).to.equal(false);
70+
});
71+
});

src/jsutils/isCollection.js

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/jsutils/isIteratableObject.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Returns true if the provided object is an Object (i.e. not a string literal)
3+
* and implements the Iterator protocol.
4+
*
5+
* This may be used in place of [Array.isArray()][isArray] to determine if
6+
* an object should be iterated-over e.g. Array, Map, Set, Int8Array,
7+
* TypedArray, etc. but excludes string literals.
8+
*
9+
* @example
10+
*
11+
* isIteratableObject([ 1, 2, 3 ]) // true
12+
* isIteratableObject(new Map()) // true
13+
* isIteratableObject('ABC') // false
14+
* isIteratableObject({ key: 'value' }) // false
15+
* isIteratableObject({ length: 1, 0: 'Alpha' }) // false
16+
*/
17+
declare function isIteratableObject(
18+
value: mixed,
19+
): boolean %checks(value instanceof Iterable);
20+
21+
// eslint-disable-next-line no-redeclare
22+
export function isIteratableObject(maybeIteratable: mixed): boolean {
23+
return (
24+
typeof maybeIteratable === 'object' &&
25+
typeof maybeIteratable?.[Symbol.iterator] === 'function'
26+
);
27+
}

src/utilities/__tests__/coerceInputValue-test.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { GraphQLInt } from '../../type/scalars';
88
import {
99
GraphQLList,
1010
GraphQLNonNull,
11-
GraphQLScalarType,
1211
GraphQLEnumType,
12+
GraphQLScalarType,
1313
GraphQLInputObjectType,
1414
} from '../../type/definition';
1515

@@ -335,6 +335,20 @@ describe('coerceInputValue', () => {
335335
expectValue(result).to.deep.equal([42]);
336336
});
337337

338+
it('returns a list for a non-list object value', () => {
339+
const TestListOfObjects = new GraphQLList(
340+
new GraphQLInputObjectType({
341+
name: 'TestObject',
342+
fields: {
343+
length: { type: GraphQLInt },
344+
},
345+
}),
346+
);
347+
348+
const result = coerceValue({ length: 100500 }, TestListOfObjects);
349+
expectValue(result).to.deep.equal([{ length: 100500 }]);
350+
});
351+
338352
it('returns an error for a non-list invalid value', () => {
339353
const result = coerceValue('INVALID', TestList);
340354
expectErrors(result).to.deep.equal([

src/utilities/astFromValue.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { objectValues } from '../polyfills/objectValues';
33
import { inspect } from '../jsutils/inspect';
44
import { invariant } from '../jsutils/invariant';
55
import { isObjectLike } from '../jsutils/isObjectLike';
6-
import { isCollection } from '../jsutils/isCollection';
6+
import { isIteratableObject } from '../jsutils/isIteratableObject';
77

88
import type { ValueNode } from '../language/ast';
99
import { Kind } from '../language/kinds';
@@ -62,7 +62,7 @@ export function astFromValue(value: mixed, type: GraphQLInputType): ?ValueNode {
6262
// the value is not an array, convert the value using the list's item type.
6363
if (isListType(type)) {
6464
const itemType = type.ofType;
65-
if (isCollection(value)) {
65+
if (isIteratableObject(value)) {
6666
const valuesNodes = [];
6767
// Since we transpile for-of in loose mode it doesn't support iterators
6868
// and it's required to first convert iteratable into array

src/utilities/coerceInputValue.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { inspect } from '../jsutils/inspect';
55
import { invariant } from '../jsutils/invariant';
66
import { didYouMean } from '../jsutils/didYouMean';
77
import { isObjectLike } from '../jsutils/isObjectLike';
8-
import { isCollection } from '../jsutils/isCollection';
98
import { suggestionList } from '../jsutils/suggestionList';
109
import { printPathArray } from '../jsutils/printPathArray';
1110
import { addPath, pathToArray } from '../jsutils/Path';
11+
import { isIteratableObject } from '../jsutils/isIteratableObject';
1212

1313
import { GraphQLError } from '../error/GraphQLError';
1414

@@ -77,7 +77,7 @@ function coerceInputValueImpl(
7777

7878
if (isListType(type)) {
7979
const itemType = type.ofType;
80-
if (isCollection(inputValue)) {
80+
if (isIteratableObject(inputValue)) {
8181
return Array.from(inputValue, (itemValue, index) => {
8282
const itemPath = addPath(path, index, undefined);
8383
return coerceInputValueImpl(itemValue, itemType, onError, itemPath);

0 commit comments

Comments
 (0)