Fix M2M resource embedding on RPC and mutations
This commit is contained in:
committed by
Steve Chávez
parent
d71d3450af
commit
75a42b77ea
+2
-1
@@ -9,7 +9,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
- #1383, Add support for HEAD request - @steve-chavez
|
- #1383, Add support for HEAD request - @steve-chavez
|
||||||
- #1378, Add support for `Prefer: count=planned` and `Prefer: count=estimated` on GET /table - @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
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -336,9 +336,13 @@ returningCols rr@(Node _ forest) = returnings
|
|||||||
-- `RETURNING name`(see QueryBuilder).
|
-- `RETURNING name`(see QueryBuilder).
|
||||||
-- This would make the embedding fail because the following JOIN would need the "client_id" column from projects.
|
-- 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`.
|
-- 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
|
fkCols = concat $ mapMaybe (\case
|
||||||
Node (_, (_, Just Relation{relFColumns=cols, relType=Parent}, _, _, _)) _ -> Just cols
|
Node (_, (_, Just Relation{relFColumns=cols, relType=relTyp}, _, _, _)) _ -> case relTyp of
|
||||||
Node (_, (_, Just Relation{relFColumns=cols, relType=Child}, _, _, _)) _ -> Just cols
|
Parent -> Just cols
|
||||||
|
Child -> Just cols
|
||||||
|
Many -> Just cols
|
||||||
|
_ -> Nothing
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
) forest
|
) forest
|
||||||
-- However if the "client_id" is present, e.g. mutateRequest to /projects?select=client_id,name,clients(name)
|
-- However if the "client_id" is present, e.g. mutateRequest to /projects?select=client_id,name,clients(name)
|
||||||
|
|||||||
@@ -150,9 +150,9 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper
|
|||||||
(find ((==) . toLower $ v) ["null","true","false"])
|
(find ((==) . toLower $ v) ["null","true","false"])
|
||||||
|
|
||||||
pgFmtJoinCondition :: JoinCondition -> SqlFragment
|
pgFmtJoinCondition :: JoinCondition -> SqlFragment
|
||||||
pgFmtJoinCondition (JoinCondition (qi, col1) (QualifiedIdentifier schema fTable, col2)) =
|
pgFmtJoinCondition (JoinCondition (QualifiedIdentifier schema1 tName, col1) (QualifiedIdentifier schema2 ftName, col2)) =
|
||||||
pgFmtColumn qi col1 <> " = " <>
|
pgFmtColumn (removeSourceCTESchema schema1 tName) col1 <> " = " <>
|
||||||
pgFmtColumn (removeSourceCTESchema schema fTable) col2
|
pgFmtColumn (removeSourceCTESchema schema2 ftName) col2
|
||||||
|
|
||||||
pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment
|
pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment
|
||||||
pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <> show op <> " ") (pgFmtLogicTree qi <$> forest) <> ")"
|
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 :: Text -> Text
|
||||||
trimNullChars = T.takeWhile (/= '\x0')
|
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 -> 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 :: SqlQuery -> Bool -> (SqlFragment, SqlFragment)
|
||||||
countF countQuery shouldCount =
|
countF countQuery shouldCount =
|
||||||
|
|||||||
@@ -644,3 +644,23 @@ spec actualPgVersion = do
|
|||||||
{ matchStatus = 200,
|
{ matchStatus = 200,
|
||||||
matchHeaders = [matchContentTypeJson]
|
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]
|
||||||
|
}
|
||||||
|
|||||||
@@ -179,6 +179,29 @@ spec actualPgVersion =
|
|||||||
`shouldRespondWith` [json|[{"id": 2, "articleStars": [{"userId": 3}]}]|]
|
`shouldRespondWith` [json|[{"id": 2, "articleStars": [{"userId": 3}]}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ 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" $
|
context "a proc that returns an empty rowset" $
|
||||||
it "returns empty json array" $ do
|
it "returns empty json array" $ do
|
||||||
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
|
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
|
||||||
|
|||||||
Vendored
+4
@@ -1749,3 +1749,7 @@ CREATE TABLE web_content (
|
|||||||
p_web_id integer references web_content(id),
|
p_web_id integer references web_content(id),
|
||||||
primary key (id)
|
primary key (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE FUNCTION getallusers() RETURNS SETOF users AS $$
|
||||||
|
SELECT * FROM test.users;
|
||||||
|
$$ LANGUAGE sql;
|
||||||
|
|||||||
Reference in New Issue
Block a user