From c1ca9a8b4ed6e915398bf04e8c7b46c9136bca29 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Thu, 27 Oct 2016 12:53:08 +0200 Subject: [PATCH 01/21] language support for NullValue --- src/language/__tests__/parser-test.js | 13 +++++++------ src/language/ast.js | 7 +++++++ src/language/kinds.js | 1 + src/language/parser.js | 16 ++++++++++++---- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/language/__tests__/parser-test.js b/src/language/__tests__/parser-test.js index a26e7b9033..bb425812b8 100644 --- a/src/language/__tests__/parser-test.js +++ b/src/language/__tests__/parser-test.js @@ -91,12 +91,6 @@ fragment MissingOn Type ).to.throw('Syntax Error GraphQL (1:9) Expected Name, found }'); }); - it('does not allow null as value', async () => { - expect( - () => parse('{ fieldWithNullableStringInput(input: null) }') - ).to.throw('Syntax Error GraphQL (1:39) Unexpected Name "null"'); - }); - it('parses multi-byte characters', async () => { // Note: \u0A0A could be naively interpretted as two line-feed chars. expect( @@ -296,6 +290,13 @@ fragment ${fragmentName} on Type { describe('parseValue', () => { + it('parses null value', () => { + expect(parseValue('null')).to.containSubset({ + kind: Kind.NULL, + loc: { start: 0, end: 4 } + }); + }); + it('parses list values', () => { expect(parseValue('[123 "abc"]')).to.containSubset({ kind: Kind.LIST, diff --git a/src/language/ast.js b/src/language/ast.js index 7c3f4b3050..81d52ae87a 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -126,6 +126,7 @@ export type Node = | FloatValue | StringValue | BooleanValue + | NullValue | EnumValue | ListValue | ObjectValue @@ -260,6 +261,7 @@ export type Value = | FloatValue | StringValue | BooleanValue + | NullValue | EnumValue | ListValue | ObjectValue; @@ -288,6 +290,11 @@ export type BooleanValue = { value: boolean; }; +export type NullValue = { + kind: 'NullValue'; + loc?: Location; +}; + export type EnumValue = { kind: 'EnumValue'; loc?: Location; diff --git a/src/language/kinds.js b/src/language/kinds.js index 3b69f086de..774fc1606d 100644 --- a/src/language/kinds.js +++ b/src/language/kinds.js @@ -34,6 +34,7 @@ export const INT = 'IntValue'; export const FLOAT = 'FloatValue'; export const STRING = 'StringValue'; export const BOOLEAN = 'BooleanValue'; +export const NULL = 'NullValue'; export const ENUM = 'EnumValue'; export const LIST = 'ListValue'; export const OBJECT = 'ObjectValue'; diff --git a/src/language/parser.js b/src/language/parser.js index 431529dcc6..af4ee1c715 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -89,6 +89,7 @@ import { FLOAT, STRING, BOOLEAN, + NULL, ENUM, LIST, OBJECT, @@ -503,12 +504,15 @@ function parseFragmentName(lexer: Lexer<*>): Name { * - FloatValue * - StringValue * - BooleanValue + * - NullValue * - EnumValue * - ListValue[?Const] * - ObjectValue[?Const] * * BooleanValue : one of `true` `false` * + * NullValue : `null` + * * EnumValue : Name but not `true`, `false` or `null` */ function parseValueLiteral(lexer: Lexer<*>, isConst: boolean): Value { @@ -547,15 +551,19 @@ function parseValueLiteral(lexer: Lexer<*>, isConst: boolean): Value { value: token.value === 'true', loc: loc(lexer, token) }; - } else if (token.value !== 'null') { + } else if (token.value === 'null') { lexer.advance(); return { - kind: (ENUM: 'EnumValue'), - value: ((token.value: any): string), + kind: (NULL: 'NullValue'), loc: loc(lexer, token) }; } - break; + lexer.advance(); + return { + kind: (ENUM: 'EnumValue'), + value: ((token.value: any): string), + loc: loc(lexer, token) + }; case TokenKind.DOLLAR: if (!isConst) { return parseVariable(lexer); From 24d3de2d3363398e67f231fc5d1e216a23da1cd1 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Thu, 27 Oct 2016 13:25:14 +0200 Subject: [PATCH 02/21] support null literal in Printer --- src/language/__tests__/schema-kitchen-sink.graphql | 1 + src/language/__tests__/schema-printer-test.js | 1 + src/language/printer.js | 1 + src/language/visitor.js | 1 + 4 files changed, 4 insertions(+) diff --git a/src/language/__tests__/schema-kitchen-sink.graphql b/src/language/__tests__/schema-kitchen-sink.graphql index d148276253..a56c0e4cc6 100644 --- a/src/language/__tests__/schema-kitchen-sink.graphql +++ b/src/language/__tests__/schema-kitchen-sink.graphql @@ -17,6 +17,7 @@ type Foo implements Bar { four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String six(argument: InputType = {key: "value"}): Type + seven(argument: Int = null): Type } type AnnotatedObject @onObject(arg: "value") { diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index 29376f32c9..4fa56275bb 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -63,6 +63,7 @@ type Foo implements Bar { four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String six(argument: InputType = {key: "value"}): Type + seven(argument: Int = null): Type } type AnnotatedObject @onObject(arg: "value") { diff --git a/src/language/printer.js b/src/language/printer.js index b908162f1f..e056ce0e9f 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -76,6 +76,7 @@ const printDocASTReducer = { FloatValue: ({ value }) => value, StringValue: ({ value }) => JSON.stringify(value), BooleanValue: ({ value }) => JSON.stringify(value), + NullValue: () => 'null', EnumValue: ({ value }) => value, ListValue: ({ values }) => '[' + join(values, ', ') + ']', ObjectValue: ({ fields }) => '{' + join(fields, ', ') + '}', diff --git a/src/language/visitor.js b/src/language/visitor.js index 480c48a9c0..a8422170dd 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -27,6 +27,7 @@ export const QueryDocumentKeys = { FloatValue: [], StringValue: [], BooleanValue: [], + NullValue: [], EnumValue: [], ListValue: [ 'values' ], ObjectValue: [ 'fields' ], From 48a463d066d845bdc10042aa1a0f15f1314139da Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Thu, 27 Oct 2016 14:03:30 +0200 Subject: [PATCH 03/21] astFromValue returns NullValue for explicit null --- src/utilities/__tests__/astFromValue-test.js | 14 +++++++++++++- src/utilities/astFromValue.js | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/utilities/__tests__/astFromValue-test.js b/src/utilities/__tests__/astFromValue-test.js index a1491a7af9..08c330f419 100644 --- a/src/utilities/__tests__/astFromValue-test.js +++ b/src/utilities/__tests__/astFromValue-test.js @@ -33,10 +33,14 @@ describe('astFromValue', () => { { kind: 'BooleanValue', value: false } ); - expect(astFromValue(null, GraphQLBoolean)).to.deep.equal( + expect(astFromValue(undefined, GraphQLBoolean)).to.deep.equal( null ); + expect(astFromValue(null, GraphQLBoolean)).to.deep.equal( + { kind: 'NullValue' } + ); + expect(astFromValue(0, GraphQLBoolean)).to.deep.equal( { kind: 'BooleanValue', value: false } ); @@ -105,6 +109,10 @@ describe('astFromValue', () => { ); expect(astFromValue(null, GraphQLString)).to.deep.equal( + { kind: 'NullValue' } + ); + + expect(astFromValue(undefined, GraphQLString)).to.deep.equal( null ); }); @@ -133,6 +141,10 @@ describe('astFromValue', () => { ); expect(astFromValue(null, GraphQLID)).to.deep.equal( + { kind: 'NullValue' } + ); + + expect(astFromValue(undefined, GraphQLID)).to.deep.equal( null ); }); diff --git a/src/utilities/astFromValue.js b/src/utilities/astFromValue.js index 0b7fae2fd9..63ff76532b 100644 --- a/src/utilities/astFromValue.js +++ b/src/utilities/astFromValue.js @@ -18,6 +18,7 @@ import type { FloatValue, StringValue, BooleanValue, + NullValue, EnumValue, ListValue, ObjectValue, @@ -28,6 +29,7 @@ import { FLOAT, STRING, BOOLEAN, + NULL, ENUM, LIST, OBJECT, @@ -58,6 +60,7 @@ import { GraphQLID } from '../type/scalars'; * | String | String / Enum Value | * | Number | Int / Float | * | Mixed | Enum Value | + * | null | NullValue | * */ export function astFromValue( @@ -73,6 +76,12 @@ export function astFromValue( return astFromValue(_value, type.ofType); } + // only explicit null, not undefined, NaN + if (_value === null) { + return ({ kind: NULL }: NullValue); + } + + // other nullish values if (isNullish(_value)) { return null; } From c5a24f33f27fc04a390ab2f8d59197cbf3b34925 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 28 Oct 2016 02:29:23 +0200 Subject: [PATCH 04/21] astFromNode does not converts NonNull values to NullValue --- src/utilities/__tests__/astFromValue-test.js | 8 ++++++++ src/utilities/astFromValue.js | 8 +++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/utilities/__tests__/astFromValue-test.js b/src/utilities/__tests__/astFromValue-test.js index 08c330f419..48a5e370db 100644 --- a/src/utilities/__tests__/astFromValue-test.js +++ b/src/utilities/__tests__/astFromValue-test.js @@ -19,6 +19,7 @@ import { GraphQLString, GraphQLBoolean, GraphQLID, + GraphQLNonNull, } from '../../type'; @@ -149,6 +150,13 @@ describe('astFromValue', () => { ); }); + it('does not converts NonNull values to NullValue', () => { + const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean); + expect(astFromValue(null, NonNullBoolean)).to.deep.equal( + null + ); + }); + const complexValue = { someArbitrary: 'complexValue' }; const myEnum = new GraphQLEnumType({ diff --git a/src/utilities/astFromValue.js b/src/utilities/astFromValue.js index 63ff76532b..b350827ba2 100644 --- a/src/utilities/astFromValue.js +++ b/src/utilities/astFromValue.js @@ -71,9 +71,11 @@ export function astFromValue( const _value = value; if (type instanceof GraphQLNonNull) { - // Note: we're not checking that the result is non-null. - // This function is not responsible for validating the input value. - return astFromValue(_value, type.ofType); + const astValue = astFromValue(_value, type.ofType); + if (astValue && astValue.kind === NULL) { + return null; + } + return astValue; } // only explicit null, not undefined, NaN From 61385b7be42cc4a3ade01eb4c60c17fb3028748d Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 28 Oct 2016 02:50:00 +0200 Subject: [PATCH 05/21] astFromValue correctly handles NonNull values --- src/utilities/__tests__/astFromValue-test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utilities/__tests__/astFromValue-test.js b/src/utilities/__tests__/astFromValue-test.js index 48a5e370db..64ed54e6fa 100644 --- a/src/utilities/__tests__/astFromValue-test.js +++ b/src/utilities/__tests__/astFromValue-test.js @@ -49,6 +49,11 @@ describe('astFromValue', () => { expect(astFromValue(1, GraphQLBoolean)).to.deep.equal( { kind: 'BooleanValue', value: true } ); + + const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean); + expect(astFromValue(0, NonNullBoolean)).to.deep.equal( + { kind: 'BooleanValue', value: false } + ); }); it('converts Int values to Int ASTs', () => { From 9a571efc7c6239f15baa5ac5a8cd0ad7234a251b Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 28 Oct 2016 03:10:17 +0200 Subject: [PATCH 06/21] test: astFromValue converts input objects with explicit nulls --- src/utilities/__tests__/astFromValue-test.js | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/utilities/__tests__/astFromValue-test.js b/src/utilities/__tests__/astFromValue-test.js index 64ed54e6fa..6e1543b202 100644 --- a/src/utilities/__tests__/astFromValue-test.js +++ b/src/utilities/__tests__/astFromValue-test.js @@ -245,4 +245,26 @@ describe('astFromValue', () => { value: { kind: 'EnumValue', value: 'HELLO' } } ] } ); }); + + it('converts input objects with explicit nulls', () => { + const inputObj = new GraphQLInputObjectType({ + name: 'MyInputObj', + fields: { + foo: { type: GraphQLFloat }, + bar: { type: myEnum }, + } + }); + + expect(astFromValue( + { foo: null }, + inputObj + )).to.deep.equal( + { kind: 'ObjectValue', + fields: [ + { kind: 'ObjectField', + name: { kind: 'Name', value: 'foo' }, + value: { kind: 'NullValue' } } ] } + ); + }); + }); From 8261a4d295a3f941e18c99a9df7bbad5dcecd454 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 28 Oct 2016 03:48:19 +0200 Subject: [PATCH 07/21] handle null values in valueFromAST --- src/execution/__tests__/variables-test.js | 30 +++++++++++++++++++++++ src/utilities/valueFromAST.js | 22 ++++++++++------- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/execution/__tests__/variables-test.js b/src/execution/__tests__/variables-test.js index 01b649766a..2011c86ea5 100644 --- a/src/execution/__tests__/variables-test.js +++ b/src/execution/__tests__/variables-test.js @@ -159,6 +159,36 @@ describe('Execute: Handles inputs', () => { }); }); + it('properly parses null value to null', async () => { + const doc = ` + { + fieldWithObjectInput(input: {a: null, b: null, c: "C", d: null}) + } + `; + const ast = parse(doc); + + return expect(await execute(schema, ast)).to.deep.equal({ + data: { + fieldWithObjectInput: '{"a":null,"b":null,"c":"C","d":null}' + } + }); + }); + + it('properly parses null value in list', async () => { + const doc = ` + { + fieldWithObjectInput(input: {b: ["A",null,"C"], c: "C"}) + } + `; + const ast = parse(doc); + + return expect(await execute(schema, ast)).to.deep.equal({ + data: { + fieldWithObjectInput: '{"b":["A",null,"C"],"c":"C"}' + } + }); + }); + it('does not use incorrect value', async () => { const doc = ` { diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js index 884d773af9..05640b3899 100644 --- a/src/utilities/valueFromAST.js +++ b/src/utilities/valueFromAST.js @@ -42,6 +42,7 @@ import type { * | String | String | * | Int / Float | Number | * | Enum Value | Mixed | + * | NullValue | null | * */ export function valueFromAST( @@ -57,13 +58,17 @@ export function valueFromAST( } if (!valueAST) { + return; + } + + if (valueAST.kind === Kind.NULL) { return null; } if (valueAST.kind === Kind.VARIABLE) { const variableName = (valueAST: Variable).name.value; if (!variables || !variables.hasOwnProperty(variableName)) { - return null; + return; } // Note: we're not doing any checking that this variable is correct. We're // assuming that this query has been validated and the variable usage here @@ -83,7 +88,7 @@ export function valueFromAST( if (type instanceof GraphQLInputObjectType) { if (valueAST.kind !== Kind.OBJECT) { - return null; + return; } const fields = type.getFields(); const fieldASTs = keyMap( @@ -93,14 +98,13 @@ export function valueFromAST( return Object.keys(fields).reduce((obj, fieldName) => { const field = fields[fieldName]; const fieldAST = fieldASTs[fieldName]; - let fieldValue = + const fieldValue = valueFromAST(fieldAST && fieldAST.value, field.type, variables); - if (isNullish(fieldValue)) { - fieldValue = field.defaultValue; - } - if (!isNullish(fieldValue)) { - obj[fieldName] = fieldValue; - } + + // If no valid field value was provided, use the default value + obj[fieldName] = fieldValue === undefined || fieldValue !== fieldValue ? + field.defaultValue : + fieldValue; return obj; }, {}); } From 588dd43ae5f41a3d9bb2f0176c6a155ab87be22c Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 28 Oct 2016 16:02:08 +0200 Subject: [PATCH 08/21] Support null in schemaPrinter --- src/execution/values.js | 17 +++++++++++------ src/jsutils/isInvalid.js | 16 ++++++++++++++++ src/type/definition.js | 2 +- src/type/directives.js | 2 +- src/utilities/__tests__/schemaPrinter-test.js | 19 +++++++++++++++++++ src/utilities/schemaPrinter.js | 3 ++- src/utilities/valueFromAST.js | 9 ++++----- 7 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 src/jsutils/isInvalid.js diff --git a/src/execution/values.js b/src/execution/values.js index ac678c7e5d..9f3abdf13e 100644 --- a/src/execution/values.js +++ b/src/execution/values.js @@ -13,6 +13,7 @@ import { forEach, isCollection } from 'iterall'; import { GraphQLError } from '../error'; import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; +import isInvalid from '../jsutils/isInvalid'; import keyMap from '../jsutils/keyMap'; import { typeFromAST } from '../utilities/typeFromAST'; import { valueFromAST } from '../utilities/valueFromAST'; @@ -66,10 +67,10 @@ export function getArgumentValues( const name = argDef.name; const valueAST = argASTMap[name] ? argASTMap[name].value : null; let value = valueFromAST(valueAST, argDef.type, variableValues); - if (isNullish(value)) { + if (isInvalid(value)) { value = argDef.defaultValue; } - if (!isNullish(value)) { + if (!isInvalid(value)) { result[name] = value; } return result; @@ -98,7 +99,7 @@ function getVariableValue( const inputType = ((type: any): GraphQLInputType); const errors = isValidJSValue(input, inputType); if (!errors.length) { - if (isNullish(input)) { + if (isInvalid(input)) { const defaultValue = definitionAST.defaultValue; if (defaultValue) { return valueFromAST(defaultValue, inputType); @@ -134,10 +135,14 @@ function coerceValue(type: GraphQLInputType, value: mixed): mixed { return coerceValue(type.ofType, _value); } - if (isNullish(_value)) { + if (_value === null) { return null; } + if (isInvalid(_value)) { + return undefined; + } + if (type instanceof GraphQLList) { const itemType = type.ofType; if (isCollection(_value)) { @@ -158,10 +163,10 @@ function coerceValue(type: GraphQLInputType, value: mixed): mixed { return Object.keys(fields).reduce((obj, fieldName) => { const field = fields[fieldName]; let fieldValue = coerceValue(field.type, _value[fieldName]); - if (isNullish(fieldValue)) { + if (isInvalid(fieldValue)) { fieldValue = field.defaultValue; } - if (!isNullish(fieldValue)) { + if (!isInvalid(fieldValue)) { obj[fieldName] = fieldValue; } return obj; diff --git a/src/jsutils/isInvalid.js b/src/jsutils/isInvalid.js new file mode 100644 index 0000000000..931dc1bbb9 --- /dev/null +++ b/src/jsutils/isInvalid.js @@ -0,0 +1,16 @@ +/* @flow */ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * Returns true if a value is null, undefined, or NaN. + */ +export default function isInvalid(value: mixed): boolean { + return value === undefined || value !== value; +} diff --git a/src/type/definition.js b/src/type/definition.js index dd62e7133a..6f77f6ea08 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -446,7 +446,7 @@ function defineFieldMap( name: argName, description: arg.description === undefined ? null : arg.description, type: arg.type, - defaultValue: arg.defaultValue === undefined ? null : arg.defaultValue + defaultValue: arg.defaultValue }; }); } diff --git a/src/type/directives.js b/src/type/directives.js index 0b1c30194d..cd8a39a2c8 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -84,7 +84,7 @@ export class GraphQLDirective { name: argName, description: arg.description === undefined ? null : arg.description, type: arg.type, - defaultValue: arg.defaultValue === undefined ? null : arg.defaultValue + defaultValue: arg.defaultValue }; }); } diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 721f212bca..d25580c645 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -213,6 +213,25 @@ type Root { ); }); + it('Prints String Field With Int Arg With Default Null', () => { + const output = printSingleFieldSchema( + { + type: GraphQLString, + args: { argOne: { type: GraphQLInt, defaultValue: null } }, + } + ); + expect(output).to.equal(` +schema { + query: Root +} + +type Root { + singleField(argOne: Int = null): String +} +` + ); + }); + it('Prints String Field With Int! Arg', () => { const output = printSingleFieldSchema( { diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index fc0976603d..b01df5529c 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -10,6 +10,7 @@ import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; +import isInvalid from '../jsutils/isInvalid'; import { astFromValue } from '../utilities/astFromValue'; import { print } from '../language/printer'; import type { GraphQLSchema } from '../type/schema'; @@ -231,7 +232,7 @@ function printArgs(args, indentation = '') { function printInputValue(arg) { let argDecl = arg.name + ': ' + String(arg.type); - if (!isNullish(arg.defaultValue)) { + if (!isInvalid(arg.defaultValue)) { argDecl += ` = ${print(astFromValue(arg.defaultValue, arg.type))}`; } return argDecl; diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js index 05640b3899..364783861b 100644 --- a/src/utilities/valueFromAST.js +++ b/src/utilities/valueFromAST.js @@ -11,6 +11,7 @@ import keyMap from '../jsutils/keyMap'; import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; +import isInvalid from '../jsutils/isInvalid'; import * as Kind from '../language/kinds'; import { GraphQLScalarType, @@ -58,7 +59,7 @@ export function valueFromAST( } if (!valueAST) { - return; + return undefined; } if (valueAST.kind === Kind.NULL) { @@ -68,7 +69,7 @@ export function valueFromAST( if (valueAST.kind === Kind.VARIABLE) { const variableName = (valueAST: Variable).name.value; if (!variables || !variables.hasOwnProperty(variableName)) { - return; + return undefined; } // Note: we're not doing any checking that this variable is correct. We're // assuming that this query has been validated and the variable usage here @@ -102,9 +103,7 @@ export function valueFromAST( valueFromAST(fieldAST && fieldAST.value, field.type, variables); // If no valid field value was provided, use the default value - obj[fieldName] = fieldValue === undefined || fieldValue !== fieldValue ? - field.defaultValue : - fieldValue; + obj[fieldName] = isInvalid(fieldValue) ? field.defaultValue : fieldValue; return obj; }, {}); } From e60b785532c4a6fe4ed5cc051be1b954dbc893d7 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 28 Oct 2016 16:46:39 +0200 Subject: [PATCH 09/21] isValidLiteralValue: check for NullValue in NonNull type --- src/utilities/isValidLiteralValue.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utilities/isValidLiteralValue.js b/src/utilities/isValidLiteralValue.js index 694872efee..8eb7c1c0e5 100644 --- a/src/utilities/isValidLiteralValue.js +++ b/src/utilities/isValidLiteralValue.js @@ -11,6 +11,7 @@ import { print } from '../language/printer'; import type { Value, ListValue, ObjectValue } from '../language/ast'; import { + NULL, VARIABLE, LIST, OBJECT @@ -41,7 +42,7 @@ export function isValidLiteralValue( ): Array { // A value must be provided if the type is non-null. if (type instanceof GraphQLNonNull) { - if (!valueAST) { + if (!valueAST || (valueAST.kind === NULL)) { if (type.ofType.name) { return [ `Expected "${String(type.ofType.name)}!", found null.` ]; } From f2c3c2085e97113fe1fcdff1994da4ec1a7da9ec Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 28 Oct 2016 17:23:01 +0200 Subject: [PATCH 10/21] Add nullish in kitchen sink test --- src/language/__tests__/kitchen-sink.graphql | 2 +- src/language/__tests__/printer-test.js | 2 +- src/language/__tests__/visitor-test.js | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/language/__tests__/kitchen-sink.graphql b/src/language/__tests__/kitchen-sink.graphql index 0e04e2e42d..993de9ad02 100644 --- a/src/language/__tests__/kitchen-sink.graphql +++ b/src/language/__tests__/kitchen-sink.graphql @@ -52,6 +52,6 @@ fragment frag on Friend { } { - unnamed(truthy: true, falsey: false), + unnamed(truthy: true, falsey: false, nullish: null), query } diff --git a/src/language/__tests__/printer-test.js b/src/language/__tests__/printer-test.js index 51b93ab2aa..25b9920739 100644 --- a/src/language/__tests__/printer-test.js +++ b/src/language/__tests__/printer-test.js @@ -132,7 +132,7 @@ fragment frag on Friend { } { - unnamed(truthy: true, falsey: false) + unnamed(truthy: true, falsey: false, nullish: null) query } `); diff --git a/src/language/__tests__/visitor-test.js b/src/language/__tests__/visitor-test.js index 3c3b6ca0ad..3a43936db0 100644 --- a/src/language/__tests__/visitor-test.js +++ b/src/language/__tests__/visitor-test.js @@ -614,6 +614,12 @@ describe('Visitor', () => { [ 'enter', 'BooleanValue', 'value', 'Argument' ], [ 'leave', 'BooleanValue', 'value', 'Argument' ], [ 'leave', 'Argument', 1, undefined ], + [ 'enter', 'Argument', 2, undefined ], + [ 'enter', 'Name', 'name', 'Argument' ], + [ 'leave', 'Name', 'name', 'Argument' ], + [ 'enter', 'NullValue', 'value', 'Argument' ], + [ 'leave', 'NullValue', 'value', 'Argument' ], + [ 'leave', 'Argument', 2, undefined ], [ 'leave', 'Field', 0, undefined ], [ 'enter', 'Field', 1, undefined ], [ 'enter', 'Name', 'name', 'Field' ], From b1339cf5f0fe49015515ebf29d37b6b0a3810644 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Mon, 31 Oct 2016 22:16:29 +0100 Subject: [PATCH 11/21] isValidLiteralValue: Accept null in if nullable type + tests --- src/utilities/isValidLiteralValue.js | 2 +- .../__tests__/ArgumentsOfCorrectType-test.js | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/utilities/isValidLiteralValue.js b/src/utilities/isValidLiteralValue.js index 8eb7c1c0e5..96a74baaec 100644 --- a/src/utilities/isValidLiteralValue.js +++ b/src/utilities/isValidLiteralValue.js @@ -51,7 +51,7 @@ export function isValidLiteralValue( return isValidLiteralValue(type.ofType, valueAST); } - if (!valueAST) { + if (!valueAST || (valueAST.kind === NULL)) { return []; } diff --git a/src/validation/__tests__/ArgumentsOfCorrectType-test.js b/src/validation/__tests__/ArgumentsOfCorrectType-test.js index 036dc8d921..5580a7d44c 100644 --- a/src/validation/__tests__/ArgumentsOfCorrectType-test.js +++ b/src/validation/__tests__/ArgumentsOfCorrectType-test.js @@ -117,6 +117,70 @@ describe('Validate: Argument values of correct type', () => { }); + describe('`null` values', () => { + + it('Null into int', () => { + expectPassesRule(ArgumentsOfCorrectType, ` + { + complicatedArgs { + intArgField(intArg: null) + } + } + `); + }); + + it('Null into boolean', () => { + expectPassesRule(ArgumentsOfCorrectType, ` + { + complicatedArgs { + booleanArgField(booleanArg: null) + } + } + `); + }); + + it('Null into string', () => { + expectPassesRule(ArgumentsOfCorrectType, ` + { + complicatedArgs { + stringArgField(stringArg: null) + } + } + `); + }); + + it('Null into float', () => { + expectPassesRule(ArgumentsOfCorrectType, ` + { + complicatedArgs { + floatArgField(floatArg: null) + } + } + `); + }); + + it('Null into ID', () => { + expectPassesRule(ArgumentsOfCorrectType, ` + { + complicatedArgs { + idArgField(idArg: null) + } + } + `); + }); + + it('Null into enum', () => { + expectPassesRule(ArgumentsOfCorrectType, ` + { + dog { + doesKnowCommand(dogCommand: null) + } + } + `); + }); + + }); + describe('Invalid String values', () => { From 1c29b5de4031f2eb95187281dd641dd339709291 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Mon, 31 Oct 2016 23:17:46 +0100 Subject: [PATCH 12/21] Tests for default null values --- .../DefaultValuesOfCorrectType-test.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/validation/__tests__/DefaultValuesOfCorrectType-test.js b/src/validation/__tests__/DefaultValuesOfCorrectType-test.js index ecbe313ae6..e9872d172d 100644 --- a/src/validation/__tests__/DefaultValuesOfCorrectType-test.js +++ b/src/validation/__tests__/DefaultValuesOfCorrectType-test.js @@ -68,6 +68,44 @@ describe('Validate: Variable default values of correct type', () => { `); }); + 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") { From 07ac6d7c3c2b9add09d9f4cb45c7049f2b95b019 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Mon, 31 Oct 2016 23:31:43 +0100 Subject: [PATCH 13/21] ArgumentsOfCorrectType - valid null into list --- .../__tests__/ArgumentsOfCorrectType-test.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/validation/__tests__/ArgumentsOfCorrectType-test.js b/src/validation/__tests__/ArgumentsOfCorrectType-test.js index 5580a7d44c..4eb080e0e3 100644 --- a/src/validation/__tests__/ArgumentsOfCorrectType-test.js +++ b/src/validation/__tests__/ArgumentsOfCorrectType-test.js @@ -117,7 +117,7 @@ describe('Validate: Argument values of correct type', () => { }); - describe('`null` values', () => { + describe('Valid `null` values', () => { it('Null into int', () => { expectPassesRule(ArgumentsOfCorrectType, ` @@ -518,7 +518,7 @@ describe('Validate: Argument values of correct type', () => { expectPassesRule(ArgumentsOfCorrectType, ` { complicatedArgs { - stringListArgField(stringListArg: ["one", "two"]) + stringListArgField(stringListArg: ["one", null, "two"]) } } `); @@ -534,6 +534,16 @@ describe('Validate: Argument values of correct type', () => { `); }); + it('Null value', () => { + expectPassesRule(ArgumentsOfCorrectType, ` + { + complicatedArgs { + stringListArgField(stringListArg: null) + } + } + `); + }); + it('Single value into List', () => { expectPassesRule(ArgumentsOfCorrectType, ` { From 2a28c0c97098c06a45dc0c351a0aa17be26a17db Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Tue, 1 Nov 2016 00:17:24 +0100 Subject: [PATCH 14/21] comment --- src/jsutils/isInvalid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsutils/isInvalid.js b/src/jsutils/isInvalid.js index 931dc1bbb9..e226abb6e0 100644 --- a/src/jsutils/isInvalid.js +++ b/src/jsutils/isInvalid.js @@ -9,7 +9,7 @@ */ /** - * Returns true if a value is null, undefined, or NaN. + * Returns true if a value is undefined, or NaN. */ export default function isInvalid(value: mixed): boolean { return value === undefined || value !== value; From 6b61bb694df102c191340b1dcb14c72d2ee5bc2b Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Tue, 1 Nov 2016 00:18:31 +0100 Subject: [PATCH 15/21] isNullish is unnecessary there --- src/utilities/astFromValue.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utilities/astFromValue.js b/src/utilities/astFromValue.js index b350827ba2..1cea7a63c6 100644 --- a/src/utilities/astFromValue.js +++ b/src/utilities/astFromValue.js @@ -12,6 +12,7 @@ import { forEach, isCollection } from 'iterall'; import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; +import isInvalid from '../jsutils/isInvalid'; import type { Value, IntValue, @@ -83,9 +84,9 @@ export function astFromValue( return ({ kind: NULL }: NullValue); } - // other nullish values - if (isNullish(_value)) { - return null; + // undefined, NaN + if (isInvalid(_value)) { + return; } // Convert JavaScript array to GraphQL list. If the GraphQLType is a list, but From bf83f7f1d071bb5942e61b607b502ee90668c2c8 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Tue, 1 Nov 2016 00:29:40 +0100 Subject: [PATCH 16/21] a note about difference between undefined and null --- src/utilities/valueFromAST.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js index 364783861b..718800d762 100644 --- a/src/utilities/valueFromAST.js +++ b/src/utilities/valueFromAST.js @@ -59,10 +59,13 @@ export function valueFromAST( } if (!valueAST) { + // it is really importand to distinguish + // between undefined and real null value here return undefined; } if (valueAST.kind === Kind.NULL) { + // this is actually valid return value return null; } From 5a3585ffe67ccc2e3995ffd55c9120ed65a8c00c Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Tue, 1 Nov 2016 00:41:41 +0100 Subject: [PATCH 17/21] one test for null is enough --- .../__tests__/ArgumentsOfCorrectType-test.js | 56 +------------------ 1 file changed, 1 insertion(+), 55 deletions(-) diff --git a/src/validation/__tests__/ArgumentsOfCorrectType-test.js b/src/validation/__tests__/ArgumentsOfCorrectType-test.js index 4eb080e0e3..ece8f9e96d 100644 --- a/src/validation/__tests__/ArgumentsOfCorrectType-test.js +++ b/src/validation/__tests__/ArgumentsOfCorrectType-test.js @@ -115,11 +115,7 @@ describe('Validate: Argument values of correct type', () => { `); }); - }); - - describe('Valid `null` values', () => { - - it('Null into int', () => { + it('null into nullable type', () => { expectPassesRule(ArgumentsOfCorrectType, ` { complicatedArgs { @@ -129,56 +125,6 @@ describe('Validate: Argument values of correct type', () => { `); }); - it('Null into boolean', () => { - expectPassesRule(ArgumentsOfCorrectType, ` - { - complicatedArgs { - booleanArgField(booleanArg: null) - } - } - `); - }); - - it('Null into string', () => { - expectPassesRule(ArgumentsOfCorrectType, ` - { - complicatedArgs { - stringArgField(stringArg: null) - } - } - `); - }); - - it('Null into float', () => { - expectPassesRule(ArgumentsOfCorrectType, ` - { - complicatedArgs { - floatArgField(floatArg: null) - } - } - `); - }); - - it('Null into ID', () => { - expectPassesRule(ArgumentsOfCorrectType, ` - { - complicatedArgs { - idArgField(idArg: null) - } - } - `); - }); - - it('Null into enum', () => { - expectPassesRule(ArgumentsOfCorrectType, ` - { - dog { - doesKnowCommand(dogCommand: null) - } - } - `); - }); - }); From 2823bd3a92abc7f7f20f01fd0d1ca9bb8a3ad4f0 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Tue, 1 Nov 2016 00:42:44 +0100 Subject: [PATCH 18/21] be consistent in return --- src/utilities/astFromValue.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/astFromValue.js b/src/utilities/astFromValue.js index 1cea7a63c6..b3aa2d98d1 100644 --- a/src/utilities/astFromValue.js +++ b/src/utilities/astFromValue.js @@ -86,7 +86,7 @@ export function astFromValue( // undefined, NaN if (isInvalid(_value)) { - return; + return null; } // Convert JavaScript array to GraphQL list. If the GraphQLType is a list, but From 8ffbfef4cbde12aa0fa171c1631d75e4eee039cc Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Tue, 1 Nov 2016 01:06:50 +0100 Subject: [PATCH 19/21] ArgumentsOfCorrectType tests from null values --- .../__tests__/ArgumentsOfCorrectType-test.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/validation/__tests__/ArgumentsOfCorrectType-test.js b/src/validation/__tests__/ArgumentsOfCorrectType-test.js index ece8f9e96d..3636c63683 100644 --- a/src/validation/__tests__/ArgumentsOfCorrectType-test.js +++ b/src/validation/__tests__/ArgumentsOfCorrectType-test.js @@ -123,6 +123,14 @@ describe('Validate: Argument values of correct type', () => { } } `); + + expectPassesRule(ArgumentsOfCorrectType, ` + { + dog(a: null, b: null, c:{ requiredField: true, intField: null }) { + name + } + } + `); }); }); @@ -666,6 +674,20 @@ describe('Validate: Argument values of correct type', () => { ]); }); + it('Null value', () => { + expectFailsRule(ArgumentsOfCorrectType, ` + { + complicatedArgs { + multipleReqs(req1: null) + } + } + `, [ + badValue('req1', 'Int!', 'null', 4, 32, [ + 'Expected "Int!", found null.' + ]), + ]); + }); + }); From d5e104ffcd540953dced48311d87e210874c8855 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Mon, 31 Oct 2016 19:17:54 -0700 Subject: [PATCH 20/21] Update valueFromAST.js Comment clarity --- src/utilities/valueFromAST.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js index 718800d762..77967f5902 100644 --- a/src/utilities/valueFromAST.js +++ b/src/utilities/valueFromAST.js @@ -59,13 +59,13 @@ export function valueFromAST( } if (!valueAST) { - // it is really importand to distinguish - // between undefined and real null value here + // When there is no AST, then there is also no value. + // Importantly, this is different from returning the value null. return undefined; } if (valueAST.kind === Kind.NULL) { - // this is actually valid return value + // This is explicitly returning the value null. return null; } From 4803be792a8f2ee40ae0f544be518ee20cd9cd99 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Mon, 31 Oct 2016 19:19:18 -0700 Subject: [PATCH 21/21] Update valueFromAST.js Valid return values --- src/utilities/valueFromAST.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js index 77967f5902..3d216a30da 100644 --- a/src/utilities/valueFromAST.js +++ b/src/utilities/valueFromAST.js @@ -61,7 +61,7 @@ export function valueFromAST( if (!valueAST) { // When there is no AST, then there is also no value. // Importantly, this is different from returning the value null. - return undefined; + return; } if (valueAST.kind === Kind.NULL) { @@ -72,7 +72,8 @@ export function valueFromAST( if (valueAST.kind === Kind.VARIABLE) { const variableName = (valueAST: Variable).name.value; if (!variables || !variables.hasOwnProperty(variableName)) { - return undefined; + // No valid return value. + return; } // Note: we're not doing any checking that this variable is correct. We're // assuming that this query has been validated and the variable usage here @@ -92,6 +93,7 @@ export function valueFromAST( if (type instanceof GraphQLInputObjectType) { if (valueAST.kind !== Kind.OBJECT) { + // No valid return value. return; } const fields = type.getFields();