feat: Allow embedding without selecting any column (#2574)
This commit is contained in:
@@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
+ Allows including the join table columns when resource embedding
|
+ Allows including the join table columns when resource embedding
|
||||||
+ Allows disambiguating a recursive m2m embed
|
+ Allows disambiguating a recursive m2m embed
|
||||||
+ Allows disambiguating an embed that has a many-to-many relationship using two foreign keys on a junction
|
+ Allows disambiguating an embed that has a many-to-many relationship using two foreign keys on a junction
|
||||||
|
- #2340, Allow embedding without selecting any column - @steve-chavez
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ import Text.ParserCombinators.Parsec (GenParser, ParseError, Parser,
|
|||||||
eof, errorPos, letter,
|
eof, errorPos, letter,
|
||||||
lookAhead, many1, noneOf,
|
lookAhead, many1, noneOf,
|
||||||
notFollowedBy, oneOf,
|
notFollowedBy, oneOf,
|
||||||
optionMaybe, sepBy1, string,
|
optionMaybe, sepBy, sepBy1,
|
||||||
try, (<?>))
|
string, try, (<?>))
|
||||||
|
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
rangeGeq, rangeLimit,
|
rangeGeq, rangeLimit,
|
||||||
@@ -323,26 +323,25 @@ pTreePath = do
|
|||||||
-- >>> P.parse pFieldForest "" "*,..client(*),other(*)"
|
-- >>> P.parse pFieldForest "" "*,..client(*),other(*)"
|
||||||
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SpreadRelation {selRelation = "client", selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]},Node {rootLabel = SelectRelation {selRelation = "other", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
|
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SpreadRelation {selRelation = "client", selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]},Node {rootLabel = SelectRelation {selRelation = "other", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
|
||||||
--
|
--
|
||||||
|
-- >>> P.parse pFieldForest "" ""
|
||||||
|
-- Right []
|
||||||
|
--
|
||||||
-- >>> P.parse pFieldForest "" "id,clients(name[])"
|
-- >>> P.parse pFieldForest "" "id,clients(name[])"
|
||||||
-- Left (line 1, column 16):
|
-- Left (line 1, column 16):
|
||||||
-- unexpected '['
|
-- unexpected '['
|
||||||
-- expecting letter, digit, "-", "!", "(", "->>", "->", "::", ")", "," or end of input
|
-- expecting letter, digit, "-", "->>", "->", "::", ")", "," or end of input
|
||||||
--
|
--
|
||||||
-- >>> P.parse pFieldForest "" "data->>-78xy"
|
-- >>> P.parse pFieldForest "" "data->>-78xy"
|
||||||
-- Left (line 1, column 11):
|
-- Left (line 1, column 11):
|
||||||
-- unexpected 'x'
|
-- unexpected 'x'
|
||||||
-- expecting digit, "->", "::", ".", "," or end of input
|
-- expecting digit, "->", "::", ".", "," or end of input
|
||||||
pFieldForest :: Parser [Tree SelectItem]
|
pFieldForest :: Parser [Tree SelectItem]
|
||||||
pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
|
pFieldForest = pFieldTree `sepBy` lexeme (char ',')
|
||||||
where
|
where
|
||||||
pFieldTree :: Parser (Tree SelectItem)
|
pFieldTree = Node <$> try pSpreadRelationSelect <*> between (char '(') (char ')') pFieldForest <|>
|
||||||
pFieldTree = try (Node <$> pSpreadRelationSelect <*> between (char '(') (char ')') pFieldForest) <|>
|
Node <$> try pRelationSelect <*> between (char '(') (char ')') pFieldForest <|>
|
||||||
try (Node <$> pRelationSelect <*> between (char '(') (char ')') pFieldForest) <|>
|
|
||||||
Node <$> pFieldSelect <*> pure []
|
Node <$> pFieldSelect <*> pure []
|
||||||
|
|
||||||
pStar :: Parser Text
|
|
||||||
pStar = string "*" $> "*"
|
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- Parse field names
|
-- Parse field names
|
||||||
--
|
--
|
||||||
@@ -480,13 +479,12 @@ aliasSeparator = char ':' >> notFollowedBy (char ':')
|
|||||||
-- Left (line 1, column 6):
|
-- Left (line 1, column 6):
|
||||||
-- unexpected '>'
|
-- unexpected '>'
|
||||||
pRelationSelect :: Parser SelectItem
|
pRelationSelect :: Parser SelectItem
|
||||||
pRelationSelect = lexeme $ try ( do
|
pRelationSelect = lexeme $ do
|
||||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||||
name <- pFieldName
|
name <- pFieldName
|
||||||
(hint, jType) <- pEmbedParams
|
(hint, jType) <- pEmbedParams
|
||||||
try (void $ lookAhead (string "("))
|
try (void $ lookAhead (string "("))
|
||||||
return $ SelectRelation name alias hint jType
|
return $ SelectRelation name alias hint jType
|
||||||
)
|
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- Parse regular fields in select
|
-- Parse regular fields in select
|
||||||
@@ -524,23 +522,22 @@ pRelationSelect = lexeme $ try ( do
|
|||||||
-- unexpected end of input
|
-- unexpected end of input
|
||||||
-- expecting letter or digit
|
-- expecting letter or digit
|
||||||
pFieldSelect :: Parser SelectItem
|
pFieldSelect :: Parser SelectItem
|
||||||
pFieldSelect = lexeme $
|
pFieldSelect = lexeme $ try (do
|
||||||
try (
|
|
||||||
do
|
|
||||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
|
||||||
fld <- pField
|
|
||||||
cast' <- optionMaybe (string "::" *> pIdentifier)
|
|
||||||
pEnd
|
|
||||||
return $ SelectField fld (toS <$> cast') alias
|
|
||||||
)
|
|
||||||
<|> do
|
|
||||||
s <- pStar
|
s <- pStar
|
||||||
pEnd
|
pEnd
|
||||||
return $ SelectField (s, []) Nothing Nothing
|
return $ SelectField (s, []) Nothing Nothing)
|
||||||
|
<|> do
|
||||||
|
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||||
|
fld <- pField
|
||||||
|
cast' <- optionMaybe (string "::" *> pIdentifier)
|
||||||
|
pEnd
|
||||||
|
return $ SelectField fld (toS <$> cast') alias
|
||||||
where
|
where
|
||||||
pEnd = try (void $ lookAhead (string ")")) <|>
|
pEnd = try (void $ lookAhead (string ")")) <|>
|
||||||
try (void $ lookAhead (string ",")) <|>
|
try (void $ lookAhead (string ",")) <|>
|
||||||
try eof
|
try eof
|
||||||
|
pStar = string "*" $> "*"
|
||||||
|
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- Parse spread relations in select
|
-- Parse spread relations in select
|
||||||
@@ -565,12 +562,11 @@ pFieldSelect = lexeme $
|
|||||||
-- Left (line 1, column 8):
|
-- Left (line 1, column 8):
|
||||||
-- unexpected '>'
|
-- unexpected '>'
|
||||||
pSpreadRelationSelect :: Parser SelectItem
|
pSpreadRelationSelect :: Parser SelectItem
|
||||||
pSpreadRelationSelect = lexeme $ try ( do
|
pSpreadRelationSelect = lexeme $ do
|
||||||
name <- string ".." >> pFieldName
|
name <- string ".." >> pFieldName
|
||||||
(hint, jType) <- pEmbedParams
|
(hint, jType) <- pEmbedParams
|
||||||
try (void $ lookAhead (string "("))
|
try (void $ lookAhead (string "("))
|
||||||
return $ SpreadRelation name hint jType
|
return $ SpreadRelation name hint jType
|
||||||
)
|
|
||||||
|
|
||||||
pEmbedParams :: Parser (Maybe Hint, Maybe JoinType)
|
pEmbedParams :: Parser (Maybe Hint, Maybe JoinType)
|
||||||
pEmbedParams = do
|
pEmbedParams = do
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ readQuery req conf@AppConfig{..} apiReq@ApiRequest{..} = do
|
|||||||
resultSet <-
|
resultSet <-
|
||||||
lift . SQL.statement mempty $
|
lift . SQL.statement mempty $
|
||||||
Statements.prepareRead
|
Statements.prepareRead
|
||||||
(QueryBuilder.readPlanToQuery req)
|
(QueryBuilder.readPlanToQuery True req)
|
||||||
(if iPreferCount == Just EstimatedCount then
|
(if iPreferCount == Just EstimatedCount then
|
||||||
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
||||||
QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows)
|
QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows)
|
||||||
@@ -163,7 +163,7 @@ invokeQuery proc CallReadPlan{crReadPlan, crCallPlan} apiReq@ApiRequest{..} conf
|
|||||||
(Proc.procReturnsScalar proc)
|
(Proc.procReturnsScalar proc)
|
||||||
(Proc.procReturnsSingle proc)
|
(Proc.procReturnsSingle proc)
|
||||||
(QueryBuilder.callPlanToQuery crCallPlan)
|
(QueryBuilder.callPlanToQuery crCallPlan)
|
||||||
(QueryBuilder.readPlanToQuery crReadPlan)
|
(QueryBuilder.readPlanToQuery True crReadPlan)
|
||||||
(QueryBuilder.readPlanToCountQuery crReadPlan)
|
(QueryBuilder.readPlanToCountQuery crReadPlan)
|
||||||
(shouldCount iPreferCount)
|
(shouldCount iPreferCount)
|
||||||
iAcceptMediaType
|
iAcceptMediaType
|
||||||
@@ -217,7 +217,7 @@ writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan} apiReq conf =
|
|||||||
in
|
in
|
||||||
lift . SQL.statement mempty $
|
lift . SQL.statement mempty $
|
||||||
Statements.prepareWrite
|
Statements.prepareWrite
|
||||||
(QueryBuilder.readPlanToQuery mrReadPlan)
|
(QueryBuilder.readPlanToQuery True mrReadPlan)
|
||||||
(QueryBuilder.mutatePlanToQuery mrMutatePlan)
|
(QueryBuilder.mutatePlanToQuery mrMutatePlan)
|
||||||
isInsert
|
isInsert
|
||||||
(iAcceptMediaType apiReq)
|
(iAcceptMediaType apiReq)
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ import PostgREST.RangeQuery (allRange)
|
|||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
readPlanToQuery :: ReadPlanTree -> SQL.Snippet
|
readPlanToQuery :: Bool -> ReadPlanTree -> SQL.Snippet
|
||||||
readPlanToQuery (Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds} forest) =
|
readPlanToQuery isRoot (Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds} forest) =
|
||||||
"SELECT " <>
|
"SELECT " <>
|
||||||
intercalateSnippet ", " ((pgFmtSelectItem qi <$> select) ++ selects) <> " " <>
|
intercalateSnippet ", " ((pgFmtSelectItem qi <$> (if isRoot && null select && null forest then defRootSelect else select)) ++ selects) <> " " <>
|
||||||
fromFrag <> " " <>
|
fromFrag <> " " <>
|
||||||
intercalateSnippet " " joins <> " " <>
|
intercalateSnippet " " joins <> " " <>
|
||||||
(if null logicForest && null relJoinConds
|
(if null logicForest && null relJoinConds
|
||||||
@@ -53,13 +53,14 @@ readPlanToQuery (Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,o
|
|||||||
where
|
where
|
||||||
fromFrag = fromF relToParent mainQi fromAlias
|
fromFrag = fromF relToParent mainQi fromAlias
|
||||||
qi = getQualifiedIdentifier relToParent mainQi fromAlias
|
qi = getQualifiedIdentifier relToParent mainQi fromAlias
|
||||||
|
defRootSelect = [(("*", []), Nothing, Nothing)] -- gets all columns in case an empty select, e.g. `/tbl?select=`, is done.
|
||||||
(selects, joins) = foldr getSelectsJoins ([],[]) forest
|
(selects, joins) = foldr getSelectsJoins ([],[]) forest
|
||||||
|
|
||||||
getSelectsJoins :: ReadPlanTree -> ([SQL.Snippet], [SQL.Snippet]) -> ([SQL.Snippet], [SQL.Snippet])
|
getSelectsJoins :: ReadPlanTree -> ([SQL.Snippet], [SQL.Snippet]) -> ([SQL.Snippet], [SQL.Snippet])
|
||||||
getSelectsJoins (Node ReadPlan{relToParent=Nothing} _) _ = ([], [])
|
getSelectsJoins (Node ReadPlan{relToParent=Nothing} _) _ = ([], [])
|
||||||
getSelectsJoins rr@(Node ReadPlan{relName, relToParent=Just rel, relAggAlias, relAlias, relJoinType, relIsSpread} _) (selects,joins) =
|
getSelectsJoins rr@(Node ReadPlan{select, relName, relToParent=Just rel, relAggAlias, relAlias, relJoinType, relIsSpread} forest) (selects,joins) =
|
||||||
let
|
let
|
||||||
subquery = readPlanToQuery rr
|
subquery = readPlanToQuery False rr
|
||||||
aliasOrName = pgFmtIdent $ fromMaybe relName relAlias
|
aliasOrName = pgFmtIdent $ fromMaybe relName relAlias
|
||||||
aggAlias = pgFmtIdent relAggAlias
|
aggAlias = pgFmtIdent relAggAlias
|
||||||
correlatedSubquery sub al cond =
|
correlatedSubquery sub al cond =
|
||||||
@@ -77,7 +78,7 @@ getSelectsJoins rr@(Node ReadPlan{relName, relToParent=Just rel, relAggAlias, re
|
|||||||
"FROM (" <> subquery <> " ) AS " <> SQL.sql aggAlias
|
"FROM (" <> subquery <> " ) AS " <> SQL.sql aggAlias
|
||||||
) aggAlias $ if relJoinType == Just JTInner then SQL.sql aggAlias <> " IS NOT NULL" else "TRUE")
|
) aggAlias $ if relJoinType == Just JTInner then SQL.sql aggAlias <> " IS NOT NULL" else "TRUE")
|
||||||
in
|
in
|
||||||
(sel:selects, joi:joins)
|
(if null select && null forest then selects else sel:selects, joi:joins)
|
||||||
|
|
||||||
mutatePlanToQuery :: MutatePlan -> SQL.Snippet
|
mutatePlanToQuery :: MutatePlan -> SQL.Snippet
|
||||||
mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _) =
|
mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _) =
|
||||||
|
|||||||
@@ -1224,3 +1224,54 @@ spec actualPgVersion = do
|
|||||||
liftIO $ do
|
liftIO $ do
|
||||||
let respHeaders = simpleHeaders r
|
let respHeaders = simpleHeaders r
|
||||||
respHeaders `shouldSatisfy` noProfileHeader
|
respHeaders `shouldSatisfy` noProfileHeader
|
||||||
|
|
||||||
|
context "empty embed" $ do
|
||||||
|
it "works on a many-to-one relationship" $ do
|
||||||
|
get "/projects?select=id,name,clients()" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"id":1,"name":"Windows 7"},
|
||||||
|
{"id":2,"name":"Windows 10"},
|
||||||
|
{"id":3,"name":"IOS"},
|
||||||
|
{"id":4,"name":"OSX"},
|
||||||
|
{"id":5,"name":"Orphan"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/projects?select=id,name,clients!inner()&clients.id=eq.2" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":3,"name":"IOS"},
|
||||||
|
{"id":4,"name":"OSX"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "works on a one-to-many relationship" $ do
|
||||||
|
get "/clients?select=id,name,projects()" `shouldRespondWith`
|
||||||
|
[json| [{"id":1,"name":"Microsoft"}, {"id":2,"name":"Apple"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/clients?select=id,name,projects!inner()&projects.name=eq.IOS" `shouldRespondWith`
|
||||||
|
[json|[{"id":2,"name":"Apple"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "works on a many-to-many relationship" $ do
|
||||||
|
get "/users?select=*,tasks!inner()" `shouldRespondWith`
|
||||||
|
[json| [{"id":1,"name":"Angela Martin"}, {"id":2,"name":"Michael Scott"}, {"id":3,"name":"Dwight Schrute"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/users?select=*,tasks!inner()&tasks.id=eq.3" `shouldRespondWith`
|
||||||
|
[json|[{"id":1,"name":"Angela Martin"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "empty root select" $
|
||||||
|
it "gives all columns" $ do
|
||||||
|
get "/projects?select=" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"name":"Windows 7","client_id":1},
|
||||||
|
{"id":2,"name":"Windows 10","client_id":1},
|
||||||
|
{"id":3,"name":"IOS","client_id":2},
|
||||||
|
{"id":4,"name":"OSX","client_id":2},
|
||||||
|
{"id":5,"name":"Orphan","client_id":null}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/rpc/getallprojects?select=" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"name":"Windows 7","client_id":1},
|
||||||
|
{"id":2,"name":"Windows 10","client_id":1},
|
||||||
|
{"id":3,"name":"IOS","client_id":2},
|
||||||
|
{"id":4,"name":"OSX","client_id":2},
|
||||||
|
{"id":5,"name":"Orphan","client_id":null}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|||||||
Reference in New Issue
Block a user