Using graphql-kotlin, we want to set up a global type that will enforce the same schema across multiple queries. Each query will have different starting data depending on the use case, meaning the method to source the data is different per implementation. Some implementations will use a data loader, others would have it at the beginning of the query. If I set up an abstract base class to define all the vals for that type, then it would need to be optional with and leave it to each implementation to instantiate. In the case that value is actually dependent on a dataloader, the instantiation of the type would have to inject a null for the attribute and then define an additional function that fetches it from the data loader, which is non-intuitive. Alternatively, the abstract base class could require a function defined for each value, but then some child implementations would be returning a CompletableFuture, and others wouldn't, which means it would require an Any type and couldn't enforce strings vs ints.
Example (an arbitrary use case that doesn't necessarily make sense but to illustrate the point)
abstract class User {
val name: String?
val email: String?
protected abstract fun address(dfe: DataFetchingEnvironment): CompletableFuture<Address> // concrete implementations differ based on different chaining requirements to get from different starting data to the final address data
}
data class UserFromEmail(
override val name: String? = null,
override val email: String?
) : User() {
fun name(dfe: DataFetchingEnvironment): CompletableFuture<String?> = dfe.getValueFromDataLoader( // email to name)
fun address(dfe: DataFetchingEnvironment): CompletableFuture<Address> = name(dfe).thenCompose { dfe.getValueFromDataLoader (name to address) }
}
data class UserFromName(
override val name: String?,
override val email: String? = null
) {
fun email(dfe: DataFetchingEnvironment): CompletableFuture<String?> = dfe.getValueFromDataLoader(// get email from name)
fun address(dfe: DataFetchingEnvironment): CompletableFuture<String?> = dfe.getValueFromDataLoader(name to address) // note this implementation doesn't require chainging data loaders
fun getUsersFromEmail(email: String) = UserFromEmail(email: email)
fun getUsersFromName(name: String) = UserFromName(name: name)
this kind of works, except there's nothing preventing implementations from not giving a data fetching function for the attributes that must be fetched by default risking having bad queries that accidentally always return null for a field.
You could add an abstract function for each attribute, but without union types, it'd have to return a CompletableFuture<String?> even if you're just returning the attribute that was provided during instantiation, so you have to wrap it as a future, which is a lot of overhead for a simple return.
abstract class User {
val name: String?
val email: String?
protected abstract fun email(dfe: DataFetchingEnvironment): CompletableFuture<String?>
protected abstract fun name(dfe: DataFetchingEnvironment): CompletableFuture<String?>
protected abstract fun address(dfe: DataFetchingEnvironment): CompletableFuture<Address> // concrete implementations differ based on different chaining requirements to get from different starting data to the final address data
}
data class UserFromEmail(
override val name: String? = null,
override val email: String?
) : User() {
fun email(dfe: DataFetchingEnvironment): CompletableFuture<String?> = CompletableFuture.completedFuture(email)
fun name(dfe: DataFetchingEnvironment): CompletableFuture<String?> = dfe.getValueFromDataLoader(//get name)
fun address(dfe: DataFetchingEnvironment): CompletableFuture<Address> = name(dfe).thenCompose { dfe.getValueFromDataLoader (name to address) }
}
data class UserFromName(
override val name: String?,
override val email: String? = null
) {
fun name(dfe: DataFetchingEnvironment): CompletableFuture<String?> = CompletableFuture.completedFuture(name)
fun email(dfe: DataFetchingEnvironment): CompletableFuture<String?> = dfe.getValueFromDataLoader(// get email)
fun address(dfe: DataFetchingEnvironment): CompletableFuture<String?> = dfe.getValueFromDataLoader(name to address) // note this implementation doesn't require chainging data loaders