Enclose entire app response in single Tx

This commit is contained in:
Joe Nelson
2014-12-12 13:55:09 -08:00
parent d763e27bb2
commit 19b62c00ee
7 changed files with 47 additions and 38 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
name: dbapi name: postgrest
version: 0.2.4.6 version: 0.2.4.6
synopsis: The database is your api synopsis: The database is your api
license: MIT license: MIT
@@ -9,7 +9,7 @@ category: Web
build-type: Simple build-type: Simple
cabal-version: >=1.10 cabal-version: >=1.10
executable dbapi executable postgrest
main-is: Main.hs main-is: Main.hs
ghc-options: -Wall -W -Werror -O2 ghc-options: -Wall -W -Werror -O2
default-language: Haskell2010 default-language: Haskell2010
+17 -19
View File
@@ -4,8 +4,6 @@ module App (app, sqlErrHandler, isSqlError) where
import Control.Monad (join) import Control.Monad (join)
import Control.Arrow ((***)) import Control.Arrow ((***))
import Control.Applicative import Control.Applicative
import Control.Monad.IO.Class (liftIO, MonadIO)
-- import Control.Exception.Base
import Data.Text hiding (map) import Data.Text hiding (map)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
@@ -18,6 +16,7 @@ import Data.List (sortBy)
import Data.Functor.Identity import Data.Functor.Identity
import Data.Scientific (isInteger, formatScientific, FPFormat(..)) import Data.Scientific (isInteger, formatScientific, FPFormat(..))
import qualified Data.Set as S import qualified Data.Set as S
import qualified Data.ByteString.Lazy as BL
import Network.HTTP.Types.Status import Network.HTTP.Types.Status
import Network.HTTP.Types.Header import Network.HTTP.Types.Header
@@ -35,20 +34,19 @@ import RangeQuery
import PgStructure import PgStructure
import Auth import Auth
app :: Request -> H.Session H.Postgres s IO Response app :: BL.ByteString -> Request -> H.Tx H.Postgres s Response
app req = app reqBody req =
case (path, verb) of case (path, verb) of
([], _) -> do ([], _) -> do
body <- H.tx Nothing $ encode <$> tables (cs schema) body <- encode <$> tables (cs schema)
return $ responseLBS status200 [jsonH] $ cs body return $ responseLBS status200 [jsonH] $ cs body
([table], "OPTIONS") -> do ([table], "OPTIONS") -> do
let t = QualifiedTable schema (cs table) let t = QualifiedTable schema (cs table)
H.tx Nothing $ do cols <- columns t
cols <- columns t pkey <- map cs <$> primaryKeyColumns t
pkey <- map cs <$> primaryKeyColumns t return $ responseLBS status200 [jsonH, allOrigins]
return $ responseLBS status200 [jsonH, allOrigins] $ encode (TableOptions cols pkey)
$ encode (TableOptions cols pkey)
([table], "GET") -> ([table], "GET") ->
if range == Just emptyRange if range == Just emptyRange
@@ -66,7 +64,7 @@ app req =
. whereT qq . whereT qq
$ selectStar qt $ selectStar qt
) )
row <- H.tx Nothing $ H.single select row <- H.single select
let (tableTotal, queryTotal, body) = let (tableTotal, queryTotal, body) =
fromMaybe (0, 0, Just "" :: Maybe Text) row fromMaybe (0, 0, Just "" :: Maybe Text) row
from = fromMaybe 0 $ rangeOffset <$> range from = fromMaybe 0 $ rangeOffset <$> range
@@ -87,8 +85,7 @@ app req =
] (cs $ fromMaybe "[]" body) ] (cs $ fromMaybe "[]" body)
(["dbapi", "users"], "POST") -> do (["dbapi", "users"], "POST") -> do
body <- liftIO $ strictRequestBody req let user = decode reqBody :: Maybe AuthUser
let user = decode body :: Maybe AuthUser
case user of case user of
Nothing -> return $ responseLBS status400 [jsonH] $ Nothing -> return $ responseLBS status400 [jsonH] $
@@ -102,7 +99,7 @@ app req =
] "" ] ""
([table], "POST") -> ([table], "POST") ->
handleJsonObj req $ \obj -> H.tx Nothing $ do handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table) let qt = QualifiedTable schema (cs table)
query = coerce $ query = coerce $
insertInto qt (map cs $ keys obj) (elems obj) insertInto qt (map cs $ keys obj) (elems obj)
@@ -123,7 +120,7 @@ app req =
] "" ] ""
([table], "PUT") -> ([table], "PUT") ->
handleJsonObj req $ \obj -> H.tx Nothing $ do handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table) let qt = QualifiedTable schema (cs table)
primaryKeys <- primaryKeyColumns qt primaryKeys <- primaryKeyColumns qt
let specifiedKeys = map (cs . fst) qq let specifiedKeys = map (cs . fst) qq
@@ -147,7 +144,7 @@ app req =
"You must specify all columns in PUT request" "You must specify all columns in PUT request"
([table], "PATCH") -> ([table], "PATCH") ->
handleJsonObj req $ \obj -> H.tx Nothing $ do handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table) let qt = QualifiedTable schema (cs table)
H.unit H.unit
$ coerce $ coerce
@@ -209,9 +206,10 @@ requestedSchema hdrs =
jsonH :: Header jsonH :: Header
jsonH = (hContentType, "application/json") jsonH = (hContentType, "application/json")
handleJsonObj :: MonadIO m => Request -> (Object -> m Response) -> m Response handleJsonObj :: BL.ByteString -> (Object -> H.Tx H.Postgres s Response)
handleJsonObj req handler = do -> H.Tx H.Postgres s Response
parse <- liftIO $ fmap eitherDecode . strictRequestBody $ req handleJsonObj reqBody handler = do
let parse = eitherDecode reqBody
case parse of case parse of
Left err -> Left err ->
return $ responseLBS status400 [jsonH] jErr return $ responseLBS status400 [jsonH] jErr
+5 -4
View File
@@ -4,7 +4,6 @@ module Auth where
import Data.Aeson import Data.Aeson
import Control.Monad (mzero) import Control.Monad (mzero)
import Control.Applicative ( (<*>), (<$>) ) import Control.Applicative ( (<*>), (<$>) )
import Control.Monad.IO.Class (liftIO)
import Crypto.BCrypt import Crypto.BCrypt
import Data.Text import Data.Text
import Data.Monoid import Data.Monoid
@@ -13,6 +12,8 @@ import qualified Hasql.Postgres as H
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import PgQuery (pgFmtLit) import PgQuery (pgFmtLit)
import System.IO.Unsafe
data AuthUser = AuthUser { data AuthUser = AuthUser {
userId :: String userId :: String
, userPass :: String , userPass :: String
@@ -50,10 +51,10 @@ setRole role = H.unit ("set role " <> cs (pgFmtLit role), [], True)
resetRole :: H.Tx H.Postgres s () resetRole :: H.Tx H.Postgres s ()
resetRole = H.unit [H.q|reset role|] resetRole = H.unit [H.q|reset role|]
addUser :: Text -> Text -> Text -> H.Session H.Postgres s IO () addUser :: Text -> Text -> Text -> H.Tx H.Postgres s ()
addUser identity pass role = do addUser identity pass role = do
Just hashed <- liftIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass) let Just hashed = unsafePerformIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass)
H.tx Nothing $ H.unit $ H.unit $
[H.q|insert into dbapi.auth (id, pass, rolname) values (?, ?, ?)|] [H.q|insert into dbapi.auth (id, pass, rolname) values (?, ?, ?)|]
identity (cs hashed :: Text) role identity (cs hashed :: Text) role
+6 -2
View File
@@ -9,6 +9,7 @@ import Control.Monad (unless)
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Control.Exception import Control.Exception
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Network.Wai (strictRequestBody)
import Network.Wai.Middleware.Cors (cors) import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Handler.Warp hiding (Connection) import Network.Wai.Handler.Warp hiding (Connection)
import Network.Wai.Middleware.Gzip (gzip, def) import Network.Wai.Middleware.Gzip (gzip, def)
@@ -43,11 +44,14 @@ main = do
(if configSecure conf then redirectInsecure else id) (if configSecure conf then redirectInsecure else id)
. gzip def . cors corsPolicy . clientErrors . gzip def . cors corsPolicy . clientErrors
. staticPolicy (only [("favicon.ico", "static/favicon.ico")]) . staticPolicy (only [("favicon.ico", "static/favicon.ico")])
anonRole = cs $ configAnonRole conf
H.session pgSettings sessSettings $ H.sessionUnlifter >>= \unlift -> H.session pgSettings sessSettings $ H.sessionUnlifter >>= \unlift ->
liftIO $ runSettings appSettings $ middle $ \req respond -> liftIO $ runSettings appSettings $ middle $ \req respond -> do
body <- strictRequestBody req
respond =<< catchJust isSqlError respond =<< catchJust isSqlError
(unlift $ authenticated (cs $ configAnonRole conf) app req) (unlift $ H.tx Nothing
$ authenticated anonRole (app body) req)
sqlErrHandler sqlErrHandler
where where
+10 -8
View File
@@ -1,4 +1,5 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Middleware where module Middleware where
@@ -38,8 +39,8 @@ import Debug.Trace
-- else Database.PostgreSQL.Simple.withSavepoint conn go -- else Database.PostgreSQL.Simple.withSavepoint conn go
-- where go = app conn req respond -- where go = app conn req respond
authenticated :: Text -> (Request -> H.Session H.Postgres s IO Response) -> authenticated :: forall s. Text -> (Request -> H.Tx H.Postgres s Response) ->
Request -> H.Session H.Postgres s IO Response Request -> H.Tx H.Postgres s Response
authenticated anon app req = do authenticated anon app req = do
attempt <- httpRequesterRole (requestHeaders req) attempt <- httpRequesterRole (requestHeaders req)
case attempt of case attempt of
@@ -51,21 +52,22 @@ authenticated anon app req = do
NoCredentials -> runInRole anon NoCredentials -> runInRole anon
where where
httpRequesterRole :: RequestHeaders -> H.Session H.Postgres s IO LoginAttempt httpRequesterRole :: RequestHeaders -> H.Tx H.Postgres s LoginAttempt
httpRequesterRole hdrs = do httpRequesterRole hdrs = do
let auth = fromMaybe "" $ lookup hAuthorization hdrs let auth = fromMaybe "" $ lookup hAuthorization hdrs
case split (==' ') (cs auth) of case split (==' ') (cs auth) of
("Basic" : b64 : _) -> ("Basic" : b64 : _) ->
case split (==':') (cs . decode . cs $ b64) of case split (==':') (cs . decode . cs $ b64) of
(u:p:_) -> H.tx Nothing $ signInRole u p (u:p:_) -> signInRole u p
_ -> return MalformedAuth _ -> return MalformedAuth
_ -> return NoCredentials _ -> return NoCredentials
runInRole :: Text -> H.Tx H.Postgres s Response
runInRole r = do runInRole r = do
H.tx Nothing $ setRole r setRole r
resp <- app req res <- app req
H.tx Nothing resetRole resetRole
return resp return res
-- instance ToJSON SqlError where -- instance ToJSON SqlError where
-- toJSON t = object [ -- toJSON t = object [
+1 -1
View File
@@ -31,7 +31,7 @@ spec = before resetDb $ around withApp $ do
request methodGet "/" [auth] "" request methodGet "/" [auth] ""
`shouldRespondWith` [json| [ `shouldRespondWith` [json| [
{"schema":"1","name":"authors_only","insertable":true} {"schema":"1","name":"authors_only","insertable":true}
] |] ] |]
{matchStatus = 200} {matchStatus = 200}
+6 -2
View File
@@ -44,10 +44,14 @@ pgSettings = H.Postgres "localhost" 5432 "dbapi_test" "" "dbapi_test"
withApp :: ActionWith Application -> IO () withApp :: ActionWith Application -> IO ()
withApp perform = withApp perform =
let anonRole = cs $ configAnonRole cfg in
perform $ middle $ \req resp -> perform $ middle $ \req resp ->
H.session pgSettings testSettings $ H.sessionUnlifter >>= \unlift -> H.session pgSettings testSettings $ H.sessionUnlifter >>= \unlift ->
liftIO $ resp =<< catchJust isSqlError liftIO $ do
(unlift $ authenticated (cs $ configAnonRole cfg) app req) body <- strictRequestBody req
resp =<< catchJust isSqlError
(unlift $ H.tx Nothing
$ authenticated anonRole (app body) req)
sqlErrHandler sqlErrHandler
where middle = cors corsPolicy where middle = cors corsPolicy