module Feature.Query.ComputedRelsSpec where import Network.Wai (Application) import Network.HTTP.Types import Test.Hspec import Test.Hspec.Wai import Test.Hspec.Wai.JSON import Protolude hiding (get) import SpecHelper spec :: SpecWith ((), Application) spec = describe "computed relationships" $ do it "can define a many-to-one relationship with SETOF and ROWS 1 and embed" $ get "/videogames?select=name,designer:computed_designers(name)" `shouldRespondWith` [json|[ {"name":"Civilization I","designer":{"name":"Sid Meier"}}, {"name":"Civilization II","designer":{"name":"Sid Meier"}}, {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}}, {"name":"Final Fantasy II","designer":{"name":"Hironobu Sakaguchi"}} ]|] { matchHeaders = [matchContentTypeJson] } it "can define a many-to-one relationship without SETOF and embed" $ get "/videogames?select=name,designer:computed_designers_noset(name)" `shouldRespondWith` [json|[ {"name":"Civilization I","designer":{"name":"Sid Meier"}}, {"name":"Civilization II","designer":{"name":"Sid Meier"}}, {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}}, {"name":"Final Fantasy II","designer":{"name":"Hironobu Sakaguchi"}} ]|] { matchHeaders = [matchContentTypeJson] } it "can define a one-to-many relationship and embed" $ get "/designers?select=name,videogames:computed_videogames(name)" `shouldRespondWith` [json|[ {"name":"Sid Meier","videogames":[{"name":"Civilization I"}, {"name":"Civilization II"}]}, {"name":"Hironobu Sakaguchi","videogames":[{"name":"Final Fantasy I"}, {"name":"Final Fantasy II"}]} ]|] { matchHeaders = [matchContentTypeJson] } it "works with !inner and count=exact" $ do request methodGet "/designers?select=name,videogames:computed_videogames!inner(name)&videogames.name=eq.Civilization%20I" [("Prefer", "count=exact")] "" `shouldRespondWith` [json|[{"name":"Sid Meier","videogames":[{"name":"Civilization I"}]}]|] { matchStatus = 200 , matchHeaders = ["Content-Range" <:> "0-0/1"] } request methodGet "/videogames?select=name,designer:computed_designers!inner(name)&designer.name=like.*Hironobu*" [("Prefer", "count=exact")] "" `shouldRespondWith` [json|[ {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}}, {"name":"Final Fantasy II","designer":{"name":"Hironobu Sakaguchi"}} ]|] { matchStatus = 200 , matchHeaders = ["Content-Range" <:> "0-1/2"] } request methodGet "/videogames?select=name,designer:computed_designers_noset!inner(name)&designer.name=like.*Hironobu*" [("Prefer", "count=exact")] "" `shouldRespondWith` [json|[ {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}}, {"name":"Final Fantasy II","designer":{"name":"Hironobu Sakaguchi"}} ]|] { matchStatus = 200 , matchHeaders = ["Content-Range" <:> "0-1/2"] } it "works with rpc" $ do get "/rpc/getallvideogames?select=name,designer:computed_designers(name)" `shouldRespondWith` [json|[ {"name":"Civilization I","designer":{"name":"Sid Meier"}}, {"name":"Civilization II","designer":{"name":"Sid Meier"}}, {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}}, {"name":"Final Fantasy II","designer":{"name":"Hironobu Sakaguchi"}} ]|] { matchHeaders = [matchContentTypeJson] } get "/rpc/getalldesigners?select=name,videogames:computed_videogames(name)" `shouldRespondWith` [json|[ {"name":"Sid Meier","videogames":[{"name":"Civilization I"}, {"name":"Civilization II"}]}, {"name":"Hironobu Sakaguchi","videogames":[{"name":"Final Fantasy I"}, {"name":"Final Fantasy II"}]} ]|] { matchHeaders = [matchContentTypeJson] } it "works with mutations" $ do request methodPost "/videogames?select=name,designer:computed_designers(name)" [("Prefer", "return=representation")] [json| {"id": 5, "name": "Chrono Trigger", "designer_id": 2} |] `shouldRespondWith` [json|[ {"name":"Chrono Trigger","designer":{"name":"Hironobu Sakaguchi"}} ]|] { matchStatus = 201 } request methodPatch "/designers?select=name,videogames:computed_videogames(name)&id=eq.1" [("Prefer", "return=representation")] [json| {"name": "Sidney K. Meier"} |] `shouldRespondWith` [json|[ { "name": "Sidney K. Meier", "videogames": [{"name":"Civilization I"}, {"name":"Civilization II"}] } ]|] { matchStatus = 200 } request methodDelete "/videogames?select=name,designer:computed_designers(name)&id=eq.3" [("Prefer", "return=representation")] "" `shouldRespondWith` [json|[ {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}} ]|] { matchStatus = 200 } it "applies data representations to response" $ do -- A smoke test for data reps in the presence of computed relations. -- The data rep here title cases the designer name before presentation. So here the lowercase version will be saved, -- but the title case version returned. Pulling in a computed relation should not confuse this. request methodPatch "/designers?select=name,videogames:computed_videogames(name)&id=eq.1" [("Prefer", "return=representation"), ("Prefer", "tx=commit")] [json| {"name": "sidney k. meier"} |] `shouldRespondWith` [json|[{"name":"Sidney K. Meier","videogames":[{"name":"Civilization I"}, {"name":"Civilization II"}]}]|] { matchStatus = 200 } -- Verify it was saved the way we requested (there's no text data rep for this column, so if we select with the wrong casing, it should fail.) get "/designers?select=id&name=eq.Sidney%20K.%20Meier" `shouldRespondWith` [json|[]|] { matchStatus = 200, matchHeaders = [matchContentTypeJson] } -- But with the right casing it works. get "/designers?select=id,name&name=eq.sidney%20k.%20meier" `shouldRespondWith` [json|[{"id": 1, "name":"Sidney K. Meier"}]|] { matchStatus = 200, matchHeaders = [matchContentTypeJson] } -- Most importantly, if you read it back even via a computed relation, the data rep should be applied. get "/videogames?select=name,designer:computed_designers(*)&id=eq.1" `shouldRespondWith` [json|[ {"name":"Civilization I","designer":{"id": 1, "name":"Sidney K. Meier"}} ]|] { matchHeaders = [matchContentTypeJson] } -- reset the test fixture request methodPatch "/designers?id=eq.1" [("Prefer", "tx=commit")] [json| {"name": "Sid Meier"} |] `shouldRespondWith` 204 -- need to poke the second one too to prevent inherent ordering from changing request methodPatch "/designers?id=eq.2" [("Prefer", "tx=commit")] [json| {"name": "Hironobu Sakaguchi"} |] `shouldRespondWith` 204 it "works with self joins" $ get "/web_content?select=name,child_web_content(name),parent_web_content(name)&id=in.(0,1)" `shouldRespondWith` [json|[ {"name":"tardis","child_web_content":[{"name":"fezz"}, {"name":"foo"}, {"name":"bar"}],"parent_web_content":{"name":"wat"}}, {"name":"fezz","child_web_content":[{"name":"wut"}],"parent_web_content":{"name":"tardis"}} ]|] { matchHeaders = [matchContentTypeJson] } it "can override many-to-one and one-to-many relationships" $ do get "/videogames?select=*,designers!inner(*)" `shouldRespondWith` [json|[]|] { matchHeaders = [matchContentTypeJson] } get "/designers?select=*,videogames!inner(*)" `shouldRespondWith` [json|[]|] { matchHeaders = [matchContentTypeJson] } it "can override one-to-one relationships(would give disambiguation errors otherwise)" $ do get "/first_1?select=*,second_1(*)" `shouldRespondWith` [json|[]|] { matchHeaders = [matchContentTypeJson] } get "/second_1?select=*,first_1(*)" `shouldRespondWith` [json|[]|] { matchHeaders = [matchContentTypeJson] } -- https://github.com/PostgREST/postgrest/issues/2455 it "creates queries with the right aliasing" $ do get "/fee?select=*,jsbaz(*,janedoe(*))" `shouldRespondWith` [json|[]|] { matchHeaders = [matchContentTypeJson] } get "/fee?select=*,jsbaz(*,johnsmith(*, fee(*)))" `shouldRespondWith` [json|[]|] { matchHeaders = [matchContentTypeJson] } it "creates queries with the right aliasing when following a normal embed" $ do get "/projects?select=name,clients(name,computed_projects(name))&limit=1" `shouldRespondWith` [json| [{"name":"Windows 7","clients":{"name":"Microsoft","computed_projects":{"name":"Windows 7"}}}] |] { matchHeaders = [matchContentTypeJson] } get "/clients?select=name,projects(name,computed_clients(name))&limit=1" `shouldRespondWith` [json|[ {"name":"Microsoft","projects":[ {"name":"Windows 7","computed_clients":{"name":"Microsoft"}}, {"name":"Windows 10","computed_clients":{"name":"Microsoft"}} ]} ]|] { matchHeaders = [matchContentTypeJson] } -- https://github.com/PostgREST/postgrest/issues/2963 context "can be defined using overloaded functions" $ do it "tables" $ do get "/items?select=*,computed_rel_overload(*)&limit=1" `shouldRespondWith` [json| [{"id":1,"computed_rel_overload":[{"id":1}]}] |] { matchHeaders = [matchContentTypeJson] } get "/items2?select=*,computed_rel_overload(*)&limit=1" `shouldRespondWith` [json| [{"id":1,"computed_rel_overload":[{"id":1},{"id":2}]}] |] { matchHeaders = [matchContentTypeJson] } it "rpc" $ do get "/rpc/search?id=1&select=*,computed_rel_overload(*)" `shouldRespondWith` [json| [{"id":1,"computed_rel_overload":[{"id":1}]}] |] { matchHeaders = [matchContentTypeJson] } get "/rpc/search2?id=1&select=*,computed_rel_overload(*)" `shouldRespondWith` [json| [{"id":1,"computed_rel_overload":[{"id":1},{"id":2}]}] |] { matchHeaders = [matchContentTypeJson] }