Martmists-GH / GraphQL-Exposed

This repository holds a prototype for binding KGraphQL to Exposed's DAO models

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

GraphQL-Exposed Prototype

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.

How it works

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()
            }
        }
    }
})

Hiding Fields

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.

Limitations

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")
        }
    }
}

License

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.

About

This repository holds a prototype for binding KGraphQL to Exposed's DAO models

License:Creative Commons Zero v1.0 Universal


Languages

Language:Kotlin 100.0%