Docs for GraphQL + reorganization (#174)

Co-authored-by: Flavio Corpa <flavio.corpa@47deg.com>
This commit is contained in:
Alejandro Serrano 2020-04-15 13:53:05 +02:00 committed by GitHub
parent 39055ed237
commit f34232bb5b
11 changed files with 272 additions and 107 deletions

View File

@ -19,14 +19,17 @@ options:
- title: Registry
url: registry/
- title: Services and servers
- title: RPC services
url: rpc/
nested_options:
- title: gRPC
url: grpc/
- title: gRPC servers
url: grpc/server/
- title: Streams
url: stream/
- title: gRPC clients
url: grpc/client/
- title: GraphQL services
url: graphql/
- title: Integrations
nested_options:

View File

@ -14,9 +14,10 @@ Mu-Haskell is a set of packages that help you build both servers and clients for
* [Schemas]({% link docs/schema.md %})
* [Serialization formats]({% link docs/serializers.md %}): Protocol Buffers and Avro
* [Registry]({% link docs/registry.md %})
* [Services and servers]({% link docs/rpc.md %})
* [gRPC servers and clients]({% link docs/grpc.md %})
* [Streams]({% link docs/stream.md %})
* [RPC services]({% link docs/rpc.md %})
* [gRPC server]({% link docs/grpc-server.md %})
* [gRPC client]({% link docs/grpc-client.md %})
* [GraphQL services]({% link docs/graphql.md %})
* Integration with other libraries
* [Databases]({% link docs/db.md %}), including resource pools
* [Using transformers]({% link docs/transformer.md %}): look here for logging

159
docs/docs/graphql.md Normal file
View File

@ -0,0 +1,159 @@
---
layout: docs
title: GraphQL services
permalink: graphql/
---
# GraphQL services
[GraphQL](https://graphql.github.io/) defines a language for queries, mutations, and subscriptions much more powerful than RPC or REST-based microservices. The key ingredient is a more complex query language, in which you do not only specify what you want to obtain, but also precisely describe the parts of the data you require.
We are going to implement a server for the following GraphQL schema, which is roughly based on the one in the [Apollo Server docs](https://www.apollographql.com/docs/apollo-server/schema/schema/). For those not used to the GraphQL schema language, we define a `Query` type with two *fields*, which represent the different queries we can do against the server. The `author` field takes one *argument*; the exclamation mark means that they are *not* optional, although they have a default value. `Book`s and `Author`s define *object* types which can be further queried; note in particular that there is a recursive reference between them, this is allowed in GraphQL schemas.
```graphql
type Query {
author(name: String! = ".*"): Author
books: [Book!]!
}
type Book {
id: Int!
title: String!
author: Author!
}
type Author {
id: Int!
name: String!
books: [Book!]!
}
```
## Importing the schema
The first step is to import this schema as a type-level definition for Mu. The `graphql` function takes three arguments:
* The first one defines the name of the *schema* type, which includes the enumerations and the input objects in the schema.
* The second one defines the name of the *service declaration*, in which we find the (result) objects from the GraphQL schema.
* The third argument is the route to the file *with respect to the project root*.
```haskell
{-# language TemplateHaskell #-}
import Mu.GraphQL.Quasi
graphql "Schema" "ServiceDefinition" "schema.graphql"
```
This might be surprising for people already used to GraphQL, the separation between input objects and enumerations, and the rest of the objects may seem quite artificial. However, this is needed because Mu-Haskell strongly separates those part of a service which only hold data, from those which may have some behavior associated with it (sometimes called *resolvers*).
## Mapping each object type
Unsurprisingly, in order to implement a server for this schema you need to define a resolver for each of the objects and fields. There's one question to be answered beforehand, though: how do you represent *result* type of those resolvers, that is, how do we represent a (**not** input) *object*? We define those using a *type mapping*, which specifies the type associated to each GraphQL object type, except for the root types like `Query`.
This is better explained with the example. The question here is: how do we represent an `Author`? Our type mapping says that simply using an `AuthorId`:
```haskell
{-# language TypeApplications, DataKinds #-}
type TypeMapping
= '[ "Author" ':-> AuthorId
, "Book" ':-> (BookId, AuthorId) ]
```
This means *two* things:
1. The result the `author` method in `Query` should be `Maybe AuthorId`. We obtain this type by noticing that in the definition of that field, `Author` has no exclamation mark, so it's optional, and the type mapping says that `Author` is mapped to an `AuthorId`.
2. The resolver for each of the fields for `Author` take an *additional* argument given by this type mapping. For example, the resolver for the `name` field should have type `AuthorId -> m String`, the argument coming from the type mapping, and the result type being defined by the schema.
You might be wondering why this is so complicated? The reason is that we don't want to do too much work upfront. In a traditional RPC service you would return the *whole* `Author`, with every field inside. In contrast, a GraphQL query defines *exactly* which fields are required, and we only want to run the resolvers we need. On the other hand, we still need to have a way to connect the dots, and we use the author identifier for that.
The following schema shows the way we traverse a GraphQL query and the types involved in it.
```graphql
{
author(name: ".*Ende.*") { --> 1. return a Maybe AuthorId
name --> 2. from that (optional) AuthorId return a Text
books { --> 3. from that AuthorId return [(BookId, AuthorId)]
title --> 4. from each (BookId, AuthorId) return a Text
}
}
}
```
Note that we could have made `Book` be represented simply by a `BookId` and then query some database to figure our the author. However, in this case we assume this query is going to be quite common, and we cache this information since the beginning. Note that from the implementation point of view, the resolver for the `author` field of `Book` should have the type:
```haskell
bookAuthor :: (BookId, AuthorId) -> m AuthorId
bookAuthor (_, aid) = pure aid
```
The argument and result types come from the type mapping, since they are both object types. Given that we have cached that information, we can return it right away.
## Implementing the server
The whole implementation looks as a big list defining each of the resolvers for each of the objects and their fields. There's only one subtlety: for *root* operations we use `method` instead of `field`. The reason is that those fields do not take any information passed by, they are the initial requests.
```haskell
{-# language ScopedTypeVariables, PartialTypeSignatures #-}
libraryServer :: forall m. (MonadServer m)
=> ServerT TypeMapping ServiceDefinition m _
= resolver
( object @"Query" ( method @"author" findAuthor
, method @"books" allBooks )
, object @"Author" ( field @"id" authorId
, field @"name" authorName
, field @"books" authorBooks )
, object @"Book" ( field @"id" bookId
, field @"author" bookAuthor
, field @"title" bookTitle ) )
where -- Query fields
findAuthor :: Text -> m (Maybe AuthorId)
allBooks :: m [(BookId, AuthorId)]
-- Author fields
authorId :: AuthorId -> m Int
authorName :: AuthorId -> m Text
authorBooks :: AuthorId -> m [(BookId, AuthorId)]
-- Book fields
bookId :: (BookId, AuthorId) -> m Int
bookAuthor :: (BookId, AuthorId) -> m AuthorId
bookAuthor (_, aid) = pure aid
bookTitle :: (BookId, AuthorId) -> m Text
-- implementation
```
In the above code we have defined all fields in a big `where` block, but of course those may be defined as top-level functions, or inline in call to `field` or `method`.
The final touch is to start the GraphQL server defined by `libraryServer`. The `Mu.GraphQL.Server` module defines tons of different ways to configure how the server behaves; the simplest option just requires a port and the name of the root type for queries.
```haskell
main = runGraphQLAppQuery 8080 libraryServer (Proxy @"Query")
```
## Subscriptions as streams
## Comparison with other libraries
There are other libraries targeting GraphQL server definition in Haskell: `graphql-api` and Morpheus GraphQL. The latter also supports defining GraphQL *clients*, a feature not (yet) implemented in Mu.
[`graphql-api`](https://github.com/haskell-graphql/graphql-api#readme) shares with Mu the encoding of the GraphQL schema in the type-level. In fact, as the [tutorial](https://haskell-graphql-api.readthedocs.io/en/latest/tutorial/Introduction.html) shows, its encoding is much closer to GraphQL's schema definition.
```haskell
type Hello
= Object "Hello" '[]
'[ Argument "who" Text :> Field "greeting" Text ]
```
This is expected: Mu's ability to target both RPC and GraphQL microservices means that sometimes there's some mismatch.
[Morpheus GraphQL](https://morpheusgraphql.com/) also exposes GraphQL servers from Haskell code. Morpheus shared with Mu the ability to import a GraphQL schema into Haskell code. However, the types and fields are not represented by a type-level encoding, but *directly* as Haskell *records*.
```haskell
data GreetingArgs = GreetingArgs { argname :: Text } deriving (Generic, GQLType)
data Hello m = Hello { greeting :: GreetingArgs -> m Text } deriving (Generic, GQLType)
```
At the moment of writing, Mu has the ability to use records for schema types. In GraphQL terms, that means that you can use Haskell records for input objects and enumerations, but resolvers for each object fields need to be defined separately, as described above.
Another interesting comparison point is how the different libraries ensure that only the required data is ever consulted. This is quite important, since otherwise we might end up in infinite loops (find an author, query the books, for each book query the author, for each author the books, ...). Both `graphql-api` and Morpheus rely on Haskell's laziness, whereas Mu asks to define a type mapping which is then used as connecting point between objects.

View File

@ -1,24 +1,12 @@
---
layout: docs
title: gRPC servers and clients
permalink: grpc/
title: gRPC clients
permalink: grpc/client/
---
# gRPC servers and clients
# gRPC clients
Mu-Haskell defines a generic notion of service and server that implements it. This generic server can then be used by `mu-grpc-server`, to provide a concrete implementation using a specific wire format. Or you can use `mu-grpc-client` to build a client.
## Running the server with `mu-grpc`
The combination of the declaration of a service API and a corresponding implementation as a `Server` may be served directly using a concrete wire protocol. One example is gRPC, provided by our sibling library `mu-grpc`. The following line starts a server at port `8080`, using Protocol Buffers as serialization layer:
```haskell
main = runGRpcApp msgProtoBuf 8080 quickstartServer
```
## Building a client
Right now there are two options for building clients: using records or with `TypeApplications`. To give a proper introduction to both options let's consider in detail an example client for the following services:
There are several options for building clients: you can choose between optics, records, and `TypeApplications`. Let's consider in detail an example client for the following service:
```protobuf
service Service {

86
docs/docs/grpc-server.md Normal file
View File

@ -0,0 +1,86 @@
---
layout: docs
title: gRPC servers
permalink: grpc/server/
---
# gRPC servers
Mu-Haskell defines a generic notion of service and server that implements it. This generic server can then be used by `mu-grpc-server`, to provide a concrete implementation using a specific wire format.
## Implementing the service
Let's get back to the example we used in the [generic RPC section]({% link docs/rpc.md %}). In order to implement the corresponding service, you have to define the behavior of each method by means of a *handler*. You can use Haskell types for your handlers, given that you had previously declared that they can be mapped back and forth the schema types using `ToSchema` and `FromSchema`. For example, the following is a handler for the `SayHello` method in `Greeter`:
```haskell
sayHello :: (MonadServer m) => HelloRequest -> m HelloResponse
sayHello (HelloRequest nm) = pure $ HelloResponse ("hi, " <> nm)
```
Notice the use of `MonadServer` in this case. This gives us the ability to:
* Run arbitrary `IO` actions by using `liftIO`,
* Return an error code by calling `serverError`.
Being polymorphic here allows us to run the same server in multiple back-ends. Furthermore, by enlarging the set of abilities required for our monad `m`, we can [integrate with other libraries]({% link docs/transformer.md %}), including logging and resource pools.
Since you can declare more than one method in a service, you need to join them into a `ServerT`. You do so by using `singleService` (since gRPC servers may only expose one), and a *tuple* of methods indexed by their name *in the gRPC definition*. In addition to the name of the service, `ServerT` has an additional parameter which records the types of the handlers. Since that list may become quite long, we can ask GHC to write it for us by using the `PartialTypeSignatures` extension and writing an underscore `_` in that position.
```haskell
{-# language PartialTypeSignatures #-}
quickstartServer :: (MonadServer m) => ServerT QuickstartService m _
quickstartServer = singleService (method @"SayHello" sayHello)
```
## Running the server with `mu-grpc`
The combination of the declaration of a service API and a corresponding implementation as a `Server` may be served directly using a concrete wire protocol. One example is gRPC, provided by our sibling library `mu-grpc`. The following line starts a server at port `8080`, using Protocol Buffers as serialization layer:
```haskell
main = runGRpcApp msgProtoBuf 8080 quickstartServer
```
# Streams
In the docs about [service definition]({% link docs/rpc.md %}) we had one single `SayHello` method which takes one value and produces one value. However, we can also declare methods which perform streaming, such as:
```protobuf
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayManyHellos (stream HelloRequest) returns (stream HelloReply) {}
}
```
Adding this method to the service definition should be easy, we just need to use `ArgStream` and `RetStream` to declare that behavior (of course, this is done automatically if you import the service from a file):
```haskell
type QuickstartService
= 'Service "Greeter"
'[ 'Method "SayHello" ...
, 'Method "SayManyHellos" '[]
'[ 'ArgStream 'Nothing '[] ('FromSchema QuickstartSchema "HelloRequest")]
('RetStream ('FromSchema QuickstartSchema "HelloResponse")) ]
```
To define the implementation of this method we build upon the great [Conduit](https://github.com/snoyberg/conduit) library. Your input is now a producer of values, as defined by that library, and you must write the results to the provided sink. Better said with an example:
```haskell
sayManyHellos
:: (MonadServer m)
=> ConduitT () HelloRequest m ()
-> ConduitT HelloResponse Void m ()
-> m ()
sayManyHellos source sink
= runConduit $ source .| C.mapM sayHello .| sink
```
In this case we are connecting the `source` to the `sink`, transforming in between each value using the `sayHello` function. More complicated pipelines can be built in this form.
Since now the service has more than one method, we need to update our server declaration to bring together this new handler:
```haskell
quickstartServer = singleService ( method @"SayHello" sayHello
, method @"SayManyHellos" sayManyHellos )
```

View File

@ -73,7 +73,7 @@ data HelloReplyMessage
, FromSchema TheSchema "HelloReply")
```
These data types should be added to the file `src/Schema.hs`, under the line that starts `grpc ...`. (See the [gRPC page]({% link docs/grpc.md %}) for information about what that line is doing.)
These data types should be added to the file `src/Schema.hs`, under the line that starts `grpc ...`. (See the [RPC services page]({% link docs/rpc.md %}) for information about what that line is doing.)
You can give the data types and their constructors any name you like. However, keep in mind that:
@ -104,7 +104,7 @@ main :: IO ()
main = runGRpcApp msgProtoBuf 8080 server
server :: (MonadServer m) => ServerT Service m _
server = Server H0
server = singleService ()
```
The simplest way to provide an implementation for a service is to define one function for each method. You can define those functions completely in terms of Haskell data types; in our case `HelloRequestMessage` and `HelloReplyMessage`. Here is an example definition:
@ -127,10 +127,10 @@ sayHello (HelloRequestMessage nm)
= pure $ record ("hi, " <> nm ^. #name)
```
How does `server` know that `sayHello` (any of the two versions) is part of the implementation of the service? We have to tell it, by adding `sayHello` to the list of methods. Unfortunately, we cannot use a normal list, so we use `(:<|>:)` to join them, and `H0` to finish it.
How does `server` know that `sayHello` (any of the two versions) is part of the implementation of the service? We have to tell it, by declaring that `sayHello` implements the `SayHello` method from the gRPC definition. If you had more methods, you list each of them using tuple syntax.
```haskell
server = Server (sayHello :<|>: H0)
server = singleService (method @"SayHello" sayHello)
```
At this point you can build the project using `stack build`, and then execute via `stack run`. This spawns a gRPC server at port 8080, which you can test using applications such as [BloomRPC](https://github.com/uw-labs/bloomrpc).

View File

@ -16,7 +16,7 @@ One of the features that WAI provides is the definition of *middleware* componen
It's a common task in web servers to send some static content for a subset of URLs (think of resources such as images or JavaScript code). `wai-middleware-static` automates that task, and also serves as the simplest example of WAI middleware.
Remember that in our [original code](intro.md) our `main` function looked as follows:
Remember that in our [gRPC server example]({% link docs/grpc-server.md %}) our `main` function looked as follows:
```haskell
main = runGRpcApp msgSerializer 8080 server

View File

@ -17,7 +17,7 @@ Once we have done that you can use functions like `fromRegistry` to try to parse
## Using the Registry
By default, [service definition](rpc.md) talks about concrete schemas and types. If you define a registry, you can also use it to accomodate different schemas. In this case, apart from the registry itself, we need to specify the *Haskell* type to use during (de)serialization, and the *version number* to use for serialization.
By default, [service definition]({% link docs/rpc.md %}) talks about concrete schemas and types. If you define a registry, you can also use it to accomodate different schemas. In this case, apart from the registry itself, we need to specify the *Haskell* type to use during (de)serialization, and the *version number* to use for serialization.
```haskell
type QuickstartService

View File

@ -1,10 +1,10 @@
---
layout: docs
title: Services and servers
title: RPC services
permalink: rpc/
---
# Services and servers
# RPC services
There are several formats in the wild used to declare service APIs, including [Avro IDL](https://avro.apache.org/docs/current/idl.html), [gRPC](https://grpc.io/), and [OpenAPI](https://swagger.io/specification/). `mu-rpc` abstract the commonalities into a single type-level format for declaring these services, building on the format-independent schema facilities of `mu-schema`. In addition, this package provides a generic notion of *server* of a service. One such server defines one behavior for each method in the service, but does not bother with (de)serialization mechanisms.
@ -98,28 +98,3 @@ In order to support both [Avro IDL](https://avro.apache.org/docs/current/idl.htm
* The *return types* gives the same two choices under the names `RetSingle` or `RetStream`, and additionally supports the declaration of methods which may raise exceptions using `RetThrows`, or methods which do not retun any useful information using `RetNothing`.
Note that depending on the concrete implementation you use to run the server, one or more of these choices may not be available. For example, gRPC only supports one argument and return value, either single or streaming, but not exceptions.
## Implementing the service
In order to implement the service, you have to define the behavior of each method by means of a *handler*. You can use Haskell types for your handlers, given that you had previously declared that they can be mapped back and forth the schema types using `ToSchema` and `FromSchema`. For example, the following is a handler for the `SayHello` method in `Greeter`:
```haskell
sayHello :: (MonadServer m) => HelloRequest -> m HelloResponse
sayHello (HelloRequest nm) = pure $ HelloResponse ("hi, " <> nm)
```
Notice the use of `MonadServer` in this case. This gives us the ability to:
* Run arbitrary `IO` actions by using `liftIO`,
* Return an error code by calling `serverError`.
Being polymorphic here allows us to run the same server in multiple back-ends. Furthermore, by enlarging the set of abilities required for our monad `m`, we can [integrate with other libraries](transformer.md), including logging and resource pools.
Since you can declare more than one method in a service, you need to join them into a `Server`. You do so by using `(:<|>:)` between each handler and ending the sequence with `H0`. In addition to the name of the service, `Server` has an additional parameter which records the types of the handlers. Since that list may become quite long, we can ask GHC to write it for us by using the `PartialTypeSignatures` extension and writing an underscore `_` in that position.
```haskell
{-# language PartialTypeSignatures #-}
quickstartServer :: (MonadServer m) => ServerT QuickstartService m _
quickstartServer = Server (sayHello :<|>: H0)
```

View File

@ -1,47 +0,0 @@
---
layout: docs
title: Streams
permalink: stream/
---
# Streams
In the docs about [service definition](rpc.md) we had one single `SayHello` method which takes one value and produces one value. However, we can also declare methods which perform streaming, such as:
```java
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayManyHellos (stream HelloRequest) returns (stream HelloReply) {}
}
```
Adding this method to the service definition should be easy, we just need to use `ArgStream` and `RetStream` to declare that behavior (of course, this is done automatically if you import the service from a file):
```haskell
type QuickstartService
= 'Service "Greeter"
'[ 'Method "SayHello" ...
, 'Method "SayManyHellos" '[]
'[ 'ArgStream 'Nothing '[] ('FromSchema QuickstartSchema "HelloRequest")]
('RetStream ('FromSchema QuickstartSchema "HelloResponse")) ]
```
To define the implementation of this method we build upon the great [Conduit](https://github.com/snoyberg/conduit) library. Your input is now a producer of values, as defined by that library, and you must write the results to the provided sink. Better said with an example:
```haskell
sayManyHellos
:: (MonadServer m)
=> ConduitT () HelloRequest m ()
-> ConduitT HelloResponse Void m ()
-> m ()
sayManyHellos source sink
= runConduit $ source .| C.mapM sayHello .| sink
```
In this case we are connecting the `source` to the `sink`, transforming in between each value using the `sayHello` function. More complicated pipelines can be built in this form.
Since now the service has more than one method, we need to update our server declaration to bring together this new handler:
```haskell
quickstartServer = Server (sayHello :<|>: sayManyHellos :<|>: H0)
```

View File

@ -6,7 +6,7 @@ permalink: transformer/
# Integration using transformers
You might be wondering: how can I integrate my favorite logging library with `mu-grpc-server`? Our [explanation of services](rpc.md) introduced `MonadServer` as the simplest set of capabilities required for a server:
You might be wondering: how can I integrate my favorite logging library with `mu-grpc-server`? Our [explanation of services]({% link docs/rpc.md %}) introduced `MonadServer` as the simplest set of capabilities required for a server:
* Finish successfully by `return`ing,
* Finish with an error code via `serverError`,
@ -18,7 +18,7 @@ But you are not tied to that simple set! You can create servers which need more
One simple example of a capability is having one single piece of information you can access. This is useful to thread configuration data, or if you use a transactional variable as information, as a way to share data between concurrent threads. This is traditionally done using a `Reader` monad.
Let us extend our [`sayHello` example](rpc.md) with a piece of configuration which states the word to use when greeting:
Let us extend our [`sayHello` example]({% link docs/grpc-server.md %}) with a piece of configuration which states the word to use when greeting:
```haskell
import Control.Monad.Reader
@ -56,7 +56,7 @@ sayHello (HelloRequest nm) = do
pure $ HelloResponse ("hi, " <> nm)
```
The most important addition with respect to the [original code](rpc.md) is in the signature. Before we only had `MonadServer m`, now we have an additional `MonadLogger m` there.
The most important addition with respect to the original code is in the signature. Before we only had `MonadServer m`, now we have an additional `MonadLogger m` there.
As we have done with the Reader example, we need to define how to handle `MonadLogger`. `monad-logger` provides [three different monad transformers](http://hackage.haskell.org/package/monad-logger-0.3.31/docs/Control-Monad-Logger.html#g:3), so you can choose whether your logging will be completely ignored, will become a Haskell value, or would fire some `IO` action like printing in the console. Each of these monad transformers comes with a `run` action which declares how to handle it; the extended function `runGRpcAppTrans` takes that handler as argument.