Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

language support for NullValue #544

Merged
merged 21 commits into from
Nov 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/execution/__tests__/variables-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
{
Expand Down
17 changes: 11 additions & 6 deletions src/execution/values.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/jsutils/isInvalid.js
Original file line number Diff line number Diff line change
@@ -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 undefined, or NaN.
*/
export default function isInvalid(value: mixed): boolean {
return value === undefined || value !== value;
}
2 changes: 1 addition & 1 deletion src/language/__tests__/kitchen-sink.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ fragment frag on Friend {
}

{
unnamed(truthy: true, falsey: false),
unnamed(truthy: true, falsey: false, nullish: null),
query
}
13 changes: 7 additions & 6 deletions src/language/__tests__/parser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/language/__tests__/printer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ fragment frag on Friend {
}

{
unnamed(truthy: true, falsey: false)
unnamed(truthy: true, falsey: false, nullish: null)
query
}
`);
Expand Down
1 change: 1 addition & 0 deletions src/language/__tests__/schema-kitchen-sink.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

@cjoudrey cjoudrey Oct 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might have been discussed elsewhere, but I'm confused why we want to support null as a default value.

How is this different from:

seven(argument: Int): Type

Except that one will have args["argument"] set to null and the other args["argument"] will be undefined when omitted.

cc @rmosolgo

Copy link

@rmosolgo rmosolgo Oct 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... Maybe to distinguish between "user wants to set this to null" and "user wants to leave this unchanged"?

For a Ruby example, the difference is, in the first case:

# seven(argument: Int) 
  # User provided `null`
  args.key?(:argument) # => true 
  args[:argument] # => nil 

  # User has not provided a value 
  args.key?(:argument) # => false 
  args[:argument] # => nil (this is the same) 

# seven(argument: Int = null) 
  # In this case, the outputs are identical: 

  # User provided `null`
  args.key?(:argument) # => true 
  args[:argument] # => nil 

  # User has not provided a value 
  args.key?(:argument) # => true 
  args[:argument] # => nil (this is the same) 

(Oops, first time I saw this, I thought this was a variable default value of null)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so by setting the default value of an input argument to null you're making it impossible to know if the user passed in null vs the user omitted the input value entirely.

six(argument: Int): Type
seven(argument: Int = null): Type

In six, the value of argument could be null | Int | undefined.

In seven, the value of argument could be null | Int.

Since both null and undefined are falsy, I can't think of a case where one would explicitly set the default value to null in order to omit the undefined case.

Maybe when using Flow/TypeScript removing this 3rd case makes it easier to understand the code?

Copy link
Contributor Author

@langpavel langpavel Oct 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default value will be present inside resolve

Nullable type without value (and without default value) will be omitted === undefined

RFC

CC @cjoudrey @rmosolgo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cjoudrey

Since both null and undefined are falsy, I can't think of a case where one would explicitly set the default value to null in order to omit the undefined case.

It is valid language construct. I doesn't care if it is semantically useful - but someone may want explicitly tell that there is no semantical difference between undefined and null

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is probably confusing as an example because I agree that it's a little strange to make a default value of null to rule out the "not provided" (aka undefined) case, but it's certainly possible now, so it's worth testing as an edge case.

Copy link
Contributor

@nodkz nodkz Nov 1, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default NULL value is super useful for me. If I persist document (for MongoDB) with null values (preallocate space for field), it saves me from moving document on disk when this value changes (add field and value).

}

type AnnotatedObject @onObject(arg: "value") {
Expand Down
1 change: 1 addition & 0 deletions src/language/__tests__/schema-printer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
6 changes: 6 additions & 0 deletions src/language/__tests__/visitor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' ],
Expand Down
7 changes: 7 additions & 0 deletions src/language/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export type Node =
| FloatValue
| StringValue
| BooleanValue
| NullValue
| EnumValue
| ListValue
| ObjectValue
Expand Down Expand Up @@ -260,6 +261,7 @@ export type Value =
| FloatValue
| StringValue
| BooleanValue
| NullValue
| EnumValue
| ListValue
| ObjectValue;
Expand Down Expand Up @@ -288,6 +290,11 @@ export type BooleanValue = {
value: boolean;
};

export type NullValue = {
kind: 'NullValue';
loc?: Location;
};

export type EnumValue = {
kind: 'EnumValue';
loc?: Location;
Expand Down
1 change: 1 addition & 0 deletions src/language/kinds.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
16 changes: 12 additions & 4 deletions src/language/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import {
FLOAT,
STRING,
BOOLEAN,
NULL,
ENUM,
LIST,
OBJECT,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/language/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, ', ') + '}',
Expand Down
1 change: 1 addition & 0 deletions src/language/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const QueryDocumentKeys = {
FloatValue: [],
StringValue: [],
BooleanValue: [],
NullValue: [],
EnumValue: [],
ListValue: [ 'values' ],
ObjectValue: [ 'fields' ],
Expand Down
2 changes: 1 addition & 1 deletion src/type/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/type/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
});
}
Expand Down
49 changes: 48 additions & 1 deletion src/utilities/__tests__/astFromValue-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
GraphQLString,
GraphQLBoolean,
GraphQLID,
GraphQLNonNull,
} from '../../type';


Expand All @@ -33,17 +34,26 @@ 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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add a test for astFromValue(null, new GraphQLNonNull(GraphQLBoolean)) to expect null

Copy link
Contributor Author

@langpavel langpavel Oct 28, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leebyron I'm little confused by this.
What is correct result for astFromValue(null, new GraphQLNonNull(*))?

In astFromValue implementation there is this comment at line 74:

Note: we're not checking that the result is non-null.
This function is not responsible for validating the input value.

So it is correct to ignore this comment here? But I think you are right.. so I should drop this comment..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, it makes sense. I added condition and test in 02d71a0

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - I like the change you made.

{ kind: 'NullValue' }
);

expect(astFromValue(0, GraphQLBoolean)).to.deep.equal(
{ kind: 'BooleanValue', value: false }
);

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', () => {
Expand Down Expand Up @@ -105,6 +115,10 @@ describe('astFromValue', () => {
);

expect(astFromValue(null, GraphQLString)).to.deep.equal(
{ kind: 'NullValue' }
);

expect(astFromValue(undefined, GraphQLString)).to.deep.equal(
null
);
});
Expand Down Expand Up @@ -133,6 +147,17 @@ describe('astFromValue', () => {
);

expect(astFromValue(null, GraphQLID)).to.deep.equal(
{ kind: 'NullValue' }
);

expect(astFromValue(undefined, GraphQLID)).to.deep.equal(
null
);
});

it('does not converts NonNull values to NullValue', () => {
const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean);
expect(astFromValue(null, NonNullBoolean)).to.deep.equal(
null
);
});
Expand Down Expand Up @@ -220,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' } } ] }
);
});

});
Loading