Tobias
08/11/2021, 5:58 AMerror: CommonErrorClass?
. It will be a bit hackish, but with some automatic schema validation it should work reliably. The problem is, I can figure out how to turn an Exception
into a custom specification?Tobias
08/11/2021, 6:02 AMSimpleKotlinDataFetcherFactoryProvider
and FunctionDataFetcher
so I can catch the exceptions.
I've tried to different solutions:
1. if I return another type, java reflections becomes angry because I return a different type than declared fromt he get emthod
2. If I throw an Exception
(GraphqlError
), I can't get the toSpecification()
to be invokedShane Myrick
08/11/2021, 6:22 PMDataFetcherExceptionHandler
from graphql-java, which will allow you to get the exception thrown and run custom logic to create the GraphQLError
and DataFetcherExceptionHandlerResult
See an example here: https://github.com/ExpediaGroup/graphql-kotlin/blob/3832f7fc67f1a1b020071585f49f88[…]s/server/spring/exceptions/CustomDataFetcherExceptionHandler.ktTobias
08/11/2021, 7:46 PMDariusz Kuc
08/11/2021, 7:51 PMtoSpecification
creates just a Map representation of an object -> personally I'd just do
suspend fun foo(): MyResult = try {
// do whatever
} catch (e: Exception) {
MyResult(
error = e.toCommonError()
)
}
internal fun Exception.toCommonError(): CommonErrorClass {
// convert exception to your object here
}
Dariusz Kuc
08/11/2021, 7:52 PMMyResult
objectTobias
08/11/2021, 8:08 PMclass MyResult(
override val error: CommonErrorClass?,
val data: Foo?
) : CommonResponseClass
So all that is necessary is that I return { "error": {....}}
Tobias
08/11/2021, 8:11 PMerror
is null or else it's an error)Dariusz Kuc
08/11/2021, 8:11 PM{
"data": {
"myQuery": {
"error": {...}
}
}
}
Tobias
08/11/2021, 8:11 PMTobias
08/11/2021, 8:11 PMDariusz Kuc
08/11/2021, 8:13 PMDataFetcherExceptionHandler
you can create custom response but you still would need to have some logic there, i.e.
DataFetcherExceptionHandlerResult.newResult().error(error).build(). // returns no data and just errors
Dariusz Kuc
08/11/2021, 8:13 PMmyQuery: MyResult
responseTobias
08/11/2021, 8:14 PMTobias
08/11/2021, 8:14 PMDariusz Kuc
08/11/2021, 8:15 PMval myResult = mapOf("error" to CommonErrorClass())
val completeResult = mapOf("myQuery" to myResult) // <- you need to know about `myQuery`
DataFetcherExceptionHandlerResult.newResult().build()
Tobias
08/11/2021, 8:15 PMTobias
08/11/2021, 8:15 PMDariusz Kuc
08/11/2021, 8:15 PMTobias
08/11/2021, 8:16 PMTobias
08/11/2021, 8:17 PMFunctionDataFetcher.runBlockingFunction
returning a type that just implements the CommonResponse
(which contains the error: ..
)Dariusz Kuc
08/11/2021, 8:17 PMTobias
08/11/2021, 8:18 PMDataFetcher.get
checks if it's the error-type and returns null for everything but the error
fieldDariusz Kuc
08/11/2021, 8:18 PMTobias
08/11/2021, 8:19 PMoverride fun runBlockingFunction(parameterValues: Map<KParameter, Any?>): Any? =
try {
kFunction.callBy(parameterValues)
} catch (exception: InvocationTargetException) {
mapException(exception)
}
and
override fun runSuspendingFunction(
parameterValues: Map<KParameter, Any?>,
coroutineContext: CoroutineContext,
coroutineStart: CoroutineStart
): CompletableFuture<Any?> = ourScope.future(context = coroutineContext, start = coroutineStart) {
try {
kFunction.callSuspendBy(parameterValues)
} catch (exception: InvocationTargetException) {
mapException(exception)
}
}
Tobias
08/11/2021, 8:20 PMTobias
08/11/2021, 8:20 PMoverride fun get(environment: DataFetchingEnvironment): Any? = environment.getSource<Any?>()?.let { instance ->
if (instance is CommonResponseClass) {
if (kProperty.name == "error") {
instance.error
} else {
null
}
} else {
kProperty.call(instance)
}
}
Tobias
08/11/2021, 8:22 PMDariusz Kuc
08/11/2021, 8:26 PMTobias
08/11/2021, 8:27 PMTobias
08/11/2021, 8:27 PMDariusz Kuc
08/11/2021, 8:30 PMsuspend fun foo(): Foo = withCommonError {
// whatever
}
suspend fun inline reified<T: CommonType> CoroutineScope.withCommonError(block: () -> T) = try {
block.apply()
} catch (e: Exception) {
// need to create T somehow -> guess using reflections could work
}
Dariusz Kuc
08/11/2021, 8:31 PMTobias
08/11/2021, 8:36 PM= withCommonError {
and just putting a {
at the end will break things silently in runtime.Tobias
08/11/2021, 8:40 PMSchemaGeneratorHooks
to make sure all function return classes extend the error interfaceTobias
08/11/2021, 8:41 PMDariusz Kuc
08/11/2021, 8:49 PMwithCommonError(Foo()) { foo -> ... }
(pass an instance to the lambda to avoid reflections)Dariusz Kuc
08/11/2021, 8:50 PMDariusz Kuc
08/11/2021, 8:51 PMDariusz Kuc
08/11/2021, 8:52 PMTobias
08/11/2021, 8:57 PMTobias
08/11/2021, 9:10 PMrocketraman
08/16/2021, 10:23 PMfun myResolver(): MyPayload
sealed interface MyPayload
data class MyPayloadSuccess(
...non-null fields here...
) : MyPayload
sealed class BaseError(
// base error fields here
)
// error classes all extend BaseError and
// implement as many payload types as
// necessary
class SomeError(...error fields)
: BaseError(...)
, MyPayload
, OtherPayload
, Etc
Its a bit of work to set up all the types, but provides fully type-safe success and error responses. I find it works out very nicely.
See also this issue/post (by me) for background: https://github.com/graphql-rules/graphql-rules/issues/62.Tobias
08/17/2021, 6:02 AMunion LikePostPayload = LikePostSuccess | LikePostError
where the error contains the data. What you're doing otherwise is possibly making clients silently get incorrect data, as the client would (without recompiling with new schema) have no way of knowing how to handle the following correctly:
union LikePostPayload = LikePostSuccess | LikePostError | LikePostMyNewError
That would break forward compatibility.Tobias
08/17/2021, 6:11 AMabstract class BaseResponse(
val error: ErrorType?,
val data: Any
) {
init {
check((error == null) != (data == null))
}
}
class LikePostResponse(
error: ErrorType? = null,
data: LikePostPayload? = null
) : BaseResponse(error, data)
rocketraman
08/17/2021, 6:14 AMhe client would (without recompiling with new schema) have no way of knowing how to handle the following correctly:Because both LikePostError and LikePostMyNewError extend from a common base error class, I don't believe LikePostMyNewError breaks forward compatibility.
rocketraman
08/17/2021, 6:16 AMI was hoping for something like thisWhy? I see no obvious advantages to this approach at all.
Tobias
08/17/2021, 6:22 AMTobias
08/17/2021, 6:23 AMTobias
08/17/2021, 6:29 AM"__typename": "LikePostSuccess"
, and you can basically get null back then (as the client doesn't ask for the extra "LikePostMyNewError")
At least if my skimming of this link is correct
https://graphql.org/learn/schema/#union-typesrocketraman
08/17/2021, 6:30 AMrocketraman
08/17/2021, 6:31 AM... on BaseError {
whatever base error fields
}
without any knowledge of the specific error types.rocketraman
08/17/2021, 6:36 AMsealed class BaseError
as a GraphQL interface type (https://graphql.org/learn/schema/#interfaces).Tobias
08/17/2021, 6:37 AMrocketraman
08/17/2021, 6:39 AMrocketraman
08/17/2021, 6:41 AMrocketraman
08/17/2021, 6:42 AMTobias
08/17/2021, 6:45 AM