diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ffc4a9c..4105bf4e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ 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 ### Changed diff --git a/src/PostgREST/DbRequestBuilder.hs b/src/PostgREST/DbRequestBuilder.hs index 9ed45c5d7..5eebc6904 100644 --- a/src/PostgREST/DbRequestBuilder.hs +++ b/src/PostgREST/DbRequestBuilder.hs @@ -59,17 +59,12 @@ rootWithRelations rootTableName allRels action = case action of ActionRead _ -> (rootTableName, allRels) -- normal read case _ -> (sourceCTEName, mapMaybe toSourceRelation allRels ++ allRels) -- mutation cases and calling proc where - -- in a relation where one of the tables matches "TableName" - -- replace the name to that table with pg_source - -- this "fake" relations is needed so that in a mutate query or proc call - -- we can look at the "returning *" part which is wrapped with a "with pg_source" - -- as just another table that has relations with other tables + -- To enable embedding in the sourceCTEName cases we need to replace the foreign key tableName in the Relation + -- with {sourceCTEName}. This way findRelation can find Relations with sourceCTEName. toSourceRelation :: Relation -> Maybe Relation - toSourceRelation r@(Relation t _ ft _ _ rt _ _) - | rootTableName == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}} - | rootTableName == tableName ft = Just $ r {relFTable=t {tableName=sourceCTEName}} - | Just rootTableName == (tableName <$> rt) = Just $ r {relLinkTable=(\tbl -> tbl {tableName=sourceCTEName}) <$> rt} - | otherwise = Nothing + toSourceRelation r@Relation{relFTable=ft} + | rootTableName == tableName ft = Just $ r {relFTable=ft {tableName=sourceCTEName}} + | otherwise = Nothing -- Build the initial tree with a Depth attribute so when a self join occurs we can differentiate the parent and child tables by having -- an alias like "table_depth", this is related to http://github.com/PostgREST/postgrest/issues/987. diff --git a/test/Feature/InsertSpec.hs b/test/Feature/InsertSpec.hs index 8f31cd602..1bd6cf600 100644 --- a/test/Feature/InsertSpec.hs +++ b/test/Feature/InsertSpec.hs @@ -588,3 +588,60 @@ spec actualPgVersion = do liftIO $ do simpleBody p2 `shouldBe` [str|[{"owner":"jroe","secret":"lolcat"}]|] simpleStatus p2 `shouldBe` created201 + + context "tables with self reference foreign keys" $ do + it "embeds parent after insert" $ + request methodPost "/web_content?select=id,name,parent_content:p_web_id(name)" + [("Prefer", "return=representation")] + [json|{"id":6, "name":"wot", "p_web_id":4}|] + `shouldRespondWith` + [json|[{"id":6,"name":"wot","parent_content":{"name":"wut"}}]|] + { matchStatus = 201 + , matchHeaders = [ matchContentTypeJson , "Location" <:> "/web_content?id=eq.6" ] + } + + it "embeds childs after update" $ + request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)" + [("Prefer", "return=representation")] + [json|{"name": "tardis-patched"}|] + `shouldRespondWith` + [json| + [ { "id": 0, "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ] + |] + { matchStatus = 200, + matchHeaders = [matchContentTypeJson] + } + + it "embeds parent, childs and grandchilds after update" $ + request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)" + [("Prefer", "return=representation")] + [json|{"name": "tardis-patched-2"}|] + `shouldRespondWith` + [json| [ + { + "id": 0, + "name": "tardis-patched-2", + "parent_content": { "name": "wat" }, + "web_content": [ + { "name": "fezz", "web_content": [ { "name": "wut" } ] }, + { "name": "foo", "web_content": [] }, + { "name": "bar", "web_content": [] } + ] + } + ] |] + { matchStatus = 200, + matchHeaders = [matchContentTypeJson] + } + + it "embeds childs after update without explicitly including the id in the ?select" $ do + pendingWith "currently failing" + request methodPatch "/web_content?id=eq.0&select=name,web_content(name)" + [("Prefer", "return=representation")] + [json|{"name": "tardis-patched"}|] + `shouldRespondWith` + [json| + [ { "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ] + |] + { matchStatus = 200, + matchHeaders = [matchContentTypeJson] + } diff --git a/test/fixtures/data.sql b/test/fixtures/data.sql index d3921378c..4027f0406 100644 --- a/test/fixtures/data.sql +++ b/test/fixtures/data.sql @@ -499,3 +499,11 @@ COPY pgrst_reserved_chars ("*id*", ":arr->ow::cast", "(inside,parens)", "a.dotte 2 | arrow-2 | parens-2 | dotted-2 | space-2 3 | arrow-3 | parens-3 | dotted-3 | space-3 \. + +TRUNCATE TABLE web_content CASCADE; +INSERT INTO web_content VALUES (5, 'wat', null); +INSERT INTO web_content VALUES (0, 'tardis', 5); +INSERT INTO web_content VALUES (1, 'fezz', 0); +INSERT INTO web_content VALUES (2, 'foo', 0); +INSERT INTO web_content VALUES (3, 'bar', 0); +INSERT INTO web_content VALUES (4, 'wut', 1); diff --git a/test/fixtures/privileges.sql b/test/fixtures/privileges.sql index 186e4e6c5..cfd0050bc 100644 --- a/test/fixtures/privileges.sql +++ b/test/fixtures/privileges.sql @@ -103,6 +103,7 @@ GRANT ALL ON TABLE , openapi_types , getallprojects_view , get_projects_above_view + , web_content TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 9c6a04db5..192408bfd 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -1742,3 +1742,10 @@ select * from getallprojects(); create view get_projects_above_view as select * from get_projects_above(1); + +CREATE TABLE web_content ( + id integer, + name text, + p_web_id integer references web_content(id), + primary key (id) +);