Skip to content

Commit 7290bd6

Browse files
authored
Merge pull request #8 from javalin/javalin-7
feat!: Javalin 7 + graphql-kotlin 10, with graphql-transport-ws subscriptions
2 parents 6b59908 + 8b0381c commit 7290bd6

34 files changed

Lines changed: 1418 additions & 599 deletions

‎.github/README.md‎

Lines changed: 164 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[![Chat at https://discord.gg/sgak4e5NKv](https://img.shields.io/badge/chat-on%20Discord-%234cb697)](https://discord.gg/sgak4e5NKv)
2-
[![Test all JDKs on all OSes](https://github.com/javalin/javalin-graphql/actions/workflows/main.yml/badge.svg)](https://github.com/javalin/javalin-graphql/actions/workflows/main.yml)
2+
[![Build](https://github.com/javalin/javalin-graphql/actions/workflows/build.yml/badge.svg)](https://github.com/javalin/javalin-graphql/actions/workflows/build.yml)
33
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4-
[![Maven](https://img.shields.io/maven-central/v/io.javalin/javalin.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.javalin%22%20AND%20a%3A%22javalin%22)
54

65
# About Javalin
76

@@ -12,77 +11,70 @@
1211

1312
## Javalin GraphQL
1413

15-
This plugin allows implementing the [GraphQL specification](https://graphql.org)
16-
with some easy steps.
14+
Serve a [GraphQL](https://graphql.org) schema, GraphiQL and subscriptions from a Javalin
15+
application. Schemas are generated from Kotlin (or Java) classes by
16+
[graphql-kotlin](https://github.com/ExpediaGroup/graphql-kotlin).
17+
18+
| | |
19+
|---|---|
20+
| Javalin | 7.x |
21+
| graphql-kotlin | 10.x |
22+
| JVM | 17+ |
23+
24+
> **Upgrading from 5.x?** The public API changed. See [Migrating from 5.x](#migrating-from-5x).
1725
1826
### Getting Started
1927

20-
Add the dependencies:
28+
Add the dependency:
2129

2230
<details>
23-
<summary>Gradle setup for Javalin 5.x</summary>
24-
25-
```groovy
26-
repositories {
27-
maven {
28-
url "https://maven.reposilite.com/releases"
29-
}
30-
}
31+
<summary>Gradle</summary>
3132

33+
```kotlin
3234
dependencies {
33-
implementation "io.javalin.community.graphql:javalin-graphql:5.0.1"
35+
implementation("io.javalin.community.graphql:javalin-graphql:7.0.0")
3436
}
3537
```
3638

3739
</details>
3840

39-
4041
<details>
41-
<summary>Maven setup for Javalin 5.x</summary>
42+
<summary>Maven</summary>
4243

4344
```xml
44-
<project>
45-
46-
<repositories>
47-
<repository>
48-
<id>reposilite-repository</id>
49-
<url>https://maven.reposilite.com/releases</url>
50-
</repository>
51-
</repositories>
52-
53-
<dependencies>
54-
<dependency>
55-
<groupId>io.javalin.community.graphql</groupId>
56-
<artifactId>javalin-graphql</artifactId>
57-
<version>5.0.1</version>
58-
</dependency>
59-
</dependencies>
60-
</project>
45+
<dependency>
46+
<groupId>io.javalin.community.graphql</groupId>
47+
<artifactId>javalin-graphql</artifactId>
48+
<version>7.0.0</version>
49+
</dependency>
6150
```
6251

6352
</details>
6453

54+
Javalin 7 does not bundle an object mapper, and this plugin uses the one your application
55+
configures. If you have not set one up, add `jackson-databind` (plus `jackson-module-kotlin`
56+
if your schema classes are written in Kotlin).
57+
6558
Register the plugin:
6659

6760
```kotlin
68-
val app = Javalin.create {
69-
val graphQLOption = GraphQLOptions("/graphql", ContextExample())
70-
.addPackage("io.javalin.examples")
71-
.register(QueryExample(message))
72-
.register(MutationExample(message))
73-
.register(SubscriptionExample())
74-
.context()
75-
it.registerPlugin(GraphQLPlugin(graphQLOption))
61+
val app = Javalin.create { config ->
62+
val options = GraphQLOptions("/graphql")
63+
.addPackage("com.example.schema")
64+
.register(QueryExample())
65+
.register(MutationExample())
66+
.register(SubscriptionExample())
67+
68+
config.registerPlugin(GraphQLPlugin(options))
7669
}
7770

78-
app.start()
71+
app.start(7070)
7972
```
8073

81-
The GraphQL is now available under the `/graphql` endpoint.
82-
83-
### Create Query
74+
GraphQL is now served under `/graphql`: `GET` returns GraphiQL, `POST` executes queries and
75+
mutations, and the WebSocket on the same path serves subscriptions.
8476

85-
This section contains an overview of all the available to create queries.
77+
### Queries
8678

8779
```kotlin
8880
@GraphQLDescription("Query Example")
@@ -93,54 +85,152 @@ class QueryExample : QueryGraphql {
9385
}
9486
```
9587

96-
After creating this class is necessary to register the class at the start of the plugin.
97-
98-
### Create Command
99-
100-
This section contains an overview of all the available to create commands.
88+
### Mutations
10189

10290
```kotlin
103-
@GraphQLDescription("Command Example")
104-
class CommandExample : CommandGraphql {
105-
fun hello(): String = "Hello world"
106-
107-
fun demoData(@GraphQLDescription("awesome input") data: DemoData): DemoData = data
91+
@GraphQLDescription("Mutation Example")
92+
class MutationExample(private var message: String) : MutationGraphql {
93+
fun changeMessage(newMessage: String): String {
94+
message = newMessage
95+
return message
96+
}
10897
}
10998
```
11099

111-
After creating this class is necessary to register the class at the start of the plugin.
100+
### Subscriptions
112101

113-
### Create Subscription
114-
115-
This section contains an overview of all the available to create a subscription.
102+
A subscription resolver returns a kotlinx [`Flow`](https://kotlinlang.org/docs/flow.html):
116103

117104
```kotlin
118105
@GraphQLDescription("Subscription Example")
119-
class SubscriptionExample: SubscriptionGraphql {
120-
fun counter(): Flux<Int> = Flux.interval(Duration.ofMillis(100)).map { 1 }
106+
class SubscriptionExample : SubscriptionGraphql {
107+
fun counter(): Flow<Int> = flow {
108+
while (true) {
109+
delay(100)
110+
emit(1)
111+
}
112+
}
121113
}
122114
```
123115

124-
After creating this class is necessary to register the class at the start of the plugin.
116+
Every class has to be registered when the plugin is built.
125117

126-
### Pass context
118+
Subscriptions are served over the
119+
[graphql-transport-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md)
120+
protocol, so a standard client such as [graphql-ws](https://github.com/enisdenjo/graphql-ws)
121+
can talk to the endpoint directly. The connection is acknowledged with `connection_init` /
122+
`connection_ack`, each operation carries an id, and a subscription is cancelled by the
123+
client's `complete` message or when the socket closes.
127124

128-
Sometimes it is necessary to pass the context in the method. You can create this context with this class.
125+
> Javalin exposes no API for WebSocket subprotocol negotiation, and the Jetty handshake
126+
> underneath echoes back whichever subprotocol the client requested first, without checking
127+
> it. A `graphql-transport-ws` client connects correctly; a client asking for a protocol this
128+
> plugin does not speak — the legacy `graphql-ws` subprotocol, say — is told yes and then
129+
> receives messages it cannot understand.
130+
131+
### Context
132+
133+
Build a context by putting values into graphql-java's `GraphQLContext`, keyed by class:
129134

130135
```kotlin
131-
class ContextExample {
132-
val globalEnvironment = "globalEnvironment"
136+
data class MyContext(val authorization: String?) {
137+
val isValid = authorization != null
133138
}
134-
```
135139

136-
After creating this class is necessary to register the class at the start of the plugin.
140+
class MyContextFactory : GraphQLContextFactory<Context> {
141+
override suspend fun generateContext(request: Context): GraphQLContext =
142+
mapOf(MyContext::class to MyContext(request.header("Authorization")))
143+
.toGraphQLContext()
144+
}
145+
```
137146

138-
Then is possible to access this context with this annotation @GraphQLContext.
147+
Read it in a resolver through the `DataFetchingEnvironment`:
139148

140149
```kotlin
141-
class QueryExample() : QueryGraphql {
142-
fun context(@GraphQLContext context: ContextExample): ContextExample {
143-
return context
144-
}
150+
class QueryExample : QueryGraphql {
151+
fun isAuthorized(environment: DataFetchingEnvironment): Boolean =
152+
environment.graphQlContext.get<MyContext>(MyContext::class)?.isValid == true
145153
}
146154
```
155+
156+
Context factories are passed to the builder, one for HTTP and one for subscriptions:
157+
158+
```kotlin
159+
val plugin = GraphQLPluginBuilder("/graphql", MyContextFactory(), MyWsContextFactory())
160+
.add("com.example.schema")
161+
.register(QueryExample())
162+
.build()
163+
164+
config.registerPlugin(plugin)
165+
```
166+
167+
### JPMS
168+
169+
The artifact declares `Automatic-Module-Name: io.javalin.community.graphql`, so a modular
170+
application can depend on it:
171+
172+
```java
173+
requires io.javalin.community.graphql;
174+
```
175+
176+
It does not ship a `module-info.java` yet. The graphql-kotlin artifacts declare neither a
177+
module descriptor nor an `Automatic-Module-Name`, so they resolve as automatic modules named
178+
after their file names; requiring those would freeze unstable names into the descriptor. See
179+
[#5](https://github.com/javalin/javalin-graphql/issues/5).
180+
181+
### Design decisions
182+
183+
The reasoning behind the 7.0 design is recorded as ADRs in [`docs/adr`](../docs/adr):
184+
185+
| | |
186+
|---|---|
187+
| [ADR-001](../docs/adr/adr-001-context-model.md) | Context model follows graphql-java instead of a plugin type |
188+
| [ADR-002](../docs/adr/adr-002-subscription-protocol.md) | Serve subscriptions over graphql-transport-ws |
189+
| [ADR-003](../docs/adr/adr-003-json-mapping.md) | Parse GraphQL payloads without graphql-kotlin's sealed types |
190+
| [ADR-004](../docs/adr/adr-004-jpms.md) | Declare an automatic module name instead of shipping module-info |
191+
192+
### Migrating from 5.x
193+
194+
**Registering the plugin.** `config.plugins.register(...)` became `config.registerPlugin(...)`.
195+
196+
**Context is no longer a type of yours.** graphql-kotlin removed its `GraphQLContext` marker
197+
interface in favour of graphql-java's map-like `GraphQLContext`.
198+
199+
```diff
200+
- data class MyContext(val authorization: String?) : GraphQLContext
201+
+ data class MyContext(val authorization: String?)
202+
203+
- class MyContextFactory : GraphQLContextFactory<MyContext, Context> {
204+
- override suspend fun generateContext(request: Context): MyContext =
205+
- MyContext(request.header("Authorization"))
206+
+ class MyContextFactory : GraphQLContextFactory<Context> {
207+
+ override suspend fun generateContext(request: Context): GraphQLContext =
208+
+ mapOf(MyContext::class to MyContext(request.header("Authorization")))
209+
+ .toGraphQLContext()
210+
}
211+
```
212+
213+
`GraphQLPluginBuilder` lost its context type parameter as a result.
214+
215+
**Context is no longer injected into resolvers.** A resolver parameter typed as your context
216+
class is now treated as a GraphQL argument. Take a `DataFetchingEnvironment` instead:
217+
218+
```diff
219+
- fun isAuthorized(context: MyContext?): Boolean = context?.isValid == true
220+
+ fun isAuthorized(environment: DataFetchingEnvironment): Boolean =
221+
+ environment.graphQlContext.get<MyContext>(MyContext::class)?.isValid == true
222+
```
223+
224+
**Subscriptions return `Flow`, not `Publisher`.** Reactor is no longer a dependency.
225+
226+
```diff
227+
- fun counter(): Flux<Int> = Flux.interval(Duration.ofMillis(100)).map { 1 }
228+
+ fun counter(): Flow<Int> = flow { while (true) { delay(100); emit(1) } }
229+
```
230+
231+
**Subscriptions speak graphql-transport-ws.** 5.x used an ad-hoc exchange — send a query
232+
frame, receive bare result data — which matched no standard. A client now has to send
233+
`connection_init` and `subscribe` messages. Off-the-shelf GraphQL clients do this for you.
234+
235+
**Removed:** `GraphQLRun` (use `GraphQLRequestHandler.executeSubscription`) and
236+
`JavalinDataLoaderRegistryFactory` (use graphql-kotlin's `KotlinDataLoaderRegistryFactory`).

‎.github/workflows/build.yml‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches: [ "**" ]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
jobs:
10+
build:
11+
name: JDK ${{ matrix.jdk }} on ${{ matrix.os }}
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
os: [ ubuntu-latest, macos-latest, windows-latest ]
17+
jdk: [ 17, 21 ]
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Set up JDK ${{ matrix.jdk }}
23+
uses: actions/setup-java@v4
24+
with:
25+
distribution: temurin
26+
java-version: ${{ matrix.jdk }}
27+
28+
- name: Set up Gradle
29+
uses: gradle/actions/setup-gradle@v4
30+
31+
- name: Build and test
32+
run: ./gradlew build --stacktrace
33+
34+
- name: Upload test reports
35+
if: failure()
36+
uses: actions/upload-artifact@v4
37+
with:
38+
name: test-reports-${{ matrix.os }}-jdk${{ matrix.jdk }}
39+
path: build/reports/tests/
40+
41+
detekt:
42+
name: Static analysis
43+
runs-on: ubuntu-latest
44+
45+
steps:
46+
- uses: actions/checkout@v4
47+
48+
- uses: actions/setup-java@v4
49+
with:
50+
distribution: temurin
51+
java-version: 21
52+
53+
- uses: gradle/actions/setup-gradle@v4
54+
55+
- name: Run detekt
56+
run: ./gradlew detekt
57+
58+
- name: Upload detekt report
59+
if: failure()
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: detekt-report
63+
path: build/reports/detekt/

0 commit comments

Comments
 (0)