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