This is a prototype project to showcase codegen for binding Exposed's DAO API to KGraphQL. In a production environment, the gql-annotations and gql-processor folders would be external libraries.
Let's look at an example DAO:
@GraphQLModel
class Film(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Film>(FilmTable)
var name by FilmTable.name
var year by FilmTable.year
var director by Person referencedOn FilmTable.director
var characters by Character via FilmCharacterTable
var actors by Person via FilmCharacterTable
}
The @GraphQLModel
annotation is processed by the gql-processor, which generates a FilmGraphQLModel
class, remapping the properties:
// Automatically generated from the public properties and @GraphQLModel annotation on Film
class FilmGraphQL(internal val instance: Film) {
val id = instance.id.value
var name: kotlin.String
get() = instance.name
set(value) { instance.name = value }
var year: kotlin.Int
get() = instance.year
set(value) { instance.year = value }
var director: com.example.db.models.PersonGraphQL
get() = instance.director.graphql
set(value) { instance.director = value.instance }
var characters: Iterable<com.example.db.models.CharacterGraphQL>
get() = instance.characters.map { com.example.db.models.CharacterGraphQL(it) }
set(value) { instance.characters = SizedCollection(value.map { it.instance }) }
var actors: Iterable<com.example.db.models.PersonGraphQL>
get() = instance.actors.map { com.example.db.models.PersonGraphQL(it) }
set(value) { instance.actors = SizedCollection(value.map { it.instance }) }
}
Then an autogenerated remapper can be used to convert DAOs to GraphQL models:
query("films") {
resolver { ->
Film.all().map(Film::graphql)
}
}
The reason for this is that the DAOs have their attributes computed lazily in a manner which is incompatible with KGraphQL's reflection-based approach. The FilmGraphQL class instead just generates getters and setters, which KGraphQL can use to access the DAO's properties indirectly. (Except for the ID property, which is always retrieved.)
However, due to the way Exposed's DAOs work, we need to be in a transaction to access the properties. As such, we need to add the following snippet to Ktor. Note that in production, this would just be install(GraphQLExposedPatch)
install(GraphQL) {
// ...
// The Executor must NOT be set to Parallel, as this will cause ConcurrentModificationExceptions in the Transaction.
executor = Executor.DataLoaderPrepared
}
install(object : Plugin<Application, Unit, Unit> {
override val key: AttributeKey<Unit> = AttributeKey("GraphQL-Database")
override fun install(pipeline: Application, configure: Unit.() -> Unit) {
pipeline.intercept(ApplicationCallPipeline.Plugins) {
if (call.request.path() == "/graphql" && call.request.httpMethod == HttpMethod.Post) {
// Start a transaction for GraphQL POST requests
newSuspendedTransaction {
proceed()
}
} else {
proceed()
}
}
}
})
Fields can be hidden by using the @HideGraphQLField
annotation:
@GraphQLModel
class Film(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Film>(FilmTable)
var name by FilmTable.name
var year by FilmTable.year
var director by Person referencedOn FilmTable.director
var characters by Character via FilmCharacterTable
var actors by Person via FilmCharacterTable
@HideGraphQLField
var hiddenField by FilmTable.hiddenField
}
hiddenField
will not be added to the FilmGraphQL class, and as such will not show up in the Schema.
Unfortunately this breaks inheritance in autogenerated models, but this can still be done by manually converting DAOs to GraphQL models.
All other features such as access rules still work, but must be defined on their GraphQL models:
type<FilmGraphQL> {
property("characters") {
// A resolver must be manually defined for accessRule to not crash
resolver { film ->
film.characters
}
accessRule { film, context ->
GraphQLError("Nobody can see the characters of a film")
}
}
}
This project is licensed under CC0; Do whatever you want with it. However, I would appreciate it if you could credit me where applicable, in the event you were to use this as basis for a proper library and processor.