WIP: put requests

This commit is contained in:
Joe Nelson
2014-09-07 22:03:45 -07:00
parent a4227b94d8
commit 22114532e6
4 changed files with 125 additions and 14 deletions
+23
View File
@@ -16,6 +16,7 @@ import Text.Read (readMaybe)
import Text.Regex.TDFA ((=~))
import Data.Map (intersection, fromList, toList)
import Data.List (sort)
import qualified Data.Set as S
import Data.Convertible.Base (convert)
import Network.HTTP.Types.Status
@@ -69,9 +70,11 @@ app conn req respond = do
case (path, verb) of
([], _) ->
responseLBS status200 [jsonContentType] <$> printTables ver conn
([table], "OPTIONS") ->
responseLBS status200 [jsonContentType] <$>
printColumns ver (unpack table) conn
([table], "GET") ->
if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error"
@@ -85,6 +88,7 @@ app conn req respond = do
("Content-Location",
"/" <> encodeUtf8 table <> "?" <> BS.pack canonical
)] r
([table], "POST") ->
jsonBodyAction req (\row -> do
allvals <- insert ver table row conn
@@ -98,6 +102,25 @@ app conn req respond = do
, (hLocation, "/" <> encodeUtf8 table <> "?" <> BS.pack params)
] ""
)
([table], "PUT") ->
jsonBodyAction req (\row -> do
keys <- primaryKeyColumns ver (unpack table) conn
let specifiedKeys = map (BS.unpack . fst) qq
if S.fromList keys /= S.fromList specifiedKeys
then return $ responseLBS status405 []
"You must speficy all and only primary keys as params"
else do
_ <- upsert ver table row qq conn
return $ responseLBS status201 [] "hi"
-- allvals <- insert ver table row conn
-- let keyvals = allvals `intersection` fromList (zip keys $ repeat SqlNull)
-- let params = urlEncodeVars $ map (\t -> (fst t, "eq." <> convert (snd t) :: String)) $ toList keyvals
-- [ jsonContentType
-- , (hLocation, "/" <> encodeUtf8 table <> "?" <> BS.pack params)
-- ] ""
)
(_, _) ->
return $ responseLBS status404 [] ""
+54 -12
View File
@@ -20,7 +20,7 @@ import Database.HDBC.PostgreSQL
import qualified Network.HTTP.Types.URI as Net
import Types (SqlRow, getRow)
import Types (SqlRow, getRow, sqlRowColumns, sqlRowValues)
-- }}}
@@ -105,16 +105,58 @@ jsonArrayRows q =
insert :: Int -> Text -> SqlRow -> Connection -> IO (M.Map String SqlValue)
insert schema table row conn = do
query <- populateSql conn ("insert into %I.%I ("++colIds++")",
map toSql $ (pack . show $ schema):table:cols)
stmt <- prepare conn (query ++ " values ("++phs++") returning *")
_ <- execute stmt values
sql <- populateSql conn $ insertClause schema table row
stmt <- prepare conn sql
_ <- execute stmt $ sqlRowValues row
Just m <- fetchRowMap stmt
return m
where
(cols, values) = unzip . getRow $ row
colIds = intercalate ", " $ map (const "%I") cols
phs = intercalate ", " $ map (const "?") values
upsert :: Int -> Text -> SqlRow -> Net.Query -> Connection -> IO (M.Map String SqlValue)
upsert schema table row qq conn = do
sql <- populateSql conn $ upsertClause schema table row qq
stmt <- prepare conn sql
_ <- execute stmt $ sqlRowValues row
Just m <- fetchRowMap stmt
return m
placeholders :: String -> SqlRow -> String
placeholders symbol = intercalate ", " . map (const symbol) . getRow
insertClause :: Int -> Text -> SqlRow -> QuotedSql
insertClause schema table row =
("insert into %I.%I (" ++ placeholders "%I" row ++ ")",
map toSql $ (pack . show $ schema) : table : sqlRowColumns row)
<> (" values (" ++ placeholders "?" row ++ ") returning *", sqlRowValues row)
updateClause :: Int -> Text -> SqlRow -> QuotedSql
updateClause schema table row =
("update %I.%I set (" ++ placeholders "%I" row ++ ")",
map toSql $ (pack . show $ schema) : table : sqlRowColumns row)
<> (" = (" ++ placeholders "?" row ++ ")", sqlRowValues row)
upsertClause :: Int -> Text -> SqlRow -> Net.Query -> QuotedSql
upsertClause schema table row qq =
("with upsert as ", []) <> updateClause schema table row
<> whereClause qq
<> (" returning *) ", []) <> insertClause schema table row
<> (" where not exists (select * from upsert)", [])
-- WITH upsert AS ($update RETURNING *) $insert WHERE NOT EXISTS (SELECT * FROM upsert);
-- $insert = "INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1";
-- $update = "UPDATE spider_count SET tally=tally+1 WHERE date='today' AND spider='Googlebot'";
-- UPDATE weather SET (temp_lo, temp_hi, prcp) = (temp_lo+1, temp_lo+15, DEFAULT)
-- WHERE city = 'San Francisco' AND date = '2003-07-03';
-- upsert :: Int -> Text -> SqlRow -> Connection -> IO (M.Map String SqlValue)
-- upsert schema table row conn = do
-- query <- populateSql conn ("update %I.%I ("++colIds++")",
-- map toSql $ (pack . show $ schema):table:cols)
-- where
-- (cols, values) = unzip . getRow $ row
-- colIds = intercalate ", " $ map (const "%I") cols
-- phs = intercalate ", " $ map (const "?") values
populateSql :: Connection -> QuotedSql -> IO String
populateSql conn sql = do
@@ -122,7 +164,7 @@ populateSql conn sql = do
return $ fromSql escaped
where
q = concat [ "select format('", fst sql, "', ", placeholders (snd sql), ")" ]
q = concat [ "select format('", fst sql, "', ", ph (snd sql), ")" ]
placeholders :: [a] -> String
placeholders = intercalate ", " . map (const "?::varchar")
ph :: [a] -> String
ph = intercalate ", " . map (const "?::varchar")
+7 -1
View File
@@ -1,6 +1,6 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Types(SqlRow(SqlRow), getRow) where
module Types where
import Database.HDBC (toSql, iToSql, SqlValue(..))
@@ -44,6 +44,12 @@ instance JSON.ToJSON SqlValue where
newtype SqlRow = SqlRow {getRow :: [(Text, SqlValue)] } deriving (Show)
sqlRowColumns :: SqlRow -> [Text]
sqlRowColumns = map fst . getRow
sqlRowValues :: SqlRow -> [SqlValue]
sqlRowValues = map snd . getRow
instance JSON.FromJSON SqlRow where
parseJSON (JSON.Object m) = foldlWithKey' add (return $ SqlRow []) m
where
+41 -1
View File
@@ -20,7 +20,7 @@ import TestTypes(IncPK, incStr, incNullableStr)
-- }}}
spec :: Spec
spec = around appWithFixture $
spec = around appWithFixture $ do
describe "Posting new record" $ do
it "accepts disparate json types" $
post "/menagerie"
@@ -66,3 +66,43 @@ spec = around appWithFixture $
matchStatus = 201,
matchHeaders = [("Location", "/compound_pk?k1=eq.12&k2=eq.42")]
}
describe "Putting record" $ do
context "to unkonwn uri" $
it "gives a 404" $
request methodPut "/fake" []
[json| { "real": false } |]
`shouldRespondWith` 404
context "to a known uri" $ do
context "without a fully-specified primary key" $
it "is not an allowed operation" $
request methodPut "/compound_pk?k1=eq.12" []
[json| { "k1":12, "k2":42 } |]
`shouldRespondWith` 405
context "with a fully-specified primary key" $ do
context "with Content-Range header" $
it "fails as per RFC7231" $
request methodPut "/compound_pk?k1=eq.1&k2=eq.2"
[("Content-Range", "0-0")]
[json| { "k1":1, "k2":2, "extra":3 } |]
`shouldRespondWith` 400
context "not specifying every column in the table" $
it "is rejected for lack of idempotence" $
request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42 } |]
`shouldRespondWith` 400
context "specifying every column in the table" $
it "succeeds with 201 and link" $ do
p <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":3 } |]
liftIO $ do
simpleStatus p `shouldBe` created201
simpleHeaders p `shouldSatisfy` matchHeader
hLocation "/compound_pk\\?k1=eq\\.12&k2=eq\\.42"
simpleBody p `shouldBe` ""