Fix wrong status 404 when PATCH request didn't change anything (#1272)
This commit is contained in:
committed by
Steve Chávez
parent
87f7e86aa7
commit
553531711b
+28
-20
@@ -162,8 +162,8 @@ app dbStructure proc conf apiRequest =
|
|||||||
, if iPreferRepresentation apiRequest == Full
|
, if iPreferRepresentation apiRequest == Full
|
||||||
then Just $ toHeader contentType
|
then Just $ toHeader contentType
|
||||||
else Nothing
|
else Nothing
|
||||||
, Just . contentRangeH 1 0 $
|
, Just $ contentRangeH 1 0 $
|
||||||
toInteger <$> if shouldCount then Just queryTotal else Nothing
|
if shouldCount then Just queryTotal else Nothing
|
||||||
, if null pkCols
|
, if null pkCols
|
||||||
then Nothing
|
then Nothing
|
||||||
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest
|
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest
|
||||||
@@ -188,21 +188,28 @@ app dbStructure proc conf apiRequest =
|
|||||||
(iPreferRepresentation apiRequest) []
|
(iPreferRepresentation apiRequest) []
|
||||||
row <- H.statement (toS $ pjRaw pJson) stm
|
row <- H.statement (toS $ pjRaw pJson) stm
|
||||||
let (_, queryTotal, _, body) = extractQueryResult row
|
let (_, queryTotal, _, body) = extractQueryResult row
|
||||||
if contentType == CTSingularJSON
|
|
||||||
&& queryTotal /= 1
|
updateIsNoOp = S.null $ pjKeys pJson
|
||||||
&& iPreferRepresentation apiRequest == Full
|
contentRangeHeader = contentRangeH 0 (queryTotal - 1) $
|
||||||
then do
|
if shouldCount then Just queryTotal else Nothing
|
||||||
HT.condemn
|
minimalHeaders = [contentRangeHeader]
|
||||||
return $ singularityError (toInteger queryTotal)
|
fullHeaders = toHeader contentType : minimalHeaders
|
||||||
else do
|
|
||||||
let r = contentRangeH 0 (toInteger $ queryTotal-1)
|
status | queryTotal == 0 && not updateIsNoOp = status404
|
||||||
(toInteger <$> if shouldCount then Just queryTotal else Nothing)
|
| iPreferRepresentation apiRequest == Full = status200
|
||||||
s = if iPreferRepresentation apiRequest == Full
|
| otherwise = status204
|
||||||
then status200
|
|
||||||
else status204
|
case (contentType, iPreferRepresentation apiRequest) of
|
||||||
return $ if iPreferRepresentation apiRequest == Full
|
(CTSingularJSON, Full)
|
||||||
then responseLBS s [toHeader contentType, r] (toS body)
|
| queryTotal == 1 -> return $ responseLBS status fullHeaders (toS body)
|
||||||
else responseLBS s [r] ""
|
| otherwise -> HT.condemn >> return (singularityError queryTotal)
|
||||||
|
|
||||||
|
(_, Full) ->
|
||||||
|
return $ responseLBS status fullHeaders (toS body)
|
||||||
|
|
||||||
|
(_, _) ->
|
||||||
|
return $ responseLBS status minimalHeaders mempty
|
||||||
|
|
||||||
|
|
||||||
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just ProcessedJSON{pjRaw, pjType, pjKeys}) ->
|
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just ProcessedJSON{pjRaw, pjType, pjKeys}) ->
|
||||||
case mutateSqlParts tSchema tName of
|
case mutateSqlParts tSchema tName of
|
||||||
@@ -246,7 +253,7 @@ app dbStructure proc conf apiRequest =
|
|||||||
row <- H.statement mempty stm
|
row <- H.statement mempty stm
|
||||||
let (_, queryTotal, _, body) = extractQueryResult row
|
let (_, queryTotal, _, body) = extractQueryResult row
|
||||||
r = contentRangeH 1 0 $
|
r = contentRangeH 1 0 $
|
||||||
toInteger <$> if shouldCount then Just queryTotal else Nothing
|
if shouldCount then Just queryTotal else Nothing
|
||||||
if contentType == CTSingularJSON
|
if contentType == CTSingularJSON
|
||||||
&& queryTotal /= 1
|
&& queryTotal /= 1
|
||||||
&& iPreferRepresentation apiRequest == Full
|
&& iPreferRepresentation apiRequest == Full
|
||||||
@@ -332,9 +339,10 @@ app dbStructure proc conf apiRequest =
|
|||||||
selectQuery = requestToQuery schema False <$> readDbRequest
|
selectQuery = requestToQuery schema False <$> readDbRequest
|
||||||
countQuery = requestToCountQuery schema <$> readDbRequest
|
countQuery = requestToCountQuery schema <$> readDbRequest
|
||||||
readSqlParts = (,) <$> selectQuery <*> countQuery
|
readSqlParts = (,) <$> selectQuery <*> countQuery
|
||||||
|
mutationDbRequest s t = mutateRequest apiRequest t (tablePKCols dbStructure s t) =<< fldNames
|
||||||
mutateSqlParts s t =
|
mutateSqlParts s t =
|
||||||
(,) <$> selectQuery
|
(,) <$> selectQuery
|
||||||
<*> (requestToQuery schema False . DbMutate <$> (mutateRequest apiRequest t (tablePKCols dbStructure s t) =<< fldNames))
|
<*> (requestToQuery schema False . DbMutate <$> mutationDbRequest s t)
|
||||||
|
|
||||||
responseContentTypeOrError :: [ContentType] -> Action -> Either Response ContentType
|
responseContentTypeOrError :: [ContentType] -> Action -> Either Response ContentType
|
||||||
responseContentTypeOrError accepts action = serves contentTypesForRequest accepts
|
responseContentTypeOrError accepts action = serves contentTypesForRequest accepts
|
||||||
@@ -381,7 +389,7 @@ rangeStatus lower upper (Just total)
|
|||||||
| (1 + upper - lower) < total = status206
|
| (1 + upper - lower) < total = status206
|
||||||
| otherwise = status200
|
| otherwise = status200
|
||||||
|
|
||||||
contentRangeH :: Integer -> Integer -> Maybe Integer -> Header
|
contentRangeH :: (Integral a, Show a) => a -> a -> Maybe a -> Header
|
||||||
contentRangeH lower upper total =
|
contentRangeH lower upper total =
|
||||||
("Content-Range", headerValue)
|
("Content-Range", headerValue)
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ pgError authed e =
|
|||||||
else [jsonType] in
|
else [jsonType] in
|
||||||
responseLBS status hdrs (encodeError e)
|
responseLBS status hdrs (encodeError e)
|
||||||
|
|
||||||
singularityError :: Integer -> Response
|
singularityError :: (Integral a, Show a) => a -> Response
|
||||||
singularityError numRows =
|
singularityError numRows =
|
||||||
responseLBS HT.status406
|
responseLBS HT.status406
|
||||||
[toHeader CTSingularJSON]
|
[toHeader CTSingularJSON]
|
||||||
|
|||||||
+31
-15
@@ -385,18 +385,18 @@ spec actualPgVersion = do
|
|||||||
describe "Patching record" $ do
|
describe "Patching record" $ do
|
||||||
|
|
||||||
context "to unknown uri" $
|
context "to unknown uri" $
|
||||||
it "gives a 404" $
|
it "indicates no table found by returning 404" $
|
||||||
request methodPatch "/fake" []
|
request methodPatch "/fake" []
|
||||||
[json| { "real": false } |]
|
[json| { "real": false } |]
|
||||||
`shouldRespondWith` 404
|
`shouldRespondWith` 404
|
||||||
|
|
||||||
context "on an empty table" $
|
context "on an empty table" $
|
||||||
it "indicates no records found to update" $
|
it "indicates no records found to update by returning 404" $
|
||||||
request methodPatch "/empty_table" []
|
request methodPatch "/empty_table" []
|
||||||
[json| { "extra":20 } |]
|
[json| { "extra":20 } |]
|
||||||
`shouldRespondWith` ""
|
`shouldRespondWith` ""
|
||||||
{ matchStatus = 204,
|
{ matchStatus = 404,
|
||||||
matchHeaders = ["Content-Range" <:> "*/*"]
|
matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
context "in a nonempty table" $ do
|
context "in a nonempty table" $ do
|
||||||
@@ -423,10 +423,15 @@ spec actualPgVersion = do
|
|||||||
[("Prefer", "return=representation")] [json| { "id":999999 } |]
|
[("Prefer", "return=representation")] [json| { "id":999999 } |]
|
||||||
`shouldRespondWith` "[]"
|
`shouldRespondWith` "[]"
|
||||||
{
|
{
|
||||||
matchStatus = 200,
|
matchStatus = 404,
|
||||||
matchHeaders = ["Content-Range" <:> "*/*"]
|
matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "gives a 404 when no rows updated" $
|
||||||
|
request methodPatch "/items?id=eq.99999999" []
|
||||||
|
[json| { "id": 42 } |]
|
||||||
|
`shouldRespondWith` 404
|
||||||
|
|
||||||
it "returns updated object as array when return=rep" $
|
it "returns updated object as array when return=rep" $
|
||||||
request methodPatch "/items?id=eq.2"
|
request methodPatch "/items?id=eq.2"
|
||||||
[("Prefer", "return=representation")] [json| { "id":2 } |]
|
[("Prefer", "return=representation")] [json| { "id":2 } |]
|
||||||
@@ -454,15 +459,26 @@ spec actualPgVersion = do
|
|||||||
[json| [{ a: "keepme", b: null }] |]
|
[json| [{ a: "keepme", b: null }] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "can update based on a computed column" $
|
context "filtering by a computed column" $ do
|
||||||
request methodPatch
|
it "is successful" $
|
||||||
"/items?always_true=eq.false"
|
request methodPatch
|
||||||
[("Prefer", "return=representation")]
|
"/items?is_first=eq.true"
|
||||||
[json| { id: 100 } |]
|
[("Prefer", "return=representation")]
|
||||||
`shouldRespondWith` "[]"
|
[json| { id: 100 } |]
|
||||||
{ matchStatus = 200,
|
`shouldRespondWith` [json| [{ id: 100 }] |]
|
||||||
matchHeaders = ["Content-Range" <:> "*/*"]
|
{ matchStatus = 200,
|
||||||
}
|
matchHeaders = [matchContentTypeJson, "Content-Range" <:> "0-0/*"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "indicates no records updated by returning 404" $
|
||||||
|
request methodPatch
|
||||||
|
"/items?always_true=eq.false"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json| { id: 100 } |]
|
||||||
|
`shouldRespondWith` "[]"
|
||||||
|
{ matchStatus = 404,
|
||||||
|
matchHeaders = []
|
||||||
|
}
|
||||||
|
|
||||||
it "can provide a representation" $ do
|
it "can provide a representation" $ do
|
||||||
_ <- post "/items"
|
_ <- post "/items"
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ spec =
|
|||||||
{ matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"] }
|
{ matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"] }
|
||||||
|
|
||||||
context "when updating rows" $ do
|
context "when updating rows" $ do
|
||||||
|
|
||||||
it "works for one row" $ do
|
it "works for one row" $ do
|
||||||
_ <- post "/addresses" [json| { id: 97, address: "A Street" } |]
|
_ <- post "/addresses" [json| { id: 97, address: "A Street" } |]
|
||||||
request methodPatch
|
request methodPatch
|
||||||
@@ -56,10 +55,9 @@ spec =
|
|||||||
it "raises an error for multiple rows" $ do
|
it "raises an error for multiple rows" $ do
|
||||||
_ <- post "/addresses" [json| { id: 98, address: "xxx" } |]
|
_ <- post "/addresses" [json| { id: 98, address: "xxx" } |]
|
||||||
_ <- post "/addresses" [json| { id: 99, address: "yyy" } |]
|
_ <- post "/addresses" [json| { id: 99, address: "yyy" } |]
|
||||||
p <- request methodPatch
|
p <- request methodPatch "/addresses?id=gt.0"
|
||||||
"/addresses?id=gt.0"
|
[("Prefer", "return=representation"), singular]
|
||||||
[("Prefer", "return=representation"), singular]
|
[json| { address: "zzz" } |]
|
||||||
[json| { address: "zzz" } |]
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleStatus p `shouldBe` notAcceptable406
|
simpleStatus p `shouldBe` notAcceptable406
|
||||||
isErrorFormat (simpleBody p) `shouldBe` True
|
isErrorFormat (simpleBody p) `shouldBe` True
|
||||||
@@ -67,15 +65,16 @@ spec =
|
|||||||
-- the rows should not be updated, either
|
-- the rows should not be updated, either
|
||||||
get "/addresses?id=eq.98" `shouldRespondWith` [str|[{"id":98,"address":"xxx"}]|]
|
get "/addresses?id=eq.98" `shouldRespondWith` [str|[{"id":98,"address":"xxx"}]|]
|
||||||
|
|
||||||
it "raises an error for zero rows" $ do
|
it "raises an error for zero rows" $
|
||||||
p <- request methodPatch "/items?id=gt.0&id=lt.0"
|
request methodPatch "/items?id=gt.0&id=lt.0"
|
||||||
[("Prefer", "return=representation"), singular] [json|{"id":1}|]
|
[("Prefer", "return=representation"), singular] [json|{"id":1}|]
|
||||||
liftIO $ do
|
`shouldRespondWith`
|
||||||
simpleStatus p `shouldBe` notAcceptable406
|
[str|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
|
||||||
isErrorFormat (simpleBody p) `shouldBe` True
|
{ matchStatus = 406
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
context "when creating rows" $ do
|
context "when creating rows" $ do
|
||||||
|
|
||||||
it "works for one row" $ do
|
it "works for one row" $ do
|
||||||
p <- request methodPost
|
p <- request methodPost
|
||||||
"/addresses"
|
"/addresses"
|
||||||
@@ -117,17 +116,17 @@ spec =
|
|||||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "raises an error when creating zero entities" $ do
|
it "raises an error when creating zero entities" $
|
||||||
p <- request methodPost
|
request methodPost "/addresses"
|
||||||
"/addresses"
|
[("Prefer", "return=representation"), singular]
|
||||||
[("Prefer", "return=representation"), singular]
|
[json| [ ] |]
|
||||||
[json| [ ] |]
|
`shouldRespondWith`
|
||||||
liftIO $ do
|
[str|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
|
||||||
simpleStatus p `shouldBe` notAcceptable406
|
{ matchStatus = 406
|
||||||
isErrorFormat (simpleBody p) `shouldBe` True
|
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
context "when deleting rows" $ do
|
context "when deleting rows" $ do
|
||||||
|
|
||||||
it "works for one row" $ do
|
it "works for one row" $ do
|
||||||
p <- request methodDelete
|
p <- request methodDelete
|
||||||
"/items?id=eq.11"
|
"/items?id=eq.11"
|
||||||
@@ -146,21 +145,24 @@ spec =
|
|||||||
, matchHeaders = ["Content-Range" <:> "0-9/*"]
|
, matchHeaders = ["Content-Range" <:> "0-9/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "raises an error when deleting zero entities" $ do
|
it "raises an error when deleting zero entities" $
|
||||||
p <- request methodDelete "/items?id=lt.0"
|
request methodDelete "/items?id=lt.0"
|
||||||
[("Prefer", "return=representation"), singular] ""
|
[("Prefer", "return=representation"), singular] ""
|
||||||
liftIO $ do
|
`shouldRespondWith`
|
||||||
simpleStatus p `shouldBe` notAcceptable406
|
[str|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
|
||||||
isErrorFormat (simpleBody p) `shouldBe` True
|
{ matchStatus = 406
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
context "when calling a stored proc" $ do
|
context "when calling a stored proc" $ do
|
||||||
|
it "fails for zero rows" $
|
||||||
it "fails for zero rows" $ do
|
request methodPost "/rpc/getproject"
|
||||||
p <- request methodPost "/rpc/getproject"
|
[singular] [json|{ "id": 9999999}|]
|
||||||
[singular] [json|{ "id": 9999999}|]
|
`shouldRespondWith`
|
||||||
liftIO $ do
|
[str|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
|
||||||
simpleStatus p `shouldBe` notAcceptable406
|
{ matchStatus = 406
|
||||||
isErrorFormat (simpleBody p) `shouldBe` True
|
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
-- this one may be controversial, should vnd.pgrst.object include
|
-- this one may be controversial, should vnd.pgrst.object include
|
||||||
-- the likes of 2 and "hello?"
|
-- the likes of 2 and "hello?"
|
||||||
@@ -174,20 +176,26 @@ spec =
|
|||||||
[singular] [json|{ "id": 1}|] `shouldRespondWith`
|
[singular] [json|{ "id": 1}|] `shouldRespondWith`
|
||||||
[str|{"id":1,"name":"Windows 7","client_id":1}|]
|
[str|{"id":1,"name":"Windows 7","client_id":1}|]
|
||||||
|
|
||||||
it "fails for multiple rows" $ do
|
it "fails for multiple rows" $
|
||||||
p <- request methodPost "/rpc/getallprojects" [singular] "{}"
|
request methodPost "/rpc/getallprojects"
|
||||||
liftIO $ do
|
[singular] "{}"
|
||||||
simpleStatus p `shouldBe` notAcceptable406
|
`shouldRespondWith`
|
||||||
isErrorFormat (simpleBody p) `shouldBe` True
|
[str|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
|
||||||
|
{ matchStatus = 406
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
it "executes the proc exactly once per request" $ do
|
it "executes the proc exactly once per request" $ do
|
||||||
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |]
|
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |]
|
||||||
`shouldRespondWith` [str|[{"id":1,"name":"Windows 7"}]|]
|
`shouldRespondWith` [str|[{"id":1,"name":"Windows 7"}]|]
|
||||||
p <- request methodPost "/rpc/setprojects" [singular]
|
|
||||||
[json| {"id_l": 1, "id_h": 2, "name": "changed"} |]
|
request methodPost "/rpc/setprojects" [singular]
|
||||||
liftIO $ do
|
[json| {"id_l": 1, "id_h": 2, "name": "changed"} |]
|
||||||
simpleStatus p `shouldBe` notAcceptable406
|
`shouldRespondWith`
|
||||||
isErrorFormat (simpleBody p) `shouldBe` True
|
[str|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
|
||||||
|
{ matchStatus = 406
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
-- should not actually have executed the function
|
-- should not actually have executed the function
|
||||||
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |]
|
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |]
|
||||||
|
|||||||
Vendored
+5
@@ -116,6 +116,11 @@ CREATE FUNCTION always_true(test.items) RETURNS boolean
|
|||||||
LANGUAGE sql STABLE
|
LANGUAGE sql STABLE
|
||||||
AS $$ SELECT true $$;
|
AS $$ SELECT true $$;
|
||||||
|
|
||||||
|
CREATE FUNCTION is_first(test.items) RETURNS boolean
|
||||||
|
LANGUAGE sql STABLE
|
||||||
|
AS $$ SELECT $1.id = 1 $$;
|
||||||
|
|
||||||
|
|
||||||
CREATE FUNCTION anti_id(test.items) RETURNS bigint
|
CREATE FUNCTION anti_id(test.items) RETURNS bigint
|
||||||
LANGUAGE sql STABLE
|
LANGUAGE sql STABLE
|
||||||
AS $_$ SELECT $1.id * -1 $_$;
|
AS $_$ SELECT $1.id * -1 $_$;
|
||||||
|
|||||||
Reference in New Issue
Block a user