graphql-engine/server/src-lib/Hasura/Server/App.hs

365 lines
12 KiB
Haskell

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module Hasura.Server.App where
import Control.Arrow ((***))
import Control.Concurrent.MVar
import Data.IORef
import Data.Aeson hiding (json)
import qualified Data.ByteString.Lazy as BL
import qualified Data.HashMap.Strict as M
import qualified Data.Text as T
import Data.Time.Clock (UTCTime,
getCurrentTime)
import Network.Wai (requestHeaders,
strictRequestBody)
import qualified Text.Mustache as M
import qualified Text.Mustache.Compile as M
import Web.Spock.Core
import qualified Network.HTTP.Client as HTTP
import qualified Network.Wai.Middleware.Static as MS
import qualified Database.PG.Query as Q
import qualified Hasura.GraphQL.Explain as GE
import qualified Hasura.GraphQL.Schema as GS
import qualified Hasura.GraphQL.Transport.HTTP as GH
import qualified Hasura.GraphQL.Transport.HTTP.Protocol as GH
import qualified Hasura.GraphQL.Transport.WebSocket as WS
import qualified Hasura.Logging as L
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.WebSockets as WS
import qualified Network.WebSockets as WS
import Hasura.Prelude hiding (get, put)
import Hasura.RQL.DDL.Schema.Table
--import Hasura.RQL.DML.Explain
import Hasura.RQL.DML.QueryTemplate
import Hasura.RQL.Types
import Hasura.Server.Auth (AuthMode (..),
getUserInfo)
import Hasura.Server.Init
import Hasura.Server.Logging
import Hasura.Server.Middleware (corsMiddleware,
mkDefaultCorsPolicy)
import Hasura.Server.Query
import Hasura.Server.Utils
import Hasura.Server.Version
import Hasura.SQL.Types
consoleTmplt :: M.Template
consoleTmplt = $(M.embedSingleTemplate "src-rsr/console.html")
isAccessKeySet :: AuthMode -> T.Text
isAccessKeySet AMNoAuth = "false"
isAccessKeySet _ = "true"
mkConsoleHTML :: AuthMode -> IO T.Text
mkConsoleHTML authMode =
bool (initErrExit errMsg) (return res) (null errs)
where
(errs, res) = M.checkedSubstitute consoleTmplt $
object [ "version" .= consoleVersion
, "isAccessKeySet" .= isAccessKeySet authMode
]
errMsg = "Fatal Error : console template rendering failed"
++ show errs
data ServerCtx
= ServerCtx
{ scIsolation :: Q.TxIsolation
, scPGPool :: Q.PGPool
, scLogger :: L.Logger
, scCacheRef :: IORef (SchemaCache, GS.GCtxMap)
, scCacheLock :: MVar ()
, scAuthMode :: AuthMode
, scManager :: HTTP.Manager
}
data HandlerCtx
= HandlerCtx
{ hcServerCtx :: ServerCtx
, hcReqBody :: BL.ByteString
, hcUser :: UserInfo
}
type Handler = ExceptT QErr (ReaderT HandlerCtx IO)
-- {-# SCC parseBody #-}
parseBody :: (FromJSON a) => Handler a
parseBody = do
reqBody <- hcReqBody <$> ask
case decode' reqBody of
Just jVal -> decodeValue jVal
Nothing -> throw400 InvalidJSON "invalid json"
onlyAdmin :: Handler ()
onlyAdmin = do
uRole <- asks (userRole . hcUser)
when (uRole /= adminRole) $
throw400 AccessDenied "You have to be an admin to access this endpoint"
buildQCtx :: Handler QCtx
buildQCtx = do
scRef <- scCacheRef . hcServerCtx <$> ask
userInfo <- asks hcUser
cache <- liftIO $ readIORef scRef
return $ QCtx userInfo $ fst cache
logResult
:: (MonadIO m)
=> Maybe UserInfo -> Wai.Request -> BL.ByteString -> ServerCtx
-> Either QErr BL.ByteString -> Maybe (UTCTime, UTCTime)
-> m ()
logResult userInfoM req reqBody sc res qTime =
liftIO $ logger $ mkAccessLog userInfoM req (reqBody, res) qTime
where
logger = L.unLogger $ scLogger sc
logError
:: MonadIO m
=> Maybe UserInfo -> Wai.Request
-> BL.ByteString -> ServerCtx -> QErr -> m ()
logError userInfoM req reqBody sc qErr =
logResult userInfoM req reqBody sc (Left qErr) Nothing
mkSpockAction
:: (MonadIO m)
=> (Bool -> QErr -> Value)
-> ServerCtx
-> Handler BL.ByteString
-> ActionT m ()
mkSpockAction qErrEncoder serverCtx handler = do
req <- request
reqBody <- liftIO $ strictRequestBody req
let headers = requestHeaders req
authMode = scAuthMode serverCtx
manager = scManager serverCtx
userInfoE <- liftIO $ runExceptT $ getUserInfo logger manager headers authMode
userInfo <- either (logAndThrow req reqBody False) return userInfoE
let handlerState = HandlerCtx serverCtx reqBody userInfo
t1 <- liftIO getCurrentTime -- for measuring response time purposes
result <- liftIO $ runReaderT (runExceptT handler) handlerState
t2 <- liftIO getCurrentTime -- for measuring response time purposes
-- log result
logResult (Just userInfo) req reqBody serverCtx result $ Just (t1, t2)
either (qErrToResp $ userRole userInfo == adminRole) resToResp result
where
logger = scLogger serverCtx
-- encode error response
qErrToResp includeInternal qErr = do
setStatus $ qeStatus qErr
json $ qErrEncoder includeInternal qErr
logAndThrow req reqBody includeInternal qErr = do
logError Nothing req reqBody serverCtx qErr
qErrToResp includeInternal qErr
resToResp resp = do
uncurry setHeader jsonHeader
lazyBytes resp
withLock :: (MonadIO m, MonadError e m)
=> MVar () -> m a -> m a
withLock lk action = do
acquireLock
res <- action `catchError` onError
releaseLock
return res
where
onError e = releaseLock >> throwError e
acquireLock = liftIO $ takeMVar lk
releaseLock = liftIO $ putMVar lk ()
v1QueryHandler :: RQLQuery -> Handler BL.ByteString
v1QueryHandler query = do
lk <- scCacheLock . hcServerCtx <$> ask
bool (fst <$> dbAction) (withLock lk dbActionReload) $
queryNeedsReload query
where
-- Hit postgres
dbAction = do
userInfo <- asks hcUser
scRef <- scCacheRef . hcServerCtx <$> ask
schemaCache <- liftIO $ readIORef scRef
pool <- scPGPool . hcServerCtx <$> ask
isoL <- scIsolation . hcServerCtx <$> ask
runQuery pool isoL userInfo (fst schemaCache) query
-- Also update the schema cache
dbActionReload = do
(resp, newSc) <- dbAction
newGCtxMap <- GS.mkGCtxMap $ scTables newSc
scRef <- scCacheRef . hcServerCtx <$> ask
liftIO $ writeIORef scRef (newSc, newGCtxMap)
return resp
v1Alpha1GQHandler :: GH.GraphQLRequest -> Handler BL.ByteString
v1Alpha1GQHandler query = do
userInfo <- asks hcUser
scRef <- scCacheRef . hcServerCtx <$> ask
cache <- liftIO $ readIORef scRef
pool <- scPGPool . hcServerCtx <$> ask
isoL <- scIsolation . hcServerCtx <$> ask
GH.runGQ pool isoL userInfo (snd cache) query
gqlExplainHandler :: GE.GQLExplain -> Handler BL.ByteString
gqlExplainHandler query = do
onlyAdmin
scRef <- scCacheRef . hcServerCtx <$> ask
cache <- liftIO $ readIORef scRef
pool <- scPGPool . hcServerCtx <$> ask
isoL <- scIsolation . hcServerCtx <$> ask
GE.explainGQLQuery pool isoL (snd cache) query
newtype QueryParser
= QueryParser { getQueryParser :: QualifiedTable -> Handler RQLQuery }
queryParsers :: M.HashMap T.Text QueryParser
queryParsers =
M.fromList
[ ("select", mkQueryParser RQSelect)
, ("insert", mkQueryParser RQInsert)
, ("update", mkQueryParser RQUpdate)
, ("delete", mkQueryParser RQDelete)
, ("count", mkQueryParser RQCount)
]
where
mkQueryParser f =
QueryParser $ \qt -> do
obj <- parseBody
let val = Object $ M.insert "table" (toJSON qt) obj
q <- decodeValue val
return $ f q
legacyQueryHandler :: TableName -> T.Text -> Handler BL.ByteString
legacyQueryHandler tn queryType =
case M.lookup queryType queryParsers of
Just queryParser -> getQueryParser queryParser qt >>= v1QueryHandler
Nothing -> throw404 "No such resource exists"
where
qt = QualifiedTable publicSchema tn
mkWaiApp
:: Q.TxIsolation
-> Maybe String
-> L.LoggerCtx
-> Q.PGPool
-> HTTP.Manager
-> AuthMode
-> CorsConfig
-> Bool
-> IO (Wai.Application, IORef (SchemaCache, GS.GCtxMap))
mkWaiApp isoLevel mRootDir loggerCtx pool httpManager mode corsCfg enableConsole = do
cacheRef <- do
pgResp <- liftIO $ runExceptT $ Q.runTx pool (Q.Serializable, Nothing) $ do
Q.catchE defaultTxErrorHandler initStateTx
sc <- buildSchemaCache
(,) sc <$> GS.mkGCtxMap (scTables sc)
either initErrExit return pgResp >>= newIORef
cacheLock <- newMVar ()
let serverCtx =
ServerCtx isoLevel pool (L.mkLogger loggerCtx) cacheRef
cacheLock mode httpManager
spockApp <- spockAsApp $ spockT id $
httpApp mRootDir corsCfg serverCtx enableConsole
let runTx tx = runExceptT $ Q.runTx pool (isoLevel, Nothing) tx
wsServerEnv <- WS.createWSServerEnv (scLogger serverCtx) httpManager cacheRef runTx
let wsServerApp = WS.createWSServerApp mode wsServerEnv
return (WS.websocketsOr WS.defaultConnectionOptions wsServerApp spockApp, cacheRef)
httpApp :: Maybe String -> CorsConfig -> ServerCtx -> Bool -> SpockT IO ()
httpApp mRootDir corsCfg serverCtx enableConsole = do
liftIO $ putStrLn "HasuraDB is now waiting for connections"
-- cors middleware
unless (ccDisabled corsCfg) $
middleware $ corsMiddleware (mkDefaultCorsPolicy $ ccDomain corsCfg)
-- API Console and Root Dir
if enableConsole then do
consoleHTML <- lift $ mkConsoleHTML $ scAuthMode serverCtx
serveApiConsole consoleHTML
else maybe (return ()) (middleware . MS.staticPolicy . MS.addBase) mRootDir
get "v1/version" $ do
uncurry setHeader jsonHeader
lazyBytes $ encode $ object [ "version" .= currentVersion ]
get ("v1/template" <//> var) tmpltGetOrDeleteH
post ("v1/template" <//> var) tmpltPutOrPostH
put ("v1/template" <//> var) tmpltPutOrPostH
delete ("v1/template" <//> var) tmpltGetOrDeleteH
post "v1/query" $ mkSpockAction encodeQErr serverCtx $ do
query <- parseBody
v1QueryHandler query
post "v1alpha1/graphql/explain" $ mkSpockAction encodeQErr serverCtx $ do
expQuery <- parseBody
gqlExplainHandler expQuery
post "v1alpha1/graphql" $ mkSpockAction GH.encodeGQErr serverCtx $ do
query <- parseBody
v1Alpha1GQHandler query
-- get "v1alpha1/graphql/schema" $
-- mkSpockAction encodeQErr serverCtx v1Alpha1GQSchemaHandler
post ("api/1/table" <//> var <//> var) $ \tableName queryType ->
mkSpockAction encodeQErr serverCtx $
legacyQueryHandler (TableName tableName) queryType
hookAny GET $ \_ -> do
let qErr = err404 NotFound "resource does not exist"
req <- request
reqBody <- liftIO $ strictRequestBody req
logError Nothing req reqBody serverCtx qErr
uncurry setHeader jsonHeader
lazyBytes $ encode qErr
where
tmpltGetOrDeleteH tmpltName = do
tmpltArgs <- tmpltArgsFromQueryParams
mkSpockAction encodeQErr serverCtx $ mkQTemplateAction tmpltName tmpltArgs
tmpltPutOrPostH tmpltName = do
tmpltArgs <- tmpltArgsFromQueryParams
mkSpockAction encodeQErr serverCtx $ do
bodyTmpltArgs <- parseBody
mkQTemplateAction tmpltName $ M.union bodyTmpltArgs tmpltArgs
tmpltArgsFromQueryParams = do
qparams <- params
return $ M.fromList $ flip map qparams $
TemplateParam *** String
mkQTemplateAction tmpltName tmpltArgs =
v1QueryHandler $ RQExecuteQueryTemplate $
ExecQueryTemplate (TQueryName tmpltName) tmpltArgs
serveApiConsole htmlFile = do
get root $ redirect "/console"
get ("console" <//> wildcard) $ const $ html htmlFile