Skip to content

Commit 2f24ea1

Browse files
committed
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.
1 parent b9d4b12 commit 2f24ea1

File tree

4 files changed

+77
-14
lines changed

4 files changed

+77
-14
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@
138138
<version>1.0.2</version>
139139
<scope>test</scope>
140140
</dependency>
141+
<dependency>
142+
<groupId>javax.servlet</groupId>
143+
<artifactId>javax.servlet-api</artifactId>
144+
<version>3.1.0</version>
145+
<scope>test</scope>
146+
</dependency>
141147
</dependencies>
142148

143149
<build>

src/main/kotlin/com/coxautodev/graphql/tools/MethodFieldResolver.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,12 @@ internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolver
127127
}
128128

129129
private fun isScalarType(environment: DataFetchingEnvironment, type: Type<*>, genericParameterType: JavaType): Boolean =
130-
when {
131-
type is ListType ->
132-
List::class.java.isAssignableFrom(this.genericType.getRawClass(genericParameterType))
130+
when (type) {
131+
is ListType -> List::class.java.isAssignableFrom(this.genericType.getRawClass(genericParameterType))
133132
&& isScalarType(environment, type.type, this.genericType.unwrapGenericType(genericParameterType))
134-
type is TypeName ->
135-
environment.graphQLSchema?.getType(type.name)?.let { isScalar(it) } ?: false
136-
type is NonNullType && type.type is TypeName ->
137-
environment.graphQLSchema?.getType((type.type as TypeName).name)?.let { isScalar(unwrapNonNull(it)) } ?: false
138-
else ->
139-
false
133+
is TypeName -> environment.graphQLSchema?.getType(type.name)?.let { isScalar(it) } ?: false
134+
is NonNullType -> isScalarType(environment, type.type, genericParameterType)
135+
else -> false
140136
}
141137

142138
override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> {

src/test/groovy/com/coxautodev/graphql/tools/EndToEndSpec.groovy

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,19 @@ class EndToEndSpec extends Specification {
197197
data.itemByUUID
198198
}
199199

200+
def "generated schema should handle non nullable scalar types"() {
201+
when:
202+
def fileParts = [new MockPart("test.doc", "Hello"), new MockPart("test.doc", "World")]
203+
def args = ["fileParts": fileParts]
204+
def data = Utils.assertNoGraphQlErrors( gql, args) {
205+
'''
206+
mutation ($fileParts: [Upload!]!) { echoFiles(fileParts: $fileParts)}
207+
'''
208+
}
209+
then:
210+
data
211+
}
212+
200213
def "generated schema should handle any java.util.Map (using HashMap) types as property maps"() {
201214
when:
202215
def data = Utils.assertNoGraphQlErrors(gql) {

src/test/kotlin/com/coxautodev/graphql/tools/EndToEndSpecHelper.kt

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@ import graphql.execution.DataFetcherResult
44
import graphql.execution.batched.Batched
55
import graphql.language.ObjectValue
66
import graphql.language.StringValue
7-
import graphql.schema.Coercing
8-
import graphql.schema.DataFetchingEnvironment
9-
import graphql.schema.GraphQLScalarType
7+
import graphql.schema.*
108
import kotlinx.coroutines.CompletableDeferred
119
import kotlinx.coroutines.channels.Channel
1210
import kotlinx.coroutines.channels.ReceiveChannel
1311
import kotlinx.coroutines.coroutineScope
1412
import org.reactivestreams.Publisher
13+
import java.io.InputStream
1514
import java.util.*
1615
import java.util.concurrent.CompletableFuture
16+
import javax.servlet.http.Part
1717

1818
fun createSchema() = SchemaParser.newParser()
1919
.schemaString(schemaDefinition)
20-
.resolvers(Query(), Mutation(), Subscription(), ItemResolver(), UnusedRootResolver(), UnusedResolver())
21-
.scalars(customScalarUUID, customScalarMap, customScalarId)
20+
.resolvers(Query(), Mutation(), Subscription(), ItemResolver(), UnusedRootResolver(), UnusedResolver(), EchoFilesResolver())
21+
.scalars(customScalarUUID, customScalarMap, customScalarId, uploadScalar)
2222
.dictionary("OtherItem", OtherItemWithWrongName::class)
2323
.dictionary("ThirdItem", ThirdItem::class)
2424
.dictionary("ComplexMapItem", ComplexMapItem::class)
@@ -31,6 +31,7 @@ val schemaDefinition = """
3131
## Private comment!
3232
scalar UUID
3333
scalar customScalarMap
34+
scalar Upload
3435
3536
type Query {
3637
# Check if items list is empty
@@ -108,6 +109,7 @@ input ComplexInputTypeTwo {
108109
109110
type Mutation {
110111
addItem(newItem: NewItemInput!): Item!
112+
echoFiles(fileParts: [Upload!]!): [String!]!
111113
}
112114
113115
type Subscription {
@@ -353,6 +355,10 @@ class ItemResolver : GraphQLResolver<Item> {
353355
}
354356
}
355357

358+
class EchoFilesResolver: GraphQLMutationResolver{
359+
fun echoFiles(fileParts: List<Part>): List<String> = fileParts.map { String(it.inputStream.readBytes()) }
360+
}
361+
356362
interface ItemInterface {
357363
val name: String
358364
val type: Type
@@ -373,6 +379,18 @@ data class ComplexNullable(val first: String, val second: String, val third: Str
373379
data class ComplexInputType(val first: String, val second: List<List<ComplexInputTypeTwo>?>?)
374380
data class ComplexInputTypeTwo(val first: String)
375381
data class ItemWithGenericProperties(val keys: List<String>)
382+
class MockPart(private val name: String, private val content: String):Part{
383+
override fun getSubmittedFileName(): String = name
384+
override fun getName(): String = name
385+
override fun write(fileName: String?) = throw IllegalArgumentException("Not supported")
386+
override fun getHeader(name: String): String? = null
387+
override fun getSize(): Long = content.toByteArray().size.toLong()
388+
override fun getContentType(): String? =null
389+
override fun getHeaders(name: String?): Collection<String> = listOf()
390+
override fun getHeaderNames(): Collection<String> = listOf()
391+
override fun getInputStream(): InputStream = content.byteInputStream()
392+
override fun delete() = throw IllegalArgumentException("Not supported")
393+
}
376394

377395
val customScalarId = GraphQLScalarType.newScalar()
378396
.name("ID")
@@ -427,3 +445,33 @@ val customScalarMap = GraphQLScalarType.newScalar()
427445
override fun parseLiteral(input: Any?): Map<String, Any> = (input as ObjectValue).objectFields.associateBy { it.name }.mapValues { (it.value.value as StringValue).value }
428446
})
429447
.build()
448+
449+
//Definition from https://github.com/graphql-java-kickstart/graphql-java-servlet/blob/master/src/main/java/graphql/servlet/core/ApolloScalars.java
450+
val uploadScalar: GraphQLScalarType = GraphQLScalarType.newScalar()
451+
.name("Upload")
452+
.description("A file part in a multipart request")
453+
.coercing(object : Coercing<Part?, Void?> {
454+
override fun serialize(dataFetcherResult: Any): Void? {
455+
throw CoercingSerializeException("Upload is an input-only type")
456+
}
457+
458+
override fun parseValue(input: Any?): Part? {
459+
return when (input) {
460+
is Part -> {
461+
input
462+
}
463+
null -> {
464+
null
465+
}
466+
else -> {
467+
throw CoercingParseValueException("Expected type ${Part::class.java.name} but was ${input.javaClass.name}")
468+
}
469+
}
470+
}
471+
472+
override fun parseLiteral(input: Any): Part? {
473+
throw CoercingParseLiteralException(
474+
"Must use variables to specify Upload values")
475+
}
476+
}).build()
477+

0 commit comments

Comments
 (0)