diff --git a/src/main/antlr/Graphql.g4 b/src/main/antlr/Graphql.g4 index d7a3b649f9..51b6be8e07 100644 --- a/src/main/antlr/Graphql.g4 +++ b/src/main/antlr/Graphql.g4 @@ -66,6 +66,7 @@ IntValue | FloatValue | StringValue | BooleanValue | +NullValue | enumValue | arrayValue | objectValue; @@ -76,6 +77,7 @@ IntValue | FloatValue | StringValue | BooleanValue | +NullValue | enumValue | arrayValueWithVariable | objectValueWithVariable; @@ -176,6 +178,8 @@ directiveLocations '|' directiveLocation BooleanValue: 'true' | 'false'; +NullValue: 'null'; + FRAGMENT: 'fragment'; QUERY: 'query'; MUTATION: 'mutation'; diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index f966621348..66b8ec8c61 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -11,6 +11,7 @@ import graphql.execution.instrumentation.parameters.FieldFetchParameters; import graphql.execution.instrumentation.parameters.FieldParameters; import graphql.language.Field; +import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironmentImpl; import graphql.schema.DataFetchingFieldSelectionSet; @@ -96,7 +97,8 @@ protected ExecutionResult resolveField(ExecutionContext executionContext, Execut InstrumentationContext fetchCtx = instrumentation.beginFieldFetch(new FieldFetchParameters(executionContext, fieldDef, environment)); Object resolvedValue = null; try { - resolvedValue = fieldDef.getDataFetcher().get(environment); + DataFetcher dataFetcher = fieldDef.getDataFetcher(); + resolvedValue = dataFetcher.get(environment); fetchCtx.onEnd(resolvedValue); } catch (Exception e) { diff --git a/src/main/java/graphql/execution/InputMapDefinesTooManyFieldsException.java b/src/main/java/graphql/execution/InputMapDefinesTooManyFieldsException.java new file mode 100644 index 0000000000..30bb61a3eb --- /dev/null +++ b/src/main/java/graphql/execution/InputMapDefinesTooManyFieldsException.java @@ -0,0 +1,18 @@ +package graphql.execution; + +import graphql.GraphQLException; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeUtil; + +/** + * https://facebook.github.io/graphql/#sec-Input-Objects + * + * - This unordered map should not contain any entries with names not defined by a field of this input object type, otherwise an error should be thrown. + */ +public class InputMapDefinesTooManyFieldsException extends GraphQLException { + + public InputMapDefinesTooManyFieldsException(GraphQLType graphQLType, String fieldName) { + super(String.format("The variables input contains a field name '%s' that is not defined for input object type '%s' ", GraphQLTypeUtil.getUnwrappedTypeName(graphQLType), fieldName)); + } + +} diff --git a/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java b/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java new file mode 100644 index 0000000000..52e1de3c23 --- /dev/null +++ b/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java @@ -0,0 +1,16 @@ +package graphql.execution; + +import graphql.GraphQLException; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeUtil; + +/** + * This is thrown if a non nullable value is coerced to a null value + */ +public class NonNullableValueCoercedAsNullException extends GraphQLException { + + public NonNullableValueCoercedAsNullException(GraphQLType graphQLType) { + super(String.format("Null value for NonNull type '%s", GraphQLTypeUtil.getUnwrappedTypeName(graphQLType))); + } + +} diff --git a/src/main/java/graphql/execution/ValuesResolver.java b/src/main/java/graphql/execution/ValuesResolver.java index f03dabf381..fe7be18ef1 100644 --- a/src/main/java/graphql/execution/ValuesResolver.java +++ b/src/main/java/graphql/execution/ValuesResolver.java @@ -2,18 +2,44 @@ import graphql.GraphQLException; -import graphql.language.*; -import graphql.schema.*; +import graphql.language.Argument; +import graphql.language.ArrayValue; +import graphql.language.NullValue; +import graphql.language.ObjectField; +import graphql.language.ObjectValue; +import graphql.language.Value; +import graphql.language.VariableDefinition; +import graphql.language.VariableReference; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; public class ValuesResolver { - public Map getVariableValues(GraphQLSchema schema, List variableDefinitions, Map inputs) { + public Map getVariableValues(GraphQLSchema schema, List variableDefinitions, Map args) { Map result = new LinkedHashMap<>(); for (VariableDefinition variableDefinition : variableDefinitions) { - result.put(variableDefinition.getName(), getVariableValue(schema, variableDefinition, inputs.get(variableDefinition.getName()))); + String varName = variableDefinition.getName(); + // we transfer the variable as field arguments if its present as value + if (args.containsKey(varName)) { + Object arg = args.get(varName); + Object variableValue = getVariableValue(schema, variableDefinition, arg); + result.put(varName, variableValue); + } } return result; } @@ -23,14 +49,19 @@ public Map getArgumentValues(List argumentTypes Map result = new LinkedHashMap<>(); Map argumentMap = argumentMap(arguments); for (GraphQLArgument fieldArgument : argumentTypes) { - Argument argument = argumentMap.get(fieldArgument.getName()); + String argName = fieldArgument.getName(); + Argument argument = argumentMap.get(argName); Object value; if (argument != null) { value = coerceValueAst(fieldArgument.getType(), argument.getValue(), variables); } else { value = fieldArgument.getDefaultValue(); } - result.put(fieldArgument.getName(), value); + // only put an arg into the result IF they specified a variable at all or + // the default value ended up being something non null + if (argumentMap.containsKey(argName) || value != null) { + result.put(argName, value); + } } return result; } @@ -48,6 +79,7 @@ private Map argumentMap(List arguments) { private Object getVariableValue(GraphQLSchema schema, VariableDefinition variableDefinition, Object inputValue) { GraphQLType type = TypeFromAST.getTypeFromAST(schema, variableDefinition.getType()); + //noinspection ConstantConditions if (!isValid(type, inputValue)) { throw new GraphQLException("Invalid value for type"); } @@ -67,7 +99,7 @@ private Object coerceValue(GraphQLType graphQLType, Object value) { if (graphQLType instanceof GraphQLNonNull) { Object returnValue = coerceValue(((GraphQLNonNull) graphQLType).getWrappedType(), value); if (returnValue == null) { - throw new GraphQLException("Null value for NonNull type " + graphQLType); + throw new NonNullableValueCoercedAsNullException(graphQLType); } return returnValue; } @@ -81,6 +113,7 @@ private Object coerceValue(GraphQLType graphQLType, Object value) { } else if (graphQLType instanceof GraphQLList) { return coerceValueForList((GraphQLList) graphQLType, value); } else if (graphQLType instanceof GraphQLInputObjectType && value instanceof Map) { + //noinspection unchecked return coerceValueForInputObjectType((GraphQLInputObjectType) graphQLType, (Map) value); } else if (graphQLType instanceof GraphQLInputObjectType) { return value; @@ -91,7 +124,15 @@ private Object coerceValue(GraphQLType graphQLType, Object value) { private Object coerceValueForInputObjectType(GraphQLInputObjectType inputObjectType, Map input) { Map result = new LinkedHashMap<>(); - for (GraphQLInputObjectField inputField : inputObjectType.getFields()) { + List fields = inputObjectType.getFields(); + List fieldNames = fields.stream().map(GraphQLInputObjectField::getName).collect(Collectors.toList()); + for (String inputFieldName : input.keySet()) { + if (!fieldNames.contains(inputFieldName)) { + throw new InputMapDefinesTooManyFieldsException(inputObjectType, inputFieldName); + } + } + + for (GraphQLInputObjectField inputField : fields) { if (input.containsKey(inputField.getName()) || alwaysHasValue(inputField)) { Object value = coerceValue(inputField.getType(), input.get(inputField.getName())); result.put(inputField.getName(), value == null ? inputField.getDefaultValue() : value); @@ -129,6 +170,9 @@ private Object coerceValueAst(GraphQLType type, Value inputValue, Map mapObjectValueFieldsByName(ObjectValue inputValue) { Map inputValueFieldsByName = new LinkedHashMap<>(); for (ObjectField objectField : inputValue.getObjectFields()) { diff --git a/src/main/java/graphql/language/AstPrinter.java b/src/main/java/graphql/language/AstPrinter.java index e596d07bf1..5457770da4 100644 --- a/src/main/java/graphql/language/AstPrinter.java +++ b/src/main/java/graphql/language/AstPrinter.java @@ -26,6 +26,7 @@ public class AstPrinter { printers.put(Argument.class, argument()); printers.put(ArrayValue.class, value()); printers.put(BooleanValue.class, value()); + printers.put(NullValue.class, value()); printers.put(Directive.class, directive()); printers.put(DirectiveDefinition.class, directiveDefinition()); printers.put(DirectiveLocation.class, directiveLocation()); @@ -399,6 +400,8 @@ static private String value(Value value) { return valueOf(((EnumValue) value).getName()); } else if (value instanceof BooleanValue) { return valueOf(((BooleanValue) value).isValue()); + } else if (value instanceof NullValue) { + return "null"; } else if (value instanceof ArrayValue) { return "[" + join(((ArrayValue) value).getValues(), ", ") + "]"; } else if (value instanceof ObjectValue) { diff --git a/src/main/java/graphql/language/NullValue.java b/src/main/java/graphql/language/NullValue.java new file mode 100644 index 0000000000..d565237be6 --- /dev/null +++ b/src/main/java/graphql/language/NullValue.java @@ -0,0 +1,33 @@ +package graphql.language; + + +import java.util.Collections; +import java.util.List; + +public class NullValue extends AbstractNode implements Value { + + public static NullValue Null = new NullValue(); + + private NullValue() { + } + + @Override + public List getChildren() { + return Collections.emptyList(); + } + + @Override + public boolean isEqualTo(Node o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + return true; + + } + + @Override + public String toString() { + return "NullValue{" + + '}'; + } +} diff --git a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java index 1aa602ce32..1156df4a8b 100644 --- a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java +++ b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java @@ -58,7 +58,10 @@ import java.util.Deque; import java.util.List; +import static graphql.language.NullValue.Null; + @Internal + public class GraphqlAntlrToLanguage extends GraphqlBaseVisitor { private final CommonTokenStream tokens; @@ -668,6 +671,9 @@ private Value getValue(GraphqlParser.ValueWithVariableContext ctx) { BooleanValue booleanValue = new BooleanValue(Boolean.parseBoolean(ctx.BooleanValue().getText())); newNode(booleanValue, ctx); return booleanValue; + } else if (ctx.NullValue() != null) { + newNode(Null, ctx); + return Null; } else if (ctx.StringValue() != null) { StringValue stringValue = new StringValue(parseString(ctx.StringValue().getText())); newNode(stringValue, ctx); @@ -713,6 +719,9 @@ private Value getValue(GraphqlParser.ValueContext ctx) { BooleanValue booleanValue = new BooleanValue(Boolean.parseBoolean(ctx.BooleanValue().getText())); newNode(booleanValue, ctx); return booleanValue; + } else if (ctx.NullValue() != null) { + newNode(Null, ctx); + return Null; } else if (ctx.StringValue() != null) { StringValue stringValue = new StringValue(parseString(ctx.StringValue().getText())); newNode(stringValue, ctx); diff --git a/src/main/java/graphql/schema/idl/SchemaGenerator.java b/src/main/java/graphql/schema/idl/SchemaGenerator.java index 890b4f901c..5e1936b692 100644 --- a/src/main/java/graphql/schema/idl/SchemaGenerator.java +++ b/src/main/java/graphql/schema/idl/SchemaGenerator.java @@ -13,6 +13,7 @@ import graphql.language.IntValue; import graphql.language.InterfaceTypeDefinition; import graphql.language.Node; +import graphql.language.NullValue; import graphql.language.ObjectTypeDefinition; import graphql.language.ObjectValue; import graphql.language.OperationTypeDefinition; @@ -475,6 +476,8 @@ private Object buildValue(Value value) { result = arrayValue.getValues().stream().map(this::buildValue).toArray(); } else if (value instanceof ObjectValue) { result = buildObjectValue((ObjectValue) value); + } else if (value instanceof NullValue) { + result = null; } return result; diff --git a/src/main/java/graphql/validation/ValidationUtil.java b/src/main/java/graphql/validation/ValidationUtil.java index e75928fcbc..2ef7f448a8 100644 --- a/src/main/java/graphql/validation/ValidationUtil.java +++ b/src/main/java/graphql/validation/ValidationUtil.java @@ -25,6 +25,9 @@ public boolean isValidLiteralValue(Value value, GraphQLType type) { if (value == null) { return !(type instanceof GraphQLNonNull); } + if (value instanceof NullValue) { + return !(type instanceof GraphQLNonNull); + } if (value instanceof VariableReference) { return true; } diff --git a/src/test/groovy/graphql/CapturingDataFetcher.groovy b/src/test/groovy/graphql/CapturingDataFetcher.groovy new file mode 100644 index 0000000000..b6a83885ef --- /dev/null +++ b/src/test/groovy/graphql/CapturingDataFetcher.groovy @@ -0,0 +1,19 @@ +package graphql + +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment + +/** + * Help to capture data fetcher arguments passed in + */ +class CapturingDataFetcher implements DataFetcher { + Map args + DataFetchingEnvironment environment + + @Override + Object get(DataFetchingEnvironment environment) { + this.environment = environment + this.args = environment.getArguments() + null + } +} diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index d69fb1dde4..551019bb98 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -157,10 +157,10 @@ class GraphQLTest extends Specification { set.add("One") set.add("Two") - def schema = GraphQLSchema.newSchema() - .query(GraphQLObjectType.newObject() + def schema = newSchema() + .query(newObject() .name("QueryType") - .field(GraphQLFieldDefinition.newFieldDefinition() + .field(newFieldDefinition() .name("set") .type(new GraphQLList(GraphQLString)) .dataFetcher({ set }))) @@ -231,7 +231,7 @@ class GraphQLTest extends Specification { .build() when: - def result = new GraphQL(schema).execute("mutation { doesNotExist }"); + def result = new GraphQL(schema).execute("mutation { doesNotExist }") then: result.errors.size() == 1 @@ -372,12 +372,12 @@ class GraphQLTest extends Specification { .build() def query = "{foo}" when: - def result = GraphQL.newGraphQL(schema).build().execute(query) + GraphQL.newGraphQL(schema).build().execute(query) then: 1 * dataFetcher.get(_) >> { DataFetchingEnvironment env -> - assert env.arguments.size() == 1 + assert env.arguments.size() == 0 assert env.arguments['bar'] == null } } diff --git a/src/test/groovy/graphql/NullValueSupportTest.groovy b/src/test/groovy/graphql/NullValueSupportTest.groovy new file mode 100644 index 0000000000..08fc6f3b4b --- /dev/null +++ b/src/test/groovy/graphql/NullValueSupportTest.groovy @@ -0,0 +1,309 @@ +package graphql + +import graphql.execution.InputMapDefinesTooManyFieldsException +import graphql.execution.NonNullableValueCoercedAsNullException +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorType +import spock.lang.Specification +import spock.lang.Unroll + +/* + * Taken from http://facebook.github.io/graphql/#sec-Input-Objects + * + * + + Test Case Original Value Variables Coerced Value + -------------------------------------------------------------------------------------------- + A { a: "abc", b: 123 } null { a: "abc", b: 123 } + B { a: 123, b: "123" } null { a: "123", b: 123 } + C { a: "abc" } null Error: Missing required field b + D { a: "abc", b: null } null Error: b must be non‐null. + E { a: null, b: 1 } null { a: null, b: 1 } + F { b: $var } { var: 123 } { b: 123 } + G { b: $var } {} Error: Missing required field b. + H { b: $var } { var: null } Error: b must be non‐null. + I { a: $var, b: 1 } { var: null } { a: null, b: 1 } + J { a: $var, b: 1 } {} { b: 1 } + + These did not come from the spec but added by us as extra tests + + K { $var } { a : "abc", b:123 } { a: "abc", b: 123 } + L { $var } { b:123 } { b: 123 } + M { $var } { a : "abc", b:null } Error: b must be non‐null. + N { $var } { a : "abc" } Error: b must be non‐null. + O { $var } { a : "abc", b: 123, c:"xyz" } Error: c is not a valid field + + */ + +class NullValueSupportTest extends Specification { + + def graphqlSpecExamples = ''' + schema { + query : Query + mutation : Mutation + } + + type Query { + a : String + b: Int! + } + + type Mutation { + mutate(inputArg : ExampleInputObject) : Query + } + + input ExampleInputObject { + a: String + b: Int! + } + + ''' + + @Unroll + "test graphql spec examples that output results : #testCase"() { + def fetcher = new CapturingDataFetcher() + + def schema = TestUtil.schema(graphqlSpecExamples, ["Mutation": ["mutate": fetcher]]) + + when: + + def result = GraphQL.newGraphQL(schema).build().execute(queryStr, "mutate", "ctx", variables) + + then: + assert result.errors.isEmpty(): "Validation Failure in case ${testCase} : $result.errors" + assert fetcher.args == expectedArgs: "Argument Failure in case ${testCase} : was ${fetcher.args}" + + where: + + testCase | queryStr | variables || expectedArgs + + // ------------------------------ + 'A' | ''' + mutation mutate { + mutate(inputArg : { a: "abc", b: 123 }) { + a + } + } + ''' | [:] || [inputArg: [a: "abc", b: 123]] + + // ------------------------------ + // coerced from string -> int and vice versus + // + // spec says it should work. but we think the spec is wrong since + // the reference implementation will not cross coerce these types + // + /* + 'B' | ''' + mutation mutate { + mutate(inputArg : { a: 123, b: "123" }) { + a + } + } + ''' | [:] || [inputArg: [a: "123", b: 123]] + */ + + // ------------------------------ + 'E' | ''' + mutation mutate { + mutate(inputArg : { a: null, b: 1 }) { + a + } + } + ''' | [:] || [inputArg: [a: null, b: 1]] + + // ------------------------------ + 'F' | ''' + mutation mutate($var : Int!) { + mutate(inputArg : { b: $var }) { + a + } + } + ''' | [var: 123] || [inputArg: [b: 123]] + + // ------------------------------ + 'I' | ''' + mutation mutate($var : String) { + mutate(inputArg : { a: $var, b: 1 }) { + a + } + } + ''' | [var: null] || [inputArg: [a: null, b: 1]] + + // ------------------------------ + 'J' | ''' + mutation mutate($var : String) { + mutate(inputArg : { a: $var, b: 1 }) { + a + } + } + ''' | [:] || [inputArg: [b: 1]] + + // ------------------------------ + 'K' | ''' + mutation mutate($var : ExampleInputObject) { + mutate(inputArg : $var) { + a + } + } + ''' | [var: [a: "abc", b: 123]] || [inputArg: [a: "abc", b: 123]] + + // ------------------------------ + 'L' | ''' + mutation mutate($var : ExampleInputObject) { + mutate(inputArg : $var) { + a + } + } + ''' | [var: [b: 123]] || [inputArg: [b: 123]] + + } + + @Unroll + "test graphql spec examples that output errors #testCase"() { + def fetcher = new CapturingDataFetcher() + + def schema = TestUtil.schema(graphqlSpecExamples, ["Mutation": ["mutate": fetcher]]) + + when: + + ExecutionResult result = null + try { + result = GraphQL.newGraphQL(schema).build().execute(queryStr, "mutate", "ctx", variables) + } catch (GraphQLException e) { + assert false: "Unexpected exception during ${testCase} : ${e.message}" + } + + then: + assert !result.errors.isEmpty(): "Expected errors in ${testCase}" + result.errors[0] instanceof ValidationError + (result.errors[0] as ValidationError).validationErrorType == expectedError + + + where: + + testCase | queryStr | variables || expectedError + + // ------------------------------ + 'C' | ''' + mutation mutate { + mutate(inputArg : { a: "abc"}) { + a + } + } + ''' | [:] || ValidationErrorType.WrongType + + // ------------------------------ + 'D' | ''' + mutation mutate { + mutate(inputArg : { a: "abc", b: null }) { + a + } + } + ''' | [:] || ValidationErrorType.WrongType + } + + @Unroll + "test graphql spec examples that output exception : #testCase"() { + def fetcher = new CapturingDataFetcher() + + def schema = TestUtil.schema(graphqlSpecExamples, ["Mutation": ["mutate": fetcher]]) + + when: + + GraphQL.newGraphQL(schema).build().execute(queryStr, "mutate", "ctx", variables) + + then: + thrown(expectedException) + + + where: + + testCase | queryStr | variables || expectedException + + // ------------------------------ + 'G' | ''' + mutation mutate($var : Int!) { + mutate(inputArg : { b: $var }) { + a + } + } + ''' | [:] || NonNullableValueCoercedAsNullException + + // ------------------------------ + 'H' | ''' + mutation mutate($var : Int!) { + mutate(inputArg : { b: $var }) { + a + } + } + ''' | [var: null] || NonNullableValueCoercedAsNullException + + // ------------------------------ + 'M' | ''' + mutation mutate($var : ExampleInputObject) { + mutate(inputArg : $var) { + a + } + } + ''' | [var: [a: "abc", b: null]] || NonNullableValueCoercedAsNullException + + // ------------------------------ + 'N' | ''' + mutation mutate($var : ExampleInputObject) { + mutate(inputArg : $var) { + a + } + } + ''' | [var: [a: "abc"]] || NonNullableValueCoercedAsNullException + + // ------------------------------ + 'O' | ''' + mutation mutate($var : ExampleInputObject) { + mutate(inputArg : $var) { + a + } + } + ''' | [var: [a: "abc", b: 123, c: "xyz"]] || InputMapDefinesTooManyFieldsException + + } + + def "nulls in literal places are supported in general"() { + + def fetcher = new CapturingDataFetcher() + + def schema = TestUtil.schema(""" + schema { query : Query } + + type Query { + list(arg : [String]) : Int + scalar(arg : String) : Int + complex(arg : ComplexInputObject) : Int + } + + input ComplexInputObject { + a: String + b: Int! + } + + """, + ["Query": [ + "list" : fetcher, + "scalar" : fetcher, + "complex": fetcher, + ]]) + + when: + def result = GraphQL.newGraphQL(schema).build().execute(queryStr, null, "ctx", [:]) + assert result.errors.isEmpty(): "Unexpected query errors : ${result.errors}" + + then: + fetcher.args == expectedArgs + + where: + queryStr | expectedArgs + '''{ list(arg : ["abc", null, "xyz"]) }''' | [arg: ["abc", null, "xyz"]] + '''{ scalar(arg : null) }''' | [arg: null] + '''{ complex(arg : null) }''' | [arg: null] + + } +} diff --git a/src/test/groovy/graphql/TestUtil.groovy b/src/test/groovy/graphql/TestUtil.groovy index 399e15e979..2cd8569bd6 100644 --- a/src/test/groovy/graphql/TestUtil.groovy +++ b/src/test/groovy/graphql/TestUtil.groovy @@ -1,6 +1,11 @@ package graphql import graphql.schema.* +import graphql.schema.idl.RuntimeWiring +import graphql.schema.idl.SchemaGenerator +import graphql.schema.idl.SchemaParser +import graphql.schema.idl.TypeRuntimeWiring +import graphql.schema.idl.errors.SchemaProblem import static graphql.Scalars.GraphQLString import static graphql.schema.GraphQLArgument.newArgument @@ -21,4 +26,26 @@ class TestUtil { .name("QueryType") .build()) .build() + + static GraphQLSchema schema(String spec, Map> dataFetchers) { + def wiring = RuntimeWiring.newRuntimeWiring() + dataFetchers.each { type, fieldFetchers -> + def tw = TypeRuntimeWiring.newTypeWiring(type).dataFetchers(fieldFetchers) + wiring.type(tw) + } + schema(spec, wiring) + } + + static GraphQLSchema schema(String spec, RuntimeWiring.Builder runtimeWiring) { + schema(spec, runtimeWiring.build()) + } + + static GraphQLSchema schema(String spec, RuntimeWiring runtimeWiring) { + try { + def registry = new SchemaParser().parse(spec) + return new SchemaGenerator().makeExecutableSchema(registry, runtimeWiring) + } catch (SchemaProblem e) { + assert false: "The schema could not be compiled : ${e}" + } + } } diff --git a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy index 30b4fb1da8..8aab82a69a 100644 --- a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy @@ -251,8 +251,8 @@ class ValuesResolverTest extends Specification { def "getArgumentValues: resolves enum literals"() { given: "the ast" - EnumValue enumValue1 = new EnumValue("PLUTO"); - EnumValue enumValue2 = new EnumValue("MARS"); + EnumValue enumValue1 = new EnumValue("PLUTO") + EnumValue enumValue2 = new EnumValue("MARS") def argument1 = new Argument("arg1", enumValue1) def argument2 = new Argument("arg2", enumValue2) @@ -383,4 +383,24 @@ class ValuesResolverTest extends Specification { [intKey: 10] | _ [intKey: 10, requiredField: null] | _ } + + def "getVariableValues: simple types with values not provided in variables map"() { + given: + + def schema = TestUtil.schemaWithInputType(GraphQLString) + VariableDefinition fooVarDef = new VariableDefinition("foo", new TypeName("String")) + VariableDefinition barVarDef = new VariableDefinition("bar", new TypeName("String")) + + when: + def resolvedValues = resolver.getVariableValues(schema, [fooVarDef, barVarDef], InputValue) + + then: + resolvedValues == outputValue + + where: + InputValue || outputValue + [foo: "added", bar: null] || [foo: "added", bar: null] + [foo: "added"] || [foo: "added"] + } + } diff --git a/src/test/groovy/graphql/language/AstPrinterTest.groovy b/src/test/groovy/graphql/language/AstPrinterTest.groovy index d7344728f4..482218ce49 100644 --- a/src/test/groovy/graphql/language/AstPrinterTest.groovy +++ b/src/test/groovy/graphql/language/AstPrinterTest.groovy @@ -323,4 +323,53 @@ query HeroNameAndFriends($episode: Episode = "JEDI") { ''' } +//------------------------------------------------- + def "ast printing of null"() { + def query = ''' +query NullEpisodeQuery { + hero(episode: null) { + name + } +} +''' + def document = parse(query) + String output = printAst(document) + + expect: + output == '''query NullEpisodeQuery { + hero(episode: null) { + name + } +} +''' + } + + //------------------------------------------------- + def "ast printing of default variables with null"() { + def query = ''' +query NullVariableDefaultValueQuery($episode: Episode = null) { + hero(episode: $episode) { + name + friends { + name + } + } +} +''' + def document = parse(query) + String output = printAst(document) + + expect: + output == '''query NullVariableDefaultValueQuery($episode: Episode = null) { + hero(episode: $episode) { + name + friends { + name + } + } +} +''' + } + + } diff --git a/src/test/groovy/graphql/parser/ParserTest.groovy b/src/test/groovy/graphql/parser/ParserTest.groovy index f9c69e1ed1..6f72f48e4f 100644 --- a/src/test/groovy/graphql/parser/ParserTest.groovy +++ b/src/test/groovy/graphql/parser/ParserTest.groovy @@ -465,4 +465,19 @@ class ParserTest extends Specification { then: noExceptionThrown() } + + + def "parses null value"() { + given: + def input = "{ foo(bar: null) }" + + when: + def document = new Parser().parseDocument(input) + def operation = document.definitions[0] as OperationDefinition + def selection = operation.selectionSet.selections[0] as Field + + then: + selection.arguments[0].value == NullValue.Null + + } } diff --git a/src/test/groovy/graphql/validation/ValidationUtilTest.groovy b/src/test/groovy/graphql/validation/ValidationUtilTest.groovy index 9e4f908127..3beaf42f40 100644 --- a/src/test/groovy/graphql/validation/ValidationUtilTest.groovy +++ b/src/test/groovy/graphql/validation/ValidationUtilTest.groovy @@ -39,6 +39,11 @@ class ValidationUtilTest extends Specification { !validationUtil.isValidLiteralValue(null, nonNull(GraphQLString)) } + def "NullValue and NonNull is invalid"() { + expect: + !validationUtil.isValidLiteralValue(NullValue.Null, nonNull(GraphQLString)) + } + def "a nonNull value for a NonNull type is valid"() { expect: validationUtil.isValidLiteralValue(new StringValue("string"), nonNull(GraphQLString)) @@ -49,6 +54,11 @@ class ValidationUtilTest extends Specification { validationUtil.isValidLiteralValue(null, GraphQLString) } + def "NullValue is valid when type is NonNull"() { + expect: + validationUtil.isValidLiteralValue(NullValue.Null, GraphQLString) + } + def "variables are valid"() { expect: validationUtil.isValidLiteralValue(new VariableReference("var"), GraphQLBoolean)