@@ -12,63 +12,146 @@ module Introduction where
12
12
13
13
import Protolude
14
14
15
+ import System.Random
16
+
15
17
import GraphQL
16
18
import GraphQL.API (Object, Field, Argument, (:>), Union)
17
19
import GraphQL.Resolver (Handler, (:<>)(..), unionValue)
18
20
`` `
19
21
20
- The core idea for this library is that we define a composite type that
21
- specifies the whole API, and then implement a matching handler.
22
+ ## A simple GraphQL service
22
23
23
- The main GraphQL entities we care about are Objects and Fields. Each
24
- Field can have arguments.
24
+ A [GraphQL](http://graphql.org/) service is made up of two things:
25
25
26
- `` ` haskell
27
- type HelloWorld = Object " HelloWorld" '[]
28
- '[ Argument " greeting" Text :> Field "me" Text
29
- ]
30
- `` `
26
+ 1. A schema that defines the service
27
+ 2. Some code that implements the service's behavior
31
28
32
- The example above is equivalent to the following GraphQL type:
29
+ We're going to build a very simple service that says hello to
30
+ people. Our GraphQL schema for this looks like:
33
31
32
+ `` `graphql
33
+ type Hello {
34
+ greeting(who: String!): String!
35
+ }
34
36
`` `
35
- type HelloWorld {
36
- me(greeting: String!): String!
37
+
38
+ Which means we have base type, an _object_ called `Hello`, which has a single
39
+ _field_ `greeting`, which takes a non-nullable `String` called `who` and
40
+ returns a `String`.
41
+
42
+ And we want to be able to send queries that look like:
43
+
44
+ `` `graphql
45
+ {
46
+ greeting(who: "world" )
37
47
}
38
48
`` `
39
49
40
- And if we had a code to handle that type (more later) we could query it like this :
50
+ And get responses like:
41
51
52
+ `` `json
53
+ {
54
+ data: {
55
+ greeting: "Hello world!"
56
+ }
57
+ }
42
58
`` `
43
- { me(greeting: "hello" ) }
59
+
60
+ ### Defining the schema
61
+
62
+ Here's how we would define the schema in Haskell:
63
+
64
+ `` `haskell
65
+ type Hello = Object " Hello" '[]
66
+ '[ Argument " who" Text :> Field " greeting" Text
67
+ ]
44
68
`` `
45
69
46
- ## Implementing a handler
70
+ Breaking this down, we define a new Haskell type `Hello`, which is a GraphQL
71
+ object (also named `" Hello" `) that implements no interfaces (hence `'[]`). It
72
+ has one field, called `" greeting" ` which returns some `Text` and takes a
73
+ single named argument `"who"` , which is also `Text`.
74
+
75
+ There are some noteworthy differences between this schema and the GraphQL
76
+ schema:
77
+
78
+ * The GraphQL schema requires a special annotation to say that a value cannot
79
+ be null, `!`. In Haskell, we instead assume that everything can't be null.
80
+ * In the GraphQL schema, the argument appears *after* the field name. In
81
+ Haskell, it appears *before*.
82
+ * In Haskell, we name the top-level type twice, once on left hand side of the
83
+ type definition and once on the right.
84
+
85
+ ### Implementing the handlers
86
+
87
+ Once we have the schema, we need to define the corresponding handlers, which
88
+ are `Handler` values.
47
89
48
- We define a corresponding handler via the `Handler m a` which takes
49
- the monad to run in (`IO` in this case) and the actual API definition
50
- (`HelloWorld`):
90
+ Here's a `Handler` for `Hello`:
51
91
52
92
`` `haskell
53
- handler :: Handler IO HelloWorld
54
- handler = pure (\greeting -> pure (greeting <> " to me" ))
93
+ hello :: Handler IO Hello
94
+ hello = pure greeting
95
+ where
96
+ greeting who = pure (" Hello " <> who <> " !")
55
97
`` `
56
98
57
- The implementation looks slightly weird, but it's weird for good
58
- reasons. In order:
99
+ The type signature, `Handler IO Hello` shows that it's a `Handler` for
100
+ `Hello`, and that it runs in the `IO` monad. (Note: nothing about this example
101
+ code requires the `IO` monad, it's just a monad that lots of people has heard
102
+ of.)
103
+
104
+ The implementation looks slightly weird, but it's weird for good reasons.
105
+
106
+ The first layer of the handler, `pure greeting`, produces the `Hello` object.
107
+ The `pure` might seem redundant here, but making this step monadic allows us
108
+ to run actions in the base monad.
109
+
110
+ The second layer of the handler, the implementation of `greeting`, produces
111
+ the value of the `greeting` field. It is monadic so that it will only be
112
+ executed when the field was requested.
113
+
114
+ Each handler is a separate monadic action so we only perform the side effects
115
+ for fields present in the query.
59
116
60
- * The first `pure` allows us to run actions in the base monad (here `IO`
61
- before returning anything. This is useful to allocate a resource
62
- like a database connection.
63
- * The `pure` in the function call allows us to **avoid running
64
- actions** when the field hasn't been requested: Each handler is a
65
- separate monadic action so we only perform the side effects for fields
66
- present in the query.
117
+ This handler is in `Identity` because it doesn't do anything particularly
118
+ monadic. It could be in `IO` or `STM` or `ExceptT Text IO` or whatever you
119
+ would like.
120
+
121
+ ### Running queries
122
+
123
+ Defining a service isn't much point unless you can query. Here's how:
124
+
125
+ `` `haskell
126
+ queryHello :: IO Response
127
+ queryHello = interpretAnonymousQuery @Hello hello " { greeting(who: \" mort\" ) }"
128
+ `` `
67
129
130
+ The actual `Response` type is fairly big, so we're most likely to turn it into
131
+ JSON:
132
+
133
+ `` `
134
+ λ Aeson.encode <$ > queryHello
135
+ "{\" greeting\" :\" Hello mort!\" }"
136
+ ```
68
137
69
138
## Combining field handlers with :<>
70
139
71
- Let's implement a simple calculator that can add and subtract integers:
140
+ How do we define an object with more than one field?
141
+
142
+ Let's implement a simple calculator that can add and subtract integers. First,
143
+ the schema:
144
+
145
+ ```graphql
146
+ type Calculator {
147
+ add(a: Int!, b: Int!): Int!,
148
+ sub(a: Int!, b: Int!): Int!,
149
+ }
150
+ ```
151
+
152
+ Here, `Calculator` is an object with two fields: `add` and `sub`.
153
+
154
+ And now the Haskell version:
72
155
73
156
``` haskell
74
157
type Calculator = Object "Calculator" '[]
@@ -77,107 +160,142 @@ type Calculator = Object "Calculator" '[]
77
160
]
78
161
```
79
162
80
- Every element in a list in Haskell has the same type, so we can't
81
- really return a list of different handlers. Instead we compose the
82
- different handlers with a new operator, `:<>`. This operator, commonly
83
- called birdface, is based on the operator for monoids, `<>`.
163
+ So far, this is the same as our `Hello` example.
84
164
85
- `` ` haskell
165
+ And its handler:
166
+
167
+ ```haskell
86
168
calculator :: Handler IO Calculator
87
169
calculator = pure (add :<> subtract')
88
170
where
89
- add a b = pure (a + b)
90
- subtract' a b = pure (a - b)
171
+ add a b = pure (a + b)
172
+ subtract' a b = pure (a - b)
91
173
```
92
174
93
- Note that we still need `pure` for each individual handler.
175
+ This handler introduces a new operator, `:<>` (pronounced "birdface"), which
176
+ is used to compose two existing handlers into a new handler. It's inspired by
177
+ the operator for monoids, `<>`.
94
178
179
+ Note that we still need `pure` for each individual handler.
95
180
96
181
## Nesting Objects
97
182
98
- Objects can be used as a type in fields. This allows us to implement a
99
- server for the classic GraphQL example query:
183
+ How do we define objects made up other objects?
100
184
185
+ One of the great things in GraphQL is that objects can be used as types for
186
+ fields. Take this classic GraphQL schema as an example:
101
187
188
+ ```graphql
189
+ type Query {
190
+ me: User!
191
+ }
192
+
193
+ type User {
194
+ name: Text!
195
+ }
102
196
```
197
+
198
+ We would query this schema with something like:
199
+
200
+ ```graphql
103
201
{
104
- me { name }
202
+ me {
203
+ name
204
+ }
105
205
}
106
206
```
107
207
108
- The Haskell schema for that looks like this :
208
+ Which would produce output like:
109
209
110
- `` ` haskell
210
+ ```json
211
+ {
212
+ data: {
213
+ me: {
214
+ name: "Mort"
215
+ }
216
+ }
217
+ }
218
+ ```
219
+
220
+ The Haskell type for this schema looks like:
221
+
222
+ ```haskell
111
223
type User = Object "User" '[] '[Field "name" Text]
112
224
type Query = Object "Query" '[] '[Field "me" User]
113
225
```
114
226
115
- Note the type `User` for `me`.
116
-
227
+ Note that `Query` refers to the type `User` when it defines the field `me`.
117
228
118
229
We write nested handlers the same way we write the top-level handler:
119
230
120
- `` ` haskell
231
+ ```haskell
121
232
user :: Handler IO User
122
- user = pure (pure " mort" )
233
+ user = pure name
234
+ where
235
+ name = pure "Mort"
123
236
124
237
query :: Handler IO Query
125
238
query = pure user
126
239
```
127
240
241
+ And that's it.
242
+
128
243
## Unions
129
244
130
- Union handlers require special treatment in Haskell because we need to
131
- return the same type for each possible, different type in the union.
245
+ GraphQL has [support for union
246
+ types](http://graphql.org/learn/schema/#union-types). These require special
247
+ treatment in Haskell.
132
248
133
- Let's define a union:
249
+ Let's define a union, first in GraphQL :
134
250
135
- `` ` haskell
136
- type UserOrCalcualtor = Union " UserOrCalcualtor" '[User, Calculator]
137
- type UnionQuery = Object "UnionQuery" '[] ' [Field "union" UserOrCalcualtor]
251
+ ```graphql
252
+ union UserOrCalculator = User | Calculator
138
253
```
139
254
140
- and a handler that returns a user :
255
+ And now in Haskell :
141
256
142
- `` ` haskell
143
- unionQuery :: Handler IO UnionQuery
144
- unionQuery = pure (unionValue @User user)
257
+ ```haskell
258
+ type UserOrCalcualtor = Union "UserOrCalcualtor" '[User, Calculator]
145
259
```
146
260
147
- Note that, while `unionValue` looks a bit like `unsafeCoerce` by
148
- forcing one type to become another type, it's actually type-safe
149
- because we use a *type-index* to pick the correct type from the
150
- union. Using e.g. `unionValue @HelloWorld handler` will not compile
151
- because `HelloWorld` is not in the union.
152
-
153
- ## Running a query
261
+ And let's define a very simple top-level object that uses `UserOrCalcualtor`:
154
262
155
263
```haskell
156
- hello :: IO Response
157
- hello = interpretAnonymousQuery @HelloWorld handler " { me(greeting: \" hey ho\" ) }"
264
+ type UnionQuery = Object "UnionQuery" '[] '[Field "union" UserOrCalcualtor]
158
265
```
159
266
160
- But our output is pretty long :
267
+ and a handler that randomly returns either a user or a calculator :
161
268
162
- `` `
163
- λ hello
164
- Success (Object' (OrderedMap {keys = [Name {unName = "me" }], toMap = fromList [(Name {unName = "me" },ValueScalar' (ConstString (String "hey ho to me" )))]}))
269
+ ```haskell
270
+ unionQuery :: Handler IO UnionQuery
271
+ unionQuery = do
272
+ returnUser <- randomIO
273
+ if returnUser
274
+ then pure (unionValue @User user)
275
+ else pure (unionValue @Calculator calculator)
165
276
```
166
277
167
- The output object `Object'` has a `ToJSON` instance:
168
-
169
- `` `
170
- λ map (\( Success o) -> Aeson.encode o) hello
171
- "{\" me\" :\" hey ho to me\" }"
172
- `` `
278
+ The important thing here is that we have to wrap the actual objects we return
279
+ using `unionValue`.
173
280
281
+ Note that while `unionValue` looks a bit like `unsafeCoerce` by forcing one
282
+ type to become another type, it's actually type-safe because we use a
283
+ *type-index* to pick the correct type from the union. Using e.g. `unionValue
284
+ @HelloWorld handler` will not compile because `HelloWorld` is not in the
285
+ union.
174
286
175
287
## Where next?
176
288
177
289
We have an
178
290
[examples](https://github.com/jml/graphql-api/tree/master/tests/Examples)
179
291
directory showing full code examples.
180
292
293
+ We also have a fair number of [end-to-end
294
+ tests](https://github.com/jml/graphql-api/tree/master/tests/EndToEndTests.hs)
295
+ based on an [example
296
+ schema](https://github.com/jml/graphql-api/tree/master/tests/ExampleSchema.hs)
297
+ that you might find interesting.
298
+
181
299
If you want to try the examples in this tutorial you can run:
182
300
183
301
```bash
0 commit comments