diff --git a/src/execution/__tests__/variables-test.js b/src/execution/__tests__/variables-test.js index 32a04162d5..2a04ae47d3 100644 --- a/src/execution/__tests__/variables-test.js +++ b/src/execution/__tests__/variables-test.js @@ -208,9 +208,9 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Argument "input" got invalid value ["foo", "bar", "baz"].\n' + - 'Expected "TestInputObject", found not an object.', + 'Argument "input" has invalid value ["foo", "bar", "baz"].', path: ['fieldWithObjectInput'], + locations: [{ line: 3, column: 39 }], }, ], }); @@ -981,9 +981,7 @@ describe('Execute: Handles inputs', () => { }, errors: [ { - message: - 'Argument "input" got invalid value WRONG_TYPE.\n' + - 'Expected type "String", found WRONG_TYPE.', + message: 'Argument "input" has invalid value WRONG_TYPE.', locations: [{ line: 2, column: 46 }], path: ['fieldWithDefaultArgumentValue'], }, diff --git a/src/execution/values.js b/src/execution/values.js index b914ce810d..50a79d939b 100644 --- a/src/execution/values.js +++ b/src/execution/values.js @@ -14,7 +14,6 @@ import keyMap from '../jsutils/keyMap'; import { coerceValue } from '../utilities/coerceValue'; import { typeFromAST } from '../utilities/typeFromAST'; import { valueFromAST } from '../utilities/valueFromAST'; -import { isValidLiteralValue } from '../utilities/isValidLiteralValue'; import * as Kind from '../language/kinds'; import { print } from '../language/printer'; import { isInputType, isNonNullType } from '../type/definition'; @@ -164,10 +163,11 @@ export function getArgumentValues( const valueNode = argumentNode.value; const coercedValue = valueFromAST(valueNode, argType, variableValues); if (isInvalid(coercedValue)) { - const errors = isValidLiteralValue(argType, valueNode); - const message = errors ? '\n' + errors.join('\n') : ''; + // Note: ValuesOfCorrectType validation should catch this before + // execution. This is a runtime check to ensure execution does not + // continue with an invalid argument value. throw new GraphQLError( - `Argument "${name}" got invalid value ${print(valueNode)}.${message}`, + `Argument "${name}" has invalid value ${print(valueNode)}.`, [argumentNode.value], ); } diff --git a/src/index.js b/src/index.js index 9955d56509..2e10464399 100644 --- a/src/index.js +++ b/src/index.js @@ -261,8 +261,6 @@ export { // All validation rules in the GraphQL Specification. specifiedRules, // Individual validation rules. - ArgumentsOfCorrectTypeRule, - DefaultValuesOfCorrectTypeRule, FieldsOnCorrectTypeRule, FragmentsOnCompositeTypesRule, KnownArgumentNamesRule, @@ -285,7 +283,9 @@ export { UniqueInputFieldNamesRule, UniqueOperationNamesRule, UniqueVariableNamesRule, + ValuesOfCorrectTypeRule, VariablesAreInputTypesRule, + VariablesDefaultValueAllowedRule, VariablesInAllowedPositionRule, } from './validation'; diff --git a/src/jsutils/orList.js b/src/jsutils/orList.js new file mode 100644 index 0000000000..f8f39bf101 --- /dev/null +++ b/src/jsutils/orList.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-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 + */ + +const MAX_LENGTH = 5; + +/** + * Given [ A, B, C ] return 'A, B, or C'. + */ +export default function orList(items: $ReadOnlyArray): string { + const selected = items.slice(0, MAX_LENGTH); + return selected.reduce( + (list, quoted, index) => + list + + (selected.length > 2 ? ', ' : ' ') + + (index === selected.length - 1 ? 'or ' : '') + + quoted, + ); +} diff --git a/src/jsutils/quotedOrList.js b/src/jsutils/quotedOrList.js index 6a5803864f..78362ca884 100644 --- a/src/jsutils/quotedOrList.js +++ b/src/jsutils/quotedOrList.js @@ -7,20 +7,11 @@ * @flow */ -const MAX_LENGTH = 5; +import orList from './orList'; /** * Given [ A, B, C ] return '"A", "B", or "C"'. */ export default function quotedOrList(items: $ReadOnlyArray): string { - const selected = items.slice(0, MAX_LENGTH); - return selected - .map(item => `"${item}"`) - .reduce( - (list, quoted, index) => - list + - (selected.length > 2 ? ', ' : ' ') + - (index === selected.length - 1 ? 'or ' : '') + - quoted, - ); + return orList(items.map(item => `"${item}"`)); } diff --git a/src/type/__tests__/enumType-test.js b/src/type/__tests__/enumType-test.js index 656d67f242..e1ba722c5e 100644 --- a/src/type/__tests__/enumType-test.js +++ b/src/type/__tests__/enumType-test.js @@ -161,8 +161,21 @@ describe('Type System: Enum Values', () => { errors: [ { message: - 'Argument "fromEnum" has invalid value "GREEN".' + - '\nExpected type "Color", found "GREEN".', + 'Expected type Color, found "GREEN"; Did you mean the enum value: GREEN?', + locations: [{ line: 1, column: 23 }], + }, + ], + }); + }); + + it('does not accept values not in the enum', async () => { + expect( + await graphql(schema, '{ colorEnum(fromEnum: GREENISH) }'), + ).to.jsonEqual({ + errors: [ + { + message: + 'Expected type Color, found GREENISH; Did you mean the enum value: GREEN?', locations: [{ line: 1, column: 23 }], }, ], @@ -180,6 +193,7 @@ describe('Type System: Enum Values', () => { { message: 'Expected a value of type "Color" but received: GREEN', locations: [{ line: 1, column: 3 }], + path: ['colorEnum'], }, ], }); @@ -189,9 +203,7 @@ describe('Type System: Enum Values', () => { expect(await graphql(schema, '{ colorEnum(fromEnum: 1) }')).to.jsonEqual({ errors: [ { - message: - 'Argument "fromEnum" has invalid value 1.' + - '\nExpected type "Color", found 1.', + message: 'Expected type Color, found 1.', locations: [{ line: 1, column: 23 }], }, ], @@ -203,9 +215,7 @@ describe('Type System: Enum Values', () => { { errors: [ { - message: - 'Argument "fromInt" has invalid value GREEN.' + - '\nExpected type "Int", found GREEN.', + message: 'Expected type Int, found GREEN.', locations: [{ line: 1, column: 22 }], }, ], diff --git a/src/utilities/TypeInfo.js b/src/utilities/TypeInfo.js index 8ac82f2e85..6d3ce7998a 100644 --- a/src/utilities/TypeInfo.js +++ b/src/utilities/TypeInfo.js @@ -62,6 +62,9 @@ export class TypeInfo { // to support non-spec-compliant codebases. You should never need to use it. // It may disappear in the future. getFieldDefFn?: typeof getFieldDef, + // Initial type may be provided in rare cases to facilitate traversals + // beginning somewhere other than documents. + initialType?: GraphQLType, ): void { this._schema = schema; this._typeStack = []; @@ -72,6 +75,17 @@ export class TypeInfo { this._argument = null; this._enumValue = null; this._getFieldDef = getFieldDefFn || getFieldDef; + if (initialType) { + if (isInputType(initialType)) { + this._inputTypeStack.push(initialType); + } + if (isCompositeType(initialType)) { + this._parentTypeStack.push(initialType); + } + if (isOutputType(initialType)) { + this._typeStack.push(initialType); + } + } } getType(): ?GraphQLOutputType { @@ -92,6 +106,12 @@ export class TypeInfo { } } + getParentInputType(): ?GraphQLInputType { + if (this._inputTypeStack.length > 1) { + return this._inputTypeStack[this._inputTypeStack.length - 2]; + } + } + getFieldDef(): ?GraphQLField<*, *> { if (this._fieldDefStack.length > 0) { return this._fieldDefStack[this._fieldDefStack.length - 1]; @@ -183,10 +203,9 @@ export class TypeInfo { break; case Kind.LIST: const listType: mixed = getNullableType(this.getInputType()); - let itemType: mixed; - if (isListType(listType)) { - itemType = listType.ofType; - } + const itemType: mixed = isListType(listType) + ? listType.ofType + : listType; this._inputTypeStack.push(isInputType(itemType) ? itemType : undefined); break; case Kind.OBJECT_FIELD: diff --git a/src/utilities/__tests__/isValidLiteralValue-test.js b/src/utilities/__tests__/isValidLiteralValue-test.js new file mode 100644 index 0000000000..21f9949e2e --- /dev/null +++ b/src/utilities/__tests__/isValidLiteralValue-test.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-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. + */ + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { isValidLiteralValue } from '../isValidLiteralValue'; +import { parseValue } from '../../language'; +import { GraphQLInt } from '../../type'; + +describe('isValidLiteralValue', () => { + it('Returns no errors for a valid value', () => { + expect(isValidLiteralValue(GraphQLInt, parseValue('123'))).to.deep.equal( + [], + ); + }); + + it('Returns errors for an invalid value', () => { + expect(isValidLiteralValue(GraphQLInt, parseValue('"abc"'))).to.deep.equal([ + { + message: 'Expected type Int, found "abc".', + locations: [{ line: 1, column: 1 }], + path: undefined, + }, + ]); + }); +}); diff --git a/src/utilities/isValidLiteralValue.js b/src/utilities/isValidLiteralValue.js index 4d9e15f6b1..43d45f14bf 100644 --- a/src/utilities/isValidLiteralValue.js +++ b/src/utilities/isValidLiteralValue.js @@ -7,123 +7,30 @@ * @flow */ -import { print } from '../language/printer'; -import type { - ValueNode, - ListValueNode, - ObjectValueNode, -} from '../language/ast'; -import * as Kind from '../language/kinds'; -import { - isScalarType, - isEnumType, - isInputObjectType, - isListType, - isNonNullType, -} from '../type/definition'; +import { TypeInfo } from './TypeInfo'; +import type { GraphQLError } from '../error/GraphQLError'; +import type { ValueNode } from '../language/ast'; +import { DOCUMENT } from '../language/kinds'; +import { visit, visitWithTypeInfo } from '../language/visitor'; import type { GraphQLInputType } from '../type/definition'; -import isInvalid from '../jsutils/isInvalid'; -import keyMap from '../jsutils/keyMap'; +import { GraphQLSchema } from '../type/schema'; +import { ValuesOfCorrectType } from '../validation/rules/ValuesOfCorrectType'; +import { ValidationContext } from '../validation/validate'; /** - * Utility for validators which determines if a value literal node is valid - * given an input type. + * Utility which determines if a value literal node is valid for an input type. * - * Note that this only validates literal values, variables are assumed to - * provide values of the correct type. + * Deprecated. Rely on validation for documents containing literal values. */ export function isValidLiteralValue( type: GraphQLInputType, valueNode: ValueNode, -): Array { - // A value must be provided if the type is non-null. - if (isNonNullType(type)) { - if (!valueNode || valueNode.kind === Kind.NULL) { - return [`Expected "${String(type)}", found null.`]; - } - return isValidLiteralValue(type.ofType, valueNode); - } - - if (!valueNode || valueNode.kind === Kind.NULL) { - return []; - } - - // This function only tests literals, and assumes variables will provide - // values of the correct type. - if (valueNode.kind === Kind.VARIABLE) { - return []; - } - - // Lists accept a non-list value as a list of one. - if (isListType(type)) { - const itemType = type.ofType; - if (valueNode.kind === Kind.LIST) { - return (valueNode: ListValueNode).values.reduce((acc, item, index) => { - const errors = isValidLiteralValue(itemType, item); - return acc.concat( - errors.map(error => `In element #${index}: ${error}`), - ); - }, []); - } - return isValidLiteralValue(itemType, valueNode); - } - - // Input objects check each defined field and look for undefined fields. - if (isInputObjectType(type)) { - if (valueNode.kind !== Kind.OBJECT) { - return [`Expected "${type.name}", found not an object.`]; - } - const fields = type.getFields(); - - const errors = []; - - // Ensure every provided field is defined. - const fieldNodes = (valueNode: ObjectValueNode).fields; - fieldNodes.forEach(providedFieldNode => { - if (!fields[providedFieldNode.name.value]) { - errors.push( - `In field "${providedFieldNode.name.value}": Unknown field.`, - ); - } - }); - - // Ensure every defined field is valid. - const fieldNodeMap = keyMap(fieldNodes, fieldNode => fieldNode.name.value); - Object.keys(fields).forEach(fieldName => { - const result = isValidLiteralValue( - fields[fieldName].type, - fieldNodeMap[fieldName] && fieldNodeMap[fieldName].value, - ); - errors.push(...result.map(error => `In field "${fieldName}": ${error}`)); - }); - - return errors; - } - - if (isEnumType(type)) { - if (valueNode.kind !== Kind.ENUM || !type.getValue(valueNode.value)) { - return [`Expected type "${type.name}", found ${print(valueNode)}.`]; - } - - return []; - } - - if (isScalarType(type)) { - // Scalars determine if a literal value is valid via parseLiteral(). - try { - const parseResult = type.parseLiteral(valueNode, null); - if (isInvalid(parseResult)) { - return [`Expected type "${type.name}", found ${print(valueNode)}.`]; - } - } catch (error) { - const printed = print(valueNode); - const message = error.message; - return [`Expected type "${type.name}", found ${printed}; ${message}`]; - } - - return []; - } - - /* istanbul ignore next */ - throw new Error(`Unknown type: ${(type: empty)}.`); +): $ReadOnlyArray { + const emptySchema = new GraphQLSchema({}); + const emptyDoc = { kind: DOCUMENT, definitions: [] }; + const typeInfo = new TypeInfo(emptySchema, undefined, type); + const context = new ValidationContext(emptySchema, emptyDoc, typeInfo); + const visitor = ValuesOfCorrectType(context); + visit(valueNode, visitWithTypeInfo(typeInfo, visitor)); + return context.getErrors(); } diff --git a/src/validation/__tests__/DefaultValuesOfCorrectType-test.js b/src/validation/__tests__/DefaultValuesOfCorrectType-test.js deleted file mode 100644 index ee93686525..0000000000 --- a/src/validation/__tests__/DefaultValuesOfCorrectType-test.js +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Copyright (c) 2015-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. - */ - -import { describe, it } from 'mocha'; -import { expectPassesRule, expectFailsRule } from './harness'; -import { - DefaultValuesOfCorrectType, - defaultForNonNullArgMessage, - badValueForDefaultArgMessage, -} from '../rules/DefaultValuesOfCorrectType'; - -function defaultForNonNullArg(varName, typeName, guessTypeName, line, column) { - return { - message: defaultForNonNullArgMessage(varName, typeName, guessTypeName), - locations: [{ line, column }], - path: undefined, - }; -} - -function badValue(varName, typeName, val, line, column, errors) { - let realErrors; - if (!errors) { - realErrors = [`Expected type "${typeName}", found ${val}.`]; - } else { - realErrors = errors; - } - return { - message: badValueForDefaultArgMessage(varName, typeName, val, realErrors), - locations: [{ line, column }], - path: undefined, - }; -} - -describe('Validate: Variable default values of correct type', () => { - it('variables with no default values', () => { - expectPassesRule( - DefaultValuesOfCorrectType, - ` - query NullableValues($a: Int, $b: String, $c: ComplexInput) { - dog { name } - } - `, - ); - }); - - it('required variables without default values', () => { - expectPassesRule( - DefaultValuesOfCorrectType, - ` - query RequiredValues($a: Int!, $b: String!) { - dog { name } - } - `, - ); - }); - - it('variables with valid default values', () => { - expectPassesRule( - DefaultValuesOfCorrectType, - ` - query WithDefaultValues( - $a: Int = 1, - $b: String = "ok", - $c: ComplexInput = { requiredField: true, intField: 3 } - ) { - dog { name } - } - `, - ); - }); - - it('variables with valid default null values', () => { - expectPassesRule( - DefaultValuesOfCorrectType, - ` - query WithDefaultValues( - $a: Int = null, - $b: String = null, - $c: ComplexInput = { requiredField: true, intField: null } - ) { - dog { name } - } - `, - ); - }); - - it('variables with invalid default null values', () => { - expectFailsRule( - DefaultValuesOfCorrectType, - ` - query WithDefaultValues( - $a: Int! = null, - $b: String! = null, - $c: ComplexInput = { requiredField: null, intField: null } - ) { - dog { name } - } - `, - [ - defaultForNonNullArg('a', 'Int!', 'Int', 3, 20), - badValue('a', 'Int!', 'null', 3, 20, ['Expected "Int!", found null.']), - defaultForNonNullArg('b', 'String!', 'String', 4, 23), - badValue('b', 'String!', 'null', 4, 23, [ - 'Expected "String!", found null.', - ]), - badValue( - 'c', - 'ComplexInput', - '{requiredField: null, intField: null}', - 5, - 28, - ['In field "requiredField": Expected "Boolean!", found null.'], - ), - ], - ); - }); - - it('no required variables with default values', () => { - expectFailsRule( - DefaultValuesOfCorrectType, - ` - query UnreachableDefaultValues($a: Int! = 3, $b: String! = "default") { - dog { name } - } - `, - [ - defaultForNonNullArg('a', 'Int!', 'Int', 2, 49), - defaultForNonNullArg('b', 'String!', 'String', 2, 66), - ], - ); - }); - - it('variables with invalid default values', () => { - expectFailsRule( - DefaultValuesOfCorrectType, - ` - query InvalidDefaultValues( - $a: Int = "one", - $b: String = 4, - $c: ComplexInput = "notverycomplex" - ) { - dog { name } - } - `, - [ - badValue('a', 'Int', '"one"', 3, 19, [ - 'Expected type "Int", found "one".', - ]), - badValue('b', 'String', '4', 4, 22, [ - 'Expected type "String", found 4.', - ]), - badValue('c', 'ComplexInput', '"notverycomplex"', 5, 28, [ - 'Expected "ComplexInput", found not an object.', - ]), - ], - ); - }); - - it('complex variables missing required field', () => { - expectFailsRule( - DefaultValuesOfCorrectType, - ` - query MissingRequiredField($a: ComplexInput = {intField: 3}) { - dog { name } - } - `, - [ - badValue('a', 'ComplexInput', '{intField: 3}', 2, 53, [ - 'In field "requiredField": Expected "Boolean!", found null.', - ]), - ], - ); - }); - - it('list variables with invalid item', () => { - expectFailsRule( - DefaultValuesOfCorrectType, - ` - query InvalidItem($a: [String] = ["one", 2]) { - dog { name } - } - `, - [ - badValue('a', '[String]', '["one", 2]', 2, 40, [ - 'In element #1: Expected type "String", found 2.', - ]), - ], - ); - }); -}); diff --git a/src/validation/__tests__/ArgumentsOfCorrectType-test.js b/src/validation/__tests__/ValuesOfCorrectType-test.js similarity index 66% rename from src/validation/__tests__/ArgumentsOfCorrectType-test.js rename to src/validation/__tests__/ValuesOfCorrectType-test.js index 23443f0e80..21f1b4f7c2 100644 --- a/src/validation/__tests__/ArgumentsOfCorrectType-test.js +++ b/src/validation/__tests__/ValuesOfCorrectType-test.js @@ -6,31 +6,44 @@ */ import { describe, it } from 'mocha'; +import { expect } from 'chai'; import { expectPassesRule, expectFailsRule } from './harness'; import { - ArgumentsOfCorrectType, + ValuesOfCorrectType, badValueMessage, -} from '../rules/ArgumentsOfCorrectType'; - -function badValue(argName, typeName, value, line, column, errors) { - let realErrors; - if (!errors) { - realErrors = [`Expected type "${typeName}", found ${value}.`]; - } else { - realErrors = errors; - } + requiredFieldMessage, + unknownFieldMessage, +} from '../rules/ValuesOfCorrectType'; + +function badValue(typeName, value, line, column, message) { + return { + message: badValueMessage(typeName, value, message), + locations: [{ line, column }], + path: undefined, + }; +} + +function requiredField(typeName, fieldName, fieldTypeName, line, column) { return { - message: badValueMessage(argName, typeName, value, realErrors), + message: requiredFieldMessage(typeName, fieldName, fieldTypeName), locations: [{ line, column }], path: undefined, }; } -describe('Validate: Argument values of correct type', () => { +function unknownField(typeName, fieldName, line, column) { + return { + message: unknownFieldMessage(typeName, fieldName), + locations: [{ line, column }], + path: undefined, + }; +} + +describe('Validate: Values of correct type', () => { describe('Valid values', () => { it('Good int value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -43,7 +56,7 @@ describe('Validate: Argument values of correct type', () => { it('Good negative int value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -56,7 +69,7 @@ describe('Validate: Argument values of correct type', () => { it('Good boolean value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -69,7 +82,7 @@ describe('Validate: Argument values of correct type', () => { it('Good string value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -82,7 +95,7 @@ describe('Validate: Argument values of correct type', () => { it('Good float value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -95,7 +108,7 @@ describe('Validate: Argument values of correct type', () => { it('Good negative float value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -108,7 +121,7 @@ describe('Validate: Argument values of correct type', () => { it('Int into Float', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -121,7 +134,7 @@ describe('Validate: Argument values of correct type', () => { it('Int into ID', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -134,7 +147,7 @@ describe('Validate: Argument values of correct type', () => { it('String into ID', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -147,7 +160,7 @@ describe('Validate: Argument values of correct type', () => { it('Good enum value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog { @@ -160,7 +173,7 @@ describe('Validate: Argument values of correct type', () => { it('Enum with undefined value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -173,7 +186,7 @@ describe('Validate: Argument values of correct type', () => { it('Enum with null value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -186,7 +199,7 @@ describe('Validate: Argument values of correct type', () => { it('null into nullable type', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -197,7 +210,7 @@ describe('Validate: Argument values of correct type', () => { ); expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog(a: null, b: null, c:{ requiredField: true, intField: null }) { @@ -212,7 +225,7 @@ describe('Validate: Argument values of correct type', () => { describe('Invalid String values', () => { it('Int into String', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -220,13 +233,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('stringArg', 'String', '1', 4, 39)], + [badValue('String', '1', 4, 39)], ); }); it('Float into String', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -234,13 +247,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('stringArg', 'String', '1.0', 4, 39)], + [badValue('String', '1.0', 4, 39)], ); }); it('Boolean into String', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -248,13 +261,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('stringArg', 'String', 'true', 4, 39)], + [badValue('String', 'true', 4, 39)], ); }); it('Unquoted String into String', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -262,7 +275,7 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('stringArg', 'String', 'BAR', 4, 39)], + [badValue('String', 'BAR', 4, 39)], ); }); }); @@ -270,7 +283,7 @@ describe('Validate: Argument values of correct type', () => { describe('Invalid Int values', () => { it('String into Int', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -278,13 +291,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('intArg', 'Int', '"3"', 4, 33)], + [badValue('Int', '"3"', 4, 33)], ); }); it('Big Int into Int', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -292,13 +305,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('intArg', 'Int', '829384293849283498239482938', 4, 33)], + [badValue('Int', '829384293849283498239482938', 4, 33)], ); }); it('Unquoted String into Int', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -306,13 +319,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('intArg', 'Int', 'FOO', 4, 33)], + [badValue('Int', 'FOO', 4, 33)], ); }); it('Simple Float into Int', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -320,13 +333,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('intArg', 'Int', '3.0', 4, 33)], + [badValue('Int', '3.0', 4, 33)], ); }); it('Float into Int', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -334,7 +347,7 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('intArg', 'Int', '3.333', 4, 33)], + [badValue('Int', '3.333', 4, 33)], ); }); }); @@ -342,7 +355,7 @@ describe('Validate: Argument values of correct type', () => { describe('Invalid Float values', () => { it('String into Float', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -350,13 +363,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('floatArg', 'Float', '"3.333"', 4, 37)], + [badValue('Float', '"3.333"', 4, 37)], ); }); it('Boolean into Float', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -364,13 +377,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('floatArg', 'Float', 'true', 4, 37)], + [badValue('Float', 'true', 4, 37)], ); }); it('Unquoted into Float', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -378,7 +391,7 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('floatArg', 'Float', 'FOO', 4, 37)], + [badValue('Float', 'FOO', 4, 37)], ); }); }); @@ -386,7 +399,7 @@ describe('Validate: Argument values of correct type', () => { describe('Invalid Boolean value', () => { it('Int into Boolean', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -394,13 +407,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('booleanArg', 'Boolean', '2', 4, 41)], + [badValue('Boolean', '2', 4, 41)], ); }); it('Float into Boolean', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -408,13 +421,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('booleanArg', 'Boolean', '1.0', 4, 41)], + [badValue('Boolean', '1.0', 4, 41)], ); }); it('String into Boolean', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -422,13 +435,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('booleanArg', 'Boolean', '"true"', 4, 41)], + [badValue('Boolean', '"true"', 4, 41)], ); }); it('Unquoted into Boolean', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -436,7 +449,7 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('booleanArg', 'Boolean', 'TRUE', 4, 41)], + [badValue('Boolean', 'TRUE', 4, 41)], ); }); }); @@ -444,7 +457,7 @@ describe('Validate: Argument values of correct type', () => { describe('Invalid ID value', () => { it('Float into ID', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -452,13 +465,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('idArg', 'ID', '1.0', 4, 31)], + [badValue('ID', '1.0', 4, 31)], ); }); it('Boolean into ID', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -466,13 +479,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('idArg', 'ID', 'true', 4, 31)], + [badValue('ID', 'true', 4, 31)], ); }); it('Unquoted into ID', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -480,7 +493,7 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('idArg', 'ID', 'SOMETHING', 4, 31)], + [badValue('ID', 'SOMETHING', 4, 31)], ); }); }); @@ -488,7 +501,7 @@ describe('Validate: Argument values of correct type', () => { describe('Invalid Enum value', () => { it('Int into Enum', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog { @@ -496,13 +509,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('dogCommand', 'DogCommand', '2', 4, 41)], + [badValue('DogCommand', '2', 4, 41)], ); }); it('Float into Enum', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog { @@ -510,13 +523,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('dogCommand', 'DogCommand', '1.0', 4, 41)], + [badValue('DogCommand', '1.0', 4, 41)], ); }); it('String into Enum', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog { @@ -524,13 +537,21 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('dogCommand', 'DogCommand', '"SIT"', 4, 41)], + [ + badValue( + 'DogCommand', + '"SIT"', + 4, + 41, + 'Did you mean the enum value: SIT?', + ), + ], ); }); it('Boolean into Enum', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog { @@ -538,13 +559,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('dogCommand', 'DogCommand', 'true', 4, 41)], + [badValue('DogCommand', 'true', 4, 41)], ); }); it('Unknown Enum Value into Enum', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog { @@ -552,13 +573,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('dogCommand', 'DogCommand', 'JUGGLE', 4, 41)], + [badValue('DogCommand', 'JUGGLE', 4, 41)], ); }); it('Different case Enum Value into Enum', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog { @@ -566,7 +587,7 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('dogCommand', 'DogCommand', 'sit', 4, 41)], + [badValue('DogCommand', 'sit', 4, 41)], ); }); }); @@ -574,7 +595,7 @@ describe('Validate: Argument values of correct type', () => { describe('Valid List value', () => { it('Good list value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -587,7 +608,7 @@ describe('Validate: Argument values of correct type', () => { it('Empty list value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -600,7 +621,7 @@ describe('Validate: Argument values of correct type', () => { it('Null value', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -613,7 +634,7 @@ describe('Validate: Argument values of correct type', () => { it('Single value into List', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -628,7 +649,7 @@ describe('Validate: Argument values of correct type', () => { describe('Invalid List value', () => { it('Incorrect item type', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -636,17 +657,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [ - badValue('stringListArg', '[String]', '["one", 2]', 4, 47, [ - 'In element #1: Expected type "String", found 2.', - ]), - ], + [badValue('String', '2', 4, 55)], ); }); it('Single value of incorrect type', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -654,7 +671,7 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('stringListArg', 'String', '1', 4, 47)], + [badValue('[String]', '1', 4, 47)], ); }); }); @@ -662,7 +679,7 @@ describe('Validate: Argument values of correct type', () => { describe('Valid non-nullable value', () => { it('Arg on optional arg', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog { @@ -675,7 +692,7 @@ describe('Validate: Argument values of correct type', () => { it('No Arg on optional arg', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog { @@ -688,7 +705,7 @@ describe('Validate: Argument values of correct type', () => { it('Multiple args', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -701,7 +718,7 @@ describe('Validate: Argument values of correct type', () => { it('Multiple args reverse order', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -714,7 +731,7 @@ describe('Validate: Argument values of correct type', () => { it('No args on multiple optional', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -727,7 +744,7 @@ describe('Validate: Argument values of correct type', () => { it('One arg on multiple optional', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -740,7 +757,7 @@ describe('Validate: Argument values of correct type', () => { it('Second arg on multiple optional', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -753,7 +770,7 @@ describe('Validate: Argument values of correct type', () => { it('Multiple reqs on mixedList', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -766,7 +783,7 @@ describe('Validate: Argument values of correct type', () => { it('Multiple reqs and one opt on mixedList', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -779,7 +796,7 @@ describe('Validate: Argument values of correct type', () => { it('All reqs and opts on mixedList', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -794,7 +811,7 @@ describe('Validate: Argument values of correct type', () => { describe('Invalid non-nullable value', () => { it('Incorrect value type', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -802,16 +819,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [ - badValue('req2', 'Int', '"two"', 4, 32), - badValue('req1', 'Int', '"one"', 4, 45), - ], + [badValue('Int!', '"two"', 4, 32), badValue('Int!', '"one"', 4, 45)], ); }); - it('Incorrect value and missing argument', () => { + it('Incorrect value and missing argument (ProvidedNonNullArguments)', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -819,13 +833,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [badValue('req1', 'Int', '"one"', 4, 32)], + [badValue('Int!', '"one"', 4, 32)], ); }); it('Null value', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -833,11 +847,7 @@ describe('Validate: Argument values of correct type', () => { } } `, - [ - badValue('req1', 'Int!', 'null', 4, 32, [ - 'Expected "Int!", found null.', - ]), - ], + [badValue('Int!', 'null', 4, 32)], ); }); }); @@ -845,7 +855,7 @@ describe('Validate: Argument values of correct type', () => { describe('Valid input object value', () => { it('Optional arg, despite required field in type', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -858,7 +868,7 @@ describe('Validate: Argument values of correct type', () => { it('Partial object, only required', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -871,7 +881,7 @@ describe('Validate: Argument values of correct type', () => { it('Partial object, required field can be falsey', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -884,7 +894,7 @@ describe('Validate: Argument values of correct type', () => { it('Partial object, including required', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -897,7 +907,7 @@ describe('Validate: Argument values of correct type', () => { it('Full object', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -916,7 +926,7 @@ describe('Validate: Argument values of correct type', () => { it('Full object with fields in different order', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -937,7 +947,7 @@ describe('Validate: Argument values of correct type', () => { describe('Invalid input object value', () => { it('Partial object, missing required', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -945,17 +955,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [ - badValue('complexArg', 'ComplexInput', '{intField: 4}', 4, 41, [ - 'In field "requiredField": Expected "Boolean!", found null.', - ]), - ], + [requiredField('ComplexInput', 'requiredField', 'Boolean!', 4, 41)], ); }); it('Partial object, invalid field type', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -966,25 +972,13 @@ describe('Validate: Argument values of correct type', () => { } } `, - [ - badValue( - 'complexArg', - 'ComplexInput', - '{stringListField: ["one", 2], requiredField: true}', - 4, - 41, - [ - 'In field "stringListField": In element #1: ' + - 'Expected type "String", found 2.', - ], - ), - ], + [badValue('String', '2', 5, 40)], ); }); it('Partial object, unknown field arg', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { complicatedArgs { @@ -994,25 +988,53 @@ describe('Validate: Argument values of correct type', () => { }) } } + `, + [unknownField('ComplexInput', 'unknownField', 6, 15)], + ); + }); + + it('reports original error for custom scalar which throws', () => { + const errors = expectFailsRule( + ValuesOfCorrectType, + ` + { + invalidArg(arg: 123) + } `, [ badValue( - 'complexArg', - 'ComplexInput', - '{requiredField: true, unknownField: "value"}', - 4, - 41, - ['In field "unknownField": Unknown field.'], + 'Invalid', + '123', + 3, + 27, + 'Invalid scalar is always invalid: 123', ), ], ); + expect(errors[0].originalError.message).to.equal( + 'Invalid scalar is always invalid: 123', + ); + }); + + it('allows custom scalar to accept complex literals', () => { + expectPassesRule( + ValuesOfCorrectType, + ` + { + test1: anyArg(arg: 123) + test2: anyArg(arg: "abc") + test3: anyArg(arg: [123, "abc"]) + test4: anyArg(arg: {deep: [123, "abc"]}) + } + `, + ); }); }); describe('Directive arguments', () => { it('with directives of valid types', () => { expectPassesRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog @include(if: true) { @@ -1028,7 +1050,7 @@ describe('Validate: Argument values of correct type', () => { it('with directive with incorrect types', () => { expectFailsRule( - ArgumentsOfCorrectType, + ValuesOfCorrectType, ` { dog @include(if: "yes") { @@ -1037,10 +1059,120 @@ describe('Validate: Argument values of correct type', () => { } `, [ - badValue('if', 'Boolean', '"yes"', 3, 28), - badValue('if', 'Boolean', 'ENUM', 4, 28), + badValue('Boolean!', '"yes"', 3, 28), + badValue('Boolean!', 'ENUM', 4, 28), ], ); }); }); + + describe('Variable default values', () => { + it('variables with valid default values', () => { + expectPassesRule( + ValuesOfCorrectType, + ` + query WithDefaultValues( + $a: Int = 1, + $b: String = "ok", + $c: ComplexInput = { requiredField: true, intField: 3 } + ) { + dog { name } + } + `, + ); + }); + + it('variables with valid default null values', () => { + expectPassesRule( + ValuesOfCorrectType, + ` + query WithDefaultValues( + $a: Int = null, + $b: String = null, + $c: ComplexInput = { requiredField: true, intField: null } + ) { + dog { name } + } + `, + ); + }); + + it('variables with invalid default null values', () => { + expectFailsRule( + ValuesOfCorrectType, + ` + query WithDefaultValues( + $a: Int! = null, + $b: String! = null, + $c: ComplexInput = { requiredField: null, intField: null } + ) { + dog { name } + } + `, + [ + badValue('Int!', 'null', 3, 22), + badValue('String!', 'null', 4, 25), + badValue('Boolean!', 'null', 5, 47), + ], + ); + }); + + it('variables with invalid default values', () => { + expectFailsRule( + ValuesOfCorrectType, + ` + query InvalidDefaultValues( + $a: Int = "one", + $b: String = 4, + $c: ComplexInput = "notverycomplex" + ) { + dog { name } + } + `, + [ + badValue('Int', '"one"', 3, 21), + badValue('String', '4', 4, 24), + badValue('ComplexInput', '"notverycomplex"', 5, 30), + ], + ); + }); + + it('variables with complex invalid default values', () => { + expectFailsRule( + ValuesOfCorrectType, + ` + query WithDefaultValues( + $a: ComplexInput = { requiredField: 123, intField: "abc" } + ) { + dog { name } + } + `, + [badValue('Boolean!', '123', 3, 47), badValue('Int', '"abc"', 3, 62)], + ); + }); + + it('complex variables missing required field', () => { + expectFailsRule( + ValuesOfCorrectType, + ` + query MissingRequiredField($a: ComplexInput = {intField: 3}) { + dog { name } + } + `, + [requiredField('ComplexInput', 'requiredField', 'Boolean!', 2, 55)], + ); + }); + + it('list variables with invalid item', () => { + expectFailsRule( + ValuesOfCorrectType, + ` + query InvalidItem($a: [String] = ["one", 2]) { + dog { name } + } + `, + [badValue('String', '2', 2, 50)], + ); + }); + }); }); diff --git a/src/validation/__tests__/VariablesDefaultValueAllowed-test.js b/src/validation/__tests__/VariablesDefaultValueAllowed-test.js new file mode 100644 index 0000000000..12c084d983 --- /dev/null +++ b/src/validation/__tests__/VariablesDefaultValueAllowed-test.js @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2015-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. + */ + +import { describe, it } from 'mocha'; +import { expectPassesRule, expectFailsRule } from './harness'; +import { + VariablesDefaultValueAllowed, + defaultForRequiredVarMessage, +} from '../rules/VariablesDefaultValueAllowed'; + +function defaultForRequiredVar(varName, typeName, guessTypeName, line, column) { + return { + message: defaultForRequiredVarMessage(varName, typeName, guessTypeName), + locations: [{ line, column }], + path: undefined, + }; +} + +describe('Validate: Variable default value is allowed', () => { + it('variables with no default values', () => { + expectPassesRule( + VariablesDefaultValueAllowed, + ` + query NullableValues($a: Int, $b: String, $c: ComplexInput) { + dog { name } + } + `, + ); + }); + + it('required variables without default values', () => { + expectPassesRule( + VariablesDefaultValueAllowed, + ` + query RequiredValues($a: Int!, $b: String!) { + dog { name } + } + `, + ); + }); + + it('variables with valid default values', () => { + expectPassesRule( + VariablesDefaultValueAllowed, + ` + query WithDefaultValues( + $a: Int = 1, + $b: String = "ok", + $c: ComplexInput = { requiredField: true, intField: 3 } + ) { + dog { name } + } + `, + ); + }); + + it('variables with valid default null values', () => { + expectPassesRule( + VariablesDefaultValueAllowed, + ` + query WithDefaultValues( + $a: Int = null, + $b: String = null, + $c: ComplexInput = { requiredField: true, intField: null } + ) { + dog { name } + } + `, + ); + }); + + it('no required variables with default values', () => { + expectFailsRule( + VariablesDefaultValueAllowed, + ` + query UnreachableDefaultValues($a: Int! = 3, $b: String! = "default") { + dog { name } + } + `, + [ + defaultForRequiredVar('a', 'Int!', 'Int', 2, 49), + defaultForRequiredVar('b', 'String!', 'String', 2, 66), + ], + ); + }); + + it('variables with invalid default null values', () => { + expectFailsRule( + VariablesDefaultValueAllowed, + ` + query WithDefaultValues($a: Int! = null, $b: String! = null) { + dog { name } + } + `, + [ + defaultForRequiredVar('a', 'Int!', 'Int', 2, 42), + defaultForRequiredVar('b', 'String!', 'String', 2, 62), + ], + ); + }); +}); diff --git a/src/validation/__tests__/harness.js b/src/validation/__tests__/harness.js index 67f430c068..6ef9725db3 100644 --- a/src/validation/__tests__/harness.js +++ b/src/validation/__tests__/harness.js @@ -283,6 +283,19 @@ const InvalidScalar = new GraphQLScalarType({ }, }); +const AnyScalar = new GraphQLScalarType({ + name: 'Any', + serialize(value) { + return value; + }, + parseLiteral(node) { + return node; // Allows any value + }, + parseValue(value) { + return value; // Allows any value + }, +}); + const QueryRoot = new GraphQLObjectType({ name: 'QueryRoot', fields: () => ({ @@ -304,6 +317,12 @@ const QueryRoot = new GraphQLObjectType({ }, type: GraphQLString, }, + anyArg: { + args: { + arg: { type: AnyScalar }, + }, + type: GraphQLString, + }, }), }); @@ -397,6 +416,7 @@ function expectInvalid(schema, rules, queryString, expectedErrors) { const errors = validate(schema, parse(queryString), rules); expect(errors).to.have.length.of.at.least(1, 'Should not validate'); expect(errors.map(formatError)).to.deep.equal(expectedErrors); + return errors; } export function expectPassesRule(rule, queryString) { diff --git a/src/validation/__tests__/validation-test.js b/src/validation/__tests__/validation-test.js index 78690fe3d9..6876d31ac5 100644 --- a/src/validation/__tests__/validation-test.js +++ b/src/validation/__tests__/validation-test.js @@ -48,8 +48,7 @@ describe('Validate: Supports full validation', () => { { locations: [{ line: 3, column: 25 }], message: - 'Argument "arg" has invalid value "bad value".\n' + - 'Expected type "Invalid", found "bad value"; ' + + 'Expected type Invalid, found "bad value"; ' + 'Invalid scalar is always invalid: bad value', path: undefined, }, diff --git a/src/validation/index.js b/src/validation/index.js index f81676aa37..f1e18c08af 100644 --- a/src/validation/index.js +++ b/src/validation/index.js @@ -11,16 +11,6 @@ export { validate, ValidationContext } from './validate'; export { specifiedRules } from './specifiedRules'; -// Spec Section: "Argument Values Type Correctness" -export { - ArgumentsOfCorrectType as ArgumentsOfCorrectTypeRule, -} from './rules/ArgumentsOfCorrectType'; - -// Spec Section: "Variable Default Values Are Correctly Typed" -export { - DefaultValuesOfCorrectType as DefaultValuesOfCorrectTypeRule, -} from './rules/DefaultValuesOfCorrectType'; - // Spec Section: "Field Selections on Objects, Interfaces, and Unions Types" export { FieldsOnCorrectType as FieldsOnCorrectTypeRule, @@ -127,11 +117,21 @@ export { UniqueVariableNames as UniqueVariableNamesRule, } from './rules/UniqueVariableNames'; +// Spec Section: "Values Type Correctness" +export { + ValuesOfCorrectType as ValuesOfCorrectTypeRule, +} from './rules/ValuesOfCorrectType'; + // Spec Section: "Variables are Input Types" export { VariablesAreInputTypes as VariablesAreInputTypesRule, } from './rules/VariablesAreInputTypes'; +// Spec Section: "Variables Default Value Is Allowed" +export { + VariablesDefaultValueAllowed as VariablesDefaultValueAllowedRule, +} from './rules/VariablesDefaultValueAllowed'; + // Spec Section: "All Variable Usages Are Allowed" export { VariablesInAllowedPosition as VariablesInAllowedPositionRule, diff --git a/src/validation/rules/ArgumentsOfCorrectType.js b/src/validation/rules/ArgumentsOfCorrectType.js deleted file mode 100644 index afe88fc096..0000000000 --- a/src/validation/rules/ArgumentsOfCorrectType.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2015-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 - */ - -import type { ValidationContext } from '../index'; -import { GraphQLError } from '../../error'; -import { print } from '../../language/printer'; -import { isValidLiteralValue } from '../../utilities/isValidLiteralValue'; -import type { GraphQLType } from '../../type/definition'; - -export function badValueMessage( - argName: string, - type: GraphQLType, - value: string, - verboseErrors?: string[], -): string { - const message = verboseErrors ? '\n' + verboseErrors.join('\n') : ''; - return `Argument "${argName}" has invalid value ${value}.${message}`; -} - -/** - * Argument values of correct type - * - * A GraphQL document is only valid if all field argument literal values are - * of the type expected by their position. - */ -export function ArgumentsOfCorrectType(context: ValidationContext): any { - return { - Argument(node) { - const argDef = context.getArgument(); - if (argDef) { - const errors = isValidLiteralValue(argDef.type, node.value); - if (errors && errors.length > 0) { - context.reportError( - new GraphQLError( - badValueMessage( - node.name.value, - argDef.type, - print(node.value), - errors, - ), - [node.value], - ), - ); - } - } - return false; - }, - }; -} diff --git a/src/validation/rules/DefaultValuesOfCorrectType.js b/src/validation/rules/DefaultValuesOfCorrectType.js deleted file mode 100644 index 544eca4bff..0000000000 --- a/src/validation/rules/DefaultValuesOfCorrectType.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2015-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 - */ - -import type { ValidationContext } from '../index'; -import { GraphQLError } from '../../error'; -import { print } from '../../language/printer'; -import { isNonNullType } from '../../type/definition'; -import { isValidLiteralValue } from '../../utilities/isValidLiteralValue'; -import type { GraphQLType } from '../../type/definition'; - -export function defaultForNonNullArgMessage( - varName: string, - type: GraphQLType, - guessType: GraphQLType, -): string { - return ( - `Variable "$${varName}" of type "${String(type)}" is required and ` + - 'will not use the default value. ' + - `Perhaps you meant to use type "${String(guessType)}".` - ); -} - -export function badValueForDefaultArgMessage( - varName: string, - type: GraphQLType, - value: string, - verboseErrors?: string[], -): string { - const message = verboseErrors ? '\n' + verboseErrors.join('\n') : ''; - return ( - `Variable "$${varName}" of type "${String(type)}" has invalid ` + - `default value ${value}.${message}` - ); -} - -/** - * Variable default values of correct type - * - * A GraphQL document is only valid if all variable default values are of the - * type expected by their definition. - */ -export function DefaultValuesOfCorrectType(context: ValidationContext): any { - return { - VariableDefinition(node) { - const name = node.variable.name.value; - const defaultValue = node.defaultValue; - const type = context.getInputType(); - if (isNonNullType(type) && defaultValue) { - context.reportError( - new GraphQLError( - defaultForNonNullArgMessage(name, type, type.ofType), - [defaultValue], - ), - ); - } - if (type && defaultValue) { - const errors = isValidLiteralValue(type, defaultValue); - if (errors && errors.length > 0) { - context.reportError( - new GraphQLError( - badValueForDefaultArgMessage( - name, - type, - print(defaultValue), - errors, - ), - [defaultValue], - ), - ); - } - } - return false; - }, - SelectionSet: () => false, - FragmentDefinition: () => false, - }; -} diff --git a/src/validation/rules/ValuesOfCorrectType.js b/src/validation/rules/ValuesOfCorrectType.js new file mode 100644 index 0000000000..34fbefe289 --- /dev/null +++ b/src/validation/rules/ValuesOfCorrectType.js @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2015-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 + */ + +import type { ValidationContext } from '../index'; +import { GraphQLError } from '../../error'; +import type { ValueNode } from '../../language/ast'; +import { print } from '../../language/printer'; +import { + isScalarType, + isEnumType, + isInputObjectType, + isListType, + isNonNullType, + getNullableType, + getNamedType, +} from '../../type/definition'; +import type { GraphQLEnumType } from '../../type/definition'; +import isInvalid from '../../jsutils/isInvalid'; +import keyMap from '../../jsutils/keyMap'; +import orList from '../../jsutils/orList'; +import suggestionList from '../../jsutils/suggestionList'; + +export function badValueMessage( + typeName: string, + valueName: string, + message?: string, +): string { + return ( + `Expected type ${typeName}, found ${valueName}` + + (message ? `; ${message}` : '.') + ); +} + +export function requiredFieldMessage( + typeName: string, + fieldName: string, + fieldTypeName: string, +): string { + return ( + `Field ${typeName}.${fieldName} of required type ` + + `${fieldTypeName} was not provided.` + ); +} + +export function unknownFieldMessage( + typeName: string, + fieldName: string, +): string { + return `Field "${fieldName}" is not defined by type ${typeName}.`; +} + +/** + * Value literals of correct type + * + * A GraphQL document is only valid if all value literals are of the type + * expected at their position. + */ +export function ValuesOfCorrectType(context: ValidationContext): any { + return { + NullValue(node) { + const type = context.getInputType(); + if (isNonNullType(type)) { + context.reportError( + new GraphQLError(badValueMessage(String(type), print(node)), node), + ); + } + }, + ListValue(node) { + // Note: TypeInfo will traverse into a list's item type, so look to the + // parent input type to check if it is a list. + const type = getNullableType(context.getParentInputType()); + if (!isListType(type)) { + isValidScalar(context, node); + return false; // Don't traverse further. + } + }, + ObjectValue(node) { + const type = getNamedType(context.getInputType()); + if (!isInputObjectType(type)) { + isValidScalar(context, node); + return false; // Don't traverse further. + } + // Ensure every required field exists. + const inputFields = type.getFields(); + const fieldNodeMap = keyMap(node.fields, field => field.name.value); + Object.keys(inputFields).forEach(fieldName => { + const fieldType = inputFields[fieldName].type; + const fieldNode = fieldNodeMap[fieldName]; + if (!fieldNode && isNonNullType(fieldType)) { + context.reportError( + new GraphQLError( + requiredFieldMessage(type.name, fieldName, String(fieldType)), + node, + ), + ); + } + }); + }, + ObjectField(node) { + const parentType = getNamedType(context.getParentInputType()); + const fieldType = context.getInputType(); + if (!fieldType && parentType) { + context.reportError( + new GraphQLError( + unknownFieldMessage(parentType.name, node.name.value), + node, + ), + ); + } + }, + EnumValue(node) { + const type = getNamedType(context.getInputType()); + if (!isEnumType(type)) { + isValidScalar(context, node); + } else if (!type.getValue(node.value)) { + context.reportError( + new GraphQLError( + badValueMessage( + type.name, + print(node), + enumTypeSuggestion(type, node), + ), + node, + ), + ); + } + }, + IntValue: node => isValidScalar(context, node), + FloatValue: node => isValidScalar(context, node), + StringValue: node => isValidScalar(context, node), + BooleanValue: node => isValidScalar(context, node), + }; +} + +/** + * Any value literal may be a valid representation of a Scalar, depending on + * that scalar type. + */ +function isValidScalar(context: ValidationContext, node: ValueNode): void { + // Report any error at the full type expected by the location. + const locationType = context.getInputType(); + if (!locationType) { + return; + } + + const type = getNamedType(locationType); + + if (!isScalarType(type)) { + const suggestions = isEnumType(type) + ? enumTypeSuggestion(type, node) + : undefined; + context.reportError( + new GraphQLError( + badValueMessage(String(locationType), print(node), suggestions), + node, + ), + ); + return; + } + + // Scalars determine if a literal value is valid via parseLiteral() which + // may throw or return an invalid value to indicate failure. + try { + const parseResult = type.parseLiteral(node, undefined /* variables */); + if (isInvalid(parseResult)) { + context.reportError( + new GraphQLError( + badValueMessage(String(locationType), print(node)), + node, + ), + ); + } + } catch (error) { + // Ensure a reference to the original error is maintained. + context.reportError( + new GraphQLError( + badValueMessage(String(locationType), print(node), error.message), + node, + undefined, + undefined, + undefined, + error, + ), + ); + } +} + +function enumTypeSuggestion(type: GraphQLEnumType, node: ValueNode): string { + const suggestions = suggestionList( + print(node), + type.getValues().map(value => value.name), + ); + return suggestions.length === 0 + ? '' + : `Did you mean the enum value: ${orList(suggestions)}?`; +} diff --git a/src/validation/rules/VariablesDefaultValueAllowed.js b/src/validation/rules/VariablesDefaultValueAllowed.js new file mode 100644 index 0000000000..9f83614ad5 --- /dev/null +++ b/src/validation/rules/VariablesDefaultValueAllowed.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-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 + */ + +import type { ValidationContext } from '../index'; +import { GraphQLError } from '../../error'; +import { isNonNullType } from '../../type/definition'; +import type { GraphQLType } from '../../type/definition'; + +export function defaultForRequiredVarMessage( + varName: string, + type: GraphQLType, + guessType: GraphQLType, +): string { + return ( + `Variable "$${varName}" of type "${String(type)}" is required and ` + + 'will not use the default value. ' + + `Perhaps you meant to use type "${String(guessType)}".` + ); +} + +/** + * Variable's default value is allowed + * + * A GraphQL document is only valid if all variable default values are allowed + * due to a variable not being required. + */ +export function VariablesDefaultValueAllowed(context: ValidationContext): any { + return { + VariableDefinition(node) { + const name = node.variable.name.value; + const defaultValue = node.defaultValue; + const type = context.getInputType(); + if (isNonNullType(type) && defaultValue) { + context.reportError( + new GraphQLError( + defaultForRequiredVarMessage(name, type, type.ofType), + [defaultValue], + ), + ); + } + return false; // Do not traverse further. + }, + SelectionSet: () => false, + FragmentDefinition: () => false, + }; +} diff --git a/src/validation/specifiedRules.js b/src/validation/specifiedRules.js index 759c1c844d..72292a1783 100644 --- a/src/validation/specifiedRules.js +++ b/src/validation/specifiedRules.js @@ -70,14 +70,14 @@ import { KnownArgumentNames } from './rules/KnownArgumentNames'; // Spec Section: "Argument Uniqueness" import { UniqueArgumentNames } from './rules/UniqueArgumentNames'; -// Spec Section: "Argument Values Type Correctness" -import { ArgumentsOfCorrectType } from './rules/ArgumentsOfCorrectType'; +// Spec Section: "Value Type Correctness" +import { ValuesOfCorrectType } from './rules/ValuesOfCorrectType'; // Spec Section: "Argument Optionality" import { ProvidedNonNullArguments } from './rules/ProvidedNonNullArguments'; -// Spec Section: "Variable Default Values Are Correctly Typed" -import { DefaultValuesOfCorrectType } from './rules/DefaultValuesOfCorrectType'; +// Spec Section: "Variables Default Value Is Allowed" +import { VariablesDefaultValueAllowed } from './rules/VariablesDefaultValueAllowed'; // Spec Section: "All Variable Usages Are Allowed" import { VariablesInAllowedPosition } from './rules/VariablesInAllowedPosition'; @@ -118,9 +118,9 @@ export const specifiedRules: Array<(context: ValidationContext) => any> = [ UniqueDirectivesPerLocation, KnownArgumentNames, UniqueArgumentNames, - ArgumentsOfCorrectType, + ValuesOfCorrectType, ProvidedNonNullArguments, - DefaultValuesOfCorrectType, + VariablesDefaultValueAllowed, VariablesInAllowedPosition, OverlappingFieldsCanBeMerged, UniqueInputFieldNames, diff --git a/src/validation/validate.js b/src/validation/validate.js index 1c37eb3665..33bfa51584 100644 --- a/src/validation/validate.js +++ b/src/validation/validate.js @@ -258,6 +258,10 @@ export class ValidationContext { return this._typeInfo.getInputType(); } + getParentInputType(): ?GraphQLInputType { + return this._typeInfo.getParentInputType(); + } + getFieldDef(): ?GraphQLField<*, *> { return this._typeInfo.getFieldDef(); }