tsweb-0.1.2: src/TsWeb/Db.hs
{-|
Description: Beam actions for Spock
This builds on "TsWeb.Types.Db"'s read-only/read-write discrimination by
providing query functions to perform read-only operations, and an 'execute'
function to run updates, inserts, and deletes (and also selects as
appropriate). All of these are performed as 'TsWeb.Types.TsActionCtxT' actions
so as to nicely integrate with Spock. Finally, because I'm not a fan of
exceptions, all of these functions trap Postgres errors and convert them into
sum-type results.
I'm not yet providing shortcuts for the beam-postgres specific functions. I'm
not actually sure that I need to (IIRC they all build Pg actions), but I will
be adding them if necessary.
-}
module TsWeb.Db
( TxOpt(..)
, QueryResult(..)
, ExecResult(..)
, query
, queryMaybe
, queryList
, execute
) where
import qualified TsWeb.Types.Db as Db
import TsWeb.Action (getExtra)
import TsWeb.Db.Beam
import TsWeb.Types (TsActionCtxT)
import TsWeb.Types.Db (ReadOnlyPool, ReadWritePool)
import Control.Exception (catch, try)
import Data.HVect (ListContains)
import Database.Beam.Postgres (Pg, Postgres)
import Database.PostgreSQL.Simple (SqlError(..))
import Database.PostgreSQL.Simple.Errors
( ConstraintViolation(..)
, constraintViolationE
)
-- | Transaction option: provide WithTx to wrap operations in BEGIN/COMMIT, or
-- NoTx to skip that.
data TxOpt
= NoTx
| WithTx
deriving (Eq, Ord, Enum, Bounded, Show)
-- | Result of a select operation. This will either succeed with a QSimply, or
-- fail with a QError (probably then an error in the db connection or table
-- definitions).
data QueryResult a
= QSimply a
| QError SqlError
deriving (Eq, Show)
-- | Run one or many Beam 'Database.Beam.runSelectReturningList' or
-- 'Database.Beam.runSelectReturningOne' operation(s) against a view's
-- ReadOnlyPool.
query ::
ListContains n ReadOnlyPool xs
=> TxOpt
-> RoPg a
-> TsActionCtxT lts xs sessdata (QueryResult a)
query opt (RoPg act) = do
ropool :: ReadOnlyPool <- getExtra
liftIO $ catch (QSimply <$> Db.withConnection ropool io) (pure . QError)
where
io conn =
case opt of
NoTx -> Db.readOnly conn act
WithTx -> Db.withTransaction conn $ Db.readOnly conn act
-- | Run a single Beam 'select', returning a single (Maybe) value
queryMaybe ::
(ListContains n ReadOnlyPool xs, FromBackendRow Postgres a)
=> Sel a
-> TsActionCtxT lts xs sessdata (QueryResult (Maybe a))
queryMaybe = query NoTx . runSelectReturningOne
-- | Run a single Beam 'select', returning a list of values
queryList ::
(ListContains n ReadOnlyPool xs, FromBackendRow Postgres a)
=> Sel a
-> TsActionCtxT lts xs sessdata (QueryResult [a])
queryList = query NoTx . runSelectReturningList
-- | The result of a select, insert, update, or delete operation. This adds a
-- constraint error to the 'QueryResult', making it nicer to filter out
-- conflicts when handling errors.
data ExecResult a
= ESimply a
| EConstraint SqlError
ConstraintViolation
| EError SqlError
deriving (Eq, Show)
-- | Run any arbitrary 'Database.Beam.Pg' monad in the context of a view,
-- returning an 'ExecResult'
execute ::
ListContains n ReadWritePool xs
=> TxOpt
-> Pg a
-> TsActionCtxT lts xs sessdata (ExecResult a)
execute opt act = do
rwpool <- getExtra
liftIO $ handleError rwpool
where
handleError rwpool =
try (Db.withConnection rwpool io) >>= \case
Right a -> return $ ESimply a
Left err ->
case constraintViolationE err of
Nothing -> return $ EError err
Just (s, c) -> return $ EConstraint s c
io conn =
case opt of
NoTx -> Db.readWrite conn act
WithTx -> Db.withTransaction conn $ Db.readWrite conn act