diff --git a/.gitignore b/.gitignore index 4ec6c85..fa021f0 100644 --- a/.gitignore +++ b/.gitignore @@ -193,3 +193,5 @@ nbdist/ !.yarn/sdks !.yarn/versions .pnp.* + +.bruno diff --git a/README.adoc b/README.adoc index 0d6d274..1818f82 100644 --- a/README.adoc +++ b/README.adoc @@ -133,3 +133,7 @@ will respond } } ---- + +=== GraphQL + +The application provides a GraphQL endpoint. It exposes the schema through http://localhost:8888/graphql/schema.graphql diff --git a/app-quarkus/build.gradle.kts b/app-quarkus/build.gradle.kts index 5ec4fed..6f5b266 100644 --- a/app-quarkus/build.gradle.kts +++ b/app-quarkus/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(libs.kotlin.faker) implementation(libs.slf4j.api) implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-smallrye-graphql") implementation("io.quarkus:quarkus-grpc") implementation("io.quarkus:quarkus-kotlin") implementation("io.quarkus:quarkus-reactive-routes") @@ -50,6 +51,7 @@ dependencies { testImplementation(libs.bundles.strikt) testImplementation("io.rest-assured:rest-assured") testImplementation("io.rest-assured:kotlin-extensions") + testImplementation("io.quarkus:quarkus-smallrye-graphql-client") testImplementation("io.quarkus:quarkus-junit5") testImplementation("io.smallrye.reactive:smallrye-mutiny-vertx-web-client") diff --git a/app-quarkus/src/main/kotlin/io/apim/samples/core/starwars/Model.kt b/app-quarkus/src/main/kotlin/io/apim/samples/core/starwars/Model.kt new file mode 100644 index 0000000..62a3d42 --- /dev/null +++ b/app-quarkus/src/main/kotlin/io/apim/samples/core/starwars/Model.kt @@ -0,0 +1,13 @@ +package io.apim.samples.core.starwars + +import java.time.LocalDate + +class Film(val id: Int, val title: String, val episode: Int, val director: String, val releaseDate: LocalDate) + +class Planet(val id: Int, val name: String, val climate: String, val gravity: String, val population: Long, val films: List) + +class Vehicle(val id: Int, val name: String, val model: String, val manufacturer: String, val length: Int, val crew: Int, val passengers: Int, val cargoCapacity: Int, val vehicleClass: String, val films: List) + +class Starship(val id: Int, val name: String, val model: String, val manufacturer: String, val length: Int, val crew: Int, val passengers: Int, val cargoCapacity: Int, val starshipClass: String, val films: List) + +class Person(val id: Int, val name: String, val birthYear: String, val homeWorld: Planet, val films: List, val vehicles: List, val starships: List) diff --git a/app-quarkus/src/main/kotlin/io/apim/samples/core/starwars/StarWarsQueryService.kt b/app-quarkus/src/main/kotlin/io/apim/samples/core/starwars/StarWarsQueryService.kt new file mode 100644 index 0000000..33386cb --- /dev/null +++ b/app-quarkus/src/main/kotlin/io/apim/samples/core/starwars/StarWarsQueryService.kt @@ -0,0 +1,55 @@ +package io.apim.samples.core.starwars + +import jakarta.enterprise.context.ApplicationScoped +import java.time.LocalDate + +@ApplicationScoped +class StarWarsQueryService { + private val films = ArrayList() + private val planets = ArrayList() + private val vehicles = ArrayList() + private val starships = ArrayList() + private val people = ArrayList() + + init { + films.add(Film(1, "A New Hope", 4, "George Lucas", LocalDate.of(1977, 5, 25))) + films.add(Film(2, "The Empire Strikes Back", 5, "Irvin Kershner", LocalDate.of(1980, 5, 21))) + films.add(Film(3, "Return of the Jedi", 6, "Richard Marquand", LocalDate.of(1983, 5, 25))) + films.add(Film(4, "The Phantom Menace", 1, "George Lucas", LocalDate.of(1999, 5, 19))) + films.add(Film(5, "Attack of the Clones", 2, "George Lucas", LocalDate.of(2002, 5, 16))) + films.add(Film(6, "Revenge of the Sith", 3, "George Lucas", LocalDate.of(2005, 5, 19))) + films.add(Film(7, "The Force Awakens", 7, "J. J. Abrams", LocalDate.of(2015, 12, 18))) + films.add(Film(8, "The Last Jedi", 8, "Rian Johnson", LocalDate.of(2017, 12, 15))) + films.add(Film(9, "The Rise of Skywalker", 9, "J. J. Abrams", LocalDate.of(2019, 12, 20))) + + planets.add(Planet(1, "Tatooine", "arid", "1 standard", 200000, listOf(films[0], films[2], films[3], films[4], films[5]))) + planets.add(Planet(2, "Alderaan", "temperate", "1 standard", 2000000000, listOf(films[0], films[2], films[3]))) + planets.add(Planet(3, "Yavin IV", "temperate, tropical", "1 standard", 1000, listOf(films[0]))) + planets.add(Planet(4, "Hoth", "frozen", "1.1 standard", 2000000000, listOf(films[1]))) + planets.add(Planet(5, "Dagobah", "murky", "N/A", 10000, listOf(films[1], films[5]))) + planets.add(Planet(6, "Bespin", "temperate", "1.5 (surface), 1 standard (Cloud City)", 6000000, listOf(films[1]))) + planets.add(Planet(7, "Endor", "forests, mountains, lakes", "0.85 standard", 30000000, listOf(films[2]))) + planets.add(Planet(8, "Naboo", "temperate", "1 standard", 4500000000, listOf(films[3], films[4], films[5]))) + + vehicles.add(Vehicle(1, "Snowspeeder", "t-47 airspeeder", "Incom corporation", 4, 2, 0, 10, "airspeeder", listOf(films[1]))) + vehicles.add(Vehicle(2, "Imperial Speeder Bike", "74-Z speeder bike", "Aratech Repulsor Company", 3, 1, 1, 4, "speeder", listOf(films[2]))) + + starships.add(Starship(1, "X-wing", "T-65 X-wing", "Incom Corporation", 12, 1, 0, 110, "starfighter", listOf(films[0],films[1],films[2]))) + starships.add(Starship(2, "Imperial shuttle", "Lambda-class T-4a shuttle", "Sienar Fleet Systems", 20, 6, 20, 80000, "armed government transport", listOf(films[1],films[2]))) + starships.add(Starship(3, "TIE Advanced x1", "Twin Ion Engine Advanced x1", "Sienar Fleet Systems", 9, 1, 0, 150, "starfighter", listOf(films[1]))) + + people.add(Person(1, "Luke Skywalker", "19BBY", planets[0], listOf(films[0], films[1], films[2], films[5]), listOf(vehicles[0], vehicles[1]), listOf(starships[0], starships[1]))) + people.add(Person(2, "C-3PO", "112BBY", planets[0], listOf(films[0], films[1], films[2], films[3], films[4], films[5]), listOf(), listOf())) + people.add(Person(3, "R2-D2", "33BBY", planets[7], listOf(films[0], films[1], films[2], films[3], films[4], films[5]), listOf(), listOf())) + people.add(Person(4, "Darth Vader", "41.9BBY", planets[0], listOf(films[0], films[1], films[2], films[5]), listOf(), listOf(starships[2]))) + } + + fun getFilms(): List = films + fun getFilm(id: Int): Film? = films.find { it.id == id } + fun getPlanets(): List = planets + fun getPlanet(id: Int): Planet? = planets.find { it.id == id } + fun getPeople(): List = people + fun getPerson(id: Int): Person? = people.find { it.id == id } + fun getPersonByFilm(film: Film): List = people.filter { p -> p.films.any { f -> f.id == film.id } } + fun getPersonByPlanet(planet: Planet): List = people.filter { p -> p.homeWorld.id == planet.id } +} diff --git a/app-quarkus/src/main/kotlin/io/apim/samples/ports/graphql/StarWarsResource.kt b/app-quarkus/src/main/kotlin/io/apim/samples/ports/graphql/StarWarsResource.kt new file mode 100644 index 0000000..19841c4 --- /dev/null +++ b/app-quarkus/src/main/kotlin/io/apim/samples/ports/graphql/StarWarsResource.kt @@ -0,0 +1,48 @@ +package io.apim.samples.ports.graphql + +import io.apim.samples.core.starwars.Film +import io.apim.samples.core.starwars.Person +import io.apim.samples.core.starwars.Planet +import io.apim.samples.core.starwars.StarWarsQueryService +import jakarta.inject.Inject +import org.eclipse.microprofile.graphql.* + +@GraphQLApi +class StarWarsResource { + @Inject + lateinit var starWarsQueryService: StarWarsQueryService + + @Query("allFilms") + @Description("Get all films") + fun getAllFilms(): List = starWarsQueryService.getFilms() + + @Query + @Description("Get a film by id") + fun getFilm(@Name("filmId") id: Int): Film? = starWarsQueryService.getFilm(id) + + @Query("allPlanets") + @Description("Get all planets") + fun getAllPlanets(): List = starWarsQueryService.getPlanets() + + @Query + @Description("Get a planet by id") + fun getPlanet(@Name("planetId") id: Int): Planet? = starWarsQueryService.getPlanet(id) + + @Query("allPeople") + @Description("Get all people") + fun getAllPeople(): List = starWarsQueryService.getPeople() + + @Query + @Description("Get a person by id") + fun getPerson(@Name("personId") id: Int): Person? = starWarsQueryService.getPerson(id) + + fun people(@Source film: Film): List = starWarsQueryService.getPersonByFilm(film) + + @JvmName("getPersonByFilms") + fun people(@Source films: List): List> = films.map { starWarsQueryService.getPersonByFilm(it) } + + fun people(@Source planet: Planet): List = starWarsQueryService.getPersonByPlanet(planet) + + @JvmName("getPersonByPlanets") + fun people(@Source planets: List): List> = planets.map { starWarsQueryService.getPersonByPlanet(it) } +} diff --git a/app-quarkus/src/test/kotlin/io/apim/samples/ports/graphql/StarWarsResourceTest.kt b/app-quarkus/src/test/kotlin/io/apim/samples/ports/graphql/StarWarsResourceTest.kt new file mode 100644 index 0000000..ab1a225 --- /dev/null +++ b/app-quarkus/src/test/kotlin/io/apim/samples/ports/graphql/StarWarsResourceTest.kt @@ -0,0 +1,73 @@ +package io.apim.samples.ports.graphql + +import io.quarkus.test.junit.QuarkusTest +import io.smallrye.graphql.client.GraphQLClient +import io.smallrye.graphql.client.core.Document +import io.smallrye.graphql.client.core.Field +import io.smallrye.graphql.client.core.Operation +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient +import jakarta.inject.Inject +import org.junit.jupiter.api.Test +import strikt.api.expectThat +import strikt.assertions.containsExactlyInAnyOrder +import strikt.assertions.isEqualTo +import strikt.assertions.isNotNull + +@QuarkusTest +class StarWarsResourceTest { + + @Inject + @GraphQLClient("star-wars") + lateinit var client: DynamicGraphQLClient + + @Test + fun `should return all films`() { + val query = Document.document( + Operation.operation("allFilms", + Field.field("allFilms", + Field.field("title") + ) + ) + ) + + val result = client.executeSync(query) + val titles = result.data.getJsonArray("allFilms").stream().map { it.asJsonObject().getString("title") }.toList() + + expectThat(titles).isNotNull() + .containsExactlyInAnyOrder( + "A New Hope", + "The Empire Strikes Back", + "Return of the Jedi", + "The Phantom Menace", + "Attack of the Clones", + "Revenge of the Sith", + "The Force Awakens", + "The Last Jedi", + "The Rise of Skywalker" + ) + } + + @Test + fun `should return a film with all people`() { + val query = """ + query getFilm{ + film(filmId: 1) { + title + episode + people { + name + } + } + } + """.trimIndent() + + val result = client.executeSync(query) + + expectThat(result.data.getJsonObject("film")).isNotNull().and { + get { getString("title") }.isEqualTo("A New Hope") + get { getInt("episode") }.isEqualTo(4) + get { getJsonArray("people").stream().map { it.asJsonObject().getString("name") }.toList() }.containsExactlyInAnyOrder("Luke Skywalker", "C-3PO", "R2-D2", "Darth Vader") + } + + } +} diff --git a/app-quarkus/src/test/resources/application.properties b/app-quarkus/src/test/resources/application.properties index e69de29..a4a0f03 100644 --- a/app-quarkus/src/test/resources/application.properties +++ b/app-quarkus/src/test/resources/application.properties @@ -0,0 +1 @@ +quarkus.smallrye-graphql-client.star-wars.url=http://localhost:${quarkus.http.test-port}/graphql