Skip to content

Commit

Permalink
Merge pull request #283 from gemini-hlsw/db-backend-example-documenta…
Browse files Browse the repository at this point in the history
…tion

Db backend example documentation
  • Loading branch information
milessabin authored Dec 13, 2022
2 parents 8c90e00 + bc20464 commit 4b8d2ad
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 12 deletions.
2 changes: 2 additions & 0 deletions demo/src/main/scala/demo/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.testcontainers.utility.DockerImageName
import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext

// #main
object Main extends IOApp {

def run(args: List[String]): IO[ExitCode] = {
Expand Down Expand Up @@ -71,3 +72,4 @@ object Main extends IOApp {
flyway.load().migrate()
}.void
}
// #main
2 changes: 2 additions & 0 deletions demo/src/main/scala/demo/world/WorldMapping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ trait WorldMapping[F[_]] extends DoobieMapping[F] {
}
// #db_tables

// #schema
val schema =
schema"""
type Query {
Expand Down Expand Up @@ -98,6 +99,7 @@ trait WorldMapping[F[_]] extends DoobieMapping[F] {
languages: [Language!]!
}
"""
// #schema

val QueryType = schema.ref("Query")
val CountryType = schema.ref("Country")
Expand Down
116 changes: 116 additions & 0 deletions docs/src/main/paradox/tutorial/db-backed-model.md
Original file line number Diff line number Diff line change
@@ -1 +1,117 @@
# DB Backed Model

In this tutorial we are going to implement GraphQL API of countries and cities of the world using Grackle backed by
a database model, i.e. provide mapping for Grackle to read data from PostgreSQL and return it as result of
GraphQL queries.

## Running the demo

The demo is packaged as submodule `demo` in the Grackle project. It is a http4s-based application which can be run
from the SBT REPL using `sbt-revolver`,

```
sbt:gsp-graphql> reStart
[info] Application demo not yet started
[info] Starting application demo in the background ...
demo Starting demo.Main.main()
[success] Total time: 0 s, completed 11-Dec-2019 16:55:35
sbt:gsp-graphql> demo [ioapp-compute-0] INFO o.h.b.c.n.NIO1SocketServerGroup - [...]
demo [ioapp-compute-0] INFO o.h.s.b.BlazeServerBuilder -
demo _ _ _ _ _
demo | |_| |_| |_ _ __| | | ___
demo | ' \ _| _| '_ \_ _(_-<
demo |_||_\__|\__| .__/ |_|/__/
demo |_|
demo [ioapp-compute-0] INFO o.h.s.b.BlazeServerBuilder - [...]
```

This application hosts the demo services for in-memory and db-backend models, as well as a web-based GraphQL client
(GraphQL Playground) which can be used to interact with them. You can run the client for db-backend model in your
browser at [http://localhost:8080/playground.html?endpoint=world](http://localhost:8080/playground.html?endpoint=world).

## Query examples

You can use the Playground to run queries against the model. Paste the following into the query field on left,

```graphql
query {
cities(namePattern: "London") {
name
country {
name
}
}
}
```

Click the play button in the centre and you should see the following response on the right,

```json
{
"data": {
"cities": [
{
"name": "London",
"country": {
"name": "United Kingdom"
}
},
{
"name": "London",
"country": {
"name": "Canada"
}
}
]
}
}
```

## The Schema

Grackle represents schemas as a Scala value of type `Schema` which can be constructed given a schema text,

@@snip [WorldSchema.scala](/demo/src/main/scala/demo/world/WorldMapping.scala) { #schema }

## Database mapping

The API is backed by mapping to database tables. Grackle contains ready to use integration
with [doobie](https://tpolecat.github.io/doobie/) for accessing SQL database via JDBC
and with [Skunk](https://tpolecat.github.io/skunk/) for accessing PostgreSQL via its native API. In this example
we will use doobie.

Let's start with defining what tables and columns are available in the database model,

@@snip [WorldMapping.scala](/demo/src/main/scala/demo/world/WorldMapping.scala) { #db_tables }

For each column we need to provide its name and doobie codec. We should also mark if value is nullable.

We define each query as SQL query, as below,

@@snip [WorldMapping.scala](/demo/src/main/scala/demo/world/WorldMapping.scala) { #root }

Now, we need to map each type from GraphQL schema using available columns from database,

@@snip [WorldMapping.scala](/demo/src/main/scala/demo/world/WorldMapping.scala) { #type_mappings }

Each GraphQL must contain key. It can contain fields from one table, but it can also contain nested types which
are translated to SQL joins using provided conditions. `Join(country.code, city.countrycode)` means joining country
and city tables where `code` in the country table is the same as `countrycode` in the city table.

## The query compiler and elaborator

Similar as in [in-memory model]((in-memory-model.html#the-query-compiler-and-elaborator)), we need to define elaborator
to transform query algebra terms into the form that can be then used as source of SQL queries,

@@snip [WorldMapping.scala](/demo/src/main/scala/demo/world/WorldMapping.scala) { #elaborator }

## Putting it all together

To expose GraphQL API with http4s we will use `GraphQLService` and `DemoServer`
from [in-memory example](in-memory-model.html#the-service).

We will use [testcontainers](https://github.com/testcontainers/testcontainers-scala) to run PostgreSQL database
in the background. The final main method, which starts PostgreSQL database in docker container, creates database
schema, writes initial data and exposes GraphQL API for in-memory and db-backend models is below,

@@snip [main.scala](/demo/src/main/scala/demo/Main.scala) { #main }
24 changes: 12 additions & 12 deletions docs/src/main/paradox/tutorial/in-memory-model.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# In-Memory Model

The GraphQL reference implementation defines an API over a simple data model representing characters and films from
the Star Wars series. Because of its appearance in the the reference implementation it is used as the basis for many
the Star Wars series. Because of its appearance in the reference implementation it is used as the basis for many
GraphQL tutorials, and many GraphQL server implementations provide it as an example. Grackle is no exception.

In this tutorial we are going to implement the Star Wars demo using Grackle backed by an in-memory model, i.e. a
simple Scala data structure which captures the information required to service the GraphQL API.

## Running the demo

The demo is packaged as submodule `demo` in the Grackle project. It is an http4s-based application which can be run
The demo is packaged as submodule `demo` in the Grackle project. It is a http4s-based application which can be run
from the SBT REPL using `sbt-revolver`,

```
Expand All @@ -36,7 +36,7 @@ at [http://localhost:8080/playground.html?endpoint=starwars](http://localhost:80

You can use the Playground to run queries against the model. Paste the following into the query field on left,

```json
```graphql
query {
hero(episode: EMPIRE) {
name
Expand Down Expand Up @@ -66,7 +66,7 @@ Click the play button in the centre and you should see the following response on

The Star Wars API is described by a GraphQL schema,

```json
```graphql
type Query {
hero(episode: Episode!): Character
character(id: ID!): Character
Expand Down Expand Up @@ -108,7 +108,7 @@ Any one of the parametrized fields in the `Query` type may be used as the top le
fields of the result type. The structure of the query follows the schema, and the structure of the result follows the
structure of the query. For example,

```
```graphql
query {
character(id: 1002) {
name
Expand Down Expand Up @@ -153,7 +153,7 @@ The API is backed by values of an ordinary Scala data types with no Grackle depe

The data structure is slightly complicated by the need to support cycles of friendship, e.g.,

```json
```graphql
query {
character(id: 1000) {
name
Expand Down Expand Up @@ -197,7 +197,7 @@ friends the `resolveFriends` method is used to locate the next `Character` value

## The query compiler and elaborator

GraphQL queries are compiled into values of a Scala ADT which represents a query algebra. These query algebra terms
The GraphQL queries are compiled into values of a Scala ADT which represents a query algebra. These query algebra terms
are then transformed in a variety of ways, resulting in a program which can be interpreted against the model to
produce the query result. The process of transforming these values is called _elaboration_, and each elaboration step
simplifies or expands the term to bring it into a form which can be executed directly by the query interpreter.
Expand Down Expand Up @@ -231,7 +231,7 @@ sealed trait Query {

A simple query like this,

```json
```graphql
query {
character(id: 1000) {
name
Expand All @@ -252,7 +252,7 @@ be of GraphQL type `Int`.

Following this initial translation the Star Wars example has a single elaboration step whose role is to translate the
selection into something executable. Elaboration uses the GraphQL schema and so is able to translate an input value
parsed as an `Int` into a GraphQL `ID`. The semantics associated with this (ie. what an `id` is and how it relates to
parsed as an `Int` into a GraphQL `ID`. The semantics associated with this (i.e. what an `id` is and how it relates to
the model) is specific to this model, so we have to provide that semantic via some model-specific code,

@@snip [StarWarsData.scala](/demo/src/main/scala/demo/starwars/StarWarsMapping.scala) { #elaborator }
Expand All @@ -274,15 +274,15 @@ Select("character", Nil,
```

Here the argument to the `character` selector has been translated into a predicate which refines the root data of the
model to the single element which satisifies it via `Unique`. The remainder of the query (`Select("name", Nil)`) is
model to the single element which satisfies it via `Unique`. The remainder of the query (`Select("name", Nil)`) is
then within the scope of that constraint. We have eliminated something with model-specific semantics (`character(id:
1000)`) in favour of something universal which can be interpreted directly against the model.

## The query interpreter and cursor

The data required to construct the response to a query is determined by the structure of the query and gives rise to a
more or less arbitrary traversal of the model. To support this Grackle provides a functional `Cursor` abstraction
which points into the model and can nativigate through GraphQL fields, arrays and values as required by a given query.
which points into the model and can navigate through GraphQL fields, arrays and values as required by a given query.

For in-memory models where the structure of the model ADT closely matches the GraphQL schema a `Cursor` can be derived
automatically by a `GenericMapping` which only needs to be supplemented with a specification of the root mappings for
Expand All @@ -293,7 +293,7 @@ For the Star Wars model the root definitions are of the following form,

@@snip [StarWarsData.scala](/demo/src/main/scala/demo/starwars/StarWarsMapping.scala) { #root }

The the first argument of the `GenericRoot` constructor correspond to the top-level selection of the query (see the
The first argument of the `GenericRoot` constructor correspond to the top-level selection of the query (see the
schema above) and the second argument is the initial model value for which a `Cursor` will be derived. When the query
is executed, navigation will start with that `Cursor` and the corresponding GraphQL type.

Expand Down

0 comments on commit 4b8d2ad

Please sign in to comment.