feat: add one to one relationship for embedding
BREAKING CHANGE: For the cases where one to one relationships are detected, json objects will be returned instead of json arrays of length 1. If you wish to override this behavior, you can use computed relationships to return arrays again.
This commit is contained in:
committed by
Steve Chavez
parent
2afe13fa89
commit
c45e85c5a6
@@ -92,10 +92,18 @@ spec = describe "computed relationships" $ do
|
||||
{"name":"fezz","child_web_content":[{"name":"wut"}],"parent_web_content":{"name":"tardis"}}
|
||||
]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can override detected relationships" $ do
|
||||
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] }
|
||||
|
||||
@@ -78,6 +78,32 @@ spec =
|
||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||
}
|
||||
|
||||
it "embeds an O2O relationship after delete" $ do
|
||||
request methodDelete "/students?id=eq.1&select=name,students_info(address)"
|
||||
[("Prefer", "return=representation")] ""
|
||||
`shouldRespondWith`
|
||||
[json|[
|
||||
{
|
||||
"name": "John Doe",
|
||||
"students_info":{"address":"Street 1"}
|
||||
}
|
||||
]|]
|
||||
{ matchStatus = 200,
|
||||
matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
request methodDelete "/students_info?id=eq.1&select=address,students(name)"
|
||||
[("Prefer", "return=representation")] ""
|
||||
`shouldRespondWith`
|
||||
[json|[
|
||||
{
|
||||
"address": "Street 1",
|
||||
"students":{"name": "John Doe"}
|
||||
}
|
||||
]|]
|
||||
{ matchStatus = 200,
|
||||
matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
context "known route, no records matched" $
|
||||
it "includes [] body if return=rep" $
|
||||
request methodDelete "/items?id=eq.101"
|
||||
|
||||
@@ -106,6 +106,20 @@ spec =
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
it "errs on an ambiguous embed that has two one-to-one relationships" $
|
||||
get "/first?select=second(*)" `shouldRespondWith`
|
||||
[json| {
|
||||
"code":"PGRST201",
|
||||
"details":[
|
||||
{"cardinality":"one-to-one","embedding":"first with second","relationship":"first_second_id_1_fkey using first(second_id_1) and second(id)"},
|
||||
{"cardinality":"one-to-one","embedding":"first with second","relationship":"first_second_id_2_fkey using first(second_id_2) and second(id)"}
|
||||
],
|
||||
"hint":"Try changing 'second' to one of the following: 'second!first_second_id_1_fkey', 'second!first_second_id_2_fkey'. Find the desired relationship in the 'details' key.","message":"Could not embed because more than one relationship was found for 'first' and 'second'"
|
||||
}|]
|
||||
{ matchStatus = 300
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
context "disambiguating requests with embed hints" $ do
|
||||
|
||||
context "using FK to specify the relationship" $ do
|
||||
|
||||
@@ -460,6 +460,51 @@ spec actualPgVersion = do
|
||||
[json|[]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "one to one relationships" $ do
|
||||
it "works when having a pk as fk" $ do
|
||||
get "/students_info?select=address,students(name)" `shouldRespondWith`
|
||||
[json|[{"address":"Street 1","students":{"name":"John Doe"}}, {"address":"Street 2","students":{"name":"Jane Doe"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/students?select=name,students_info(address)" `shouldRespondWith`
|
||||
[json|[{"name":"John Doe","students_info":{"address":"Street 1"}},{"name":"Jane Doe","students_info":{"address":"Street 2"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when having a fk with a unique constraint" $ do
|
||||
get "/country?select=name,capital(name)" `shouldRespondWith`
|
||||
[json|[{"name":"Afghanistan","capital":{"name":"Kabul"}}, {"name":"Algeria","capital":{"name":"Algiers"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/capital?select=name,country(name)" `shouldRespondWith`
|
||||
[json|[{"name":"Kabul","country":{"name":"Afghanistan"}}, {"name":"Algiers","country":{"name":"Algeria"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when using column as target" $ do
|
||||
get "/capital?select=name,country_id(name)" `shouldRespondWith`
|
||||
[json|[{"name":"Kabul","country_id":{"name":"Afghanistan"}}, {"name":"Algiers","country_id":{"name":"Algeria"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/capital?select=name,capital_country_id_fkey(name)" `shouldRespondWith`
|
||||
[json|[{"name":"Kabul","capital_country_id_fkey":{"name":"Afghanistan"}}, {"name":"Algiers","capital_country_id_fkey":{"name":"Algeria"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/country?select=name,capital_country_id_fkey(name)" `shouldRespondWith`
|
||||
[json|[{"name":"Afghanistan","capital_country_id_fkey":{"name":"Kabul"}}, {"name":"Algeria","capital_country_id_fkey":{"name":"Algiers"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/country?select=name,id(name)" `shouldRespondWith`
|
||||
[json|[{"name":"Afghanistan","id":{"name":"Kabul"}}, {"name":"Algeria","id":{"name":"Algiers"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when using column as hint" $ do
|
||||
get "/country?select=name,capital!id(name)" `shouldRespondWith`
|
||||
[json|[{"name":"Afghanistan","capital":{"name":"Kabul"}}, {"name":"Algeria","capital":{"name":"Algiers"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/country?select=name,capital!country_id(name)" `shouldRespondWith`
|
||||
[json|[{"name":"Afghanistan","capital":{"name":"Kabul"}}, {"name":"Algeria","capital":{"name":"Algiers"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/capital?select=name,country!id(name)" `shouldRespondWith`
|
||||
[json|[{"name":"Kabul","country":{"name":"Afghanistan"}}, {"name":"Algiers","country":{"name":"Algeria"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/capital?select=name,country!country_id(name)" `shouldRespondWith`
|
||||
[json|[{"name":"Kabul","country":{"name":"Afghanistan"}}, {"name":"Algiers","country":{"name":"Algeria"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
describe "computed columns" $ do
|
||||
it "computed column on table" $
|
||||
get "/items?id=eq.1&select=id,always_true" `shouldRespondWith`
|
||||
@@ -707,6 +752,20 @@ spec actualPgVersion = do
|
||||
[json| [{"name":"George Orwell","entities":[3, 4],"books":[{"title":"1984"}]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with one to one relationships" $ do
|
||||
get "/students_view?select=name,students_info(address)" `shouldRespondWith`
|
||||
[json| [{"name":"John Doe","students_info":{"address":"Street 1"}}, {"name":"Jane Doe","students_info":{"address":"Street 2"}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/students_view?select=name,students_info_view(address)" `shouldRespondWith`
|
||||
[json| [{"name":"John Doe","students_info_view":{"address":"Street 1"}}, {"name":"Jane Doe","students_info_view":{"address":"Street 2"}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/students_info_view?select=address,students(name)" `shouldRespondWith`
|
||||
[json| [{"address":"Street 1","students":{"name":"John Doe"}}, {"address":"Street 2","students":{"name":"Jane Doe"}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/students_info_view?select=address,students_view(name)" `shouldRespondWith`
|
||||
[json| [{"address":"Street 1","students_view":{"name":"John Doe"}}, {"address":"Street 2","students_view":{"name":"Jane Doe"}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
describe "aliased embeds" $ do
|
||||
it "works with child relation" $
|
||||
get "/space?select=id,zones:zone(id,name),stores:zone(id,name)&zones.zone_type_id=eq.2&stores.zone_type_id=eq.3" `shouldRespondWith`
|
||||
|
||||
@@ -269,6 +269,20 @@ spec actualPgVersion =
|
||||
]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can embed an O2O relationship" $ do
|
||||
get "/rpc/allcapitals?select=name,country(name)"
|
||||
`shouldRespondWith` [json|[
|
||||
{"name":"Kabul","country":{"name":"Afghanistan"}},
|
||||
{"name":"Algiers","country":{"name":"Algeria"}}]
|
||||
|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/rpc/allcountries?select=name,capital(name)"
|
||||
`shouldRespondWith` [json|[
|
||||
{"name":"Afghanistan","capital":{"name":"Kabul"}},
|
||||
{"name":"Algeria","capital":{"name":"Algiers"}}
|
||||
]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
when (actualPgVersion >= pgVersion110) $
|
||||
it "can embed if rpc returns domain of table type" $ do
|
||||
post "/rpc/getproject_domain?select=id,name,client:clients(id),tasks(id)"
|
||||
|
||||
@@ -374,6 +374,34 @@ spec = do
|
||||
matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
it "embeds an O2O relationship after update" $ do
|
||||
request methodPatch "/students?id=eq.1&select=name,students_info(address)"
|
||||
[("Prefer", "return=representation")]
|
||||
[json|{"name": "Johnny Doe"}|]
|
||||
`shouldRespondWith`
|
||||
[json|[
|
||||
{
|
||||
"name": "Johnny Doe",
|
||||
"students_info":{"address":"Street 1"}
|
||||
}
|
||||
]|]
|
||||
{ matchStatus = 200,
|
||||
matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
request methodPatch "/students_info?id=eq.1&select=address,students(name)"
|
||||
[("Prefer", "return=representation")]
|
||||
[json|{"address": "New Street 1"}|]
|
||||
`shouldRespondWith`
|
||||
[json|[
|
||||
{
|
||||
"address": "New Street 1",
|
||||
"students":{"name": "John Doe"}
|
||||
}
|
||||
]|]
|
||||
{ matchStatus = 200,
|
||||
matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
context "table with limited privileges" $ do
|
||||
it "succeeds updating row and gives a 204 when using return=minimal" $
|
||||
request methodPatch "/app_users?id=eq.1"
|
||||
|
||||
Vendored
+12
@@ -812,3 +812,15 @@ 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);
|
||||
|
||||
TRUNCATE TABLE students CASCADE;
|
||||
INSERT INTO students(id, code, name) VALUES (1, '0001', 'John Doe'), (2, '0002', 'Jane Doe');
|
||||
|
||||
TRUNCATE TABLE students_info CASCADE;
|
||||
INSERT INTO students_info(id, code, address) VALUES (1, '0001', 'Street 1'), (2, '0002', 'Street 2');
|
||||
|
||||
TRUNCATE TABLE country CASCADE;
|
||||
INSERT INTO country(id, name) VALUES (1, 'Afghanistan'), (2, 'Algeria');
|
||||
|
||||
TRUNCATE TABLE capital CASCADE;
|
||||
INSERT INTO capital(id, name, country_id) VALUES (1, 'Kabul', 1), (2, 'Algiers', 2);
|
||||
|
||||
Vendored
+10
@@ -199,6 +199,16 @@ GRANT ALL ON TABLE
|
||||
, unsafe_delete_items
|
||||
, videogames
|
||||
, designers
|
||||
, students
|
||||
, students_info
|
||||
, students_view
|
||||
, students_info_view
|
||||
, country
|
||||
, capital
|
||||
, first
|
||||
, second
|
||||
, first_1
|
||||
, second_1
|
||||
TO postgrest_test_anonymous;
|
||||
|
||||
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
|
||||
|
||||
Vendored
+74
-1
@@ -2400,7 +2400,7 @@ CREATE TABLE contact (
|
||||
|
||||
CREATE TABLE clientinfo (
|
||||
id serial primary key
|
||||
, clientid int unique references client(id)
|
||||
, clientid int references client(id)
|
||||
, other text
|
||||
);
|
||||
|
||||
@@ -2753,3 +2753,76 @@ $$ 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;
|
||||
|
||||
CREATE TABLE test.students(
|
||||
id int
|
||||
, code text
|
||||
, name text
|
||||
, primary key(id, code)
|
||||
);
|
||||
|
||||
CREATE TABLE test.students_info(
|
||||
id int
|
||||
, code text
|
||||
, address text
|
||||
, primary key(id, code)
|
||||
, foreign key (id, code) references test.students(id, code) on delete cascade
|
||||
);
|
||||
|
||||
CREATE TABLE test.country(
|
||||
id int primary key
|
||||
, name text
|
||||
);
|
||||
|
||||
CREATE TABLE test.capital(
|
||||
id int primary key
|
||||
, name text
|
||||
, country_id int unique
|
||||
, foreign key (country_id) references test.country(id)
|
||||
);
|
||||
|
||||
CREATE FUNCTION test.allcountries() RETURNS SETOF test.country AS $$
|
||||
SELECT * FROM test.country;
|
||||
$$ LANGUAGE sql STABLE;
|
||||
|
||||
CREATE FUNCTION test.allcapitals() RETURNS SETOF test.capital AS $$
|
||||
SELECT * FROM test.capital;
|
||||
$$ LANGUAGE sql STABLE;
|
||||
|
||||
create view students_view as
|
||||
select * from students;
|
||||
|
||||
create view students_info_view as
|
||||
select * from students_info;
|
||||
|
||||
create table test.second (
|
||||
id int primary key,
|
||||
name text
|
||||
);
|
||||
|
||||
create table test.first (
|
||||
id int primary key,
|
||||
name text,
|
||||
second_id_1 int references test.second unique,
|
||||
second_id_2 int references test.second unique
|
||||
);
|
||||
|
||||
create table test.second_1 (
|
||||
id int primary key,
|
||||
name text
|
||||
);
|
||||
|
||||
create table test.first_1 (
|
||||
id int primary key,
|
||||
name text,
|
||||
second_id_1 int references test.second unique,
|
||||
second_id_2 int references test.second unique
|
||||
);
|
||||
|
||||
CREATE FUNCTION test.second_1(test.first_1) RETURNS SETOF test.second_1 AS $$
|
||||
SELECT * FROM test.second_1 WHERE id = $1.second_id_1;
|
||||
$$ LANGUAGE sql STABLE ROWS 1;
|
||||
|
||||
CREATE FUNCTION test.first_1(test.second_1) RETURNS SETOF test.first_1 AS $$
|
||||
SELECT * FROM test.first_1 WHERE second_id_1 = $1.id;
|
||||
$$ LANGUAGE sql STABLE ROWS 1;
|
||||
|
||||
Reference in New Issue
Block a user