diff --git a/kgraphql-example/src/main/kotlin/com/apurebase/kgraphql/Application.kt b/kgraphql-example/src/main/kotlin/com/apurebase/kgraphql/Application.kt index 46df522f..f195a8ba 100644 --- a/kgraphql-example/src/main/kotlin/com/apurebase/kgraphql/Application.kt +++ b/kgraphql-example/src/main/kotlin/com/apurebase/kgraphql/Application.kt @@ -23,6 +23,10 @@ fun Application.module() { install(GraphQL) { useDefaultPrettyPrinter = true playground = true + /** + * Change 2: Added debug option to GraphQL Configuration (flag to output exception information to extensions) + */ + debug = false endpoint = "/" wrap { diff --git a/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt b/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt index 23ac8325..7e01d6a0 100644 --- a/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt +++ b/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt @@ -10,7 +10,6 @@ import io.ktor.response.* import io.ktor.routing.* import io.ktor.util.* import kotlinx.coroutines.coroutineScope -import kotlinx.serialization.json.* import kotlinx.serialization.json.Json.Default.decodeFromString class GraphQL(val schema: Schema) { @@ -27,6 +26,11 @@ class GraphQL(val schema: Schema) { var endpoint: String = "/graphql" + /** + * Change 2: Added debug option to GraphQL Configuration (flag to output exception information to extensions) + */ + var debug: Boolean = false + fun context(block: ContextBuilder.(ApplicationCall) -> Unit) { contextSetup = block } @@ -83,31 +87,12 @@ class GraphQL(val schema: Schema) { } } catch (e: Throwable) { if (e is GraphQLError) { - context.respond(HttpStatusCode.OK, e.serialize()) + context.respond(HttpStatusCode.OK, e.serialize(config.debug)) } else throw e } } return GraphQL(schema) } - - private fun GraphQLError.serialize(): String = buildJsonObject { - put("errors", buildJsonArray { - addJsonObject { - put("message", message) - put("locations", buildJsonArray { - locations?.forEach { - addJsonObject { - put("line", it.line) - put("column", it.column) - } - } - }) - put("path", buildJsonArray { - // TODO: Build this path. https://spec.graphql.org/June2018/#example-90475 - }) - } - }) - }.toString() } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/GraphQLError.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/GraphQLError.kt index 45723413..1d9b0301 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/GraphQLError.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/GraphQLError.kt @@ -1,8 +1,10 @@ package com.apurebase.kgraphql +import com.apurebase.kgraphql.helpers.toJsonElement import com.apurebase.kgraphql.schema.model.ast.ASTNode import com.apurebase.kgraphql.schema.model.ast.Location.Companion.getLocation import com.apurebase.kgraphql.schema.model.ast.Source +import kotlinx.serialization.json.* open class GraphQLError( @@ -33,10 +35,19 @@ open class GraphQLError( /** * The original error thrown from a field resolver during execution. */ - val originalError: Throwable? = null + val originalError: Throwable? = null, + + /** + * Change 1: Added extensions to the error response. + */ + val extensionsErrorType: String? = "INTERNAL_SERVER_ERROR", + val extensionsErrorDetail: Map? = null ) : Exception(message) { constructor(message: String, node: ASTNode?) : this(message, nodes = node?.let(::listOf)) + constructor(message: String, extensionsErrorType: String?,extensionsErrorDetail:Map?) : this(message,null,null,null,null,extensionsErrorType,extensionsErrorDetail ) + constructor(message: String, extensionsErrorType: String?) : this(message,null,null,null,null,extensionsErrorType ) + /** * An array of { line, column } locations within the source GraphQL document @@ -71,4 +82,69 @@ open class GraphQLError( return output } + + /** + * Change 1: Added extensions to the error response. + * Change 2: Added debug option to GraphQL Configuration (flag to output exception information to extensions) + * Change 3: Moved serialize (), which was defined as an extension of GraphQLError, into GraphQLError. option + */ + open val extensions: Map?by lazy { + val extensions = mutableMapOf() + extensionsErrorType?.let{ extensions.put("type",extensionsErrorType) } + extensionsErrorDetail?.let { extensions.put("detail",extensionsErrorDetail) } + extensions + } + + open fun extensionsDebug(): Map { + val extensions = mutableMapOf() + extensionsErrorType?.let{ extensions.put("type",extensionsErrorType) } + extensionsErrorDetail?.let { extensions.put("detail",extensionsErrorDetail) } + extensions.put("debug",this.debugInfo()) + return extensions + } + + open fun debugInfo(): Map { + val exception = mutableMapOf() + val stackList = this.stackTrace + if (!stackList[0].fileName.isNullOrEmpty()) { + exception.put("fileName",stackList[0].fileName) + exception.put("line",stackList[0].lineNumber.toString()) + } + if (!stackList[0].methodName.isNullOrEmpty()) { + exception.put("method",stackList[0].methodName) + } + if (!stackList[0].className.isNullOrEmpty()) { + exception.put("classPath", stackList[0].className) + } + exception.put("stackTrace", stackList) + return exception + } + + open fun serialize(debug:Boolean=false): String = buildJsonObject { + put("errors", buildJsonArray { + addJsonObject { + put("message", message) + put("locations", buildJsonArray { + locations?.forEach { + addJsonObject { + put("liane", it.line) + put("column", it.column) + } + } + }) + put("path", buildJsonArray { + // TODO: Build this path. https://spec.graphql.org/June2018/#example-90475 + }) + if (!debug) { + extensions?.let { + put("extensions", it.toJsonElement()) + } + } else { + extensionsDebug().let { + put("extensions", it.toJsonElement()) + } + } + } + }) + }.toString() } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/helpers/KGraphQLExtensions.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/helpers/KGraphQLExtensions.kt index 6b03cf95..91c3de13 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/helpers/KGraphQLExtensions.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/helpers/KGraphQLExtensions.kt @@ -1,6 +1,10 @@ package com.apurebase.kgraphql.helpers import com.apurebase.kgraphql.schema.execution.Execution +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive /** * This returns a list of all scalar fields requested on this type. @@ -16,3 +20,44 @@ fun Execution.getFields(): List = when (this) { } else -> listOf() }.distinct() + +/** + * Collection : Convert to JsonElement + */ +fun Collection<*>.toJsonElement(): JsonElement { + val list: MutableList = mutableListOf() + this.forEach { + val value = it as? Any ?: return@forEach + when(value) { + is Number -> list.add(JsonPrimitive(value)) + is Boolean -> list.add(JsonPrimitive(value)) + is String -> list.add(JsonPrimitive(value)) + is Map<*, *> -> list.add((value).toJsonElement()) + is Collection<*> -> list.add(value.toJsonElement()) + is Array<*> -> list.add(value.toList().toJsonElement()) + else -> list.add(JsonPrimitive(value.toString())) // other type + } + } + return JsonArray(list) +} + +/** + * Map : Convert to JsonElement + */ +fun Map<*, *>.toJsonElement(): JsonElement { + val map: MutableMap = mutableMapOf() + this.forEach { + val key = it.key as? String ?: return@forEach + val value = it.value ?: return@forEach + when(value) { + is Number? -> map[key] = JsonPrimitive(value) + is Boolean? -> map[key] = JsonPrimitive(value) + is String? -> map[key] = JsonPrimitive(value) + is Map<*, *> -> map[key] = (value).toJsonElement() + is Collection<*> -> map[key] = value.toJsonElement() + is Array<*> -> map[key] = value.toList().toJsonElement() + else -> map[key] = JsonPrimitive(value.toString()) // other type + } + } + return JsonObject(map) +} \ No newline at end of file