Skip to content

Commit 8057e39

Browse files
committed
enum: 'parseLiteral/parseValue/serialize' now throw on invalid values
Continuation of #1821
1 parent 1c6b53e commit 8057e39

File tree

9 files changed

+118
-90
lines changed

9 files changed

+118
-90
lines changed

src/execution/__tests__/executor-test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { GraphQLInt, GraphQLBoolean, GraphQLString } from '../../type/scalars';
1414
import {
1515
GraphQLList,
1616
GraphQLNonNull,
17+
GraphQLScalarType,
1718
GraphQLInterfaceType,
1819
GraphQLObjectType,
1920
} from '../../type/definition';
@@ -1063,6 +1064,39 @@ describe('Execute: Handles basic execution tasks', () => {
10631064
expect(asyncResult).to.deep.equal(asyncResult);
10641065
});
10651066

1067+
it('fails when serialize of custom scalar does not return a value', () => {
1068+
const customScalar = new GraphQLScalarType({
1069+
name: 'CustomScalar',
1070+
serialize() {
1071+
/* returns nothing */
1072+
},
1073+
});
1074+
const schema = new GraphQLSchema({
1075+
query: new GraphQLObjectType({
1076+
name: 'Query',
1077+
fields: {
1078+
customScalar: {
1079+
type: customScalar,
1080+
resolve: () => 'CUSTOM_VALUE',
1081+
},
1082+
},
1083+
}),
1084+
});
1085+
1086+
const result = execute({ schema, document: parse('{ customScalar }') });
1087+
expect(result).to.deep.equal({
1088+
data: { customScalar: null },
1089+
errors: [
1090+
{
1091+
message:
1092+
'Expected a value of type "CustomScalar" but received: "CUSTOM_VALUE"',
1093+
locations: [{ line: 1, column: 3 }],
1094+
path: ['customScalar'],
1095+
},
1096+
],
1097+
});
1098+
});
1099+
10661100
it('executes ignoring invalid non-executable definitions', () => {
10671101
const schema = new GraphQLSchema({
10681102
query: new GraphQLObjectType({

src/type/__tests__/enumType-test.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ describe('Type System: Enum Values', () => {
150150
errors: [
151151
{
152152
message:
153-
'Expected value of type "Color", found "GREEN". Did you mean the enum value "GREEN"?',
153+
'Enum "Color" cannot represent non-enum value: "GREEN". Did you mean the enum value "GREEN"?',
154154
locations: [{ line: 1, column: 23 }],
155155
},
156156
],
@@ -164,7 +164,7 @@ describe('Type System: Enum Values', () => {
164164
errors: [
165165
{
166166
message:
167-
'Expected value of type "Color", found GREENISH. Did you mean the enum value "GREEN"?',
167+
'Value "GREENISH" does not exist in "Color" enum. Did you mean the enum value "GREEN"?',
168168
locations: [{ line: 1, column: 23 }],
169169
},
170170
],
@@ -178,7 +178,7 @@ describe('Type System: Enum Values', () => {
178178
errors: [
179179
{
180180
message:
181-
'Expected value of type "Color", found green. Did you mean the enum value "GREEN"?',
181+
'Value "green" does not exist in "Color" enum. Did you mean the enum value "GREEN"?',
182182
locations: [{ line: 1, column: 23 }],
183183
},
184184
],
@@ -192,7 +192,7 @@ describe('Type System: Enum Values', () => {
192192
data: { colorEnum: null },
193193
errors: [
194194
{
195-
message: 'Expected a value of type "Color" but received: "GREEN"',
195+
message: 'Enum "Color" cannot represent value: "GREEN"',
196196
locations: [{ line: 1, column: 3 }],
197197
path: ['colorEnum'],
198198
},
@@ -206,7 +206,7 @@ describe('Type System: Enum Values', () => {
206206
expect(result).to.deep.equal({
207207
errors: [
208208
{
209-
message: 'Expected value of type "Color", found 1.',
209+
message: 'Enum "Color" cannot represent non-enum value: 1.',
210210
locations: [{ line: 1, column: 23 }],
211211
},
212212
],
@@ -262,7 +262,7 @@ describe('Type System: Enum Values', () => {
262262
errors: [
263263
{
264264
message:
265-
'Variable "$color" got invalid value 2; Expected type "Color".',
265+
'Variable "$color" got invalid value 2; Enum "Color" cannot represent non-string value: 2.',
266266
locations: [{ line: 1, column: 8 }],
267267
},
268268
],
@@ -390,7 +390,7 @@ describe('Type System: Enum Values', () => {
390390
errors: [
391391
{
392392
message:
393-
'Expected a value of type "Complex" but received: { someRandomValue: 123 }',
393+
'Enum "Complex" cannot represent value: { someRandomValue: 123 }',
394394
locations: [{ line: 6, column: 9 }],
395395
path: ['bad'],
396396
},

src/type/definition.js

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import { type Path } from '../jsutils/Path';
1010
import devAssert from '../jsutils/devAssert';
1111
import keyValMap from '../jsutils/keyValMap';
1212
import instanceOf from '../jsutils/instanceOf';
13+
import didYouMean from '../jsutils/didYouMean';
1314
import isObjectLike from '../jsutils/isObjectLike';
1415
import identityFunc from '../jsutils/identityFunc';
1516
import defineToJSON from '../jsutils/defineToJSON';
17+
import suggestionList from '../jsutils/suggestionList';
1618
import defineToStringTag from '../jsutils/defineToStringTag';
1719
import { type PromiseOrValue } from '../jsutils/PromiseOrValue';
1820
import {
@@ -22,6 +24,7 @@ import {
2224
} from '../jsutils/ObjMap';
2325

2426
import { Kind } from '../language/kinds';
27+
import { print } from '../language/printer';
2528
import {
2629
type ScalarTypeDefinitionNode,
2730
type ObjectTypeDefinitionNode,
@@ -44,6 +47,8 @@ import {
4447
type ValueNode,
4548
} from '../language/ast';
4649

50+
import { GraphQLError } from '../error/GraphQLError';
51+
4752
import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped';
4853

4954
import { type GraphQLSchema } from './schema';
@@ -1234,28 +1239,54 @@ export class GraphQLEnumType /* <T> */ {
12341239

12351240
serialize(outputValue: mixed /* T */): ?string {
12361241
const enumValue = this._valueLookup.get(outputValue);
1237-
if (enumValue) {
1238-
return enumValue.name;
1242+
if (enumValue === undefined) {
1243+
throw new GraphQLError(
1244+
`Enum "${this.name}" cannot represent value: ${inspect(outputValue)}`,
1245+
);
12391246
}
1247+
return enumValue.name;
12401248
}
12411249

12421250
parseValue(inputValue: mixed): ?any /* T */ {
1243-
if (typeof inputValue === 'string') {
1244-
const enumValue = this.getValue(inputValue);
1245-
if (enumValue) {
1246-
return enumValue.value;
1247-
}
1251+
if (typeof inputValue !== 'string') {
1252+
const valueStr = inspect(inputValue);
1253+
throw new GraphQLError(
1254+
`Enum "${this.name}" cannot represent non-string value: ${valueStr}.` +
1255+
didYouMeanEnumValue(this, valueStr),
1256+
);
12481257
}
1258+
1259+
const enumValue = this.getValue(inputValue);
1260+
if (enumValue == null) {
1261+
throw new GraphQLError(
1262+
`Value "${inputValue}" does not exist in "${this.name}" enum.` +
1263+
didYouMeanEnumValue(this, inputValue),
1264+
);
1265+
}
1266+
return enumValue.value;
12491267
}
12501268

12511269
parseLiteral(valueNode: ValueNode, _variables: ?ObjMap<mixed>): ?any /* T */ {
12521270
// Note: variables will be resolved to a value before calling this function.
1253-
if (valueNode.kind === Kind.ENUM) {
1254-
const enumValue = this.getValue(valueNode.value);
1255-
if (enumValue) {
1256-
return enumValue.value;
1257-
}
1271+
if (valueNode.kind !== Kind.ENUM) {
1272+
const valueStr = print(valueNode);
1273+
throw new GraphQLError(
1274+
`Enum "${this.name}" cannot represent non-enum value: ${valueStr}.` +
1275+
didYouMeanEnumValue(this, valueStr),
1276+
valueNode,
1277+
);
12581278
}
1279+
1280+
const enumValue = this.getValue(valueNode.value);
1281+
if (enumValue == null) {
1282+
const valueStr = print(valueNode);
1283+
throw new GraphQLError(
1284+
`Value "${valueStr}" does not exist in "${this.name}" enum.` +
1285+
didYouMeanEnumValue(this, valueStr),
1286+
valueNode,
1287+
);
1288+
}
1289+
return enumValue.value;
12591290
}
12601291

12611292
toConfig(): {|
@@ -1294,6 +1325,16 @@ export class GraphQLEnumType /* <T> */ {
12941325
defineToStringTag(GraphQLEnumType);
12951326
defineToJSON(GraphQLEnumType);
12961327

1328+
function didYouMeanEnumValue(
1329+
enumType: GraphQLEnumType,
1330+
unknownValueStr: string,
1331+
): string {
1332+
const allNames = enumType.getValues().map(value => value.name);
1333+
const suggestedValues = suggestionList(unknownValueStr, allNames);
1334+
1335+
return didYouMean('the enum value', suggestedValues);
1336+
}
1337+
12971338
function defineEnumValues(
12981339
typeName: string,
12991340
valueMap: GraphQLEnumValueConfigMap /* <T> */,

src/utilities/__tests__/astFromValue-test.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,14 @@ describe('astFromValue', () => {
219219
});
220220

221221
// Note: case sensitive
222-
expect(astFromValue('hello', myEnum)).to.deep.equal(null);
222+
expect(() => astFromValue('hello', myEnum)).to.throw(
223+
'Enum "MyEnum" cannot represent value: "hello"',
224+
);
223225

224226
// Note: Not a valid enum value
225-
expect(astFromValue('VALUE', myEnum)).to.deep.equal(null);
227+
expect(() => astFromValue('UNKNOWN_VALUE', myEnum)).to.throw(
228+
'Enum "MyEnum" cannot represent value: "UNKNOWN_VALUE"',
229+
);
226230
});
227231

228232
it('converts array values to List ASTs', () => {

src/utilities/__tests__/coerceInputValue-test.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ describe('coerceInputValue', () => {
140140
const result = coerceValue('foo', TestEnum);
141141
expectErrors(result).to.deep.equal([
142142
{
143-
error: 'Expected type "TestEnum". Did you mean the enum value "FOO"?',
143+
error:
144+
'Value "foo" does not exist in "TestEnum" enum. Did you mean the enum value "FOO"?',
144145
path: [],
145146
value: 'foo',
146147
},
@@ -151,7 +152,7 @@ describe('coerceInputValue', () => {
151152
const result1 = coerceValue(123, TestEnum);
152153
expectErrors(result1).to.deep.equal([
153154
{
154-
error: 'Expected type "TestEnum".',
155+
error: 'Enum "TestEnum" cannot represent non-string value: 123.',
155156
path: [],
156157
value: 123,
157158
},
@@ -160,7 +161,8 @@ describe('coerceInputValue', () => {
160161
const result2 = coerceValue({ field: 'value' }, TestEnum);
161162
expectErrors(result2).to.deep.equal([
162163
{
163-
error: 'Expected type "TestEnum".',
164+
error:
165+
'Enum "TestEnum" cannot represent non-string value: { field: "value" }.',
164166
path: [],
165167
value: { field: 'value' },
166168
},

src/utilities/coerceInputValue.js

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import { type Path, addPath, pathToArray } from '../jsutils/Path';
1515
import { GraphQLError } from '../error/GraphQLError';
1616
import {
1717
type GraphQLInputType,
18-
isScalarType,
19-
isEnumType,
18+
isLeafType,
2019
isInputObjectType,
2120
isListType,
2221
isNonNullType,
@@ -157,7 +156,7 @@ function coerceInputValueImpl(
157156
return coercedValue;
158157
}
159158

160-
if (isScalarType(type)) {
159+
if (isLeafType(type)) {
161160
let parseResult;
162161

163162
// Scalars determine if a input value is valid via parseValue(), which can
@@ -194,28 +193,6 @@ function coerceInputValueImpl(
194193
return parseResult;
195194
}
196195

197-
if (isEnumType(type)) {
198-
if (typeof inputValue === 'string') {
199-
const enumValue = type.getValue(inputValue);
200-
if (enumValue) {
201-
return enumValue.value;
202-
}
203-
}
204-
const suggestions = suggestionList(
205-
String(inputValue),
206-
type.getValues().map(enumValue => enumValue.name),
207-
);
208-
onError(
209-
pathToArray(path),
210-
inputValue,
211-
new GraphQLError(
212-
`Expected type "${type.name}".` +
213-
didYouMean('the enum value', suggestions),
214-
),
215-
);
216-
return;
217-
}
218-
219196
// Not reachable. All possible input types have been considered.
220197
invariant(false, 'Unexpected input type: ' + inspect((type: empty)));
221198
}

src/utilities/valueFromAST.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ import { type ValueNode } from '../language/ast';
1313

1414
import {
1515
type GraphQLInputType,
16-
isScalarType,
17-
isEnumType,
16+
isLeafType,
1817
isInputObjectType,
1918
isListType,
2019
isNonNullType,
@@ -133,18 +132,7 @@ export function valueFromAST(
133132
return coercedObj;
134133
}
135134

136-
if (isEnumType(type)) {
137-
if (valueNode.kind !== Kind.ENUM) {
138-
return; // Invalid: intentionally return no value.
139-
}
140-
const enumValue = type.getValue(valueNode.value);
141-
if (!enumValue) {
142-
return; // Invalid: intentionally return no value.
143-
}
144-
return enumValue.value;
145-
}
146-
147-
if (isScalarType(type)) {
135+
if (isLeafType(type)) {
148136
// Scalars fulfill parsing a literal value via parseLiteral().
149137
// Invalid values represent a failure to parse correctly, in which case
150138
// no value is returned.
@@ -154,7 +142,7 @@ export function valueFromAST(
154142
} catch (_error) {
155143
return; // Invalid: intentionally return no value.
156144
}
157-
if (isInvalid(result)) {
145+
if (result === undefined) {
158146
return; // Invalid: intentionally return no value.
159147
}
160148
return result;

0 commit comments

Comments
 (0)