diff --git a/src/PostgREST/Error.hs b/src/PostgREST/Error.hs index 94197698a7..f997772352 100644 --- a/src/PostgREST/Error.hs +++ b/src/PostgREST/Error.hs @@ -122,9 +122,9 @@ instance JSON.ToJSON ApiRequestError where "message" .= ("Cannot apply filter because '" <> resource <> "' is not an embedded resource in this request" :: Text), "details" .= JSON.Null, "hint" .= ("Verify that '" <> resource <> "' is included in the 'select' query parameter." :: Text)] - toJSON (BodyFilterNotAllowed message) = JSON.object [ + toJSON (BodyFilterNotAllowed method isRpc) = JSON.object [ "code" .= ApiRequestErrorCode18, - "message" .= message, + "message" .= ("Body filter _eq is not allowed for " <> if isRpc then "RPC" else T.decodeUtf8 method), "details" .= JSON.Null, "hint" .= JSON.Null] diff --git a/src/PostgREST/Query/QueryBuilder.hs b/src/PostgREST/Query/QueryBuilder.hs index 77dd4cfcd4..7fd80eba6f 100644 --- a/src/PostgREST/Query/QueryBuilder.hs +++ b/src/PostgREST/Query/QueryBuilder.hs @@ -43,7 +43,7 @@ readRequestToQuery (Node (Select colSelects mainQi tblAlias implJoins logicFores intercalateSnippet " " joins <> " " <> (if null logicForest && null joinConditions_ then mempty - else "WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition joinConditions_)) <> " " <> + else "WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi Nothing) logicForest ++ map pgFmtJoinCondition joinConditions_)) <> " " <> orderF qi ordts <> " " <> limitOffsetF range where @@ -90,7 +90,7 @@ mutateRequestToQuery (Insert mainQi iCols body onConflct putConditions returning "SELECT " <> SQL.sql cols <> " " <> SQL.sql ("FROM json_populate_recordset (null::" <> fromQi mainQi <> ", " <> selectBody <> ") _ ") <> -- Only used for PUT - (if null putConditions then mempty else "WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree (QualifiedIdentifier mempty "_") <$> putConditions)) <> + (if null putConditions then mempty else "WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree (QualifiedIdentifier mempty "_") Nothing <$> putConditions)) <> SQL.sql (BS.unwords [ maybe "" (\(oncDo, oncCols) -> if null oncCols then @@ -120,16 +120,16 @@ mutateRequestToQuery (Update mainQi uCols body logicForest range ordts returning | range == allRange = "WITH " <> normalizedBody body <> " " <> "UPDATE " <> mainTbl <> " SET " <> SQL.sql nonRangeCols <> " " <> - "FROM (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " )) pgrst_recordset_body " <> - whereLogic <> " " <> + "FROM (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " )) pgrst_update_body " <> + whereLogic (Just (BodyRecordset "pgrst_update_body" True)) <> " " <> SQL.sql (returningF mainQi returnings) | otherwise = "WITH " <> normalizedBody body <> ", " <> - "pgrst_recordset_body AS (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " ) LIMIT 1), " <> + "pgrst_update_body AS (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " ) LIMIT 1), " <> "pgrst_affected_rows AS (" <> "SELECT " <> SQL.sql rangeIdF <> " FROM " <> mainTbl <> " " <> - whereLogic <> " " <> + whereLogic (Just (BodyRecordset "pgrst_update_body" False)) <> " " <> orderF mainQi ordts <> " " <> limitOffsetF range <> ") " <> @@ -141,11 +141,11 @@ mutateRequestToQuery (Update mainQi uCols body logicForest range ordts returning where mainTbl = SQL.sql (fromQi mainQi) - logicForestF = intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest) - whereLogic = if null logicForest then mempty else " WHERE " <> logicForestF + logicForestF recordset = intercalateSnippet " AND " (pgFmtLogicTree mainQi recordset <$> logicForest) + whereLogic recordset = if null logicForest then mempty else " WHERE " <> logicForestF recordset emptyBodyReturnedColumns = if null returnings then "NULL" else BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings) - nonRangeCols = BS.intercalate ", " (pgFmtIdent <> const " = pgrst_recordset_body." <> pgFmtIdent <$> S.toList uCols) - rangeCols = BS.intercalate ", " ((\col -> pgFmtIdent col <> " = (SELECT " <> pgFmtIdent col <> " FROM pgrst_recordset_body) ") <$> S.toList uCols) + nonRangeCols = BS.intercalate ", " (pgFmtIdent <> const " = pgrst_update_body." <> pgFmtIdent <$> S.toList uCols) + rangeCols = BS.intercalate ", " ((\col -> pgFmtIdent col <> " = (SELECT " <> pgFmtIdent col <> " FROM pgrst_update_body) ") <$> S.toList uCols) (whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts) mutateRequestToQuery (Delete mainQi logicForest range ordts returnings) @@ -168,7 +168,7 @@ mutateRequestToQuery (Delete mainQi logicForest range ordts returnings) SQL.sql (returningF mainQi returnings) where - whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest) + whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi Nothing <$> logicForest) (whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts) requestToCallProcQuery :: CallRequest -> SQL.Snippet @@ -233,7 +233,7 @@ readRequestToCountQuery (Node (Select{from=mainQi, fromAlias=tblAlias, implicitJ then mempty else " WHERE " ) <> intercalateSnippet " AND " ( - map (pgFmtLogicTree treeQi) logicForest ++ + map (pgFmtLogicTree treeQi Nothing) logicForest ++ map pgFmtJoinCondition joinConditions_ ++ subQueries ) diff --git a/src/PostgREST/Query/SqlFragment.hs b/src/PostgREST/Query/SqlFragment.hs index 4c26e8e72c..807fe523f1 100644 --- a/src/PostgREST/Query/SqlFragment.hs +++ b/src/PostgREST/Query/SqlFragment.hs @@ -57,7 +57,8 @@ import PostgREST.RangeQuery (NonnegRange, allRange, rangeLimit, rangeOffset) import PostgREST.Request.ReadQuery (SelectItem) import PostgREST.Request.Types (Alias, BodyOperator (..), - Field, Filter (..), + BodyRecordset (..), Field, + Filter (..), FtsOperator (..), JoinCondition (..), JsonOperand (..), @@ -253,8 +254,8 @@ pgFmtOrderTerm qi ot = nullOrder OrderNullsLast = "NULLS LAST" -pgFmtFilter :: QualifiedIdentifier -> Filter -> SQL.Snippet -pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper of +pgFmtFilter :: QualifiedIdentifier -> Maybe BodyRecordset -> Filter -> SQL.Snippet +pgFmtFilter table bodyRec (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper of Op op val -> pgFmtFieldOp op <> " " <> case op of OpLike -> unknownLiteral (T.map star val) OpILike -> unknownLiteral (T.map star val) @@ -280,12 +281,17 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper Fts op lang val -> pgFmtFieldFts op <> "(" <> ftsLang lang <> unknownLiteral val <> ") " - BodOp op val -> pgFmtFieldBodOp op <> " " <> SQL.sql (pgFmtColumn (QualifiedIdentifier mempty "pgrst_recordset_body") val) + BodOp op val -> pgFmtFieldBodOp op <> " " <> fmtBodOpFilter val where ftsLang = maybe mempty (\l -> unknownLiteral l <> ", ") pgFmtFieldOp op = pgFmtField table fld <> " " <> SQL.sql (singleValOperator op) pgFmtFieldFts op = pgFmtField table fld <> " " <> SQL.sql (ftsOperator op) pgFmtFieldBodOp op = pgFmtField table fld <> " " <> SQL.sql (bodySingleOperator op) + fmtBodOpFilter val = case bodyRec of + Just (BodyRecordset bodName direct) + | direct -> SQL.sql (pgFmtColumn (QualifiedIdentifier mempty (decodeUtf8 bodName)) val) + | otherwise -> SQL.sql ("(SELECT " <> pgFmtIdent val <> " FROM " <> bodName <> ")") + Nothing -> mempty notOp = if hasNot then "NOT" else mempty star c = if c == '*' then '%' else c @@ -293,14 +299,14 @@ pgFmtJoinCondition :: JoinCondition -> SQL.Snippet pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) = SQL.sql $ pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2 -pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SQL.Snippet -pgFmtLogicTree qi (Expr hasNot op forest) = SQL.sql notOp <> " (" <> intercalateSnippet (opSql op) (pgFmtLogicTree qi <$> forest) <> ")" +pgFmtLogicTree :: QualifiedIdentifier -> Maybe BodyRecordset -> LogicTree -> SQL.Snippet +pgFmtLogicTree qi bodyRec (Expr hasNot op forest) = SQL.sql notOp <> " (" <> intercalateSnippet (opSql op) (pgFmtLogicTree qi bodyRec <$> forest) <> ")" where notOp = if hasNot then "NOT" else mempty opSql And = " AND " opSql Or = " OR " -pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt +pgFmtLogicTree qi bodyRec (Stmnt flt) = pgFmtFilter qi bodyRec flt pgFmtJsonPath :: JsonPath -> SQL.Snippet pgFmtJsonPath = \case diff --git a/src/PostgREST/Request/ApiRequest.hs b/src/PostgREST/Request/ApiRequest.hs index 779556f4fa..42a7899a21 100644 --- a/src/PostgREST/Request/ApiRequest.hs +++ b/src/PostgREST/Request/ApiRequest.hs @@ -222,7 +222,7 @@ apiRequest conf@AppConfig{..} dbStructure req reqBody queryparams@QueryParams{.. | isInvalidRange = Left InvalidRange | shouldParsePayload && isLeft payload = either (Left . InvalidBody) witness payload | not expectParams && not (L.null qsParams) = Left $ ParseRequestError "Unexpected param or filter missing operator" ("Failed to parse " <> show qsParams) - | bodyFilterNotAllowed = Left $ BodyFilterNotAllowed "Body filter _eq is not allowed for this method" + | bodyFilterNotAllowed = Left $ BodyFilterNotAllowed method pathIsProc | method `elem` ["PATCH", "DELETE"] && not (null qsRanges) && null qsOrder = Left LimitNoOrderError | method == "PUT" && topLevelRange /= allRange = Left PutRangeNotAllowedError | otherwise = do diff --git a/src/PostgREST/Request/Types.hs b/src/PostgREST/Request/Types.hs index a50c9f19a5..8cb603dca0 100644 --- a/src/PostgREST/Request/Types.hs +++ b/src/PostgREST/Request/Types.hs @@ -32,6 +32,7 @@ module PostgREST.Request.Types , SimpleOperator(..) , FtsOperator(..) , BodyOperator(..) + , BodyRecordset(..) ) where import qualified Data.ByteString.Lazy as LBS @@ -50,7 +51,7 @@ import Protolude data ApiRequestError = AmbiguousRelBetween Text Text [Relationship] | AmbiguousRpc [ProcDescription] - | BodyFilterNotAllowed Text + | BodyFilterNotAllowed ByteString Bool | MediaTypeError [ByteString] | InvalidBody ByteString | InvalidFilters @@ -233,3 +234,10 @@ data FtsOperator data BodyOperator = BodyOpEqual deriving Eq + +-- | Information of the transformed body using json_populate_recordset +-- to be used for filtering using body operators +data BodyRecordset = BodyRecordset + { brName :: ByteString + , isDirectRef :: Bool + } diff --git a/test/spec/Feature/Query/DeleteSpec.hs b/test/spec/Feature/Query/DeleteSpec.hs index 24741cfd22..f751aabcfb 100644 --- a/test/spec/Feature/Query/DeleteSpec.hs +++ b/test/spec/Feature/Query/DeleteSpec.hs @@ -70,6 +70,12 @@ spec = , matchHeaders = ["Content-Range" <:> "*/*"] } + it "fails if a body filter operator is given" $ + request methodDelete "/tasks?id=_eq.id" [] mempty + `shouldRespondWith` + [json|{"details":null,"message":"Body filter _eq is not allowed for DELETE","code":"PGRST118","hint":null} |] + { matchStatus = 400 } + context "known route, no records matched" $ it "includes [] body if return=rep" $ request methodDelete "/items?id=eq.101" diff --git a/test/spec/Feature/Query/QuerySpec.hs b/test/spec/Feature/Query/QuerySpec.hs index 8a959a62eb..763f394776 100644 --- a/test/spec/Feature/Query/QuerySpec.hs +++ b/test/spec/Feature/Query/QuerySpec.hs @@ -983,6 +983,12 @@ spec actualPgVersion = do , matchHeaders = [matchContentTypeJson] } + it "fails if a body filter operator is given" $ + get "/ghostBusters?id=_eq.id" `shouldRespondWith` [json| {"details":null,"message":"Body filter _eq is not allowed for GET","code":"PGRST118","hint":null} |] + { matchStatus = 400 + , matchHeaders = [matchContentTypeJson] + } + it "will embed a collection" $ get "/Escap3e;?select=ghostBusters(*)" `shouldRespondWith` [json| [{"ghostBusters":[{"escapeId":1}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":3}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":5}]}] |] diff --git a/test/spec/Feature/Query/RpcSpec.hs b/test/spec/Feature/Query/RpcSpec.hs index eb558d17d5..ddfd72f9aa 100644 --- a/test/spec/Feature/Query/RpcSpec.hs +++ b/test/spec/Feature/Query/RpcSpec.hs @@ -822,6 +822,14 @@ spec actualPgVersion = `shouldRespondWith` "3" { matchHeaders = [matchContentTypeJson] } + it "fails if a body filter operator is given" $ do + get "/rpc/sayhello?name=_eq.John" + `shouldRespondWith` [json| {"details":null,"message":"Body filter _eq is not allowed for RPC","code":"PGRST118","hint":null} |] + { matchStatus = 400 } + post "/rpc/sayhello?name=_eq.name" [json|{name: "John"}|] + `shouldRespondWith` [json| {"details":null,"message":"Body filter _eq is not allowed for RPC","code":"PGRST118","hint":null} |] + { matchStatus = 400 } + context "bulk RPC with params=multiple-objects" $ do it "works with a scalar function an returns a json array" $ request methodPost "/rpc/add_them" [("Prefer", "params=multiple-objects")] diff --git a/test/spec/Feature/Query/UpdateSpec.hs b/test/spec/Feature/Query/UpdateSpec.hs index c08afd1ad8..18cb0b2b44 100644 --- a/test/spec/Feature/Query/UpdateSpec.hs +++ b/test/spec/Feature/Query/UpdateSpec.hs @@ -280,6 +280,75 @@ spec = do matchHeaders = ["Content-Range" <:> "*/*"] } + context "when using underscore body filters" $ do + it "updates table" $ do + get "/body_update_items" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": null } + , { "id": 2, "name": "item-2", "observation": null } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPatch "/body_update_items?id=_eq.id" + [("Prefer", "tx=commit")] + [json|{ "id": 2, "name": "item-2 - 2nd", "observation": "Lost item" }|] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Content-Range" <:> "0-0/*" + , "Preference-Applied" <:> "tx=commit" ] + } + + get "/body_update_items?order=id" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": null } + , { "id": 2, "name": "item-2 - 2nd", "observation": "Lost item" } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPost "/rpc/reset_items_tables" + [("Prefer", "tx=commit")] + [json| {"tbl_name": "body_update_items"} |] + `shouldRespondWith` "" + { matchStatus = 204 } + + it "updates table alongside a query filter" $ do + get "/body_update_items" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": null } + , { "id": 2, "name": "item-2", "observation": null } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPatch "/body_update_items?name=eq.item-2&id=_eq.id" + [("Prefer", "tx=commit")] + [json|{ "id": 2, "name": "item-2 - 2nd", "observation": "Lost item" }|] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Content-Range" <:> "0-0/*" + , "Preference-Applied" <:> "tx=commit" ] + } + + get "/body_update_items?order=id" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": null } + , { "id": 2, "name": "item-2 - 2nd", "observation": "Lost item" } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPost "/rpc/reset_items_tables" + [("Prefer", "tx=commit")] + [json| {"tbl_name": "body_update_items"} |] + `shouldRespondWith` "" + { matchStatus = 204 } + context "with unicode values" $ it "succeeds and returns values intact" $ do request methodPatch "/no_pk?a=eq.1" @@ -300,6 +369,40 @@ spec = do request methodPatch "/articles?id=eq.2001&columns=body" [("Prefer", "return=representation")] [json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith` 404 + it "updates alongside underscore body filters" $ do + get "/body_update_items" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": null } + , { "id": 2, "name": "item-2", "observation": null } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPatch "/body_update_items?id=_eq.id&columns=observation" + [("Prefer", "tx=commit")] + [json|{ "id": 2, "name": "item-2 - 2nd", "observation": "Lost item" }|] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Content-Range" <:> "0-0/*" + , "Preference-Applied" <:> "tx=commit" ] + } + + get "/body_update_items?order=id" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": null } + , { "id": 2, "name": "item-2", "observation": "Lost item" } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPost "/rpc/reset_items_tables" + [("Prefer", "tx=commit")] + [json| {"tbl_name": "body_update_items"} |] + `shouldRespondWith` "" + { matchStatus = 204 } + context "tables with self reference foreign keys" $ do it "embeds children after update" $ request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)" @@ -489,6 +592,39 @@ spec = do `shouldRespondWith` "" { matchStatus = 204 } + it "works with the limit query param plus an underscore body filter" $ do + get "/limited_update_items" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1" } + , { "id": 2, "name": "item-2" } + , { "id": 3, "name": "item-3" } + ]|] + + request methodPatch "/limited_update_items?order=id&limit=1&id=_eq.id" + [("Prefer", "tx=commit")] + [json| {"id": 3, "name": "updated-item"} |] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Preference-Applied" <:> "tx=commit" ] + } + + get "/limited_update_items?order=id" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1" } + , { "id": 2, "name": "item-2" } + , { "id": 3, "name": "updated-item" } + ]|] + + request methodPost "/rpc/reset_items_tables" + [("Prefer", "tx=commit")] + [json| {"tbl_name": "limited_update_items"} |] + `shouldRespondWith` "" + { matchStatus = 204 } + it "works with the limit and offset query params" $ do get "/limited_update_items" `shouldRespondWith` @@ -649,7 +785,7 @@ spec = do { matchStatus = 204 } context "bulk updates" $ do - it "can update tables with simple pk" $ do + it "can update tables with simple pk using underscore body filters" $ do get "/bulk_update_items" `shouldRespondWith` [json|[ @@ -686,7 +822,7 @@ spec = do `shouldRespondWith` "" { matchStatus = 204 } - it "can update tables with composite pk" $ do + it "can update tables with composite pk using underscore body filters" $ do get "/bulk_update_items_cpk" `shouldRespondWith` [json|[ @@ -717,9 +853,117 @@ spec = do , { "id": 3, "name": "item-3", "observation": null } ]|] + it "updates using underscore body filters for any key" $ do + get "/bulk_update_items" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": null } + , { "id": 2, "name": "item-2", "observation": null } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPatch "/bulk_update_items?name=_eq.name" + [("Prefer", "tx=commit")] + [json|[ + { "name": "item-1", "observation": "Lost item" } + , { "name": "item-2", "observation": "Damaged item" } + , { "name": "item-x", "observation": "Does not exist" } + ]|] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Content-Range" <:> "0-1/*" + , "Preference-Applied" <:> "tx=commit" ] + } + + get "/bulk_update_items?order=id" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": "Lost item" } + , { "id": 2, "name": "item-2", "observation": "Damaged item" } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPost "/rpc/reset_items_tables" + [("Prefer", "tx=commit")] + [json| {"tbl_name": "bulk_update_items"} |] + `shouldRespondWith` "" + { matchStatus = 204 } + + it "updates using underscore body filters and query filters" $ do + get "/bulk_update_items" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": null } + , { "id": 2, "name": "item-2", "observation": null } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPatch "/bulk_update_items?name=_eq.name&id=eq.2" + [("Prefer", "tx=commit")] + [json|[ + { "name": "item-1", "observation": "Lost item" } + , { "name": "item-2", "observation": "Damaged item" } + , { "name": "item-x", "observation": "Does not exist" } + ]|] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Content-Range" <:> "0-0/*" + , "Preference-Applied" <:> "tx=commit" ] + } + + get "/bulk_update_items?order=id" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": null } + , { "id": 2, "name": "item-2", "observation": "Damaged item" } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPost "/rpc/reset_items_tables" + [("Prefer", "tx=commit")] + [json| {"tbl_name": "bulk_update_items"} |] + `shouldRespondWith` "" + { matchStatus = 204 } + + it "updates using underscore body filters and ?columns" $ do + get "/bulk_update_items" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": null } + , { "id": 2, "name": "item-2", "observation": null } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPatch "/bulk_update_items?name=_eq.name&columns=observation" + [("Prefer", "tx=commit")] + [json|[ + { "name": "item-1", "observation": "Lost item" } + , { "name": "item-2", "observation": "Damaged item" } + , { "name": "item-x", "observation": "Does not exist" } + ]|] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Content-Range" <:> "0-1/*" + , "Preference-Applied" <:> "tx=commit" ] + } + + get "/bulk_update_items?order=id" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": "Lost item" } + , { "id": 2, "name": "item-2", "observation": "Damaged item" } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + request methodPost "/rpc/reset_items_tables" [("Prefer", "tx=commit")] - [json| {"tbl_name": "bulk_update_items_cpk"} |] + [json| {"tbl_name": "bulk_update_items"} |] `shouldRespondWith` "" { matchStatus = 204 } @@ -760,6 +1004,43 @@ spec = do `shouldRespondWith` "" { matchStatus = 204 } + it "updates the full table with only ?columns, taking only the first item in the json array body" $ do + get "/bulk_update_items" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": null } + , { "id": 2, "name": "item-2", "observation": null } + , { "id": 3, "name": "item-3", "observation": null } + ]|] + + request methodPatch "/bulk_update_items?columns=observation" + [("Prefer", "tx=commit")] + [json|[ + { "name": "item-4", "observation": "Damaged item" } + , { "name": "item-3 - 3rd", "observation": null } + ]|] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Content-Range" <:> "0-2/*" + , "Preference-Applied" <:> "tx=commit" ] + } + + get "/bulk_update_items?order=id" + `shouldRespondWith` + [json|[ + { "id": 1, "name": "item-1", "observation": "Damaged item" } + , { "id": 2, "name": "item-2", "observation": "Damaged item" } + , { "id": 3, "name": "item-3", "observation": "Damaged item" } + ]|] + + request methodPost "/rpc/reset_items_tables" + [("Prefer", "tx=commit")] + [json| {"tbl_name": "bulk_update_items"} |] + `shouldRespondWith` "" + { matchStatus = 204 } + it "updates with limit and offset taking only the first item in the json array body" $ do get "/bulk_update_items" `shouldRespondWith` diff --git a/test/spec/fixtures/data.sql b/test/spec/fixtures/data.sql index ef2d0bd78e..4fe5165e41 100644 --- a/test/spec/fixtures/data.sql +++ b/test/spec/fixtures/data.sql @@ -787,6 +787,9 @@ INSERT INTO test.bulk_update_items (id, name, observation) VALUES (1, 'item-1', TRUNCATE TABLE test.bulk_update_items_cpk CASCADE; INSERT INTO test.bulk_update_items_cpk (id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL); +TRUNCATE TABLE test.body_update_items CASCADE; +INSERT INTO test.body_update_items (id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL); + TRUNCATE TABLE shops CASCADE; INSERT INTO shops(id, address, shop_geom) VALUES(1, '1369 Cambridge St', 'SRID=4326;POINT(-71.10044 42.373695)'); INSERT INTO shops(id, address, shop_geom) VALUES(2, '757 Massachusetts Ave', 'SRID=4326;POINT(-71.10543 42.366432)'); diff --git a/test/spec/fixtures/privileges.sql b/test/spec/fixtures/privileges.sql index ca6d1e52e2..dc6fd2e44b 100644 --- a/test/spec/fixtures/privileges.sql +++ b/test/spec/fixtures/privileges.sql @@ -191,6 +191,7 @@ GRANT ALL ON TABLE , view_test , bulk_update_items , bulk_update_items_cpk + , body_update_items , shops , shop_bles , "SPECIAL ""@/\#~_-".names diff --git a/test/spec/fixtures/schema.sql b/test/spec/fixtures/schema.sql index 70068b87ba..e4c26c9651 100644 --- a/test/spec/fixtures/schema.sql +++ b/test/spec/fixtures/schema.sql @@ -2653,6 +2653,12 @@ CREATE TABLE test.bulk_update_items_cpk ( PRIMARY KEY (id, name) ); +CREATE TABLE test.body_update_items ( + id INT PRIMARY KEY, + name TEXT, + observation TEXT +); + create extension if not exists postgis with schema extensions; create table shops (