Remove PUT restriction for all columns/single row
Fixes https://github.com/PostgREST/postgrest/issues/1452. The single row restriction can be lifted because the PUT will consider only the first object of the array.
This commit is contained in:
committed by
Steve Chavez
parent
9a52632024
commit
10c363b588
@@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #1471, Fix POST, PATCH, DELETE with ?select= and return=minimal and PATCH with empty body - @wolfgangwalther
|
- #1471, Fix POST, PATCH, DELETE with ?select= and return=minimal and PATCH with empty body - @wolfgangwalther
|
||||||
- #1500, Fix missing `openapi-server-proxy-uri` config option - @steve-chavez
|
- #1500, Fix missing `openapi-server-proxy-uri` config option - @steve-chavez
|
||||||
- #1508, Fix `Content-Profile` not working for POST RPC - @steve-chavez
|
- #1508, Fix `Content-Profile` not working for POST RPC - @steve-chavez
|
||||||
|
- #1452, Fix PUT restriction for all columns - @steve-chavez
|
||||||
|
|
||||||
## [7.0.0] - 2020-04-03
|
## [7.0.0] - 2020-04-03
|
||||||
|
|
||||||
|
|||||||
@@ -191,11 +191,10 @@ userApiRequest confSchemas rootSpec req reqBody
|
|||||||
(CTOther "application/x-www-form-urlencoded", _) ->
|
(CTOther "application/x-www-form-urlencoded", _) ->
|
||||||
let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody
|
let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody
|
||||||
keys = S.fromList $ M.keys json in
|
keys = S.fromList $ M.keys json in
|
||||||
Right $ ProcessedJSON (JSON.encode json) PJObject keys
|
Right $ ProcessedJSON (JSON.encode json) keys
|
||||||
(ct, _) ->
|
(ct, _) ->
|
||||||
Left $ toS $ "Content-Type not acceptable: " <> toMime ct
|
Left $ toS $ "Content-Type not acceptable: " <> toMime ct
|
||||||
rpcPrmsToJson = ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams)
|
rpcPrmsToJson = ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) (S.fromList $ fst <$> rpcQParams)
|
||||||
PJObject (S.fromList $ fst <$> rpcQParams)
|
|
||||||
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
||||||
action =
|
action =
|
||||||
case method of
|
case method of
|
||||||
@@ -334,14 +333,14 @@ payloadAttributes raw json =
|
|||||||
JSON.Object x -> S.fromList (M.keys x) == canonicalKeys
|
JSON.Object x -> S.fromList (M.keys x) == canonicalKeys
|
||||||
_ -> False) arr in
|
_ -> False) arr in
|
||||||
if areKeysUniform
|
if areKeysUniform
|
||||||
then Just $ ProcessedJSON raw (PJArray $ V.length arr) canonicalKeys
|
then Just $ ProcessedJSON raw canonicalKeys
|
||||||
else Nothing
|
else Nothing
|
||||||
Just _ -> Nothing
|
Just _ -> Nothing
|
||||||
Nothing -> Just emptyPJArray
|
Nothing -> Just emptyPJArray
|
||||||
|
|
||||||
JSON.Object o -> Just $ ProcessedJSON raw PJObject (S.fromList $ M.keys o)
|
JSON.Object o -> Just $ ProcessedJSON raw (S.fromList $ M.keys o)
|
||||||
|
|
||||||
-- truncate everything else to an empty array.
|
-- truncate everything else to an empty array.
|
||||||
_ -> Just emptyPJArray
|
_ -> Just emptyPJArray
|
||||||
where
|
where
|
||||||
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) (PJArray 0) S.empty
|
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
|
||||||
|
|||||||
+3
-11
@@ -219,22 +219,14 @@ app dbStructure proc cols conf apiRequest =
|
|||||||
else
|
else
|
||||||
return $ responseLBS status headers rBody
|
return $ responseLBS status headers rBody
|
||||||
|
|
||||||
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just ProcessedJSON{pjRaw, pjType, pjKeys}) ->
|
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
|
||||||
case mutateSqlParts tSchema tName of
|
case mutateSqlParts tSchema tName of
|
||||||
Left errorResponse -> return errorResponse
|
Left errorResponse -> return errorResponse
|
||||||
Right (sq, mq) -> do
|
Right (sq, mq) ->
|
||||||
let isSingle = case pjType of
|
|
||||||
PJArray len -> len == 1
|
|
||||||
PJObject -> True
|
|
||||||
colNames = colName <$> tableCols dbStructure tSchema tName
|
|
||||||
if topLevelRange /= allRange
|
if topLevelRange /= allRange
|
||||||
then return . errorResponseFor $ PutRangeNotAllowedError
|
then return . errorResponseFor $ PutRangeNotAllowedError
|
||||||
else if not isSingle
|
|
||||||
then return . errorResponseFor $ PutSingletonError
|
|
||||||
else if S.fromList colNames /= pjKeys
|
|
||||||
then return . errorResponseFor $ PutPayloadIncompleteError
|
|
||||||
else do
|
else do
|
||||||
row <- H.statement (toS pjRaw) $
|
row <- H.statement (toS $ pjRaw pJson) $
|
||||||
createWriteStatement sq mq (contentType == CTSingularJSON) False
|
createWriteStatement sq mq (contentType == CTSingularJSON) False
|
||||||
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) [] pgVer
|
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) [] pgVer
|
||||||
let (_, queryTotal, _, body, gucHeaders) = row
|
let (_, queryTotal, _, body, gucHeaders) = row
|
||||||
|
|||||||
+9
-17
@@ -223,10 +223,8 @@ data SimpleError
|
|||||||
= GucHeadersError
|
= GucHeadersError
|
||||||
| BinaryFieldError ContentType
|
| BinaryFieldError ContentType
|
||||||
| ConnectionLostError
|
| ConnectionLostError
|
||||||
| PutSingletonError
|
|
||||||
| PutMatchingPkError
|
| PutMatchingPkError
|
||||||
| PutRangeNotAllowedError
|
| PutRangeNotAllowedError
|
||||||
| PutPayloadIncompleteError
|
|
||||||
| JwtTokenMissing
|
| JwtTokenMissing
|
||||||
| JwtTokenInvalid Text
|
| JwtTokenInvalid Text
|
||||||
| SingularityError Integer
|
| SingularityError Integer
|
||||||
@@ -234,17 +232,15 @@ data SimpleError
|
|||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
instance PgrstError SimpleError where
|
instance PgrstError SimpleError where
|
||||||
status GucHeadersError = HT.status500
|
status GucHeadersError = HT.status500
|
||||||
status (BinaryFieldError _) = HT.status406
|
status (BinaryFieldError _) = HT.status406
|
||||||
status ConnectionLostError = HT.status503
|
status ConnectionLostError = HT.status503
|
||||||
status PutSingletonError = HT.status400
|
status PutMatchingPkError = HT.status400
|
||||||
status PutMatchingPkError = HT.status400
|
status PutRangeNotAllowedError = HT.status400
|
||||||
status PutRangeNotAllowedError = HT.status400
|
status JwtTokenMissing = HT.status500
|
||||||
status PutPayloadIncompleteError = HT.status400
|
status (JwtTokenInvalid _) = HT.unauthorized401
|
||||||
status JwtTokenMissing = HT.status500
|
status (SingularityError _) = HT.status406
|
||||||
status (JwtTokenInvalid _) = HT.unauthorized401
|
status (ContentTypeError _) = HT.status415
|
||||||
status (SingularityError _) = HT.status406
|
|
||||||
status (ContentTypeError _) = HT.status415
|
|
||||||
|
|
||||||
headers (SingularityError _) = [toHeader CTSingularJSON]
|
headers (SingularityError _) = [toHeader CTSingularJSON]
|
||||||
headers (JwtTokenInvalid m) = [toHeader CTApplicationJSON, invalidTokenHeader m]
|
headers (JwtTokenInvalid m) = [toHeader CTApplicationJSON, invalidTokenHeader m]
|
||||||
@@ -258,12 +254,8 @@ instance JSON.ToJSON SimpleError where
|
|||||||
toJSON ConnectionLostError = JSON.object [
|
toJSON ConnectionLostError = JSON.object [
|
||||||
"message" .= ("Database connection lost, retrying the connection." :: Text)]
|
"message" .= ("Database connection lost, retrying the connection." :: Text)]
|
||||||
|
|
||||||
toJSON PutSingletonError = JSON.object [
|
|
||||||
"message" .= ("PUT payload must contain a single row" :: Text)]
|
|
||||||
toJSON PutRangeNotAllowedError = JSON.object [
|
toJSON PutRangeNotAllowedError = JSON.object [
|
||||||
"message" .= ("Range header and limit/offset querystring parameters are not allowed for PUT" :: Text)]
|
"message" .= ("Range header and limit/offset querystring parameters are not allowed for PUT" :: Text)]
|
||||||
toJSON PutPayloadIncompleteError = JSON.object [
|
|
||||||
"message" .= ("You must specify all columns in the payload when using PUT" :: Text)]
|
|
||||||
toJSON PutMatchingPkError = JSON.object [
|
toJSON PutMatchingPkError = JSON.object [
|
||||||
"message" .= ("Payload values do not match URL in primary key column(s)" :: Text)]
|
"message" .= ("Payload values do not match URL in primary key column(s)" :: Text)]
|
||||||
|
|
||||||
|
|||||||
@@ -309,9 +309,8 @@ data PayloadJSON =
|
|||||||
ProcessedJSON {
|
ProcessedJSON {
|
||||||
-- | This is the raw ByteString that comes from the request body.
|
-- | This is the raw ByteString that comes from the request body.
|
||||||
-- We cache this instead of an Aeson Value because it was detected that for large payloads the encoding
|
-- We cache this instead of an Aeson Value because it was detected that for large payloads the encoding
|
||||||
-- had high memory usage, see #1005 for more details
|
-- had high memory usage, see https://github.com/PostgREST/postgrest/pull/1005 for more details
|
||||||
pjRaw :: BL.ByteString
|
pjRaw :: BL.ByteString
|
||||||
, pjType :: PJType
|
|
||||||
-- | Keys of the object or if it's an array these keys are guaranteed to be the same across all its objects
|
-- | Keys of the object or if it's an array these keys are guaranteed to be the same across all its objects
|
||||||
, pjKeys :: S.Set Text
|
, pjKeys :: S.Set Text
|
||||||
}|
|
}|
|
||||||
|
|||||||
@@ -180,26 +180,6 @@ spec =
|
|||||||
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]
|
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]
|
||||||
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
|
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "fails if the payload has more than one row" $
|
|
||||||
put "/tiobe_pls?name=eq.Go"
|
|
||||||
[str| [ { "name": "Go", "rank": 19 }, { "name": "Swift", "rank": 12 } ]|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|{"message":"PUT payload must contain a single row"}|]
|
|
||||||
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "fails if not all columns are specified" $ do
|
|
||||||
put "/tiobe_pls?name=eq.Go"
|
|
||||||
[str| [ { "name": "Go" } ]|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|{"message":"You must specify all columns in the payload when using PUT"}|]
|
|
||||||
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
put "/employees?first_name=eq.Susan&last_name=eq.Heidt"
|
|
||||||
[str| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000" } ]|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|{"message":"You must specify all columns in the payload when using PUT"}|]
|
|
||||||
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "rejects every other filter than pk cols eq's" $ do
|
it "rejects every other filter than pk cols eq's" $ do
|
||||||
put "/tiobe_pls?rank=eq.19"
|
put "/tiobe_pls?rank=eq.19"
|
||||||
[str| [ { "name": "Go", "rank": 19 } ]|]
|
[str| [ { "name": "Go", "rank": 19 } ]|]
|
||||||
@@ -280,6 +260,14 @@ spec =
|
|||||||
put "/tiobe_pls?name=eq.Go" [str| [ { "name": "Go", "rank": 13 } ]|] `shouldRespondWith` 204
|
put "/tiobe_pls?name=eq.Go" [str| [ { "name": "Go", "rank": 13 } ]|] `shouldRespondWith` 204
|
||||||
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json| [ { "name": "Go", "rank": 13 } ]|] { matchHeaders = [matchContentTypeJson] }
|
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json| [ { "name": "Go", "rank": 13 } ]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "succeeds if the payload has more than one row, but it only puts the first element" $
|
||||||
|
request methodPut "/tiobe_pls?name=eq.Go"
|
||||||
|
[("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]
|
||||||
|
[str| [ { "name": "Go", "rank": 19 }, { "name": "Swift", "rank": 12 } ] |]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{ "name": "Go", "rank": 19 }|]
|
||||||
|
{ matchStatus = 200 , matchHeaders = [matchContentTypeSingular] }
|
||||||
|
|
||||||
it "succeeds on table with composite pk" $ do
|
it "succeeds on table with composite pk" $ do
|
||||||
get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
|
get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
|
|||||||
Reference in New Issue
Block a user