diff --git a/src/execution/__tests__/executor-test.js b/src/execution/__tests__/executor-test.js index 134a4ad4d0..60bef79d46 100644 --- a/src/execution/__tests__/executor-test.js +++ b/src/execution/__tests__/executor-test.js @@ -14,6 +14,7 @@ import { GraphQLInt, GraphQLBoolean, GraphQLString } from '../../type/scalars'; import { GraphQLList, GraphQLNonNull, + GraphQLScalarType, GraphQLInterfaceType, GraphQLObjectType, } from '../../type/definition'; @@ -1063,6 +1064,39 @@ describe('Execute: Handles basic execution tasks', () => { expect(asyncResult).to.deep.equal(asyncResult); }); + it('fails when serialize of custom scalar does not return a value', () => { + const customScalar = new GraphQLScalarType({ + name: 'CustomScalar', + serialize() { + /* returns nothing */ + }, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + customScalar: { + type: customScalar, + resolve: () => 'CUSTOM_VALUE', + }, + }, + }), + }); + + const result = execute({ schema, document: parse('{ customScalar }') }); + expect(result).to.deep.equal({ + data: { customScalar: null }, + errors: [ + { + message: + 'Expected a value of type "CustomScalar" but received: "CUSTOM_VALUE"', + locations: [{ line: 1, column: 3 }], + path: ['customScalar'], + }, + ], + }); + }); + it('executes ignoring invalid non-executable definitions', () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ diff --git a/src/type/__tests__/enumType-test.js b/src/type/__tests__/enumType-test.js index e3c798966d..9b4bcb11cf 100644 --- a/src/type/__tests__/enumType-test.js +++ b/src/type/__tests__/enumType-test.js @@ -150,7 +150,7 @@ describe('Type System: Enum Values', () => { errors: [ { message: - 'Expected value of type "Color", found "GREEN". Did you mean the enum value "GREEN"?', + 'Enum "Color" cannot represent non-enum value: "GREEN". Did you mean the enum value "GREEN"?', locations: [{ line: 1, column: 23 }], }, ], @@ -164,7 +164,7 @@ describe('Type System: Enum Values', () => { errors: [ { message: - 'Expected value of type "Color", found GREENISH. Did you mean the enum value "GREEN"?', + 'Value "GREENISH" does not exist in "Color" enum. Did you mean the enum value "GREEN"?', locations: [{ line: 1, column: 23 }], }, ], @@ -178,7 +178,7 @@ describe('Type System: Enum Values', () => { errors: [ { message: - 'Expected value of type "Color", found green. Did you mean the enum value "GREEN"?', + 'Value "green" does not exist in "Color" enum. Did you mean the enum value "GREEN"?', locations: [{ line: 1, column: 23 }], }, ], @@ -192,7 +192,7 @@ describe('Type System: Enum Values', () => { data: { colorEnum: null }, errors: [ { - message: 'Expected a value of type "Color" but received: "GREEN"', + message: 'Enum "Color" cannot represent value: "GREEN"', locations: [{ line: 1, column: 3 }], path: ['colorEnum'], }, @@ -206,7 +206,7 @@ describe('Type System: Enum Values', () => { expect(result).to.deep.equal({ errors: [ { - message: 'Expected value of type "Color", found 1.', + message: 'Enum "Color" cannot represent non-enum value: 1.', locations: [{ line: 1, column: 23 }], }, ], @@ -262,7 +262,7 @@ describe('Type System: Enum Values', () => { errors: [ { message: - 'Variable "$color" got invalid value 2; Expected type "Color".', + 'Variable "$color" got invalid value 2; Enum "Color" cannot represent non-string value: 2.', locations: [{ line: 1, column: 8 }], }, ], @@ -390,7 +390,7 @@ describe('Type System: Enum Values', () => { errors: [ { message: - 'Expected a value of type "Complex" but received: { someRandomValue: 123 }', + 'Enum "Complex" cannot represent value: { someRandomValue: 123 }', locations: [{ line: 6, column: 9 }], path: ['bad'], }, diff --git a/src/type/definition.js b/src/type/definition.js index 731b461b18..f03c27a6fb 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -10,9 +10,11 @@ import { type Path } from '../jsutils/Path'; import devAssert from '../jsutils/devAssert'; import keyValMap from '../jsutils/keyValMap'; import instanceOf from '../jsutils/instanceOf'; +import didYouMean from '../jsutils/didYouMean'; import isObjectLike from '../jsutils/isObjectLike'; import identityFunc from '../jsutils/identityFunc'; import defineToJSON from '../jsutils/defineToJSON'; +import suggestionList from '../jsutils/suggestionList'; import defineToStringTag from '../jsutils/defineToStringTag'; import { type PromiseOrValue } from '../jsutils/PromiseOrValue'; import { @@ -22,6 +24,7 @@ import { } from '../jsutils/ObjMap'; import { Kind } from '../language/kinds'; +import { print } from '../language/printer'; import { type ScalarTypeDefinitionNode, type ObjectTypeDefinitionNode, @@ -44,6 +47,8 @@ import { type ValueNode, } from '../language/ast'; +import { GraphQLError } from '../error/GraphQLError'; + import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped'; import { type GraphQLSchema } from './schema'; @@ -1234,28 +1239,54 @@ export class GraphQLEnumType /* */ { serialize(outputValue: mixed /* T */): ?string { const enumValue = this._valueLookup.get(outputValue); - if (enumValue) { - return enumValue.name; + if (enumValue === undefined) { + throw new GraphQLError( + `Enum "${this.name}" cannot represent value: ${inspect(outputValue)}`, + ); } + return enumValue.name; } parseValue(inputValue: mixed): ?any /* T */ { - if (typeof inputValue === 'string') { - const enumValue = this.getValue(inputValue); - if (enumValue) { - return enumValue.value; - } + if (typeof inputValue !== 'string') { + const valueStr = inspect(inputValue); + throw new GraphQLError( + `Enum "${this.name}" cannot represent non-string value: ${valueStr}.` + + didYouMeanEnumValue(this, valueStr), + ); } + + const enumValue = this.getValue(inputValue); + if (enumValue == null) { + throw new GraphQLError( + `Value "${inputValue}" does not exist in "${this.name}" enum.` + + didYouMeanEnumValue(this, inputValue), + ); + } + return enumValue.value; } parseLiteral(valueNode: ValueNode, _variables: ?ObjMap): ?any /* T */ { // Note: variables will be resolved to a value before calling this function. - if (valueNode.kind === Kind.ENUM) { - const enumValue = this.getValue(valueNode.value); - if (enumValue) { - return enumValue.value; - } + if (valueNode.kind !== Kind.ENUM) { + const valueStr = print(valueNode); + throw new GraphQLError( + `Enum "${this.name}" cannot represent non-enum value: ${valueStr}.` + + didYouMeanEnumValue(this, valueStr), + valueNode, + ); } + + const enumValue = this.getValue(valueNode.value); + if (enumValue == null) { + const valueStr = print(valueNode); + throw new GraphQLError( + `Value "${valueStr}" does not exist in "${this.name}" enum.` + + didYouMeanEnumValue(this, valueStr), + valueNode, + ); + } + return enumValue.value; } toConfig(): {| @@ -1294,6 +1325,16 @@ export class GraphQLEnumType /* */ { defineToStringTag(GraphQLEnumType); defineToJSON(GraphQLEnumType); +function didYouMeanEnumValue( + enumType: GraphQLEnumType, + unknownValueStr: string, +): string { + const allNames = enumType.getValues().map(value => value.name); + const suggestedValues = suggestionList(unknownValueStr, allNames); + + return didYouMean('the enum value', suggestedValues); +} + function defineEnumValues( typeName: string, valueMap: GraphQLEnumValueConfigMap /* */, diff --git a/src/utilities/__tests__/astFromValue-test.js b/src/utilities/__tests__/astFromValue-test.js index 862ba46aae..38dc324a0d 100644 --- a/src/utilities/__tests__/astFromValue-test.js +++ b/src/utilities/__tests__/astFromValue-test.js @@ -219,10 +219,14 @@ describe('astFromValue', () => { }); // Note: case sensitive - expect(astFromValue('hello', myEnum)).to.deep.equal(null); + expect(() => astFromValue('hello', myEnum)).to.throw( + 'Enum "MyEnum" cannot represent value: "hello"', + ); // Note: Not a valid enum value - expect(astFromValue('VALUE', myEnum)).to.deep.equal(null); + expect(() => astFromValue('UNKNOWN_VALUE', myEnum)).to.throw( + 'Enum "MyEnum" cannot represent value: "UNKNOWN_VALUE"', + ); }); it('converts array values to List ASTs', () => { diff --git a/src/utilities/__tests__/coerceInputValue-test.js b/src/utilities/__tests__/coerceInputValue-test.js index a933fbd06e..c88f734c3c 100644 --- a/src/utilities/__tests__/coerceInputValue-test.js +++ b/src/utilities/__tests__/coerceInputValue-test.js @@ -140,7 +140,8 @@ describe('coerceInputValue', () => { const result = coerceValue('foo', TestEnum); expectErrors(result).to.deep.equal([ { - error: 'Expected type "TestEnum". Did you mean the enum value "FOO"?', + error: + 'Value "foo" does not exist in "TestEnum" enum. Did you mean the enum value "FOO"?', path: [], value: 'foo', }, @@ -151,7 +152,7 @@ describe('coerceInputValue', () => { const result1 = coerceValue(123, TestEnum); expectErrors(result1).to.deep.equal([ { - error: 'Expected type "TestEnum".', + error: 'Enum "TestEnum" cannot represent non-string value: 123.', path: [], value: 123, }, @@ -160,7 +161,8 @@ describe('coerceInputValue', () => { const result2 = coerceValue({ field: 'value' }, TestEnum); expectErrors(result2).to.deep.equal([ { - error: 'Expected type "TestEnum".', + error: + 'Enum "TestEnum" cannot represent non-string value: { field: "value" }.', path: [], value: { field: 'value' }, }, diff --git a/src/utilities/coerceInputValue.js b/src/utilities/coerceInputValue.js index c85e700340..032a7f80f4 100644 --- a/src/utilities/coerceInputValue.js +++ b/src/utilities/coerceInputValue.js @@ -15,8 +15,7 @@ import { type Path, addPath, pathToArray } from '../jsutils/Path'; import { GraphQLError } from '../error/GraphQLError'; import { type GraphQLInputType, - isScalarType, - isEnumType, + isLeafType, isInputObjectType, isListType, isNonNullType, @@ -157,7 +156,7 @@ function coerceInputValueImpl( return coercedValue; } - if (isScalarType(type)) { + if (isLeafType(type)) { let parseResult; // Scalars determine if a input value is valid via parseValue(), which can @@ -194,28 +193,6 @@ function coerceInputValueImpl( return parseResult; } - if (isEnumType(type)) { - if (typeof inputValue === 'string') { - const enumValue = type.getValue(inputValue); - if (enumValue) { - return enumValue.value; - } - } - const suggestions = suggestionList( - String(inputValue), - type.getValues().map(enumValue => enumValue.name), - ); - onError( - pathToArray(path), - inputValue, - new GraphQLError( - `Expected type "${type.name}".` + - didYouMean('the enum value', suggestions), - ), - ); - return; - } - // Not reachable. All possible input types have been considered. invariant(false, 'Unexpected input type: ' + inspect((type: empty))); } diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js index 71f10c595a..d1065753a4 100644 --- a/src/utilities/valueFromAST.js +++ b/src/utilities/valueFromAST.js @@ -13,8 +13,7 @@ import { type ValueNode } from '../language/ast'; import { type GraphQLInputType, - isScalarType, - isEnumType, + isLeafType, isInputObjectType, isListType, isNonNullType, @@ -133,18 +132,7 @@ export function valueFromAST( return coercedObj; } - if (isEnumType(type)) { - if (valueNode.kind !== Kind.ENUM) { - return; // Invalid: intentionally return no value. - } - const enumValue = type.getValue(valueNode.value); - if (!enumValue) { - return; // Invalid: intentionally return no value. - } - return enumValue.value; - } - - if (isScalarType(type)) { + if (isLeafType(type)) { // Scalars fulfill parsing a literal value via parseLiteral(). // Invalid values represent a failure to parse correctly, in which case // no value is returned. @@ -154,7 +142,7 @@ export function valueFromAST( } catch (_error) { return; // Invalid: intentionally return no value. } - if (isInvalid(result)) { + if (result === undefined) { return; // Invalid: intentionally return no value. } return result; diff --git a/src/validation/__tests__/ValuesOfCorrectType-test.js b/src/validation/__tests__/ValuesOfCorrectType-test.js index 367b56a96e..5a471e9aa9 100644 --- a/src/validation/__tests__/ValuesOfCorrectType-test.js +++ b/src/validation/__tests__/ValuesOfCorrectType-test.js @@ -464,7 +464,7 @@ describe('Validate: Values of correct type', () => { } `).to.deep.equal([ { - message: 'Expected value of type "DogCommand", found 2.', + message: 'Enum "DogCommand" cannot represent non-enum value: 2.', locations: [{ line: 4, column: 41 }], }, ]); @@ -479,7 +479,7 @@ describe('Validate: Values of correct type', () => { } `).to.deep.equal([ { - message: 'Expected value of type "DogCommand", found 1.0.', + message: 'Enum "DogCommand" cannot represent non-enum value: 1.0.', locations: [{ line: 4, column: 41 }], }, ]); @@ -495,7 +495,7 @@ describe('Validate: Values of correct type', () => { `).to.deep.equal([ { message: - 'Expected value of type "DogCommand", found "SIT". Did you mean the enum value "SIT"?', + 'Enum "DogCommand" cannot represent non-enum value: "SIT". Did you mean the enum value "SIT"?', locations: [{ line: 4, column: 41 }], }, ]); @@ -510,7 +510,7 @@ describe('Validate: Values of correct type', () => { } `).to.deep.equal([ { - message: 'Expected value of type "DogCommand", found true.', + message: 'Enum "DogCommand" cannot represent non-enum value: true.', locations: [{ line: 4, column: 41 }], }, ]); @@ -525,7 +525,7 @@ describe('Validate: Values of correct type', () => { } `).to.deep.equal([ { - message: 'Expected value of type "DogCommand", found JUGGLE.', + message: 'Value "JUGGLE" does not exist in "DogCommand" enum.', locations: [{ line: 4, column: 41 }], }, ]); @@ -541,7 +541,7 @@ describe('Validate: Values of correct type', () => { `).to.deep.equal([ { message: - 'Expected value of type "DogCommand", found sit. Did you mean the enum value "SIT"?', + 'Value "sit" does not exist in "DogCommand" enum. Did you mean the enum value "SIT"?', locations: [{ line: 4, column: 41 }], }, ]); diff --git a/src/validation/rules/ValuesOfCorrectType.js b/src/validation/rules/ValuesOfCorrectType.js index a915c0aadb..ca5f728670 100644 --- a/src/validation/rules/ValuesOfCorrectType.js +++ b/src/validation/rules/ValuesOfCorrectType.js @@ -10,14 +10,12 @@ import suggestionList from '../../jsutils/suggestionList'; import { GraphQLError } from '../../error/GraphQLError'; -import { Kind } from '../../language/kinds'; import { print } from '../../language/printer'; import { type ValueNode } from '../../language/ast'; import { type ASTVisitor } from '../../language/visitor'; import { - isScalarType, - isEnumType, + isLeafType, isInputObjectType, isListType, isNonNullType, @@ -115,23 +113,7 @@ function isValidValueNode(context: ValidationContext, node: ValueNode): void { const type = getNamedType(locationType); - if (isEnumType(type)) { - if (node.kind !== Kind.ENUM || !type.getValue(node.value)) { - const allNames = type.getValues().map(value => value.name); - const suggestedValues = suggestionList(print(node), allNames); - - context.reportError( - new GraphQLError( - `Expected value of type "${type.name}", found ${print(node)}.` + - didYouMean('the enum value', suggestedValues), - node, - ), - ); - } - return; - } - - if (!isScalarType(type)) { + if (!isLeafType(type)) { const typeStr = inspect(locationType); context.reportError( new GraphQLError(