-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BREAKING/BUGFIX Strict coercion of scalar types #1382
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/** | ||
* 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 isFinite(value: mixed): boolean %checks(typeof value === | ||
'number'); | ||
|
||
/* eslint-disable no-redeclare */ | ||
// $FlowFixMe workaround for: https://github.com/facebook/flow/issues/4441 | ||
const isFinite = | ||
Number.isFinite || | ||
function(value) { | ||
return typeof value === 'number' && isFinite(value); | ||
}; | ||
export default isFinite; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
*/ | ||
|
||
import inspect from '../jsutils/inspect'; | ||
import isFinite from '../jsutils/isFinite'; | ||
import isInteger from '../jsutils/isInteger'; | ||
import { GraphQLScalarType, isNamedType } from './definition'; | ||
import { Kind } from '../language/kinds'; | ||
|
@@ -20,38 +21,46 @@ import { Kind } from '../language/kinds'; | |
const MAX_INT = 2147483647; | ||
const MIN_INT = -2147483648; | ||
|
||
function coerceInt(value: mixed): number { | ||
function serializeInt(value: mixed): number { | ||
if (Array.isArray(value)) { | ||
throw new TypeError( | ||
`Int cannot represent an array value: [${String(value)}]`, | ||
`Int cannot represent an array value: ${inspect(value)}`, | ||
); | ||
} | ||
if (value === '') { | ||
const num = Number(value); | ||
if (value === '' || !isInteger(num)) { | ||
throw new TypeError( | ||
'Int cannot represent non-integer value: (empty string)', | ||
`Int cannot represent non-integer value: ${inspect(value)}`, | ||
); | ||
} | ||
const num = Number(value); | ||
if (!isInteger(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 num; | ||
} | ||
|
||
if (num > MAX_INT || num < MIN_INT) { | ||
function coerceInt(value: mixed): number { | ||
if (!isInteger(value)) { | ||
throw new TypeError( | ||
'Int cannot represent non 32-bit signed integer value: ' + inspect(value), | ||
`Int cannot represent non-integer value: ${inspect(value)}`, | ||
); | ||
} | ||
return num; | ||
if (value > MAX_INT || value < MIN_INT) { | ||
throw new TypeError( | ||
`Int cannot represent non 32-bit signed integer value: ${inspect(value)}`, | ||
); | ||
} | ||
return value; | ||
} | ||
|
||
export const GraphQLInt = new GraphQLScalarType({ | ||
name: 'Int', | ||
description: | ||
'The `Int` scalar type represents non-fractional signed whole numeric ' + | ||
'values. Int can represent values between -(2^31) and 2^31 - 1. ', | ||
serialize: coerceInt, | ||
serialize: serializeInt, | ||
parseValue: coerceInt, | ||
parseLiteral(ast) { | ||
if (ast.kind === Kind.INT) { | ||
|
@@ -64,24 +73,28 @@ export const GraphQLInt = new GraphQLScalarType({ | |
}, | ||
}); | ||
|
||
function coerceFloat(value: mixed): number { | ||
function serializeFloat(value: mixed): number { | ||
if (Array.isArray(value)) { | ||
throw new TypeError( | ||
`Float cannot represent an array value: [${String(value)}]`, | ||
`Float cannot represent an array value: ${inspect(value)}`, | ||
); | ||
} | ||
if (value === '') { | ||
const num = Number(value); | ||
if (value === '' || !isFinite(num)) { | ||
throw new TypeError( | ||
'Float cannot represent non numeric value: (empty string)', | ||
`Float cannot represent non numeric value: ${inspect(value)}`, | ||
); | ||
} | ||
const num = Number(value); | ||
if (isFinite(num)) { | ||
return num; | ||
return num; | ||
} | ||
|
||
function coerceFloat(value: mixed): number { | ||
if (!isFinite(value)) { | ||
throw new TypeError( | ||
`Float cannot represent non numeric value: ${inspect(value)}`, | ||
); | ||
} | ||
throw new TypeError( | ||
'Float cannot represent non numeric value: ' + inspect(value), | ||
); | ||
return value; | ||
} | ||
|
||
export const GraphQLFloat = new GraphQLScalarType({ | ||
|
@@ -90,7 +103,7 @@ export const GraphQLFloat = new GraphQLScalarType({ | |
'The `Float` scalar type represents signed double-precision fractional ' + | ||
'values as specified by ' + | ||
'[IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ', | ||
serialize: coerceFloat, | ||
serialize: serializeFloat, | ||
parseValue: coerceFloat, | ||
parseLiteral(ast) { | ||
return ast.kind === Kind.FLOAT || ast.kind === Kind.INT | ||
|
@@ -99,7 +112,7 @@ export const GraphQLFloat = new GraphQLScalarType({ | |
}, | ||
}); | ||
|
||
function coerceString(value: mixed): string { | ||
function serializeString(value: mixed): string { | ||
if (Array.isArray(value)) { | ||
throw new TypeError( | ||
`String cannot represent an array value: ${inspect(value)}`, | ||
|
@@ -108,38 +121,84 @@ function coerceString(value: mixed): string { | |
return String(value); | ||
} | ||
|
||
function coerceString(value: mixed): string { | ||
if (typeof value !== 'string') { | ||
throw new TypeError( | ||
`String cannot represent a non string value: ${inspect(value)}`, | ||
); | ||
} | ||
return value; | ||
} | ||
|
||
export const GraphQLString = new GraphQLScalarType({ | ||
name: 'String', | ||
description: | ||
'The `String` scalar type represents textual data, represented as UTF-8 ' + | ||
'character sequences. The String type is most often used by GraphQL to ' + | ||
'represent free-form human-readable text.', | ||
serialize: coerceString, | ||
serialize: serializeString, | ||
parseValue: coerceString, | ||
parseLiteral(ast) { | ||
return ast.kind === Kind.STRING ? ast.value : undefined; | ||
}, | ||
}); | ||
|
||
function coerceBoolean(value: mixed): boolean { | ||
function serializeBoolean(value: mixed): boolean { | ||
if (Array.isArray(value)) { | ||
throw new TypeError( | ||
`Boolean cannot represent an array value: [${String(value)}]`, | ||
`Boolean cannot represent an array value: ${inspect(value)}`, | ||
); | ||
} | ||
return Boolean(value); | ||
} | ||
|
||
function coerceBoolean(value: mixed): boolean { | ||
if (typeof value !== 'boolean') { | ||
throw new TypeError( | ||
`Boolean cannot represent a non boolean value: ${inspect(value)}`, | ||
); | ||
} | ||
return value; | ||
} | ||
|
||
export const GraphQLBoolean = new GraphQLScalarType({ | ||
name: 'Boolean', | ||
description: 'The `Boolean` scalar type represents `true` or `false`.', | ||
serialize: coerceBoolean, | ||
serialize: serializeBoolean, | ||
parseValue: coerceBoolean, | ||
parseLiteral(ast) { | ||
return ast.kind === Kind.BOOLEAN ? ast.value : undefined; | ||
}, | ||
}); | ||
|
||
function serializeID(value: mixed): string { | ||
// Support serializing objects with custom valueOf() functions - a common way | ||
// to represent an object identifier (ex. MongoDB). | ||
const result = | ||
value && | ||
typeof value.valueOf === 'function' && | ||
value.valueOf !== Object.prototype.valueOf | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we don't need this check since:
So it would be enough to just call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're probably right, I'll double check by running the tests |
||
? value.valueOf() | ||
: value; | ||
if ( | ||
typeof result !== 'string' && | ||
(typeof result !== 'number' || !isInteger(result)) | ||
) { | ||
throw new TypeError(`ID cannot represent value: ${inspect(value)}`); | ||
} | ||
return String(result); | ||
} | ||
|
||
function coerceID(value: mixed): string { | ||
if ( | ||
typeof value !== 'string' && | ||
(typeof value !== 'number' || !isInteger(value)) | ||
) { | ||
throw new TypeError(`ID cannot represent value: ${inspect(value)}`); | ||
} | ||
return String(value); | ||
} | ||
|
||
export const GraphQLID = new GraphQLScalarType({ | ||
name: 'ID', | ||
description: | ||
|
@@ -148,8 +207,8 @@ export const GraphQLID = new GraphQLScalarType({ | |
'response as a String; however, it is not intended to be human-readable. ' + | ||
'When expected as an input type, any string (such as `"4"`) or integer ' + | ||
'(such as `4`) input value will be accepted as an ID.', | ||
serialize: coerceString, | ||
parseValue: coerceString, | ||
serialize: serializeID, | ||
parseValue: coerceID, | ||
parseLiteral(ast) { | ||
return ast.kind === Kind.STRING || ast.kind === Kind.INT | ||
? ast.value | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make it strict similar to
serializeID
so it would expect only string and object with eithervalueOf
ortoString
?Or at least throw on
serializeString({})
instead of returning[object Object]
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree the [object Object] scenario is useless - it should only string coerce if there's a toString implementation worth calling