feat: add computed relationships

* work for select, mutations, rpc
* overrides detected relationships
This commit is contained in:
steve-chavez
2022-08-17 22:50:06 -05:00
committed by Steve Chavez
parent 06c9e246f4
commit d6ec171bcb
12 changed files with 281 additions and 23 deletions
+101
View File
@@ -0,0 +1,101 @@
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 and embed" $
get "/videogames?select=name,designers:computed_designers(name)"
`shouldRespondWith`
[json|[
{"name":"Civilization I","designers":{"name":"Sid Meier"}},
{"name":"Civilization II","designers":{"name":"Sid Meier"}},
{"name":"Final Fantasy I","designers":{"name":"Hironobu Sakaguchi"}},
{"name":"Final Fantasy II","designers":{"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"]
}
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 "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 detected relationships" $ do
get "/videogames?select=*,designers!inner(*)"
`shouldRespondWith`
[json|[]|] { matchHeaders = [matchContentTypeJson] }
get "/designers?select=*,videogames!inner(*)"
`shouldRespondWith`
[json|[]|] { matchHeaders = [matchContentTypeJson] }
+2
View File
@@ -37,6 +37,7 @@ import qualified Feature.OpenApi.RootSpec
import qualified Feature.OpenApi.SecurityOpenApiSpec
import qualified Feature.OptionsSpec
import qualified Feature.Query.AndOrParamsSpec
import qualified Feature.Query.ComputedRelsSpec
import qualified Feature.Query.DeleteSpec
import qualified Feature.Query.EmbedDisambiguationSpec
import qualified Feature.Query.EmbedInnerJoinSpec
@@ -147,6 +148,7 @@ main = do
, ("Feature.Query.SingularSpec" , Feature.Query.SingularSpec.spec)
, ("Feature.Query.UpdateSpec" , Feature.Query.UpdateSpec.spec)
, ("Feature.Query.UpsertSpec" , Feature.Query.UpsertSpec.spec actualPgVersion)
, ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec)
]
hspec $ do
+6
View File
@@ -806,3 +806,9 @@ TRUNCATE TABLE unsafe_update_items CASCADE;
INSERT INTO unsafe_update_items(id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);
TRUNCATE TABLE unsafe_delete_items CASCADE;
INSERT INTO unsafe_delete_items(id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);
TRUNCATE TABLE designers CASCADE;
INSERT INTO designers(id, name) VALUES (1, 'Sid Meier'), (2, 'Hironobu Sakaguchi');
TRUNCATE TABLE videogames CASCADE;
INSERT INTO videogames(id, name, designer_id) VALUES (1, 'Civilization I', 1), (2, 'Civilization II', 1), (3, 'Final Fantasy I', 2), (4, 'Final Fantasy II', 2);
+2
View File
@@ -197,6 +197,8 @@ GRANT ALL ON TABLE
, safe_delete_items
, unsafe_update_items
, unsafe_delete_items
, videogames
, designers
TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
+46
View File
@@ -2707,3 +2707,49 @@ CREATE OR REPLACE FUNCTION test.load_safeupdate() RETURNS VOID AS $$
BEGIN
LOAD 'safeupdate';
END; $$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TABLE designers (
id int primary key
, name text
);
CREATE TABLE videogames (
id int primary key
, name text
, designer_id int references designers(id)
);
-- computed relationships
CREATE FUNCTION test.computed_designers(test.videogames) RETURNS SETOF test.designers AS $$
SELECT * FROM test.designers WHERE id = $1.designer_id;
$$ LANGUAGE sql STABLE ROWS 1;
CREATE FUNCTION test.computed_videogames(test.designers) RETURNS SETOF test.videogames AS $$
SELECT * FROM test.videogames WHERE designer_id = $1.id;
$$ LANGUAGE sql STABLE;
CREATE FUNCTION test.getallvideogames() RETURNS SETOF test.videogames AS $$
SELECT * FROM test.videogames;
$$ LANGUAGE sql STABLE;
CREATE FUNCTION test.getalldesigners() RETURNS SETOF test.designers AS $$
SELECT * FROM test.designers;
$$ LANGUAGE sql STABLE;
-- self join for computed relationships
CREATE FUNCTION test.child_web_content(test.web_content) RETURNS SETOF test.web_content AS $$
SELECT * FROM test.web_content WHERE $1.id = p_web_id;
$$ LANGUAGE sql STABLE;
CREATE FUNCTION test.parent_web_content(test.web_content) RETURNS SETOF test.web_content AS $$
SELECT * FROM test.web_content WHERE $1.p_web_id = id;
$$ LANGUAGE sql STABLE ROWS 1;
-- overriding computed rels that empty the results
CREATE FUNCTION test.designers(test.videogames) RETURNS SETOF test.designers AS $$
SELECT * FROM test.designers WHERE FALSE;
$$ LANGUAGE sql STABLE ROWS 1;
CREATE FUNCTION test.videogames(test.designers) RETURNS SETOF test.videogames AS $$
SELECT * FROM test.videogames WHERE FALSE;
$$ LANGUAGE sql STABLE;