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:
Erlend Hamberg
2016-08-19 14:02:14 -07:00
committed by Joe Nelson
parent df6cbc4afa
commit 35c5b190b4
5 changed files with 54 additions and 9 deletions
+1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+3 -6
View File
@@ -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)
+29
View File
@@ -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"