{-# LANGUAGE DataKinds #-}
module Server where
import Protolude (
Applicative (pure),
Eq ((==)),
IO,
Int,
Maybe (Just, Nothing),
Proxy (Proxy),
Semigroup ((<>)),
Text,
const,
putText,
show,
($),
(&),
(||),
)
import Protolude qualified as P
import Data.Aeson (Object)
import Data.Text qualified as T
import Network.Wai (Application, Middleware)
import Network.Wai.Application.Static (defaultWebAppSettings)
import Network.Wai.Handler.Warp (
defaultSettings,
runSettings,
setOnException,
setPort,
)
import Network.Wai.Middleware.Cors (
cors,
corsMethods,
corsRequestHeaders,
simpleCorsResourcePolicy,
simpleMethods,
)
import Network.Wai.Parse (
defaultParseRequestBodyOptions,
setMaxRequestFilesSize,
setMaxRequestNumFiles,
)
import Prettyprinter (Doc)
import Prettyprinter.Render.Terminal (AnsiStyle)
import Servant (
Context (EmptyContext, (:.)),
NoContent,
err303,
serveDirectoryWith,
)
import Servant.API (
Get,
JSON,
PlainText,
Post,
Raw,
ReqBody,
(:<|>) ((:<|>)),
(:>),
)
import Servant.HTML.Blaze (HTML)
import Servant.Multipart (
MultipartOptions (generalOptions),
Tmp,
defaultMultipartOptions,
)
import Servant.Server (Server)
import Servant.Server qualified as Servant
import WaiAppStatic.Types (
LookupResult (LRFile, LRFolder, LRNotFound),
StaticSettings (ssLookupFile),
unsafeToPiece,
)
import AirGQL.Config qualified as AirGQL (Config (maxDbSize), defaultConfig)
import AirGQL.ExternalAppContext (
ExternalAppContext (
ExternalAppContext,
baseUrl,
sqlite,
sqliteLib
),
)
import AirGQL.Lib (SQLPost)
import AirGQL.Servant.Database (
apiDatabaseSchemaGetHandler,
apiDatabaseVacuumPostHandler,
)
import AirGQL.Servant.GraphQL (
gqlQueryPostHandler,
playgroundDefaultQueryHandler,
readOnlyGqlPostHandler,
)
import AirGQL.Servant.SqlQuery (sqlQueryPostHandler)
import AirGQL.Types.SchemaConf (SchemaConf (pragmaConf), defaultSchemaConf)
import AirGQL.Types.SqlQueryPostResult (SqlQueryPostResult)
import AirGQL.Types.Types (GQLPost)
import Config as TaskLite (Config)
import Lib (getDbPath)
{- FOURMOLU_DISABLE -}
-- ATTENTION: Order of handlers matters!
type PlatformAPI =
-- sqlQueryPostHandler
"sql"
:> ReqBody '[JSON] SQLPost
:> Post '[JSON] SqlQueryPostResult
-- Redirect to GraphiQL playground
-- redirectToPlayground
:<|> "graphql" :> Get '[HTML] NoContent
-- gqlQueryPostHandler
:<|> "graphql"
:> ReqBody '[JSON] GQLPost
:> Post '[JSON] Object
-- readOnlyGqlPostHandler
:<|> "readonly" :> "graphql"
:> ReqBody '[JSON] GQLPost
:> Post '[JSON] Object
-- playgroundDefaultQueryHandler
:<|> "playground" :> "default-query"
:> Get '[PlainText] Text
-- apiDatabaseSchemaGetHandler
:<|> "schema"
:> Get '[PlainText] Text
-- apiDatabaseVacuumPostHandler
:<|> "vacuum"
:> Post '[JSON] Object
{- FOURMOLU_ENABLE -}
platformAPI :: Proxy (PlatformAPI :<|> Raw)
platformAPI = Proxy
redirectToPlayground :: Servant.Handler a
redirectToPlayground =
P.throwError
err303
{ Servant.errHeaders =
[("Location", P.encodeUtf8 "/graphiql")]
}
platformServer :: ExternalAppContext -> Text -> Server PlatformAPI
platformServer ctx dbPath = do
sqlQueryPostHandler defaultSchemaConf.pragmaConf dbPath
:<|> redirectToPlayground
:<|> gqlQueryPostHandler defaultSchemaConf dbPath
:<|> readOnlyGqlPostHandler dbPath
:<|> playgroundDefaultQueryHandler dbPath
:<|> apiDatabaseSchemaGetHandler ctx dbPath
:<|> apiDatabaseVacuumPostHandler dbPath
platformApp :: ExternalAppContext -> Text -> Application
platformApp ctx dbPath = do
let
maxFileSizeInByte :: Int = AirGQL.defaultConfig.maxDbSize
multipartOpts :: MultipartOptions Tmp
multipartOpts =
(defaultMultipartOptions (Proxy :: Proxy Tmp))
{ generalOptions =
setMaxRequestNumFiles 1 $
setMaxRequestFilesSize
(P.fromIntegral maxFileSizeInByte)
defaultParseRequestBodyOptions
}
context :: Context '[MultipartOptions Tmp]
context =
multipartOpts :. EmptyContext
webappServerSettings :: Text -> StaticSettings
webappServerSettings root =
let
webAppSettings = defaultWebAppSettings $ T.unpack root
lookup pieces = do
lookupResult <- ssLookupFile webAppSettings pieces
case lookupResult of
LRFile file -> pure $ LRFile file
LRFolder folder -> pure $ LRFolder folder
LRNotFound ->
ssLookupFile
webAppSettings
[unsafeToPiece "index.html"]
in
webAppSettings{ssLookupFile = lookup}
Servant.serveWithContext platformAPI context $
platformServer ctx dbPath
:<|> serveDirectoryWith (webappServerSettings "tasklite-app/build")
corsMiddleware :: Middleware
corsMiddleware =
let
policy =
simpleCorsResourcePolicy
{ corsRequestHeaders = ["Content-Type", "Authorization"]
, corsMethods = "PUT" : simpleMethods
}
in
cors (const $ Just policy)
-- | Uses AirGQL to provide a GraphQL endpoint at /graphql
startServer :: AirGQL.Config -> TaskLite.Config -> IO (Doc AnsiStyle)
startServer _airgqlConf taskliteConf = do
let
port :: Int = 7458
runWarp =
runSettings $
defaultSettings
& setPort port
& setOnException
( \_ exception -> do
let exceptionText :: Text = show exception
if (exceptionText == "Thread killed by timeout manager")
|| ( exceptionText
== "Warp: Client closed connection prematurely"
)
then pure ()
else do
putText exceptionText
)
ctx =
ExternalAppContext
{ sqlite = ""
, sqliteLib = Nothing
, baseUrl = ""
}
putText $
"\n\n"
<> "Starting GraphQL server at http://localhost:"
<> show port
<> "/graphql"
dbPath <- P.liftIO $ getDbPath taskliteConf
runWarp $ corsMiddleware $ platformApp ctx (T.pack dbPath)
pure P.mempty