Use pooling and add a general Reader record to the stack by default
This commit is contained in:
parent
303c923552
commit
d8f5110b02
@ -42,6 +42,7 @@ executable Main
|
|||||||
, lucid
|
, lucid
|
||||||
, mtl
|
, mtl
|
||||||
, postgresql-simple
|
, postgresql-simple
|
||||||
|
, resource-pool
|
||||||
, servant-server
|
, servant-server
|
||||||
, text
|
, text
|
||||||
, utf8-string
|
, utf8-string
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# servant-effectful-template
|
# servant-effectful-template
|
||||||
|
|
||||||
A nix starting template for web projects utilizing
|
A work in progress Nix starting template for web projects utilizing
|
||||||
|
|
||||||
- [Haskell](https://wiki.haskell.org/Haskell)
|
- [Haskell](https://wiki.haskell.org/Haskell)
|
||||||
- [Effectful](https://github.com/haskell-effectful/effectful)
|
- [Effectful](https://github.com/haskell-effectful/effectful)
|
||||||
|
17
src/Core.hs
17
src/Core.hs
@ -5,7 +5,8 @@ import Control.Monad.Catch (catch)
|
|||||||
import Data.Aeson (FromJSON, ToJSON)
|
import Data.Aeson (FromJSON, ToJSON)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.ByteString.Lazy.UTF8 (fromString)
|
import Data.ByteString.Lazy.UTF8 (fromString)
|
||||||
import Database.PostgreSQL.Simple (Query)
|
import Data.Pool (Pool)
|
||||||
|
import Database.PostgreSQL.Simple (Connection, Query)
|
||||||
import Database.PostgreSQL.Simple.FromRow (FromRow, field, fromRow)
|
import Database.PostgreSQL.Simple.FromRow (FromRow, field, fromRow)
|
||||||
import Database.PostgreSQL.Simple.ToField (ToField)
|
import Database.PostgreSQL.Simple.ToField (ToField)
|
||||||
import Database.PostgreSQL.Simple.ToRow (ToRow, toRow)
|
import Database.PostgreSQL.Simple.ToRow (ToRow, toRow)
|
||||||
@ -13,6 +14,7 @@ import Effectful
|
|||||||
import Effectful.Dispatch.Dynamic
|
import Effectful.Dispatch.Dynamic
|
||||||
import Effectful.Error.Static (Error, throwError)
|
import Effectful.Error.Static (Error, throwError)
|
||||||
import Effectful.FileSystem
|
import Effectful.FileSystem
|
||||||
|
import Effectful.Reader.Static
|
||||||
import GHC.Generics (Generic)
|
import GHC.Generics (Generic)
|
||||||
import Servant hiding ((:>), throwError)
|
import Servant hiding ((:>), throwError)
|
||||||
import qualified Servant as S
|
import qualified Servant as S
|
||||||
@ -47,7 +49,9 @@ instance FromJSON User
|
|||||||
--
|
--
|
||||||
-- Effects
|
-- Effects
|
||||||
--
|
--
|
||||||
type AppEff = Eff '[Database, FileSystem, Logger, Error ServerError, IOE]
|
type AppEff = Eff '[Database, Reader AppEnv, FileSystem, Logger, Error ServerError, IOE]
|
||||||
|
|
||||||
|
data AppEnv = AppEnv { pool :: Pool Connection }
|
||||||
|
|
||||||
-- Database
|
-- Database
|
||||||
data Database :: Effect where
|
data Database :: Effect where
|
||||||
@ -56,7 +60,7 @@ data Database :: Effect where
|
|||||||
|
|
||||||
type instance DispatchOf Database = 'Dynamic
|
type instance DispatchOf Database = 'Dynamic
|
||||||
|
|
||||||
type DatabaseEffects es = (IOE :> es, Logger :> es, Error ServerError :> es)
|
type DatabaseEffects es = (Reader AppEnv :> es, Logger :> es, Error ServerError :> es, IOE :> es)
|
||||||
|
|
||||||
databaseRead :: (Database :> es, Error ServerError :> es) => (Query, Int) -> Eff es (Maybe User)
|
databaseRead :: (Database :> es, Error ServerError :> es) => (Query, Int) -> Eff es (Maybe User)
|
||||||
databaseRead = send . DatabaseRead
|
databaseRead = send . DatabaseRead
|
||||||
@ -78,11 +82,12 @@ runLoggerIO = interpret $ \_ -> \case
|
|||||||
WriteLog msg -> log msg
|
WriteLog msg -> log msg
|
||||||
where
|
where
|
||||||
log :: (IOE :> es, Error ServerError :> es) => String -> Eff es ()
|
log :: (IOE :> es, Error ServerError :> es) => String -> Eff es ()
|
||||||
log msg = adapt $ putStrLn msg
|
log msg = liftIOE $ putStrLn msg
|
||||||
|
|
||||||
adapt :: (IOE :> es, Error ServerError :> es) => IO a -> Eff es a
|
-- Utility
|
||||||
|
liftIOE :: (IOE :> es, Error ServerError :> es) => IO a -> Eff es a
|
||||||
-- Lift IO into Eff and catch IOExceptions
|
-- Lift IO into Eff and catch IOExceptions
|
||||||
adapt m = liftIO m `catch` \(e::IOException) -> do
|
liftIOE m = liftIO m `catch` \(e::IOException) -> do
|
||||||
-- Log IOExceptions to stdout
|
-- Log IOExceptions to stdout
|
||||||
liftIO $ putStrLn $ "ERROR Exception: " ++ show e
|
liftIO $ putStrLn $ "ERROR Exception: " ++ show e
|
||||||
-- Throw a custom Servant ServerError
|
-- Throw a custom Servant ServerError
|
||||||
|
@ -5,10 +5,13 @@ import Core
|
|||||||
import Control.Exception (IOException)
|
import Control.Exception (IOException)
|
||||||
import Data.Aeson (ToJSON)
|
import Data.Aeson (ToJSON)
|
||||||
import Data.Maybe (listToMaybe)
|
import Data.Maybe (listToMaybe)
|
||||||
|
import Data.Pool
|
||||||
import Database.PostgreSQL.Simple
|
import Database.PostgreSQL.Simple
|
||||||
import Effectful
|
import Effectful
|
||||||
import Effectful.Dispatch.Dynamic
|
import Effectful.Dispatch.Dynamic
|
||||||
import Effectful.Error.Static (Error)
|
import Effectful.Error.Static (Error)
|
||||||
|
import Effectful.Reader.Static
|
||||||
|
import Effectful.State.Static.Local (State, get, put, evalState)
|
||||||
import Servant hiding ((:>), throwError)
|
import Servant hiding ((:>), throwError)
|
||||||
|
|
||||||
runDatabaseDebug :: DatabaseEffects es => Eff (Database : es) a -> Eff es a
|
runDatabaseDebug :: DatabaseEffects es => Eff (Database : es) a -> Eff es a
|
||||||
@ -19,9 +22,8 @@ runDatabaseDebug = interpret $ \_ -> \case
|
|||||||
read _ values =
|
read _ values =
|
||||||
writeLog "Mocked a READ database operation" >>= \_ -> pure $
|
writeLog "Mocked a READ database operation" >>= \_ -> pure $
|
||||||
Just (User values "Mock User")
|
Just (User values "Mock User")
|
||||||
write _ values =
|
write _ values = writeLog $
|
||||||
writeLog $ "Mocked a WRITE database operation with a user named "
|
"Mocked a WRITE database operation with a user named " ++ values
|
||||||
++ values
|
|
||||||
|
|
||||||
runDatabaseIO :: DatabaseEffects es => Eff (Database : es) a -> Eff es a
|
runDatabaseIO :: DatabaseEffects es => Eff (Database : es) a -> Eff es a
|
||||||
runDatabaseIO = interpret $ \_ -> \case
|
runDatabaseIO = interpret $ \_ -> \case
|
||||||
@ -30,19 +32,28 @@ runDatabaseIO = interpret $ \_ -> \case
|
|||||||
where
|
where
|
||||||
read :: DatabaseEffects es => Query -> Int -> Eff es (Maybe User)
|
read :: DatabaseEffects es => Query -> Int -> Eff es (Maybe User)
|
||||||
read statement values = do
|
read statement values = do
|
||||||
conn <- adapt $ openConn
|
AppEnv { pool } <- ask
|
||||||
users <- adapt $ query conn statement (Only values)
|
liftIOE $ withResource pool $ \conn -> do
|
||||||
pure $ listToMaybe users
|
r <- query conn statement (Only values)
|
||||||
|
pure $ listToMaybe r
|
||||||
|
|
||||||
write :: DatabaseEffects es => Query -> String -> Eff es ()
|
write :: DatabaseEffects es => Query -> String -> Eff es ()
|
||||||
write statement values = do
|
write statement values = do
|
||||||
conn <- adapt openConn
|
AppEnv { pool } <- ask
|
||||||
adapt $ execute conn statement (Only values)
|
liftIOE $ withResource pool $ \conn -> do
|
||||||
|
execute conn statement (Only values)
|
||||||
writeLog $ "Wrote user to database using statement:\n" ++ show statement
|
writeLog $ "Wrote user to database using statement:\n" ++ show statement
|
||||||
|
|
||||||
openConn :: IO Connection
|
openConn :: IO Connection
|
||||||
openConn = connectPostgreSQL "host=localhost port=5432 dbname=demo connect_timeout=10"
|
openConn = connectPostgreSQL "host=localhost port=5432 dbname=demo connect_timeout=10"
|
||||||
|
|
||||||
|
createConnectionPool :: IO (Pool Connection)
|
||||||
|
createConnectionPool = newPool $ defaultPoolConfig
|
||||||
|
(connectPostgreSQL "host=localhost dbname=demo")
|
||||||
|
close
|
||||||
|
60
|
||||||
|
10
|
||||||
|
|
||||||
queryUser :: Int -> (Query, Int)
|
queryUser :: Int -> (Query, Int)
|
||||||
queryUser userId = ("SELECT id, name FROM users WHERE id = ?;", userId)
|
queryUser userId = ("SELECT id, name FROM users WHERE id = ?;", userId)
|
||||||
|
|
||||||
|
16
src/Main.hs
16
src/Main.hs
@ -9,6 +9,7 @@ import Data.List
|
|||||||
import Effectful
|
import Effectful
|
||||||
import Effectful.Error.Static (Error, runError, runErrorNoCallStack, throwError)
|
import Effectful.Error.Static (Error, runError, runErrorNoCallStack, throwError)
|
||||||
import Effectful.FileSystem
|
import Effectful.FileSystem
|
||||||
|
import Effectful.Reader.Static
|
||||||
import Network.Wai.Handler.Warp (run)
|
import Network.Wai.Handler.Warp (run)
|
||||||
import Servant hiding ((:>), throwError)
|
import Servant hiding ((:>), throwError)
|
||||||
import qualified Servant as S
|
import qualified Servant as S
|
||||||
@ -17,22 +18,27 @@ import qualified Servant as S
|
|||||||
-- Application
|
-- Application
|
||||||
--
|
--
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = run port $ serve proxy app
|
main = do
|
||||||
|
pool <- createConnectionPool
|
||||||
|
let env = AppEnv { pool = pool }
|
||||||
|
run port $ serve proxy $ app env
|
||||||
|
|
||||||
app :: Server AppAPI
|
app :: AppEnv -> Server AppAPI
|
||||||
app = α $ rootHandler
|
app env = transformEff env
|
||||||
|
$ rootHandler
|
||||||
:<|> userListHandler
|
:<|> userListHandler
|
||||||
:<|> userGetHandler
|
:<|> userGetHandler
|
||||||
:<|> userPostHandler
|
:<|> userPostHandler
|
||||||
|
|
||||||
α :: ServerT AppAPI AppEff -> ServerT AppAPI Handler
|
transformEff :: AppEnv -> ServerT AppAPI AppEff -> ServerT AppAPI Handler
|
||||||
α = hoistServer proxy
|
transformEff env = hoistServer proxy
|
||||||
$ Handler
|
$ Handler
|
||||||
. ExceptT
|
. ExceptT
|
||||||
. runEff
|
. runEff
|
||||||
. runErrorNoCallStack
|
. runErrorNoCallStack
|
||||||
. runLoggerIO
|
. runLoggerIO
|
||||||
. runFileSystem
|
. runFileSystem
|
||||||
|
. runReader env
|
||||||
. runDatabaseIO
|
. runDatabaseIO
|
||||||
|
|
||||||
port :: Int
|
port :: Int
|
||||||
|
Loading…
x
Reference in New Issue
Block a user