Files
Suwayomi-Server/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt
renovate[bot] c117d380a3 Update exposed to v1 (major) (#1868)
* Update exposed to v1

* Update Exposed

* Add Kotlinx.DateTime extensions

* Update H2

* Review comments

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2026-05-12 12:53:41 -04:00

119 lines
4.4 KiB
Kotlin

/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package suwayomi.tachidesk.graphql.server
import com.expediagroup.graphql.server.execution.GraphQLRequestParser
import com.expediagroup.graphql.server.types.GraphQLBatchRequest
import com.expediagroup.graphql.server.types.GraphQLRequest
import com.expediagroup.graphql.server.types.GraphQLServerRequest
import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.http.Context
import io.javalin.http.UploadedFile
import io.javalin.json.fromJsonStream
import io.javalin.json.fromJsonString
import java.io.IOException
class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
private val logger = KotlinLogging.logger {}
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
override suspend fun parseRequest(context: Context): GraphQLServerRequest? {
return try {
val jsonMapper = context.jsonMapper()
val contentType = context.contentType()
val formParam =
if (
contentType?.contains("application/x-www-form-urlencoded") == true ||
contentType?.contains("multipart/form-data") == true
) {
context.formParam("operations")
?: throw IllegalArgumentException("Cannot find 'operations' body")
} else {
return context.bodyInputStream().use { jsonMapper.fromJsonStream<GraphQLServerRequest>(it) }
}
val request =
jsonMapper.fromJsonString<GraphQLServerRequest>(formParam)
val map =
context
.formParam("map")
?.let {
jsonMapper.fromJsonString<Map<String, List<String>>>(it)
}.orEmpty()
val mapItems =
map
.flatMap { (key, variables) ->
val file = context.uploadedFile(key)
variables.map { fullVariable ->
val variable = fullVariable.removePrefix("variables.").substringBefore('.')
val listIndex = fullVariable.substringAfterLast('.').toIntOrNull()
MapItem(
variable,
listIndex,
file,
)
}
}.groupBy { it.variable }
when (request) {
is GraphQLRequest -> {
request.copy(variables = request.variables?.modifyFiles(mapItems))
}
is GraphQLBatchRequest -> {
request.copy(
requests =
request.requests.map {
it.copy(
variables = it.variables?.modifyFiles(mapItems),
)
},
)
}
}
} catch (e: IOException) {
logger.error(e) { "Error when parsing request" }
null
}
}
data class MapItem(
val variable: String,
val listIndex: Int?,
val file: UploadedFile?,
)
/**
* Example [this]: { "file": null }
* Example: '{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }'
* Example map "{ "0": ["variables.file"] }"
* TODO nested objects
*/
private fun Map<String, Any?>.modifyFiles(map: Map<String, List<MapItem>>): Map<String, Any?> =
mapValues { (name, value) ->
if (map.containsKey(name)) {
val items = map[name].orEmpty()
if (items.size > 1) {
if (value is List<*>) {
value.mapIndexed { index, any ->
any ?: items.firstOrNull { it.listIndex == index }?.file
}
} else {
value
}
} else {
value ?: items.firstOrNull()?.file
}
} else {
value
}
}
}