Repository: graphql-java-kickstart/graphql-java-tools
Pull request #358 (2020-01-22 13:04:53)

Treat array of non-null scalars as scalars
This is fix for #240, and test for same issue.

When array of non-nullable scalars is input, an error is thrown from Jackson ObjectMapper. The problem then was, NonNullType was ignored by MethodFieldResolver.isScalarType. Changes in #342 handles NonNullType but it doesn't handle NonNullType.type is ListType.


Patch:
diff --git a/pom.xml b/pom.xml
index b2d8ed8..2e3cb55 100644
--- a/pom.xml
+++ b/pom.xml
@@ -138,6 +138,12 @@
             <version>1.0.2</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>3.1.0</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/MethodFieldResolver.kt b/src/main/kotlin/com/coxautodev/graphql/tools/MethodFieldResolver.kt
index b9027c4..e473d45 100644
--- a/src/main/kotlin/com/coxautodev/graphql/tools/MethodFieldResolver.kt
+++ b/src/main/kotlin/com/coxautodev/graphql/tools/MethodFieldResolver.kt
@@ -127,16 +127,12 @@ internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolver
     }
 
     private fun isScalarType(environment: DataFetchingEnvironment, type: Type<*>, genericParameterType: JavaType): Boolean =
-            when {
-                type is ListType ->
-                    List::class.java.isAssignableFrom(this.genericType.getRawClass(genericParameterType))
+            when (type) {
+                is ListType -> List::class.java.isAssignableFrom(this.genericType.getRawClass(genericParameterType))
                         && isScalarType(environment, type.type, this.genericType.unwrapGenericType(genericParameterType))
-                type is TypeName ->
-                    environment.graphQLSchema?.getType(type.name)?.let { isScalar(it) } ?: false
-                type is NonNullType && type.type is TypeName ->
-                    environment.graphQLSchema?.getType((type.type as TypeName).name)?.let { isScalar(unwrapNonNull(it)) } ?: false
-                else ->
-                    false
+                is TypeName -> environment.graphQLSchema?.getType(type.name)?.let { isScalar(it) } ?: false
+                is NonNullType -> isScalarType(environment, type.type, genericParameterType)
+                else -> false
             }
 
     override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> {


Test patch:
diff --git a/src/test/groovy/com/coxautodev/graphql/tools/EndToEndSpec.groovy b/src/test/groovy/com/coxautodev/graphql/tools/EndToEndSpec.groovy
index e75cbcc..8ec955c 100644
--- a/src/test/groovy/com/coxautodev/graphql/tools/EndToEndSpec.groovy
+++ b/src/test/groovy/com/coxautodev/graphql/tools/EndToEndSpec.groovy
@@ -197,6 +197,19 @@ class EndToEndSpec extends Specification {
             data.itemByUUID
     }
 
+    def "generated schema should handle non nullable scalar types"() {
+        when:
+        def fileParts = [new MockPart("test.doc", "Hello"), new MockPart("test.doc", "World")]
+        def args = ["fileParts": fileParts]
+        def data = Utils.assertNoGraphQlErrors( gql,  args) {
+            '''
+              mutation ($fileParts: [Upload!]!) { echoFiles(fileParts: $fileParts)}
+            '''
+        }
+        then:
+        (data["echoFiles"] as ArrayList<String>).join(",") == "Hello,World"
+    }
+
     def "generated schema should handle any java.util.Map (using HashMap) types as property maps"() {
         when:
         def data = Utils.assertNoGraphQlErrors(gql) {
diff --git a/src/test/kotlin/com/coxautodev/graphql/tools/EndToEndSpecHelper.kt b/src/test/kotlin/com/coxautodev/graphql/tools/EndToEndSpecHelper.kt
index 8e27e7e..342f756 100644
--- a/src/test/kotlin/com/coxautodev/graphql/tools/EndToEndSpecHelper.kt
+++ b/src/test/kotlin/com/coxautodev/graphql/tools/EndToEndSpecHelper.kt
@@ -4,21 +4,21 @@ import graphql.execution.DataFetcherResult
 import graphql.execution.batched.Batched
 import graphql.language.ObjectValue
 import graphql.language.StringValue
-import graphql.schema.Coercing
-import graphql.schema.DataFetchingEnvironment
-import graphql.schema.GraphQLScalarType
+import graphql.schema.*
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.channels.ReceiveChannel
 import kotlinx.coroutines.coroutineScope
 import org.reactivestreams.Publisher
+import java.io.InputStream
 import java.util.*
 import java.util.concurrent.CompletableFuture
+import javax.servlet.http.Part
 
 fun createSchema() = SchemaParser.newParser()
         .schemaString(schemaDefinition)
-        .resolvers(Query(), Mutation(), Subscription(), ItemResolver(), UnusedRootResolver(), UnusedResolver())
-        .scalars(customScalarUUID, customScalarMap, customScalarId)
+        .resolvers(Query(), Mutation(), Subscription(), ItemResolver(), UnusedRootResolver(), UnusedResolver(), EchoFilesResolver())
+        .scalars(customScalarUUID, customScalarMap, customScalarId, uploadScalar)
         .dictionary("OtherItem", OtherItemWithWrongName::class)
         .dictionary("ThirdItem", ThirdItem::class)
         .dictionary("ComplexMapItem", ComplexMapItem::class)
@@ -31,6 +31,7 @@ val schemaDefinition = """
 ## Private comment!
 scalar UUID
 scalar customScalarMap
+scalar Upload
 
 type Query {
     # Check if items list is empty
@@ -108,6 +109,7 @@ input ComplexInputTypeTwo {
 
 type Mutation {
     addItem(newItem: NewItemInput!): Item!
+    echoFiles(fileParts: [Upload!]!): [String!]!
 }
 
 type Subscription {
@@ -353,6 +355,10 @@ class ItemResolver : GraphQLResolver<Item> {
     }
 }
 
+class EchoFilesResolver: GraphQLMutationResolver{
+    fun echoFiles(fileParts: List<Part>): List<String> = fileParts.map { String(it.inputStream.readBytes()) }
+}
+
 interface ItemInterface {
     val name: String
     val type: Type
@@ -373,6 +379,18 @@ data class ComplexNullable(val first: String, val second: String, val third: Str
 data class ComplexInputType(val first: String, val second: List<List<ComplexInputTypeTwo>?>?)
 data class ComplexInputTypeTwo(val first: String)
 data class ItemWithGenericProperties(val keys: List<String>)
+class MockPart(private val name: String, private val content: String):Part{
+    override fun getSubmittedFileName(): String  = name
+    override fun getName(): String = name
+    override fun write(fileName: String?) = throw IllegalArgumentException("Not supported")
+    override fun getHeader(name: String): String? = null
+    override fun getSize(): Long  = content.toByteArray().size.toLong()
+    override fun getContentType(): String? =null
+    override fun getHeaders(name: String?): Collection<String> = listOf()
+    override fun getHeaderNames(): Collection<String>  = listOf()
+    override fun getInputStream(): InputStream  = content.byteInputStream()
+    override fun delete()  = throw IllegalArgumentException("Not supported")
+}
 
 val customScalarId = GraphQLScalarType.newScalar()
         .name("ID")
@@ -427,3 +445,33 @@ val customScalarMap = GraphQLScalarType.newScalar()
             override fun parseLiteral(input: Any?): Map<String, Any> = (input as ObjectValue).objectFields.associateBy { it.name }.mapValues { (it.value.value as StringValue).value }
         })
         .build()
+
+//Definition from https://github.com/graphql-java-kickstart/graphql-java-servlet/blob/master/src/main/java/graphql/servlet/core/ApolloScalars.java
+val uploadScalar: GraphQLScalarType = GraphQLScalarType.newScalar()
+        .name("Upload")
+        .description("A file part in a multipart request")
+        .coercing(object : Coercing<Part?, Void?> {
+            override fun serialize(dataFetcherResult: Any): Void? {
+                throw CoercingSerializeException("Upload is an input-only type")
+            }
+
+            override fun parseValue(input: Any?): Part? {
+                return when (input) {
+                    is Part -> {
+                        input
+                    }
+                    null -> {
+                        null
+                    }
+                    else -> {
+                        throw CoercingParseValueException("Expected type ${Part::class.java.name} but was ${input.javaClass.name}")
+                    }
+                }
+            }
+
+            override fun parseLiteral(input: Any): Part? {
+                throw CoercingParseLiteralException(
+                        "Must use variables to specify Upload values")
+            }
+        }).build()
+