Merge branch 'patch'

This commit is contained in:
Joe Nelson
2014-11-06 23:53:50 -08:00
6 changed files with 122 additions and 26 deletions
+10 -3
View File
@@ -146,14 +146,21 @@ app conn req respond =
cols <- columns ver (cs table) conn cols <- columns ver (cs table) conn
let colNames = S.fromList $ map (cs . colName) cols let colNames = S.fromList $ map (cs . colName) cols
let specifiedCols = S.fromList $ map fst $ getRow row let specifiedCols = S.fromList $ map fst $ getRow row
return $ if colNames == specifiedCols then if colNames == specifiedCols then do
responseLBS status200 [ jsonContentType ] "" _ <- upsert ver table row qq conn
return $ responseLBS status204 [ jsonContentType ] ""
else if S.null colNames then responseLBS status404 [] "" else return $ if S.null colNames then responseLBS status404 [] ""
else responseLBS status400 [] else responseLBS status400 []
"You must specify all columns in PUT request" "You must specify all columns in PUT request"
) )
([table], "PATCH") ->
jsonBodyAction req (\row -> do
_ <- update ver table row qq conn
return $ responseLBS status204 [ jsonContentType ] ""
)
(_, _) -> (_, _) ->
return $ responseLBS status404 [] "" return $ responseLBS status404 [] ""
+17 -9
View File
@@ -2,6 +2,7 @@
module PgQuery ( module PgQuery (
getRows getRows
, insert , insert
, update
, upsert , upsert
, addUser , addUser
, signInRole , signInRole
@@ -187,14 +188,21 @@ signInRole user pass conn = do
checkPass :: BS.ByteString -> BS.ByteString -> Bool checkPass :: BS.ByteString -> BS.ByteString -> Bool
checkPass = validatePassword checkPass = validatePassword
upsert :: Schema -> Text -> SqlRow -> Net.Query -> Connection -> IO (M.Map String SqlValue) upsert :: Schema -> Text -> SqlRow -> Net.Query -> Connection ->
IO (M.Map String SqlValue)
upsert schema table row qq conn = do upsert schema table row qq conn = do
stmt <- prepare conn $ cs sql stmt <- prepare conn $ cs $ upsertClause schema table row qq
_ <- execute stmt $ join $ replicate 2 $ sqlRowValues row _ <- execute stmt $ join $ replicate 2 $ sqlRowValues row
Just m <- fetchRowMap stmt m <- fetchRowMap stmt
return m return $ fromMaybe M.empty m
where sql = upsertClause schema table row qq update :: Schema -> Text -> SqlRow -> Net.Query -> Connection ->
IO (M.Map String SqlValue)
update schema table row qq conn = do
stmt <- prepare conn $ cs $ updateClause schema table row qq
_ <- execute stmt $ sqlRowValues row
m <- fetchRowMap stmt
return $ fromMaybe M.empty m
placeholders :: Text -> SqlRow -> Text placeholders :: Text -> SqlRow -> Text
placeholders symbol = intercalate ", " . map (const symbol) . getRow placeholders symbol = intercalate ", " . map (const symbol) . getRow
@@ -213,16 +221,16 @@ insertClauseViaSelect schema table row =
intercalate ", " (map pgFmtIdent (sqlRowColumns row)) intercalate ", " (map pgFmtIdent (sqlRowColumns row))
<> ") select " <> placeholders "?" row <> ") select " <> placeholders "?" row
updateClause :: Schema -> Text -> SqlRow -> Text updateClause :: Schema -> Text -> SqlRow -> Net.Query -> Text
updateClause schema table row = updateClause schema table row qq =
"update " <> pgFmtIdent schema <> "." <> pgFmtIdent table <> " set (" <> "update " <> pgFmtIdent schema <> "." <> pgFmtIdent table <> " set (" <>
intercalate ", " (map pgFmtIdent (sqlRowColumns row)) intercalate ", " (map pgFmtIdent (sqlRowColumns row))
<> ") = (" <> placeholders "?" row <> ")" <> ") = (" <> placeholders "?" row <> ")"
<> whereClause qq
upsertClause :: Schema -> Text -> SqlRow -> Net.Query -> Text upsertClause :: Schema -> Text -> SqlRow -> Net.Query -> Text
upsertClause schema table row qq = upsertClause schema table row qq =
"with upsert as (" <> updateClause schema table row "with upsert as (" <> updateClause schema table row qq
<> whereClause qq
<> " returning *) " <> insertClauseViaSelect schema table row <> " returning *) " <> insertClauseViaSelect schema table row
<> " where not exists (select * from upsert) returning *" <> " where not exists (select * from upsert) returning *"
+67 -6
View File
@@ -13,8 +13,9 @@ import qualified Data.Aeson as JSON
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Network.HTTP.Types.Header import Network.HTTP.Types.Header
import Network.HTTP.Types import Network.HTTP.Types
import Control.Monad (replicateM_)
import TestTypes(IncPK, incStr, incNullableStr) import TestTypes(IncPK(..), CompoundPK(..))
-- }}} -- }}}
@@ -106,17 +107,39 @@ spec = around appWithFixture $ do
[json| { "k1":12, "k2":42 } |] [json| { "k1":12, "k2":42 } |]
`shouldRespondWith` 400 `shouldRespondWith` 400
context "specifying every column in the table" $ context "specifying every column in the table" $ do
it "succeeds with 201 and link" $ do it "can create a new record" $ do
p <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" [] p <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":3 } |] [json| { "k1":12, "k2":42, "extra":3 } |]
liftIO $ do liftIO $ do
simpleBody p `shouldBe` "" simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` status200 simpleStatus p `shouldBe` status204
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
liftIO $ do
length rows `shouldBe` 1
let record = head rows
compoundK1 record `shouldBe` 12
compoundK2 record `shouldBe` 42
compoundExtra record `shouldBe` Just 3
it "can update an existing record" $ do
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":4 } |]
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":5 } |]
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
liftIO $ do
length rows `shouldBe` 1
let record = head rows
compoundExtra record `shouldBe` Just 5
context "with an auto-incrementing primary key" $ context "with an auto-incrementing primary key" $
it "succeeds with 201 and link" $ it "succeeds with 204" $
request methodPut "/auto_incrementing_pk?id=eq.1" [] request methodPut "/auto_incrementing_pk?id=eq.1" []
[json| { [json| {
"id":1, "id":1,
@@ -126,6 +149,44 @@ spec = around appWithFixture $ do
} |] } |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ResponseMatcher {
matchBody = Nothing, matchBody = Nothing,
matchStatus = 200, matchStatus = 204,
matchHeaders = [] matchHeaders = []
} }
describe "Patching record" $ do
context "to unkonwn uri" $
it "gives a 404" $
request methodPatch "/fake" []
[json| { "real": false } |]
`shouldRespondWith` 404
context "on an empty table" $
it "succeeds with no effect" $
request methodPatch "/simple_pk" []
[json| { "extra":20 } |]
`shouldRespondWith` 204
context "in a nonempty table" $ do
it "can update a single item" $ do
g <- get "/items?id=eq.42"
liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "\\*/0"
request methodPatch "/items?id=eq.1" []
[json| { "id":42 } |]
`shouldRespondWith` 204
g' <- get "/items?id=eq.42"
liftIO $ simpleHeaders g'
`shouldSatisfy` matchHeader "Content-Range" "0-0/1"
it "can update multiple items" $ do
replicateM_ 10 $ post "/auto_incrementing_pk"
[json| { non_nullable_string: "a" } |]
replicateM_ 10 $ post "/auto_incrementing_pk"
[json| { non_nullable_string: "b" } |]
_ <- request methodPatch
"/auto_incrementing_pk?non_nullable_string=eq.a" []
[json| { non_nullable_string: "c" } |]
g <- get "/auto_incrementing_pk?non_nullable_string=eq.c"
liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "0-9/10"
+25 -4
View File
@@ -1,6 +1,8 @@
module TestTypes ( module TestTypes (
IncPK(..), IncPK(..)
fromList , CompoundPK(..)
, incFromList
, compoundFromList
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
@@ -26,9 +28,28 @@ instance JSON.FromJSON IncPK where
r .: "inserted_at" r .: "inserted_at"
parseJSON _ = mzero parseJSON _ = mzero
fromList :: [(String, SqlValue)] -> IncPK incFromList :: [(String, SqlValue)] -> IncPK
fromList row = IncPK incFromList row = IncPK
(fromSql . fromJust $ lookup "id" row) (fromSql . fromJust $ lookup "id" row)
(fromSql . fromJust $ lookup "nullable_string" row) (fromSql . fromJust $ lookup "nullable_string" row)
(fromSql . fromJust $ lookup "non_nullable_string" row) (fromSql . fromJust $ lookup "non_nullable_string" row)
(fromSql . fromJust $ lookup "inserted_at" row) (fromSql . fromJust $ lookup "inserted_at" row)
data CompoundPK = CompoundPK {
compoundK1 :: Int
, compoundK2 :: Int
, compoundExtra :: Maybe Int
}
instance JSON.FromJSON CompoundPK where
parseJSON (JSON.Object r) = CompoundPK <$>
r .: "k1" <*>
r .: "k2" <*>
r .: "extra"
parseJSON _ = mzero
compoundFromList :: [(String, SqlValue)] -> CompoundPK
compoundFromList row = CompoundPK
(fromSql . fromJust $ lookup "k1" row)
(fromSql . fromJust $ lookup "k2" row)
(fromSql . fromJust $ lookup "extra" row)
-1
View File
@@ -15,7 +15,6 @@ import Network.HTTP.Types.Status (ok200)
spec :: Spec spec :: Spec
spec = let spec = let
dbErrApp conn _ res = do dbErrApp conn _ res = do
putStrLn "In fake app"
_ <- insert "1" "items" (SqlRow []) conn _ <- insert "1" "items" (SqlRow []) conn
runRaw conn "select 1/0" runRaw conn "select 1/0"
_ <- insert "1" "items" (SqlRow []) conn _ <- insert "1" "items" (SqlRow []) conn
+3 -3
View File
@@ -10,7 +10,7 @@ import Database.HDBC (IConnection, SqlValue, toSql, prepare,
import PgQuery (LoginAttempt(..), insert, addUser, signInRole, checkPass import PgQuery (LoginAttempt(..), insert, addUser, signInRole, checkPass
, pgFmtIdent, pgFmtLit) , pgFmtIdent, pgFmtLit)
import Types (SqlRow(SqlRow)) import Types (SqlRow(SqlRow))
import TestTypes (fromList, incStr, incNullableStr, incInsert, incId) import TestTypes (incFromList, incStr, incNullableStr, incInsert, incId)
import Data.Map (toList) import Data.Map (toList)
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Monoid ((<>)) import Data.Monoid ((<>))
@@ -34,13 +34,13 @@ spec = around dbWithSchema $ do
it "inserts and responds with a full object description" $ \conn -> do it "inserts and responds with a full object description" $ \conn -> do
r <- insert "1" "auto_incrementing_pk" (SqlRow [ r <- insert "1" "auto_incrementing_pk" (SqlRow [
("non_nullable_string", toSql ("a string"::String))]) conn ("non_nullable_string", toSql ("a string"::String))]) conn
let returnRow = fromList . toList $ r let returnRow = incFromList . toList $ r
incStr returnRow `shouldBe` "a string" incStr returnRow `shouldBe` "a string"
incNullableStr returnRow `shouldBe` Nothing incNullableStr returnRow `shouldBe` Nothing
incInsert returnRow `shouldSatisfy` not . null incInsert returnRow `shouldSatisfy` not . null
incId returnRow `shouldSatisfy` (>= 0) incId returnRow `shouldSatisfy` (>= 0)
tRows <- quickALQuery conn "select * from \"1\".auto_incrementing_pk" [] tRows <- quickALQuery conn "select * from \"1\".auto_incrementing_pk" []
[returnRow] `shouldBe` map fromList tRows [returnRow] `shouldBe` map incFromList tRows
it "throws an exception if the PK is not unique" $ \conn -> do it "throws an exception if the PK is not unique" $ \conn -> do
r <- insert "1" "auto_incrementing_pk" (SqlRow [ r <- insert "1" "auto_incrementing_pk" (SqlRow [