diff --git a/CHANGELOG.md b/CHANGELOG.md index 4105bf4e2..e547a515a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #1383, Add support for HEAD request - @steve-chavez - #1378, Add support for `Prefer: count=planned` and `Prefer: count=estimated` on GET /table - @steve-chavez -- #1301, Allow self join resource embedding on PATCH - @herulume, @steve-chavez +- #1301, Fix self join resource embedding on PATCH - @herulume, @steve-chavez +- #1389, Fix many to many resource embedding on RPC/PATCH - @steve-chavez ### Changed diff --git a/src/PostgREST/DbRequestBuilder.hs b/src/PostgREST/DbRequestBuilder.hs index ee82563a5..6dcc84d6a 100644 --- a/src/PostgREST/DbRequestBuilder.hs +++ b/src/PostgREST/DbRequestBuilder.hs @@ -336,9 +336,13 @@ returningCols rr@(Node _ forest) = returnings -- `RETURNING name`(see QueryBuilder). -- This would make the embedding fail because the following JOIN would need the "client_id" column from projects. -- So this adds the foreign key columns to ensure the embedding succeeds, result would be `RETURNING name, client_id`. + -- This also works for the other relType's. fkCols = concat $ mapMaybe (\case - Node (_, (_, Just Relation{relFColumns=cols, relType=Parent}, _, _, _)) _ -> Just cols - Node (_, (_, Just Relation{relFColumns=cols, relType=Child}, _, _, _)) _ -> Just cols + Node (_, (_, Just Relation{relFColumns=cols, relType=relTyp}, _, _, _)) _ -> case relTyp of + Parent -> Just cols + Child -> Just cols + Many -> Just cols + _ -> Nothing _ -> Nothing ) forest -- However if the "client_id" is present, e.g. mutateRequest to /projects?select=client_id,name,clients(name) diff --git a/src/PostgREST/QueryBuilder/Private.hs b/src/PostgREST/QueryBuilder/Private.hs index b91226542..b1d00539c 100644 --- a/src/PostgREST/QueryBuilder/Private.hs +++ b/src/PostgREST/QueryBuilder/Private.hs @@ -150,9 +150,9 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper (find ((==) . toLower $ v) ["null","true","false"]) pgFmtJoinCondition :: JoinCondition -> SqlFragment -pgFmtJoinCondition (JoinCondition (qi, col1) (QualifiedIdentifier schema fTable, col2)) = - pgFmtColumn qi col1 <> " = " <> - pgFmtColumn (removeSourceCTESchema schema fTable) col2 +pgFmtJoinCondition (JoinCondition (QualifiedIdentifier schema1 tName, col1) (QualifiedIdentifier schema2 ftName, col2)) = + pgFmtColumn (removeSourceCTESchema schema1 tName) col1 <> " = " <> + pgFmtColumn (removeSourceCTESchema schema2 ftName) col2 pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <> show op <> " ") (pgFmtLogicTree qi <$> forest) <> ")" @@ -183,8 +183,11 @@ pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias trimNullChars :: Text -> Text trimNullChars = T.takeWhile (/= '\x0') +-- On mutation and calling proc cases we wrap the target table in a WITH {sourceCTEName} +-- if this happens remove the schema `FROM "schema"."{sourceCTEName}"` and use only the +-- `FROM "{sourceCTEName}"`. If the schema remains the FROM would be invalid. removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier -removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == sourceCTEName then "" else schema) tbl +removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == sourceCTEName then mempty else schema) tbl countF :: SqlQuery -> Bool -> (SqlFragment, SqlFragment) countF countQuery shouldCount = diff --git a/test/Feature/InsertSpec.hs b/test/Feature/InsertSpec.hs index 51bad80c9..73c443d67 100644 --- a/test/Feature/InsertSpec.hs +++ b/test/Feature/InsertSpec.hs @@ -644,3 +644,23 @@ spec actualPgVersion = do { matchStatus = 200, matchHeaders = [matchContentTypeJson] } + + it "embeds an M2M relationship plus parent after update" $ + request methodPatch "/users?id=eq.1&select=name,tasks(name,project:projects(name))" + [("Prefer", "return=representation")] + [json|{"name": "Kevin Malone"}|] + `shouldRespondWith` + [json|[ + { + "name": "Kevin Malone", + "tasks": [ + { "name": "Design w7", "project": { "name": "Windows 7" } }, + { "name": "Code w7", "project": { "name": "Windows 7" } }, + { "name": "Design w10", "project": { "name": "Windows 10" } }, + { "name": "Code w10", "project": { "name": "Windows 10" } } + ] + } + ]|] + { matchStatus = 200, + matchHeaders = [matchContentTypeJson] + } diff --git a/test/Feature/RpcSpec.hs b/test/Feature/RpcSpec.hs index e3580a0c3..8751f7a35 100644 --- a/test/Feature/RpcSpec.hs +++ b/test/Feature/RpcSpec.hs @@ -179,6 +179,29 @@ spec actualPgVersion = `shouldRespondWith` [json|[{"id": 2, "articleStars": [{"userId": 3}]}]|] { matchHeaders = [matchContentTypeJson] } + it "can embed an M2M relationship table" $ + get "/rpc/getallusers?select=name,tasks(name)&id=gt.1" + `shouldRespondWith` [json|[ + {"name":"Michael Scott", "tasks":[{"name":"Design IOS"}, {"name":"Code IOS"}, {"name":"Design OSX"}]}, + {"name":"Dwight Schrute","tasks":[{"name":"Design w7"}, {"name":"Design IOS"}]} + ]|] + { matchHeaders = [matchContentTypeJson] } + + it "can embed an M2M relationship table that has a parent relationship table" $ + get "/rpc/getallusers?select=name,tasks(name,project:projects(name))&id=gt.1" + `shouldRespondWith` [json|[ + {"name":"Michael Scott","tasks":[ + {"name":"Design IOS","project":{"name":"IOS"}}, + {"name":"Code IOS","project":{"name":"IOS"}}, + {"name":"Design OSX","project":{"name":"OSX"}} + ]}, + {"name":"Dwight Schrute","tasks":[ + {"name":"Design w7","project":{"name":"Windows 7"}}, + {"name":"Design IOS","project":{"name":"IOS"}} + ]} + ]|] + { matchHeaders = [matchContentTypeJson] } + context "a proc that returns an empty rowset" $ it "returns empty json array" $ do post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith` diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 192408bfd..d7ca13ffa 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -1749,3 +1749,7 @@ CREATE TABLE web_content ( p_web_id integer references web_content(id), primary key (id) ); + +CREATE FUNCTION getallusers() RETURNS SETOF users AS $$ + SELECT * FROM test.users; +$$ LANGUAGE sql;