graphql-engine/server/src-lib/Hasura/GraphQL/Execute.hs

131 lines
4.5 KiB
Haskell
Raw Normal View History

module Hasura.GraphQL.Execute
( GQExecPlan(..)
, getExecPlan
, execRemoteGQ
) where
import Control.Exception (try)
import Control.Lens
import qualified Data.ByteString.Lazy as BL
import qualified Data.CaseInsensitive as CI
import qualified Data.HashMap.Strict as Map
import qualified Data.HashSet as Set
import qualified Data.String.Conversions as CS
import qualified Data.Text as T
import qualified Language.GraphQL.Draft.Syntax as G
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Types as N
import qualified Network.Wreq as Wreq
import Hasura.EncJSON
import Hasura.GraphQL.Schema
import Hasura.GraphQL.Transport.HTTP.Protocol
import Hasura.HTTP
import Hasura.Prelude
import Hasura.RQL.DDL.Headers
import Hasura.RQL.Types
import qualified Hasura.GraphQL.Validate as VQ
import qualified Hasura.GraphQL.Validate.Types as VT
data GQExecPlan
= GExPHasura !GCtx !VQ.RootSelSet
| GExPRemote !RemoteSchemaInfo !G.TypedOperationDefinition
getExecPlan
:: (MonadError QErr m)
=> UserInfo
-> SchemaCache
-> GraphQLRequest
-> m GQExecPlan
getExecPlan userInfo sc req = do
(gCtx, _) <- flip runStateT sc $ getGCtx (userRole userInfo) gCtxRoleMap
queryParts <- flip runReaderT gCtx $ VQ.getQueryParts req
let opDef = VQ.qpOpDef queryParts
topLevelNodes = getTopLevelNodes opDef
-- gather TypeLoc of topLevelNodes
typeLocs = gatherTypeLocs gCtx topLevelNodes
-- see if they are all the same
typeLoc <- assertSameLocationNodes typeLocs
case typeLoc of
VT.HasuraType ->
GExPHasura gCtx <$> runReaderT (VQ.validateGQ queryParts) gCtx
VT.RemoteType _ rsi ->
return $ GExPRemote rsi opDef
where
gCtxRoleMap = scGCtxMap sc
execRemoteGQ
:: (MonadIO m, MonadError QErr m)
=> HTTP.Manager
-> UserInfo
-> [N.Header]
-> BL.ByteString
-- ^ the raw request string
-> RemoteSchemaInfo
-> G.TypedOperationDefinition
-> m EncJSON
execRemoteGQ manager userInfo reqHdrs q rsi opDef = do
let opTy = G._todType opDef
when (opTy == G.OperationTypeSubscription) $
throw400 NotSupported "subscription to remote server is not supported"
hdrs <- getHeadersFromConf hdrConf
let confHdrs = map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) hdrs
clientHdrs = bool [] filteredHeaders fwdClientHdrs
options = wreqOptions manager (userInfoToHdrs ++ clientHdrs ++ confHdrs)
res <- liftIO $ try $ Wreq.postWith options (show url) q
resp <- either httpThrow return res
return $ encJFromLBS $ resp ^. Wreq.responseBody
where
RemoteSchemaInfo url hdrConf fwdClientHdrs = rsi
httpThrow :: (MonadError QErr m) => HTTP.HttpException -> m a
httpThrow err = throw500 $ T.pack . show $ err
userInfoToHdrs = map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $
userInfoToList userInfo
filteredHeaders = flip filter reqHdrs $ \(n, _) ->
n `notElem` [ "Content-Length", "Content-MD5", "User-Agent", "Host"
, "Origin", "Referer" , "Accept", "Accept-Encoding"
, "Accept-Language", "Accept-Datetime"
, "Cache-Control", "Connection", "DNT"
]
assertSameLocationNodes
:: (MonadError QErr m) => [VT.TypeLoc] -> m VT.TypeLoc
assertSameLocationNodes typeLocs =
case Set.toList (Set.fromList typeLocs) of
-- this shouldn't happen
[] -> return VT.HasuraType
[loc] -> return loc
_ -> throw400 NotSupported msg
where
msg = "cannot mix top level fields from two different graphql servers"
-- TODO: we should fix this function asap
-- as this will fail when there is a fragment at the top level
getTopLevelNodes :: G.TypedOperationDefinition -> [G.Name]
getTopLevelNodes opDef =
mapMaybe f $ G._todSelectionSet opDef
where
f = \case
G.SelectionField fld -> Just $ G._fName fld
G.SelectionFragmentSpread _ -> Nothing
G.SelectionInlineFragment _ -> Nothing
gatherTypeLocs :: GCtx -> [G.Name] -> [VT.TypeLoc]
gatherTypeLocs gCtx nodes =
catMaybes $ flip map nodes $ \node ->
VT._fiLoc <$> Map.lookup node schemaNodes
where
schemaNodes =
let qr = VT._otiFields $ _gQueryRoot gCtx
mr = VT._otiFields <$> _gMutRoot gCtx
in maybe qr (Map.union qr) mr