mu-graphql-0.3.0.0: src/Mu/GraphQL/Server.hs
{-# language DataKinds #-}
{-# language FlexibleContexts #-}
{-# language GADTs #-}
{-# language OverloadedLists #-}
{-# language OverloadedStrings #-}
{-# language RankNTypes #-}
{-# language ScopedTypeVariables #-}
{-# language TypeApplications #-}
{-|
Description : Execute a Mu 'Server' using GraphQL
This module allows you to server a Mu 'Server'
as a WAI 'Application' using GraphQL.
The simples way is to use 'runGraphQLAppQuery'
(if you only provide GraphQL queries) or
'runGraphQLApp' (if you also have mutations
or subscriptions). All other variants provide
more control over the settings.
-}
module Mu.GraphQL.Server (
GraphQLApp
-- * Run an GraphQL resolver directly
, runGraphQLApp
, runGraphQLAppSettings
, runGraphQLAppQuery
, runGraphQLAppTrans
-- * Build a WAI 'Application'
, graphQLApp
, graphQLAppQuery
, graphQLAppTrans
, graphQLAppTransQuery
) where
import Control.Applicative ((<|>))
import Control.Monad (join)
import qualified Data.Aeson as A
import Data.Aeson.Text (encodeToLazyText)
import Data.ByteString.Lazy (fromStrict, toStrict)
import qualified Data.HashMap.Strict as HM
import Data.Proxy
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import qualified Data.Text.Lazy.Encoding as T
import Language.GraphQL.Draft.Parser (parseExecutableDoc)
import Network.HTTP.Types.Header (hContentType)
import Network.HTTP.Types.Method (StdMethod (..), parseMethod)
import Network.HTTP.Types.Status (ok200)
import Network.Wai
import Network.Wai.Handler.Warp (Port, Settings, run, runSettings)
import qualified Network.Wai.Handler.WebSockets as WS
import qualified Network.WebSockets as WS
import Mu.GraphQL.Query.Parse
import Mu.GraphQL.Query.Run
import Mu.GraphQL.Subscription.Protocol
import Mu.Server
data GraphQLInput = GraphQLInput T.Text VariableMapC (Maybe T.Text)
instance A.FromJSON GraphQLInput where
parseJSON = A.withObject "GraphQLInput" $
\v -> GraphQLInput
<$> v A..: "query"
<*> (v A..: "variables" <|> pure HM.empty)
<*> v A..:? "operationName"
-- | Turn a Mu GraphQL 'Server' into a WAI 'Application'.
-- Use this version when your server has not only
-- queries, but also mutations or subscriptions.
graphQLApp ::
( GraphQLApp p qr mut sub ServerErrorIO chn hs )
=> ServerT chn p ServerErrorIO hs
-> Proxy qr
-> Proxy mut
-> Proxy sub
-> Application
graphQLApp = graphQLAppTrans id
-- | Turn a Mu GraphQL 'Server' into a WAI 'Application'.
-- Use this version when your server has only queries.
graphQLAppQuery ::
forall qr p chn hs.
( GraphQLApp p ('Just qr) 'Nothing 'Nothing ServerErrorIO chn hs )
=> ServerT chn p ServerErrorIO hs
-> Proxy qr
-> Application
graphQLAppQuery svr _
= graphQLApp svr (Proxy @('Just qr)) (Proxy @'Nothing) (Proxy @'Nothing)
-- | Turn a Mu GraphQL 'Server' into a WAI 'Application'
-- using a combined transformer stack.
-- See also documentation for 'graphQLAppQuery'.
graphQLAppTransQuery ::
forall qr m p chn hs.
( GraphQLApp p ('Just qr) 'Nothing 'Nothing m chn hs )
=> (forall a. m a -> ServerErrorIO a)
-> ServerT chn p m hs
-> Proxy qr
-> Application
graphQLAppTransQuery f svr _
= graphQLAppTrans f svr (Proxy @('Just qr)) (Proxy @'Nothing) (Proxy @'Nothing)
-- | Turn a Mu GraphQL 'Server' into a WAI 'Application'
-- using a combined transformer stack.
-- See also documentation for 'graphQLApp'.
graphQLAppTrans ::
( GraphQLApp p qr mut sub m chn hs )
=> (forall a. m a -> ServerErrorIO a)
-> ServerT chn p m hs
-> Proxy qr
-> Proxy mut
-> Proxy sub
-> Application
graphQLAppTrans f server q m s
= WS.websocketsOr WS.defaultConnectionOptions
(wsGraphQLAppTrans f server q m s)
(httpGraphQLAppTrans f server q m s)
httpGraphQLAppTrans ::
( GraphQLApp p qr mut sub m chn hs )
=> (forall a. m a -> ServerErrorIO a)
-> ServerT chn p m hs
-> Proxy qr
-> Proxy mut
-> Proxy sub
-> Application
httpGraphQLAppTrans f server q m s req res =
case parseMethod (requestMethod req) of
Left err -> toError $ decodeUtf8 err
Right GET -> do
let qst = queryString req
opN = decodeUtf8 <$> join (lookup "operationName" qst)
case (fmap decodeUtf8 <$> lookup "query" qst, lookup "variables" qst) of
(Just (Just qry), Just (Just vars)) ->
case A.eitherDecode $ fromStrict vars of
Left err -> toError $ T.pack err
Right vrs -> execQuery opN vrs qry
(Just (Just qry), _) -> execQuery opN HM.empty qry
_ -> toError "Error parsing query"
Right POST -> do
body <- strictRequestBody req
case lookup hContentType $ requestHeaders req of
Just "application/json" ->
case A.eitherDecode body of
Left err -> toError $ T.pack err
Right (GraphQLInput qry vars opName) -> execQuery opName vars qry
Just "application/graphql" ->
execQuery Nothing HM.empty (decodeUtf8 $ toStrict body)
_ -> toError "No `Content-Type` header found!"
_ -> toError "Unsupported method"
where
execQuery :: Maybe T.Text -> VariableMapC -> T.Text -> IO ResponseReceived
execQuery opn vals qry =
case parseExecutableDoc qry of
Left err -> toError err
Right doc -> runPipeline f server q m s opn vals doc >>= toResponse
toError :: T.Text -> IO ResponseReceived
toError err = toResponse $ A.object [ ("errors", A.Array [ A.object [ ("message", A.String err) ] ])]
toResponse :: A.Value -> IO ResponseReceived
toResponse = res . responseBuilder ok200 [] . T.encodeUtf8Builder . encodeToLazyText
wsGraphQLAppTrans
:: ( GraphQLApp p qr mut sub m chn hs )
=> (forall a. m a -> ServerErrorIO a)
-> ServerT chn p m hs
-> Proxy qr
-> Proxy mut
-> Proxy sub
-> WS.ServerApp
wsGraphQLAppTrans f server q m s conn
= do conn' <- WS.acceptRequest conn
flip protocol conn' $ runSubscriptionPipeline f server q m s
-- | Run a Mu 'graphQLApp' using the given 'Settings'.
--
-- Go to 'Network.Wai.Handler.Warp' to declare 'Settings'.
runGraphQLAppSettings ::
( GraphQLApp p qr mut sub ServerErrorIO chn hs )
=> Settings
-> ServerT chn p ServerErrorIO hs
-> Proxy qr
-> Proxy mut
-> Proxy sub
-> IO ()
runGraphQLAppSettings st svr q m s = runSettings st (graphQLApp svr q m s)
-- | Run a Mu 'graphQLApp' on the given port.
runGraphQLApp ::
( GraphQLApp p qr mut sub ServerErrorIO chn hs )
=> Port
-> ServerT chn p ServerErrorIO hs
-> Proxy qr
-> Proxy mut
-> Proxy sub
-> IO ()
runGraphQLApp port svr q m s = run port (graphQLApp svr q m s)
-- | Run a Mu 'graphQLApp' on a transformer stack on the given port.
runGraphQLAppTrans ::
( GraphQLApp p qr mut sub m chn hs )
=> Port
-> (forall a. m a -> ServerErrorIO a)
-> ServerT chn p m hs
-> Proxy qr
-> Proxy mut
-> Proxy sub
-> IO ()
runGraphQLAppTrans port f svr q m s = run port (graphQLAppTrans f svr q m s)
-- | Run a query-only Mu 'graphQLApp' on the given port.
runGraphQLAppQuery ::
( GraphQLApp p ('Just qr) 'Nothing 'Nothing ServerErrorIO chn hs )
=> Port
-> ServerT chn p ServerErrorIO hs
-> Proxy qr
-> IO ()
runGraphQLAppQuery port svr q = run port (graphQLAppQuery svr q)