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 -1
View File
@@ -190,7 +190,7 @@ userApiRequest schema req reqBody =
endingIn xx key = lastWord `elem` xx
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) $
readMaybe =<< join (lookup "offset" qParams)
urlRange = restrictRange
+20 -2
View File
@@ -21,6 +21,8 @@ import qualified Hasql.Transaction as HT
import Text.Parsec.Error
import Text.ParserCombinators.Parsec (parse)
import qualified Text.InterpolatedString.Perl6 as P6 (q)
import Network.HTTP.Types.Header
import Network.HTTP.Types.Status
import Network.HTTP.Types.URI (renderSimpleQuery)
@@ -129,6 +131,13 @@ app dbStructure conf apiRequest =
Left e -> return $ responseLBS status400 [jsonH] $ toS e
Right (sq,mq) -> do
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 stm = createWriteStatement qi sq mq isSingle (iPreferRepresentation apiRequest) pKeys (contentType == TextCSV) payload
row <- H.query uniform stm
@@ -145,10 +154,19 @@ app dbStructure conf apiRequest =
case mutateSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ toS e
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
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
| iPreferRepresentation apiRequest == Full -> status200
| otherwise -> status204
+3 -6
View File
@@ -162,13 +162,10 @@ makeGetParams cs =
, makePreferParam ["plurality=singular", "count=none"]
]
makeReturnPreferenceParam :: Param
makeReturnPreferenceParam =
makePreferParam ["return=representation", "return=minimal", "return=none"]
makePostParams :: Text -> [Param]
makePostParams tn =
[ makeReturnPreferenceParam
[ makePreferParam ["return=representation", "return=representation;plurality=singular",
"return=minimal", "return=none"]
, (mempty :: Param)
& name .~ "body"
& description ?~ tn
@@ -178,7 +175,7 @@ makePostParams tn =
makeDeleteParams :: [Param]
makeDeleteParams =
[ makeReturnPreferenceParam ]
[ makePreferParam ["return=representation", "return=minimal", "return=none"] ]
makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)