Skip to content

Commit c23f578

Browse files
langpavelleebyron
authored andcommitted
language support for NullValue (#544)
* language support for NullValue * support null literal in Printer * astFromValue returns NullValue for explicit null * astFromNode does not converts NonNull values to NullValue * astFromValue correctly handles NonNull values * test: astFromValue converts input objects with explicit nulls * handle null values in valueFromAST * Support null in schemaPrinter * isValidLiteralValue: check for NullValue in NonNull type * Add nullish in kitchen sink test * isValidLiteralValue: Accept null in if nullable type + tests * Tests for default null values * ArgumentsOfCorrectType - valid null into list * comment * isNullish is unnecessary there * a note about difference between undefined and null * one test for null is enough * be consistent in return * ArgumentsOfCorrectType tests from null values * Update valueFromAST.js Comment clarity * Update valueFromAST.js Valid return values
1 parent 90121be commit c23f578

24 files changed

+284
-38
lines changed

src/execution/__tests__/variables-test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,36 @@ describe('Execute: Handles inputs', () => {
159159
});
160160
});
161161

162+
it('properly parses null value to null', async () => {
163+
const doc = `
164+
{
165+
fieldWithObjectInput(input: {a: null, b: null, c: "C", d: null})
166+
}
167+
`;
168+
const ast = parse(doc);
169+
170+
return expect(await execute(schema, ast)).to.deep.equal({
171+
data: {
172+
fieldWithObjectInput: '{"a":null,"b":null,"c":"C","d":null}'
173+
}
174+
});
175+
});
176+
177+
it('properly parses null value in list', async () => {
178+
const doc = `
179+
{
180+
fieldWithObjectInput(input: {b: ["A",null,"C"], c: "C"})
181+
}
182+
`;
183+
const ast = parse(doc);
184+
185+
return expect(await execute(schema, ast)).to.deep.equal({
186+
data: {
187+
fieldWithObjectInput: '{"b":["A",null,"C"],"c":"C"}'
188+
}
189+
});
190+
});
191+
162192
it('does not use incorrect value', async () => {
163193
const doc = `
164194
{

src/execution/values.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { forEach, isCollection } from 'iterall';
1313
import { GraphQLError } from '../error';
1414
import invariant from '../jsutils/invariant';
1515
import isNullish from '../jsutils/isNullish';
16+
import isInvalid from '../jsutils/isInvalid';
1617
import keyMap from '../jsutils/keyMap';
1718
import { typeFromAST } from '../utilities/typeFromAST';
1819
import { valueFromAST } from '../utilities/valueFromAST';
@@ -66,10 +67,10 @@ export function getArgumentValues(
6667
const name = argDef.name;
6768
const valueAST = argASTMap[name] ? argASTMap[name].value : null;
6869
let value = valueFromAST(valueAST, argDef.type, variableValues);
69-
if (isNullish(value)) {
70+
if (isInvalid(value)) {
7071
value = argDef.defaultValue;
7172
}
72-
if (!isNullish(value)) {
73+
if (!isInvalid(value)) {
7374
result[name] = value;
7475
}
7576
return result;
@@ -98,7 +99,7 @@ function getVariableValue(
9899
const inputType = ((type: any): GraphQLInputType);
99100
const errors = isValidJSValue(input, inputType);
100101
if (!errors.length) {
101-
if (isNullish(input)) {
102+
if (isInvalid(input)) {
102103
const defaultValue = definitionAST.defaultValue;
103104
if (defaultValue) {
104105
return valueFromAST(defaultValue, inputType);
@@ -134,10 +135,14 @@ function coerceValue(type: GraphQLInputType, value: mixed): mixed {
134135
return coerceValue(type.ofType, _value);
135136
}
136137

137-
if (isNullish(_value)) {
138+
if (_value === null) {
138139
return null;
139140
}
140141

142+
if (isInvalid(_value)) {
143+
return undefined;
144+
}
145+
141146
if (type instanceof GraphQLList) {
142147
const itemType = type.ofType;
143148
if (isCollection(_value)) {
@@ -158,10 +163,10 @@ function coerceValue(type: GraphQLInputType, value: mixed): mixed {
158163
return Object.keys(fields).reduce((obj, fieldName) => {
159164
const field = fields[fieldName];
160165
let fieldValue = coerceValue(field.type, _value[fieldName]);
161-
if (isNullish(fieldValue)) {
166+
if (isInvalid(fieldValue)) {
162167
fieldValue = field.defaultValue;
163168
}
164-
if (!isNullish(fieldValue)) {
169+
if (!isInvalid(fieldValue)) {
165170
obj[fieldName] = fieldValue;
166171
}
167172
return obj;

src/jsutils/isInvalid.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* @flow */
2+
/**
3+
* Copyright (c) 2015, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree. An additional grant
8+
* of patent rights can be found in the PATENTS file in the same directory.
9+
*/
10+
11+
/**
12+
* Returns true if a value is undefined, or NaN.
13+
*/
14+
export default function isInvalid(value: mixed): boolean {
15+
return value === undefined || value !== value;
16+
}

src/language/__tests__/kitchen-sink.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,6 @@ fragment frag on Friend {
5252
}
5353

5454
{
55-
unnamed(truthy: true, falsey: false),
55+
unnamed(truthy: true, falsey: false, nullish: null),
5656
query
5757
}

src/language/__tests__/parser-test.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,6 @@ fragment MissingOn Type
9191
).to.throw('Syntax Error GraphQL (1:9) Expected Name, found }');
9292
});
9393

94-
it('does not allow null as value', async () => {
95-
expect(
96-
() => parse('{ fieldWithNullableStringInput(input: null) }')
97-
).to.throw('Syntax Error GraphQL (1:39) Unexpected Name "null"');
98-
});
99-
10094
it('parses multi-byte characters', async () => {
10195
// Note: \u0A0A could be naively interpretted as two line-feed chars.
10296
expect(
@@ -296,6 +290,13 @@ fragment ${fragmentName} on Type {
296290

297291
describe('parseValue', () => {
298292

293+
it('parses null value', () => {
294+
expect(parseValue('null')).to.containSubset({
295+
kind: Kind.NULL,
296+
loc: { start: 0, end: 4 }
297+
});
298+
});
299+
299300
it('parses list values', () => {
300301
expect(parseValue('[123 "abc"]')).to.containSubset({
301302
kind: Kind.LIST,

src/language/__tests__/printer-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ fragment frag on Friend {
132132
}
133133
134134
{
135-
unnamed(truthy: true, falsey: false)
135+
unnamed(truthy: true, falsey: false, nullish: null)
136136
query
137137
}
138138
`);

src/language/__tests__/schema-kitchen-sink.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type Foo implements Bar {
1717
four(argument: String = "string"): String
1818
five(argument: [String] = ["string", "string"]): String
1919
six(argument: InputType = {key: "value"}): Type
20+
seven(argument: Int = null): Type
2021
}
2122

2223
type AnnotatedObject @onObject(arg: "value") {

src/language/__tests__/schema-printer-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type Foo implements Bar {
6363
four(argument: String = "string"): String
6464
five(argument: [String] = ["string", "string"]): String
6565
six(argument: InputType = {key: "value"}): Type
66+
seven(argument: Int = null): Type
6667
}
6768
6869
type AnnotatedObject @onObject(arg: "value") {

src/language/__tests__/visitor-test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,12 @@ describe('Visitor', () => {
614614
[ 'enter', 'BooleanValue', 'value', 'Argument' ],
615615
[ 'leave', 'BooleanValue', 'value', 'Argument' ],
616616
[ 'leave', 'Argument', 1, undefined ],
617+
[ 'enter', 'Argument', 2, undefined ],
618+
[ 'enter', 'Name', 'name', 'Argument' ],
619+
[ 'leave', 'Name', 'name', 'Argument' ],
620+
[ 'enter', 'NullValue', 'value', 'Argument' ],
621+
[ 'leave', 'NullValue', 'value', 'Argument' ],
622+
[ 'leave', 'Argument', 2, undefined ],
617623
[ 'leave', 'Field', 0, undefined ],
618624
[ 'enter', 'Field', 1, undefined ],
619625
[ 'enter', 'Name', 'name', 'Field' ],

src/language/ast.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export type Node =
126126
| FloatValue
127127
| StringValue
128128
| BooleanValue
129+
| NullValue
129130
| EnumValue
130131
| ListValue
131132
| ObjectValue
@@ -260,6 +261,7 @@ export type Value =
260261
| FloatValue
261262
| StringValue
262263
| BooleanValue
264+
| NullValue
263265
| EnumValue
264266
| ListValue
265267
| ObjectValue;
@@ -288,6 +290,11 @@ export type BooleanValue = {
288290
value: boolean;
289291
};
290292

293+
export type NullValue = {
294+
kind: 'NullValue';
295+
loc?: Location;
296+
};
297+
291298
export type EnumValue = {
292299
kind: 'EnumValue';
293300
loc?: Location;

0 commit comments

Comments
 (0)