Include location header in post response
This commit is contained in:
+6
-1
@@ -14,9 +14,11 @@ library
|
|||||||
, HDBC, HDBC-postgresql
|
, HDBC, HDBC-postgresql
|
||||||
, warp, wai >= 3.0.1 && < 3.0.2
|
, warp, wai >= 3.0.1 && < 3.0.2
|
||||||
, http-types, scientific, time
|
, http-types, scientific, time
|
||||||
|
, HTTP, convertible
|
||||||
, bytestring, aeson, network
|
, bytestring, aeson, network
|
||||||
, text, optparse-applicative
|
, text, optparse-applicative
|
||||||
, unordered-containers
|
, unordered-containers
|
||||||
|
, containers
|
||||||
, regex-base
|
, regex-base
|
||||||
, http-media, regex-tdfa
|
, http-media, regex-tdfa
|
||||||
, Ranged-sets
|
, Ranged-sets
|
||||||
@@ -34,9 +36,11 @@ executable dbapi
|
|||||||
build-depends: base >=4.6 && <5
|
build-depends: base >=4.6 && <5
|
||||||
, HDBC, HDBC-postgresql
|
, HDBC, HDBC-postgresql
|
||||||
, warp, wai >= 3.0.1 && < 3.0.2
|
, warp, wai >= 3.0.1 && < 3.0.2
|
||||||
|
, HTTP, convertible
|
||||||
, http-types, scientific, time
|
, http-types, scientific, time
|
||||||
, bytestring, aeson, network
|
, bytestring, aeson, network
|
||||||
, text, optparse-applicative
|
, text, optparse-applicative
|
||||||
|
, containers
|
||||||
, unordered-containers
|
, unordered-containers
|
||||||
, regex-base
|
, regex-base
|
||||||
, http-media, regex-tdfa
|
, http-media, regex-tdfa
|
||||||
@@ -57,7 +61,8 @@ Test-Suite spec
|
|||||||
, hspec-wai
|
, hspec-wai
|
||||||
, HDBC, HDBC-postgresql
|
, HDBC, HDBC-postgresql
|
||||||
, warp, wai >= 3.0.1 && < 3.0.2
|
, warp, wai >= 3.0.1 && < 3.0.2
|
||||||
, wai-extra
|
, HTTP, convertible
|
||||||
|
, wai-extra, containers
|
||||||
, http-types, scientific, time
|
, http-types, scientific, time
|
||||||
, bytestring, aeson, network
|
, bytestring, aeson, network
|
||||||
, text, optparse-applicative
|
, text, optparse-applicative
|
||||||
|
|||||||
+21
-9
@@ -12,7 +12,10 @@ import Options.Applicative hiding (columns)
|
|||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
import Text.Read (readMaybe)
|
import Text.Read (readMaybe)
|
||||||
import Text.Regex.TDFA ((=~))
|
import Text.Regex.TDFA ((=~))
|
||||||
|
import Data.Map (intersection, fromList, toList)
|
||||||
|
import Data.Convertible.Base (convert)
|
||||||
|
|
||||||
|
import Network.HTTP.Base (urlEncodeVars)
|
||||||
import Network.HTTP.Types.Status
|
import Network.HTTP.Types.Status
|
||||||
import Network.HTTP.Types.Header
|
import Network.HTTP.Types.Header
|
||||||
|
|
||||||
@@ -23,10 +26,12 @@ import qualified Data.ByteString.Char8 as BS
|
|||||||
|
|
||||||
import Database.HDBC.PostgreSQL (Connection)
|
import Database.HDBC.PostgreSQL (Connection)
|
||||||
import Database.HDBC.Types (SqlError, seErrorMsg)
|
import Database.HDBC.Types (SqlError, seErrorMsg)
|
||||||
import PgStructure (printTables, printColumns)
|
import Database.HDBC.SqlValue (SqlValue(..))
|
||||||
|
import PgStructure (printTables, printColumns, primaryKeyColumns)
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import Data.Text (pack, unpack)
|
import Data.Text (pack, unpack)
|
||||||
|
import Data.Text.Encoding (encodeUtf8)
|
||||||
|
|
||||||
import PgQuery
|
import PgQuery
|
||||||
import RangeQuery
|
import RangeQuery
|
||||||
@@ -50,26 +55,33 @@ jsonBodyAction req handler = do
|
|||||||
Right body -> handler body
|
Right body -> handler body
|
||||||
|
|
||||||
jsonBody :: Request -> IO (Either String SqlRow)
|
jsonBody :: Request -> IO (Either String SqlRow)
|
||||||
jsonBody = (fmap JSON.eitherDecode) . strictRequestBody
|
jsonBody = fmap JSON.eitherDecode . strictRequestBody
|
||||||
|
|
||||||
app :: Connection -> Application
|
app :: Connection -> Application
|
||||||
app conn req respond = do
|
app conn req respond = do
|
||||||
r <- try $
|
r <- try $
|
||||||
case (path, verb) of
|
case (path, verb) of
|
||||||
([], _) ->
|
([], _) ->
|
||||||
responseLBS status200 [jsonContentType] <$> (printTables ver conn)
|
responseLBS status200 [jsonContentType] <$> printTables ver conn
|
||||||
([table], "OPTIONS") ->
|
([table], "OPTIONS") ->
|
||||||
responseLBS status200 [jsonContentType] <$> (
|
responseLBS status200 [jsonContentType] <$>
|
||||||
printColumns ver (unpack table) conn)
|
printColumns ver (unpack table) conn
|
||||||
([table], "GET") ->
|
([table], "GET") ->
|
||||||
if range == Just emptyRange
|
if range == Just emptyRange
|
||||||
then return $ responseLBS status416 [] "HTTP Range error"
|
then return $ responseLBS status416 [] "HTTP Range error"
|
||||||
else respondWithRangedResult <$>
|
else respondWithRangedResult <$>
|
||||||
(getRows (show ver) (unpack table) qq range conn)
|
getRows (show ver) (unpack table) qq range conn
|
||||||
([table], "POST") ->
|
([table], "POST") ->
|
||||||
jsonBodyAction req (\row ->
|
jsonBodyAction req (\row -> do
|
||||||
responseLBS status201 [jsonContentType] <$> (
|
allvals <- insert ver table row conn
|
||||||
insert ver table row conn))
|
keys <- primaryKeyColumns ver (unpack table) conn
|
||||||
|
let keyvals = allvals `intersection` fromList (zip keys $ repeat SqlNull)
|
||||||
|
let params = urlEncodeVars $ map (\t -> (fst t, "eq." <> convert (snd t) :: String)) $ toList keyvals
|
||||||
|
return $ responseLBS status201
|
||||||
|
[ jsonContentType
|
||||||
|
, (hLocation, "/" <> encodeUtf8 table <> "?" <> BS.pack params)
|
||||||
|
] ""
|
||||||
|
)
|
||||||
(_, _) ->
|
(_, _) ->
|
||||||
return $ responseLBS status404 [] ""
|
return $ responseLBS status404 [] ""
|
||||||
|
|
||||||
|
|||||||
+8
-10
@@ -9,8 +9,7 @@ import Data.Functor ( (<$>) )
|
|||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.List (intersperse, intercalate)
|
import Data.List (intersperse, intercalate)
|
||||||
import Data.Monoid ((<>), mconcat)
|
import Data.Monoid ((<>), mconcat)
|
||||||
import Data.HashMap.Strict (fromList)
|
import qualified Data.Map as M
|
||||||
import qualified Data.Aeson as JSON
|
|
||||||
|
|
||||||
import qualified RangeQuery as R
|
import qualified RangeQuery as R
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
@@ -66,8 +65,8 @@ wherePred (column, predicate) =
|
|||||||
("%I " <> op <> "%L", map toSql [column, value])
|
("%I " <> op <> "%L", map toSql [column, value])
|
||||||
|
|
||||||
where
|
where
|
||||||
opCode:rest = BS.split ':' $ fromMaybe ":" predicate
|
opCode:rest = BS.split '.' $ fromMaybe "." predicate
|
||||||
value = BS.intercalate ":" rest
|
value = BS.intercalate "." rest
|
||||||
op = case opCode of
|
op = case opCode of
|
||||||
"eq" -> "="
|
"eq" -> "="
|
||||||
"gt" -> ">"
|
"gt" -> ">"
|
||||||
@@ -104,15 +103,14 @@ jsonArrayRows :: QuotedSql -> QuotedSql
|
|||||||
jsonArrayRows q =
|
jsonArrayRows q =
|
||||||
("array_to_json(array_agg(row_to_json(t))) from (", []) <> q <> (") t", [])
|
("array_to_json(array_agg(row_to_json(t))) from (", []) <> q <> (") t", [])
|
||||||
|
|
||||||
insert :: Int -> Text -> SqlRow -> Connection -> IO BL.ByteString
|
insert :: Int -> Text -> SqlRow -> Connection -> IO (M.Map String SqlValue)
|
||||||
insert schema table row conn = do
|
insert schema table row conn = do
|
||||||
query <- populateSql conn ("insert into %I.%I ("++colIds++")", map toSql $ (pack . show $ schema):table:cols)
|
query <- populateSql conn ("insert into %I.%I ("++colIds++")",
|
||||||
|
map toSql $ (pack . show $ schema):table:cols)
|
||||||
stmt <- prepare conn (query ++ " values ("++phs++") returning *")
|
stmt <- prepare conn (query ++ " values ("++phs++") returning *")
|
||||||
_ <- execute stmt values
|
_ <- execute stmt values
|
||||||
keys <- getColumnNames stmt
|
Just m <- fetchRowMap stmt
|
||||||
Just vals <- fetchRow stmt
|
return m
|
||||||
let rowMap = fromList $ zip keys vals
|
|
||||||
return $ JSON.encode rowMap
|
|
||||||
where
|
where
|
||||||
(cols, values) = unzip . getRow $ row
|
(cols, values) = unzip . getRow $ row
|
||||||
colIds = intercalate ", " $ map (const "%I") cols
|
colIds = intercalate ", " $ map (const "%I") cols
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ import Test.Hspec.Wai.JSON
|
|||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import Network.Wai.Test (SResponse(..))
|
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import Data.Aeson ((.:))
|
import Data.Aeson ((.:))
|
||||||
import Data.Maybe (fromJust)
|
|
||||||
import Control.Applicative ((<$>), (<*>))
|
import Control.Applicative ((<$>), (<*>))
|
||||||
import Control.Monad (mzero)
|
import Control.Monad (mzero)
|
||||||
|
|
||||||
@@ -102,12 +100,11 @@ spec = around appWithFixture $ do
|
|||||||
{ "non_nullable_string":"not null"} |]
|
{ "non_nullable_string":"not null"} |]
|
||||||
`shouldRespondWith` 201
|
`shouldRespondWith` 201
|
||||||
|
|
||||||
it "responds with the created row" $ do
|
it "links to the created resource" $ do
|
||||||
r <- post "/auto_incrementing_pk" [json|
|
post "/auto_incrementing_pk" [json|
|
||||||
{ "non_nullable_string":"not null"} |]
|
{ "non_nullable_string":"not null"} |]
|
||||||
let row = fromJust (JSON.decode $ simpleBody r :: Maybe IncPK)
|
`shouldRespondWith` ResponseMatcher {
|
||||||
liftIO $ do
|
matchBody = Nothing,
|
||||||
incStr row `shouldBe` "not null"
|
matchStatus = 201,
|
||||||
incNullableStr row `shouldBe` Nothing
|
matchHeaders = [("Location", "/auto_incrementing_pk?id=eq.2")]
|
||||||
-- Add assertions to check timestamp and id
|
}
|
||||||
-- OR: change behavior to return no body at all
|
|
||||||
|
|||||||
Reference in New Issue
Block a user