This repository has been archived by the owner on Apr 28, 2019. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Http.elm
366 lines (284 loc) · 12.5 KB
/
Http.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
module Graphqelm.Http exposing (Error(..), QueryRequestMethod(..), Request, ignoreParsedErrorData, mapError, mutationRequest, queryRequest, queryRequestWithHttpGet, send, toTask, withCredentials, withHeader, withQueryParams, withTimeout)
{-| Send requests to your GraphQL endpoint. See [this live code demo](https://rebrand.ly/graphqelm)
or the [`examples/`](https://github.com/dillonkearns/graphqelm/tree/master/examples)
folder for some end-to-end examples.
The builder syntax is inspired by Luke Westby's
[elm-http-builder package](http://package.elm-lang.org/packages/lukewestby/elm-http-builder/latest).
## Data Types
@docs Request, Error
## Begin `Request` Pipeline
@docs queryRequest, mutationRequest, queryRequestWithHttpGet
@docs QueryRequestMethod
## Configure `Request` Options
@docs withHeader, withTimeout, withCredentials, withQueryParams
## Perform `Request`
@docs send, toTask
## Map `Error`s
@docs mapError, ignoreParsedErrorData
-}
import Graphqelm.Document as Document
import Graphqelm.Http.GraphqlError as GraphqlError
import Graphqelm.Http.QueryHelper as QueryHelper
import Graphqelm.Http.QueryParams as QueryParams
import Graphqelm.Operation exposing (RootMutation, RootQuery)
import Graphqelm.SelectionSet exposing (SelectionSet)
import Http
import Json.Decode
import Json.Encode
import Task exposing (Task)
import Time exposing (Time)
{-| An internal request as it's built up. Once it's built up, send the
request with `Graphqelm.Http.send`.
-}
type Request decodesTo
= Request
{ details : Details decodesTo
, headers : List Http.Header
, baseUrl : String
, expect : Json.Decode.Decoder decodesTo
, timeout : Maybe Time
, withCredentials : Bool
, queryParams : List ( String, String )
}
{-| Union type to pass in to `queryRequestWithHttpGet`. Only applies to queries.
Mutations don't accept this configuration option and will always use POST.
-}
type QueryRequestMethod
= AlwaysGet
| GetIfShortEnough
type Details decodesTo
= Query (Maybe QueryRequestMethod) (SelectionSet decodesTo RootQuery)
| Mutation (SelectionSet decodesTo RootMutation)
{-| Initialize a basic request from a Query. You can add on options with `withHeader`,
`withTimeout`, `withCredentials`, and send it with `Graphqelm.Http.send`.
-}
queryRequest : String -> SelectionSet decodesTo RootQuery -> Request decodesTo
queryRequest baseUrl query =
{ headers = []
, baseUrl = baseUrl
, expect = Document.decoder query
, timeout = Nothing
, withCredentials = False
, details = Query Nothing query
, queryParams = []
}
|> Request
{-| Exactly like `queryRequest`, but with an option to use the HTTP GET request
method. You will probably want to use `GetIfShortEnough`, which uses GET if the
full URL ends up being under 2000 characters, or POST otherwise, since [some browsers
don't support URLs over a certain length](https://stackoverflow.com/questions/812925/what-is-the-maximum-possible-length-of-a-query-string?noredirect=1&lq=1).
`GetIfShortEnough` will typically do what you need. If you must use GET no matter
what when hitting your endpoint, you can use `AlwaysGet`.
`queryRequest` always uses POST since some GraphQL API's don't support GET
requests (for example, the Github API assumes that you are doing an introspection
query if you make a GET request). But for semantic reasons, GET requests
are sometimes useful for sending GraphQL Query requests. That is, a GraphQL Query
does not perform side-effects on the server like a Mutation does, so a GET
indicates this and allows some servers to cache requests. See
[this github thread from the Apollo project](https://github.com/apollographql/apollo-client/issues/813)
for more details.
-}
queryRequestWithHttpGet : String -> QueryRequestMethod -> SelectionSet decodesTo RootQuery -> Request decodesTo
queryRequestWithHttpGet baseUrl requestMethod query =
{ headers = []
, baseUrl = baseUrl
, expect = Document.decoder query
, timeout = Nothing
, withCredentials = False
, details = Query (Just requestMethod) query
, queryParams = []
}
|> Request
{-| Initialize a basic request from a Mutation. You can add on options with `withHeader`,
`withTimeout`, `withCredentials`, and send it with `Graphqelm.Http.send`.
-}
mutationRequest : String -> SelectionSet decodesTo RootMutation -> Request decodesTo
mutationRequest baseUrl mutationSelectionSet =
{ details = Mutation mutationSelectionSet
, headers = []
, baseUrl = baseUrl
, expect = Document.decoder mutationSelectionSet
, timeout = Nothing
, withCredentials = False
, queryParams = []
}
|> Request
{-| Represents the two types of errors you can get, an Http error or a GraphQL error.
See the `Graphqelm.Http.GraphqlError` module docs for more details.
-}
type Error parsedData
= GraphqlError (GraphqlError.PossiblyParsedData parsedData) (List GraphqlError.GraphqlError)
| HttpError Http.Error
{-| Map the error data if it is `ParsedData`.
-}
mapError : (a -> b) -> Error a -> Error b
mapError mapFn error =
case error of
GraphqlError possiblyParsedData graphqlErrorList ->
case possiblyParsedData of
GraphqlError.ParsedData parsedData ->
GraphqlError (GraphqlError.ParsedData (mapFn parsedData)) graphqlErrorList
GraphqlError.UnparsedData unparsedData ->
GraphqlError (GraphqlError.UnparsedData unparsedData) graphqlErrorList
HttpError httpError ->
HttpError httpError
{-| Useful when you don't want to deal with the recovered data if there is `ParsedData`.
Just a shorthand for `mapError` that will turn any `ParsedData` into `()`.
-}
ignoreParsedErrorData : Error parsedData -> Error ()
ignoreParsedErrorData error =
mapError (\_ -> ()) error
type alias DataResult parsedData =
Result ( GraphqlError.PossiblyParsedData parsedData, List GraphqlError.GraphqlError ) parsedData
convertResult : Result Http.Error (DataResult decodesTo) -> Result (Error decodesTo) decodesTo
convertResult httpResult =
case httpResult of
Ok successOrError ->
case successOrError of
Ok value ->
Ok value
Err ( possiblyParsedData, error ) ->
Err (GraphqlError possiblyParsedData error)
Err httpError ->
Err (HttpError httpError)
{-| Send the `Graphqelm.Request`
You can use it on its own, or with a library like
[RemoteData](http://package.elm-lang.org/packages/krisajenkins/remotedata/latest/).
import Graphqelm.Http
import Graphqelm.OptionalArgument exposing (OptionalArgument(Null, Present))
import RemoteData exposing (RemoteData)
type Msg
= GotResponse RemoteData (Graphqelm.Http.Error Response) Response
makeRequest : Cmd Msg
makeRequest =
query
|> Graphqelm.Http.queryRequest "https://graphqelm.herokuapp.com/"
|> Graphqelm.Http.withHeader "authorization" "Bearer abcdefgh12345678"
-- If you're not using remote data, it's just
-- |> Graphqelm.Http.send GotResponse
-- With remote data, it's as below
|> Graphqelm.Http.send (RemoteData.fromResult >> GotResponse)
If any errors are present, you will get a `GraphqlError` that includes the details.
GraphQL allows for partial data to be returned in the case of errors so you can
inspect the data returned in the `GraphqlError` if you would like to try to recover
any data that made it through in the response.
-}
send : (Result (Error decodesTo) decodesTo -> msg) -> Request decodesTo -> Cmd msg
send resultToMessage graphqelmRequest =
graphqelmRequest
|> toRequest
|> Http.send (convertResult >> resultToMessage)
toRequest : Request decodesTo -> Http.Request (DataResult decodesTo)
toRequest (Request request) =
(case request.details of
Query forcedRequestMethod querySelectionSet ->
let
queryRequestDetails =
QueryHelper.build
(case forcedRequestMethod of
Just AlwaysGet ->
Just QueryHelper.Get
Just GetIfShortEnough ->
Nothing
Nothing ->
Just QueryHelper.Post
)
request.baseUrl
request.queryParams
querySelectionSet
in
{ method =
case queryRequestDetails.method of
QueryHelper.Get ->
"GET"
QueryHelper.Post ->
"Post"
, headers = request.headers
, url = queryRequestDetails.url
, body = queryRequestDetails.body
, expect = Http.expectJson (decoderOrError request.expect)
, timeout = request.timeout
, withCredentials = request.withCredentials
}
Mutation mutationSelectionSet ->
{ method = "POST"
, headers = request.headers
, url = request.baseUrl |> QueryParams.urlWithQueryParams request.queryParams
, body =
Http.jsonBody
(Json.Encode.object
[ ( "query"
, Json.Encode.string (Document.serializeMutation mutationSelectionSet)
)
]
)
, expect = Http.expectJson (decoderOrError request.expect)
, timeout = request.timeout
, withCredentials = request.withCredentials
}
)
|> Http.request
{-| Convert a Request to a Task. See `Graphqelm.Http.send` for an example of
how to build up a Request.
-}
toTask : Request decodesTo -> Task (Error decodesTo) decodesTo
toTask request =
request
|> toRequest
|> Http.toTask
|> Task.mapError HttpError
|> Task.andThen failTaskOnHttpSuccessWithErrors
failTaskOnHttpSuccessWithErrors : DataResult decodesTo -> Task (Error decodesTo) decodesTo
failTaskOnHttpSuccessWithErrors successOrError =
case successOrError of
Ok value ->
Task.succeed value
Err ( possiblyParsedData, graphqlErrorGraphqlErrorList ) ->
Task.fail (GraphqlError possiblyParsedData graphqlErrorGraphqlErrorList)
decoderOrError : Json.Decode.Decoder a -> Json.Decode.Decoder (DataResult a)
decoderOrError decoder =
Json.Decode.oneOf
[ errorDecoder decoder
, decoder |> Json.Decode.map Ok
]
errorDecoder : Json.Decode.Decoder a -> Json.Decode.Decoder (DataResult a)
errorDecoder decoder =
Json.Decode.oneOf
[ decoder |> Json.Decode.map GraphqlError.ParsedData |> Json.Decode.andThen decodeErrorWithData
, Json.Decode.field "data" Json.Decode.value |> Json.Decode.map GraphqlError.UnparsedData |> Json.Decode.andThen decodeErrorWithData
]
decodeErrorWithData : GraphqlError.PossiblyParsedData a -> Json.Decode.Decoder (DataResult a)
decodeErrorWithData data =
GraphqlError.decoder |> Json.Decode.map ((,) data) |> Json.Decode.map Err
{-| Add a header.
makeRequest : Cmd Msg
makeRequest =
query
|> Graphqelm.Http.queryRequest "https://api.github.com/graphql"
|> Graphqelm.Http.withHeader "authorization" "Bearer <my token>"
|> Graphqelm.Http.send (RemoteData.fromResult >> GotResponse)
-}
withHeader : String -> String -> Request decodesTo -> Request decodesTo
withHeader key value (Request request) =
Request { request | headers = Http.header key value :: request.headers }
{-| Add query params. The values will be Uri encoded.
makeRequest : Cmd Msg
makeRequest =
query
|> Graphqelm.Http.queryRequest "https://api.github.com/graphql"
|> Graphqelm.Http.withQueryParams [ ( "version", "1.2.3" ) ]
|> Graphqelm.Http.send (RemoteData.fromResult >> GotResponse)
-}
withQueryParams : List ( String, String ) -> Request decodesTo -> Request decodesTo
withQueryParams additionalQueryParams (Request request) =
Request { request | queryParams = request.queryParams ++ additionalQueryParams }
{-| Add a timeout.
-}
withTimeout : Time -> Request decodesTo -> Request decodesTo
withTimeout timeout (Request request) =
Request { request | timeout = Just timeout }
{-| Set with credentials to true.
-}
withCredentials : Request decodesTo -> Request decodesTo
withCredentials (Request request) =
Request { request | withCredentials = True }