From 35c5b190b4de2732872caae14b4138d6c3236e20 Mon Sep 17 00:00:00 2001 From: Erlend Hamberg Date: Fri, 19 Aug 2016 23:02:14 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + src/PostgREST/ApiRequest.hs | 2 +- src/PostgREST/App.hs | 22 ++++++++++++++++++++-- src/PostgREST/OpenAPI.hs | 9 +++------ test/Feature/InsertSpec.hs | 29 +++++++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de1bb9231..25cb70188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Ability to set addresses to listen on - @hudayou - Filtering, shaping and embedding with &select for the /rpc path - @ruslantalpa - Output names of used-defined types (instead of 'USER-DEFINED') - @martingms +- Implement support for singular representation responses for POST/PATCH requests - @ehamberg ### Fixed - Do not apply limit to parent items - @ruslantalpa diff --git a/src/PostgREST/ApiRequest.hs b/src/PostgREST/ApiRequest.hs index ad5de09be..b751b7e0e 100644 --- a/src/PostgREST/ApiRequest.hs +++ b/src/PostgREST/ApiRequest.hs @@ -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 diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index de484d6bc..890a951d5 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -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 diff --git a/src/PostgREST/OpenAPI.hs b/src/PostgREST/OpenAPI.hs index acf191f1a..b1d92ee28 100644 --- a/src/PostgREST/OpenAPI.hs +++ b/src/PostgREST/OpenAPI.hs @@ -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) diff --git a/test/Feature/InsertSpec.hs b/test/Feature/InsertSpec.hs index 965c426c0..4622e6c47 100644 --- a/test/Feature/InsertSpec.hs +++ b/test/Feature/InsertSpec.hs @@ -398,6 +398,35 @@ spec = do [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 _ <- post "/json" [json| { data: {"escaped":"bar"} } |] request methodPatch "/json?data->>escaped=eq.bar"