diff --git a/src/jsutils/isInteger.js b/src/jsutils/isInteger.js new file mode 100644 index 0000000000..3e897d6fc6 --- /dev/null +++ b/src/jsutils/isInteger.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +declare function isInteger(value: mixed): boolean %checks(typeof value === + 'number'); + +/* eslint-disable no-redeclare */ +// $FlowFixMe workaround for: https://github.com/facebook/flow/issues/4441 +const isInteger = + Number.isInteger || + function(value) { + return ( + typeof value === 'number' && + isFinite(value) && + Math.floor(value) === value + ); + }; +export default isInteger; diff --git a/src/subscription/__tests__/subscribe-test.js b/src/subscription/__tests__/subscribe-test.js index 6d9e9f440d..06a4d9e50f 100644 --- a/src/subscription/__tests__/subscribe-test.js +++ b/src/subscription/__tests__/subscribe-test.js @@ -474,8 +474,7 @@ describe('Subscription Initialization Phase', () => { { message: 'Variable "$priority" got invalid value "meow"; Expected ' + - 'type Int; Int cannot represent non 32-bit signed ' + - 'integer value: meow', + 'type Int; Int cannot represent non-integer value: meow', locations: [{ line: 2, column: 21 }], }, ], diff --git a/src/type/__tests__/serialization-test.js b/src/type/__tests__/serialization-test.js index d6b832765b..ad0f618ef1 100644 --- a/src/type/__tests__/serialization-test.js +++ b/src/type/__tests__/serialization-test.js @@ -56,14 +56,17 @@ describe('Type System: Scalar coercion', () => { 'Int cannot represent non 32-bit signed integer value: -1e+100', ); expect(() => GraphQLInt.serialize('one')).to.throw( - 'Int cannot represent non 32-bit signed integer value: one', + 'Int cannot represent non-integer value: one', ); // Doesn't represent number expect(() => GraphQLInt.serialize('')).to.throw( - 'Int cannot represent non 32-bit signed integer value: (empty string)', + 'Int cannot represent non-integer value: (empty string)', ); expect(() => GraphQLInt.serialize(NaN)).to.throw( - 'Int cannot represent non 32-bit signed integer value: NaN', + 'Int cannot represent non-integer value: NaN', + ); + expect(() => GraphQLInt.serialize(Infinity)).to.throw( + 'Int cannot represent non-integer value: Infinity', ); expect(() => GraphQLInt.serialize([5])).to.throw( 'Int cannot represent an array value: [5]', @@ -85,6 +88,9 @@ describe('Type System: Scalar coercion', () => { expect(() => GraphQLFloat.serialize(NaN)).to.throw( 'Float cannot represent non numeric value: NaN', ); + expect(() => GraphQLFloat.serialize(Infinity)).to.throw( + 'Float cannot represent non numeric value: Infinity', + ); expect(() => GraphQLFloat.serialize('one')).to.throw( 'Float cannot represent non numeric value: one', ); diff --git a/src/type/scalars.js b/src/type/scalars.js index 0e8e9c2745..72400b2591 100644 --- a/src/type/scalars.js +++ b/src/type/scalars.js @@ -8,6 +8,7 @@ */ import inspect from '../jsutils/inspect'; +import isInteger from '../jsutils/isInteger'; import { GraphQLScalarType, isNamedType } from './definition'; import { Kind } from '../language/kinds'; @@ -27,22 +28,22 @@ function coerceInt(value: mixed): number { } if (value === '') { throw new TypeError( - 'Int cannot represent non 32-bit signed integer value: (empty string)', + 'Int cannot represent non-integer value: (empty string)', ); } const num = Number(value); - if (num !== num || num > MAX_INT || num < MIN_INT) { + if (!isInteger(num)) { throw new TypeError( - 'Int cannot represent non 32-bit signed integer value: ' + inspect(value), + 'Int cannot represent non-integer value: ' + inspect(value), ); } - const int = Math.floor(num); - if (int !== num) { + + if (num > MAX_INT || num < MIN_INT) { throw new TypeError( - 'Int cannot represent non-integer value: ' + inspect(value), + 'Int cannot represent non 32-bit signed integer value: ' + inspect(value), ); } - return int; + return num; } export const GraphQLInt = new GraphQLScalarType({ @@ -75,7 +76,7 @@ function coerceFloat(value: mixed): number { ); } const num = Number(value); - if (num === num) { + if (isFinite(num)) { return num; } throw new TypeError( diff --git a/src/utilities/__tests__/coerceValue-test.js b/src/utilities/__tests__/coerceValue-test.js index 6950ca452a..075b320c5b 100644 --- a/src/utilities/__tests__/coerceValue-test.js +++ b/src/utilities/__tests__/coerceValue-test.js @@ -62,31 +62,45 @@ describe('coerceValue', () => { expectValue(result).to.equal(null); }); - it('returns a single error for empty value', () => { + it('returns a single error for empty string as value', () => { const result = coerceValue('', GraphQLInt); expectErrors(result).to.deep.equal([ - 'Expected type Int; Int cannot represent non 32-bit signed integer value: (empty string)', + 'Expected type Int; Int cannot represent non-integer value: (empty string)', ]); }); - it('returns error for float input as int', () => { + it('returns a single error for 2^32 input as int', () => { + const result = coerceValue(Math.pow(2, 32), GraphQLInt); + expectErrors(result).to.deep.equal([ + 'Expected type Int; Int cannot represent non 32-bit signed integer value: 4294967296', + ]); + }); + + it('returns a single error for float input as int', () => { const result = coerceValue('1.5', GraphQLInt); expectErrors(result).to.deep.equal([ 'Expected type Int; Int cannot represent non-integer value: 1.5', ]); }); + it('returns a single error for Infinity input as int', () => { + const result = coerceValue(Infinity, GraphQLInt); + expectErrors(result).to.deep.equal([ + 'Expected type Int; Int cannot represent non-integer value: Infinity', + ]); + }); + it('returns a single error for char input', () => { const result = coerceValue('a', GraphQLInt); expectErrors(result).to.deep.equal([ - 'Expected type Int; Int cannot represent non 32-bit signed integer value: a', + 'Expected type Int; Int cannot represent non-integer value: a', ]); }); it('returns a single error for char input', () => { const result = coerceValue('meow', GraphQLInt); expectErrors(result).to.deep.equal([ - 'Expected type Int; Int cannot represent non 32-bit signed integer value: meow', + 'Expected type Int; Int cannot represent non-integer value: meow', ]); }); }); @@ -112,13 +126,20 @@ describe('coerceValue', () => { expectValue(result).to.equal(null); }); - it('returns a single error for empty value', () => { + it('returns a single error for empty string input', () => { const result = coerceValue('', GraphQLFloat); expectErrors(result).to.deep.equal([ 'Expected type Float; Float cannot represent non numeric value: (empty string)', ]); }); + it('returns a single error for Infinity input', () => { + const result = coerceValue(Infinity, GraphQLFloat); + expectErrors(result).to.deep.equal([ + 'Expected type Float; Float cannot represent non numeric value: Infinity', + ]); + }); + it('returns a single error for char input', () => { const result = coerceValue('a', GraphQLFloat); expectErrors(result).to.deep.equal([ @@ -191,15 +212,15 @@ describe('coerceValue', () => { it('returns no error for an invalid field', () => { const result = coerceValue({ foo: 'abc' }, TestInputObject); expectErrors(result).to.deep.equal([ - 'Expected type Int at value.foo; Int cannot represent non 32-bit signed integer value: abc', + 'Expected type Int at value.foo; Int cannot represent non-integer value: abc', ]); }); it('returns multiple errors for multiple invalid fields', () => { const result = coerceValue({ foo: 'abc', bar: 'def' }, TestInputObject); expectErrors(result).to.deep.equal([ - 'Expected type Int at value.foo; Int cannot represent non 32-bit signed integer value: abc', - 'Expected type Int at value.bar; Int cannot represent non 32-bit signed integer value: def', + 'Expected type Int at value.foo; Int cannot represent non-integer value: abc', + 'Expected type Int at value.bar; Int cannot represent non-integer value: def', ]); });