https://github.com/martmists-gh/graphql-exposed
This repository holds a prototype for binding KGraphQL to Exposed's DAO models
https://github.com/martmists-gh/graphql-exposed
exposed exposed-orm graphql kotlin ksp
Last synced: 5 months ago
JSON representation
This repository holds a prototype for binding KGraphQL to Exposed's DAO models
- Host: GitHub
- URL: https://github.com/martmists-gh/graphql-exposed
- Owner: Martmists-GH
- License: cc0-1.0
- Created: 2023-07-07T21:23:06.000Z (about 2 years ago)
- Default Branch: master
- Last Pushed: 2023-07-07T21:23:22.000Z (about 2 years ago)
- Last Synced: 2025-02-16T14:58:22.471Z (8 months ago)
- Topics: exposed, exposed-orm, graphql, kotlin, ksp
- Language: Kotlin
- Homepage:
- Size: 69.3 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# 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:
```kotlin
@GraphQLModel
class Film(id: EntityID) : IntEntity(id) {
companion object : IntEntityClass(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:
```kotlin
// 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
get() = instance.characters.map { com.example.db.models.CharacterGraphQL(it) }
set(value) { instance.characters = SizedCollection(value.map { it.instance }) }
var actors: Iterable
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:
```kotlin
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)`
```kotlin
install(GraphQL) {
// ...
// The Executor must NOT be set to Parallel, as this will cause ConcurrentModificationExceptions in the Transaction.
executor = Executor.DataLoaderPrepared
}install(object : Plugin {
override val key: AttributeKey = 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:
```kotlin
@GraphQLModel
class Film(id: EntityID) : IntEntity(id) {
companion object : IntEntityClass(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:
```kotlin
type {
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.