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;

src/language/kinds.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const INT = 'IntValue';
3434
export const FLOAT = 'FloatValue';
3535
export const STRING = 'StringValue';
3636
export const BOOLEAN = 'BooleanValue';
37+
export const NULL = 'NullValue';
3738
export const ENUM = 'EnumValue';
3839
export const LIST = 'ListValue';
3940
export const OBJECT = 'ObjectValue';

src/language/parser.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import {
8989
FLOAT,
9090
STRING,
9191
BOOLEAN,
92+
NULL,
9293
ENUM,
9394
LIST,
9495
OBJECT,
@@ -503,12 +504,15 @@ function parseFragmentName(lexer: Lexer<*>): Name {
503504
* - FloatValue
504505
* - StringValue
505506
* - BooleanValue
507+
* - NullValue
506508
* - EnumValue
507509
* - ListValue[?Const]
508510
* - ObjectValue[?Const]
509511
*
510512
* BooleanValue : one of `true` `false`
511513
*
514+
* NullValue : `null`
515+
*
512516
* EnumValue : Name but not `true`, `false` or `null`
513517
*/
514518
function parseValueLiteral(lexer: Lexer<*>, isConst: boolean): Value {
@@ -547,15 +551,19 @@ function parseValueLiteral(lexer: Lexer<*>, isConst: boolean): Value {
547551
value: token.value === 'true',
548552
loc: loc(lexer, token)
549553
};
550-
} else if (token.value !== 'null') {
554+
} else if (token.value === 'null') {
551555
lexer.advance();
552556
return {
553-
kind: (ENUM: 'EnumValue'),
554-
value: ((token.value: any): string),
557+
kind: (NULL: 'NullValue'),
555558
loc: loc(lexer, token)
556559
};
557560
}
558-
break;
561+
lexer.advance();
562+
return {
563+
kind: (ENUM: 'EnumValue'),
564+
value: ((token.value: any): string),
565+
loc: loc(lexer, token)
566+
};
559567
case TokenKind.DOLLAR:
560568
if (!isConst) {
561569
return parseVariable(lexer);

src/language/printer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const printDocASTReducer = {
7676
FloatValue: ({ value }) => value,
7777
StringValue: ({ value }) => JSON.stringify(value),
7878
BooleanValue: ({ value }) => JSON.stringify(value),
79+
NullValue: () => 'null',
7980
EnumValue: ({ value }) => value,
8081
ListValue: ({ values }) => '[' + join(values, ', ') + ']',
8182
ObjectValue: ({ fields }) => '{' + join(fields, ', ') + '}',

src/language/visitor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const QueryDocumentKeys = {
2727
FloatValue: [],
2828
StringValue: [],
2929
BooleanValue: [],
30+
NullValue: [],
3031
EnumValue: [],
3132
ListValue: [ 'values' ],
3233
ObjectValue: [ 'fields' ],

src/type/definition.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ function defineFieldMap(
446446
name: argName,
447447
description: arg.description === undefined ? null : arg.description,
448448
type: arg.type,
449-
defaultValue: arg.defaultValue === undefined ? null : arg.defaultValue
449+
defaultValue: arg.defaultValue
450450
};
451451
});
452452
}

src/type/directives.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class GraphQLDirective {
8484
name: argName,
8585
description: arg.description === undefined ? null : arg.description,
8686
type: arg.type,
87-
defaultValue: arg.defaultValue === undefined ? null : arg.defaultValue
87+
defaultValue: arg.defaultValue
8888
};
8989
});
9090
}

src/utilities/__tests__/astFromValue-test.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
GraphQLString,
2020
GraphQLBoolean,
2121
GraphQLID,
22+
GraphQLNonNull,
2223
} from '../../type';
2324

2425

@@ -33,17 +34,26 @@ describe('astFromValue', () => {
3334
{ kind: 'BooleanValue', value: false }
3435
);
3536

36-
expect(astFromValue(null, GraphQLBoolean)).to.deep.equal(
37+
expect(astFromValue(undefined, GraphQLBoolean)).to.deep.equal(
3738
null
3839
);
3940

41+
expect(astFromValue(null, GraphQLBoolean)).to.deep.equal(
42+
{ kind: 'NullValue' }
43+
);
44+
4045
expect(astFromValue(0, GraphQLBoolean)).to.deep.equal(
4146
{ kind: 'BooleanValue', value: false }
4247
);
4348

4449
expect(astFromValue(1, GraphQLBoolean)).to.deep.equal(
4550
{ kind: 'BooleanValue', value: true }
4651
);
52+
53+
const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean);
54+
expect(astFromValue(0, NonNullBoolean)).to.deep.equal(
55+
{ kind: 'BooleanValue', value: false }
56+
);
4757
});
4858

4959
it('converts Int values to Int ASTs', () => {
@@ -105,6 +115,10 @@ describe('astFromValue', () => {
105115
);
106116

107117
expect(astFromValue(null, GraphQLString)).to.deep.equal(
118+
{ kind: 'NullValue' }
119+
);
120+
121+
expect(astFromValue(undefined, GraphQLString)).to.deep.equal(
108122
null
109123
);
110124
});
@@ -133,6 +147,17 @@ describe('astFromValue', () => {
133147
);
134148

135149
expect(astFromValue(null, GraphQLID)).to.deep.equal(
150+
{ kind: 'NullValue' }
151+
);
152+
153+
expect(astFromValue(undefined, GraphQLID)).to.deep.equal(
154+
null
155+
);
156+
});
157+
158+
it('does not converts NonNull values to NullValue', () => {
159+
const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean);
160+
expect(astFromValue(null, NonNullBoolean)).to.deep.equal(
136161
null
137162
);
138163
});
@@ -220,4 +245,26 @@ describe('astFromValue', () => {
220245
value: { kind: 'EnumValue', value: 'HELLO' } } ] }
221246
);
222247
});
248+
249+
it('converts input objects with explicit nulls', () => {
250+
const inputObj = new GraphQLInputObjectType({
251+
name: 'MyInputObj',
252+
fields: {
253+
foo: { type: GraphQLFloat },
254+
bar: { type: myEnum },
255+
}
256+
});
257+
258+
expect(astFromValue(
259+
{ foo: null },
260+
inputObj
261+
)).to.deep.equal(
262+
{ kind: 'ObjectValue',
263+
fields: [
264+
{ kind: 'ObjectField',
265+
name: { kind: 'Name', value: 'foo' },
266+
value: { kind: 'NullValue' } } ] }
267+
);
268+
});
269+
223270
});

0 commit comments

Comments
 (0)