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

fix: Incorrect argument type for JSON variables #1064

Merged
merged 3 commits into from
Oct 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 34 additions & 9 deletions core/src/main/scala/caliban/execution/Field.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package caliban.execution
import caliban.InputValue
import caliban.Value
import caliban.Value.{ BooleanValue, NullValue }
import caliban.introspection.adt.{ __DeprecatedArgs, __Type }
import caliban.introspection.adt.{ __DeprecatedArgs, __Type, __TypeKind }
import caliban.parsing.SourceMapper
import caliban.parsing.adt.Definition.ExecutableDefinition.FragmentDefinition
import caliban.parsing.adt.Selection.{ FragmentSpread, InlineFragment, Field => F }
import caliban.parsing.adt.{ Directive, LocationInfo, Selection, VariableDefinition }
import caliban.parsing.adt.{ Directive, LocationInfo, Selection, Type, VariableDefinition }
import caliban.schema.{ RootType, Types }

case class Field(
Expand Down Expand Up @@ -58,7 +58,7 @@ object Field {
alias,
field.fields,
None,
resolveVariables(arguments, variableDefinitions, variableValues),
resolveVariables(arguments, variableDefinitions, variableValues, rootType),
() => sourceMapper.getLocation(index),
directives ++ schemaDirectives
)
Expand Down Expand Up @@ -96,24 +96,49 @@ object Field {
private def resolveVariables(
arguments: Map[String, InputValue],
variableDefinitions: List[VariableDefinition],
variableValues: Map[String, InputValue]
variableValues: Map[String, InputValue],
rootType: RootType
): Map[String, InputValue] = {
def resolveVariable(value: InputValue): InputValue =
value match {
case InputValue.ListValue(values) => InputValue.ListValue(values.map(resolveVariable))
case InputValue.ObjectValue(fields) =>
InputValue.ObjectValue(fields.map({ case (k, v) => k -> resolveVariable(v) }))
case InputValue.VariableValue(name) =>
lazy val defaultInputValue = (for {
definition <- variableDefinitions.find(_.name == name)
inputValue <- definition.defaultValue
} yield inputValue) getOrElse NullValue
variableValues.getOrElse(name, defaultInputValue)
(for {
definition <- variableDefinitions.find(_.name == name)
defaultValue = definition.defaultValue getOrElse NullValue
value = variableValues.getOrElse(name, defaultValue)
} yield resolveEnumValues(value, definition, rootType)) getOrElse NullValue
case value: Value => value
}
arguments.map({ case (k, v) => k -> resolveVariable(v) })
}

// Since we cannot separate a String from an Enum when variables
// are parsed, we need to translate from strings to enums here
// if we have a valid enum field.
private def resolveEnumValues(
value: InputValue,
definition: VariableDefinition,
rootType: RootType
): InputValue = {
val t = Type
.innerType(definition.variableType)

rootType.types
.get(t)
.map(_.kind)
.flatMap { kind =>
(kind, value) match {
case (__TypeKind.ENUM, Value.StringValue(v)) =>
Some(Value.EnumValue(v))
case _ => None
}
}
.getOrElse(value)
}

private def subtypeNames(typeName: String, rootType: RootType): Option[List[String]] =
rootType.types
.get(typeName)
Expand Down
93 changes: 93 additions & 0 deletions core/src/test/scala/caliban/execution/FieldArgsSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package caliban.execution

import zio._

import caliban.CalibanError
import caliban.GraphQL._
import caliban.Macros.gqldoc
import caliban.InputValue
import caliban.Value.EnumValue
import caliban.RootResolver
import caliban.schema.Annotations.GQLDefault
import zio.test.Assertion._
import zio.test._
import zio.test.environment.TestEnvironment
import caliban.Value
import caliban.GraphQLRequest

object FieldArgsSpec extends DefaultRunnableSpec {
sealed trait COLOR
object COLOR {
case object GREEN extends COLOR
case object BLUE extends COLOR
}

override def spec = suite("FieldArgsSpec")(
testM("it forward args of correct type") {
case class QueryInput(color: COLOR, string: String)
case class Query(query: Field => QueryInput => UIO[String])
val query =
"""query{
| query(string: "test", color: BLUE)
|}""".stripMargin

for {
ref <- Ref.make[Option[Field]](None)
api = graphQL(
RootResolver(
Query(
query = info =>
i =>
ref.set(Option(info)) *>
ZIO.succeed(i.string)
)
)
)
interpreter <- api.interpreter
_ <- interpreter.execute(query)

res <- ref.get
} yield assertTrue(
res.get.arguments.get("color").get == EnumValue("BLUE")
)
}
) +
testM("it forward args as correct type from variables") {
case class QueryInput(color: COLOR, string: String)
case class Query(query: Field => QueryInput => UIO[String])
val query =
"""query MyQuery($color: COLOR!) {
| query(string: "test", color: $color)
|}""".stripMargin

for {
ref <- Ref.make[Option[Field]](None)
api = graphQL(
RootResolver(
Query(
query = info => {
i =>
ref.set(Option(info)) *>
ZIO.succeed(i.string)
}
)
)
)
interpreter <- api.interpreter
qres <- interpreter.executeRequest(
request = GraphQLRequest(
query = Some(query),
// "color" is a string here since it will come directly from
// parsed JSON which is unaware that it should be an Enum
variables = Some(Map("color" -> Value.StringValue("BLUE")))
),
skipValidation = false,
enableIntrospection = true,
queryExecution = QueryExecution.Parallel
)
res <- ref.get
} yield assertTrue(
res.get.arguments.get("color").get == EnumValue("BLUE")
)
}
}