ExpediaGroup / graphql-kotlin

Libraries for running GraphQL in Kotlin

Home Page:https://opensource.expediagroup.com/graphql-kotlin/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support per-request BatchLoaderContextProviders in data loaders and free up BatchLoaderEnvironment key contexts

alex-lange opened this issue · comments

Is your feature request related to a problem? Please describe.

Per the graphql-java data loader docs :

The data loader library supports two types of context being passed to the batch loader. The first is an overall context object per dataloader, and the second is a map of per loaded key context objects.

The current KotlinDataLoader interface makes it difficult to provide per-request objects (e.g. the GraphQLContext) for the former "overall context". We would usually define this BatchLoaderContextProvider when creating the data loader, but the KotlinDataLoader#getDataLoader() function (which instantiates the data loaders for each request) has no means of passing in per-request context to make the BatchLoaderContextProvider helpful.

And while it is possible to define the latter "key context" objects when the data loader load function is called, the graphql-kotlin documentation recommends overloading this behavior by passing in the GraphQLContext and using the DataFetchingEnvironment#getGraphQLContext() extension function to access it.

By overloading per-item key contexts with the GraphQLContext, we are unable to use both types of contexts in our data loaders.

Describe the solution you'd like

Ideally the API would have a way to specify the BatchLoaderContextProvider (or something similar) that is automatically included in the KotlinDataLoader#getDataLoader call. This definition would probably trickle down from the GraphQLRequestHandler#executeRequest, which calls KotlinDataLoaderRegistryFactory#generate, which then calls KotlinDataLoader#getDataLoader.

If this were possible, we could use it to provide the GraphQLContext in the "overall context", and free up the "key context" for what it is intended to be used for.

Describe alternatives you've considered

We were able to work around this by defining our own GraphQLRequestHandler subclass that essentially changes this line of executeRequest to have a custom DataLoaderRegistryFactory whose generate function allows for the GraphQLContext on input. This is a pretty rough approach because the functions that executeRequest calls are all private to GraphQLRequestHandler, meaning if we want to change only that line in executeRequest in our subclass, we cannot re-use the logic that calls the private functions.

So, a small change that would clean up our "quick and dirty" solution would be to change GraphQLRequestHandler's private methods to protected.


Additional context

In case it is helpful, here is an example of my described "quick and dirty" work around:

class RequestHandlerWithDataloaderContext(
	private val graphQLEngine: GraphQL,
	private val dataloaderFactories: List<DataLoaderWithContextFactory<*, *>>,
	private val batchLoaderContextProviderFactory: (GraphQLContext) -> BatchLoaderContextProvider
) : GraphQLRequestHandler(
	graphQLEngine,
	// We're not going to use the built-in registry factory
	dataLoaderRegistryFactory = null
) {
	// This is effectively what a new KotlinDataLoaderRegistryFactory would do if it supported BatchLoaderContextProvider in its generate() function
	private fun generateDataLoaderRegistry(batchLoaderContextProvider: BatchLoaderContextProvider): KotlinDataLoaderRegistry {
		val registry = DataLoaderRegistry()
		dataloaderFactories.forEach { d ->
			registry.register(
				d::class.jvmName,
				d.getDataLoader(batchLoaderContextProvider)
			)
		}
		return KotlinDataLoaderRegistry(registry)
	}

	override suspend fun executeRequest(
		graphQLRequest: GraphQLServerRequest,
		graphQLContext: GraphQLContext
	): GraphQLServerResponse {
		val dataLoaderRegistry = generateDataLoaderRegistry(batchLoaderContextProviderFactory(graphQLContext))

		return when (graphQLRequest) {
			is GraphQLRequest -> {
				val batchGraphQLContext = graphQLContext + getBatchContext2(1, dataLoaderRegistry)
				execute2(graphQLRequest, batchGraphQLContext, dataLoaderRegistry)
			}
			else -> TODO()
		}
	}
   ...
   // Rest of the class is copies of the private functions (e.g. `execute` is copied to `execute2`, `getBatchContext` is copied to `getBatchContext2`)
interface DataLoaderWithContextFactory<K, V> {
	val loader: BatchLoaderWithContext<K, V>

	fun getDataLoader(batchLoaderContextProvider: BatchLoaderContextProvider): DataLoader<K, V> =
		DataLoaderFactory.newDataLoader(
			loader,
			DataLoaderOptions.newOptions().setBatchLoaderContextProvider(batchLoaderContextProvider)
		)
}

Sorry for the late response,

And while it is possible to define the latter "key context" objects when the data loader load function is called, the graphql-kotlin documentation recommends overloading this behavior by passing in the GraphQLContext and using the DataFetchingEnvironment#getGraphQLContext() extension function to access it.

the main reason why we recommended this approach is to access to the standardized CoroutineScope to execute a coroutine in a dispatch fn of the DataLoader, there should be a way to free up the key context objects per load call by adding an abstraction over our KotlinDataLoaders, will look into it.

Apologies for the delay --
#1785