Singular response for PATCH request (#634)
* Allow a singular entity to be returned from an update Since a `PATCH` will be an update that could affect many rows, there might be more than one object returned. By allowing asking for a singular response, the/an object will be returned instead of a list. This is useful in cases when the `PATCH` is against single entity (i.e. `/items?pkey=eq.99`). * Add tests for plurality=singular for `PATCH` requests * Disallow updating more than one row if `plurality=singular` As discussed in #634, we don't want to allow updating several rows with a `PATCH` request when the `Prefer` header specifies `return=representation;plurality=singular` as this would almost certainly be a client error. * Add test for patching multiple objects with singular response Patching > 1 object with `return=representation;plurality=singular` should return `400 Bad Request`. * Only add singleton range for read API requests * Disallow inserting more than one row if `plurality=singular` Disallow inserting several rows with a `POST` request when the `Prefer` header specifies `return=representation;plurality=singular` as this would almost certainly be a client error. * Only import `q` from `Text.InterpolatedString.Perl6` * Update OpenAPI response for PATCH/POST to mention `plurality=singular` The OpenAPI response for PATCH/POST requests now includes the `Prefer` value `return=representation;plurality=singular`. * Add entry in changelog
This commit is contained in:
committed by
Joe Nelson
parent
df6cbc4afa
commit
35c5b190b4
@@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Ability to set addresses to listen on - @hudayou
|
- Ability to set addresses to listen on - @hudayou
|
||||||
- Filtering, shaping and embedding with &select for the /rpc path - @ruslantalpa
|
- Filtering, shaping and embedding with &select for the /rpc path - @ruslantalpa
|
||||||
- Output names of used-defined types (instead of 'USER-DEFINED') - @martingms
|
- Output names of used-defined types (instead of 'USER-DEFINED') - @martingms
|
||||||
|
- Implement support for singular representation responses for POST/PATCH requests - @ehamberg
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Do not apply limit to parent items - @ruslantalpa
|
- Do not apply limit to parent items - @ruslantalpa
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ userApiRequest schema req reqBody =
|
|||||||
endingIn xx key = lastWord `elem` xx
|
endingIn xx key = lastWord `elem` xx
|
||||||
where lastWord = last $ T.split (=='.') key
|
where lastWord = last $ T.split (=='.') key
|
||||||
|
|
||||||
headerRange = if singular then singletonRange 0 else rangeRequested hdrs
|
headerRange = if singular && method == "GET" then singletonRange 0 else rangeRequested hdrs
|
||||||
urlOffsetRange = rangeGeq . fromMaybe (0::Integer) $
|
urlOffsetRange = rangeGeq . fromMaybe (0::Integer) $
|
||||||
readMaybe =<< join (lookup "offset" qParams)
|
readMaybe =<< join (lookup "offset" qParams)
|
||||||
urlRange = restrictRange
|
urlRange = restrictRange
|
||||||
|
|||||||
+20
-2
@@ -21,6 +21,8 @@ import qualified Hasql.Transaction as HT
|
|||||||
import Text.Parsec.Error
|
import Text.Parsec.Error
|
||||||
import Text.ParserCombinators.Parsec (parse)
|
import Text.ParserCombinators.Parsec (parse)
|
||||||
|
|
||||||
|
import qualified Text.InterpolatedString.Perl6 as P6 (q)
|
||||||
|
|
||||||
import Network.HTTP.Types.Header
|
import Network.HTTP.Types.Header
|
||||||
import Network.HTTP.Types.Status
|
import Network.HTTP.Types.Status
|
||||||
import Network.HTTP.Types.URI (renderSimpleQuery)
|
import Network.HTTP.Types.URI (renderSimpleQuery)
|
||||||
@@ -129,6 +131,13 @@ app dbStructure conf apiRequest =
|
|||||||
Left e -> return $ responseLBS status400 [jsonH] $ toS e
|
Left e -> return $ responseLBS status400 [jsonH] $ toS e
|
||||||
Right (sq,mq) -> do
|
Right (sq,mq) -> do
|
||||||
let isSingle = (==1) $ V.length rows
|
let isSingle = (==1) $ V.length rows
|
||||||
|
when (not isSingle && iPreferSingular apiRequest) $
|
||||||
|
HT.sql [P6.q| DO $$
|
||||||
|
BEGIN RAISE EXCEPTION cardinality_violation
|
||||||
|
USING MESSAGE =
|
||||||
|
'plurality=singular specified, but more than one object would be inserted';
|
||||||
|
END $$;
|
||||||
|
|]
|
||||||
let pKeys = map pkName $ filter (filterPk schema table) allPrKeys -- would it be ok to move primary key detection in the query itself?
|
let pKeys = map pkName $ filter (filterPk schema table) allPrKeys -- would it be ok to move primary key detection in the query itself?
|
||||||
let stm = createWriteStatement qi sq mq isSingle (iPreferRepresentation apiRequest) pKeys (contentType == TextCSV) payload
|
let stm = createWriteStatement qi sq mq isSingle (iPreferRepresentation apiRequest) pKeys (contentType == TextCSV) payload
|
||||||
row <- H.query uniform stm
|
row <- H.query uniform stm
|
||||||
@@ -145,10 +154,19 @@ app dbStructure conf apiRequest =
|
|||||||
case mutateSqlParts of
|
case mutateSqlParts of
|
||||||
Left e -> return $ responseLBS status400 [jsonH] $ toS e
|
Left e -> return $ responseLBS status400 [jsonH] $ toS e
|
||||||
Right (sq,mq) -> do
|
Right (sq,mq) -> do
|
||||||
let stm = createWriteStatement qi sq mq False (iPreferRepresentation apiRequest) [] (contentType == TextCSV) payload
|
let singular = iPreferSingular apiRequest
|
||||||
|
let representation = iPreferRepresentation apiRequest
|
||||||
|
let stm = createWriteStatement qi sq mq singular representation [] (contentType == TextCSV) payload
|
||||||
row <- H.query uniform stm
|
row <- H.query uniform stm
|
||||||
let (_, queryTotal, _, body) = extractQueryResult row
|
let (_, queryTotal, _, body) = extractQueryResult row
|
||||||
r = contentRangeH 0 (toInteger $ queryTotal-1) (toInteger <$> Just queryTotal)
|
when (singular && queryTotal > 1) $
|
||||||
|
HT.sql [P6.q| DO $$
|
||||||
|
BEGIN RAISE EXCEPTION cardinality_violation
|
||||||
|
USING MESSAGE =
|
||||||
|
'plurality=singular specified, but more than one object would be updated';
|
||||||
|
END $$;
|
||||||
|
|]
|
||||||
|
let r = contentRangeH 0 (toInteger $ queryTotal-1) (toInteger <$> Just queryTotal)
|
||||||
s = case () of _ | queryTotal == 0 -> status404
|
s = case () of _ | queryTotal == 0 -> status404
|
||||||
| iPreferRepresentation apiRequest == Full -> status200
|
| iPreferRepresentation apiRequest == Full -> status200
|
||||||
| otherwise -> status204
|
| otherwise -> status204
|
||||||
|
|||||||
@@ -162,13 +162,10 @@ makeGetParams cs =
|
|||||||
, makePreferParam ["plurality=singular", "count=none"]
|
, makePreferParam ["plurality=singular", "count=none"]
|
||||||
]
|
]
|
||||||
|
|
||||||
makeReturnPreferenceParam :: Param
|
|
||||||
makeReturnPreferenceParam =
|
|
||||||
makePreferParam ["return=representation", "return=minimal", "return=none"]
|
|
||||||
|
|
||||||
makePostParams :: Text -> [Param]
|
makePostParams :: Text -> [Param]
|
||||||
makePostParams tn =
|
makePostParams tn =
|
||||||
[ makeReturnPreferenceParam
|
[ makePreferParam ["return=representation", "return=representation;plurality=singular",
|
||||||
|
"return=minimal", "return=none"]
|
||||||
, (mempty :: Param)
|
, (mempty :: Param)
|
||||||
& name .~ "body"
|
& name .~ "body"
|
||||||
& description ?~ tn
|
& description ?~ tn
|
||||||
@@ -178,7 +175,7 @@ makePostParams tn =
|
|||||||
|
|
||||||
makeDeleteParams :: [Param]
|
makeDeleteParams :: [Param]
|
||||||
makeDeleteParams =
|
makeDeleteParams =
|
||||||
[ makeReturnPreferenceParam ]
|
[ makePreferParam ["return=representation", "return=minimal", "return=none"] ]
|
||||||
|
|
||||||
makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
|
makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
|
||||||
makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
|
makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
|
||||||
|
|||||||
@@ -398,6 +398,35 @@ spec = do
|
|||||||
[json| { id: 99 } |]
|
[json| { id: 99 } |]
|
||||||
`shouldRespondWith` [json| [{id:99}] |]
|
`shouldRespondWith` [json| [{id:99}] |]
|
||||||
|
|
||||||
|
context "in a table" $ do
|
||||||
|
it "can provide a singular representation when updating one entity" $ do
|
||||||
|
_ <- post "/addresses" [json| { id: 97, address: "A Street" } |]
|
||||||
|
p <- request methodPatch
|
||||||
|
"/addresses?id=eq.97"
|
||||||
|
[("Prefer", "return=representation;plurality=singular")]
|
||||||
|
[json| { address: "B Street" } |]
|
||||||
|
liftIO $ simpleBody p `shouldBe` [str|{"id":97,"address":"B Street"}|]
|
||||||
|
it "raises an error when attempting to update multiple entities with plurality=singular" $ do
|
||||||
|
_ <- post "/addresses" [json| { id: 98, address: "xxx" } |]
|
||||||
|
_ <- post "/addresses" [json| { id: 99, address: "yyy" } |]
|
||||||
|
p <- request methodPatch
|
||||||
|
"/addresses?id=gt.0"
|
||||||
|
[("Prefer", "return=representation;plurality=singular")]
|
||||||
|
[json| { address: "zzz" } |]
|
||||||
|
liftIO $ simpleStatus p `shouldBe` status400
|
||||||
|
it "can provide a singular representation when creating one entity" $ do
|
||||||
|
p <- request methodPost
|
||||||
|
"/addresses"
|
||||||
|
[("Prefer", "return=representation;plurality=singular")]
|
||||||
|
[json| [ { id: 100, address: "xxx" } ] |]
|
||||||
|
liftIO $ simpleBody p `shouldBe` [str|{"id":100,"address":"xxx"}|]
|
||||||
|
it "raises an error when attempting to create multiple entities with plurality=singular" $ do
|
||||||
|
p <- request methodPost
|
||||||
|
"/addresses"
|
||||||
|
[("Prefer", "return=representation;plurality=singular")]
|
||||||
|
[json| [ { id: 100, address: "xxx" }, { id: 101, address: "xxx" } ] |]
|
||||||
|
liftIO $ simpleStatus p `shouldBe` status400
|
||||||
|
|
||||||
it "can set a json column to escaped value" $ do
|
it "can set a json column to escaped value" $ do
|
||||||
_ <- post "/json" [json| { data: {"escaped":"bar"} } |]
|
_ <- post "/json" [json| { data: {"escaped":"bar"} } |]
|
||||||
request methodPatch "/json?data->>escaped=eq.bar"
|
request methodPatch "/json?data->>escaped=eq.bar"
|
||||||
|
|||||||
Reference in New Issue
Block a user