feat: filter top-level resource with embed filter

This is enabled by adding `!inner` to the embedded resource

/projects?select=*,clients!inner(*)&clients.id=eq.12

This behaviour can be enabled by default with the config option

db-embed-default-join='inner'

Which saves the need for specifying `!inner` on every request.
If this is enabled, the previous behavior can be restored
per request by specifying `!left`  on the embedded resource.

/projects?select=*,clients!left(*)&clients.id=eq.12`

Tested on M20/02M/M2M relationships, views, RPC.
This commit is contained in:
steve-chavez
2021-10-04 13:46:32 -05:00
committed by Steve Chavez
parent bf91187e63
commit ee56dd5db1
27 changed files with 445 additions and 44 deletions
+3
View File
@@ -14,6 +14,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
+ Enables uploading bytea to a function with `Content-Type: application/octet-stream` + Enables uploading bytea to a function with `Content-Type: application/octet-stream`
+ Enables uploading raw text to a function with `Content-Type: text/plain` + Enables uploading raw text to a function with `Content-Type: text/plain`
- #1938, Allow escaping inside double quotes with a backslash, e.g. `?col=in.("Double\"Quote")`, `?col=in.("Back\\slash")` - @steve-chavez - #1938, Allow escaping inside double quotes with a backslash, e.g. `?col=in.("Double\"Quote")`, `?col=in.("Back\\slash")` - @steve-chavez
- #1075, Allow filtering top-level resource based on embedded resources filters - @steve-chavez, @Iced-Sun
+ This is enabled by adding `!inner` to the embedded resource, e.g. `/projects?select=*,clients!inner(*)&clients.id=eq.12`
+ This behavior can be enabled by default with the `db-embed-default-join='inner'` config option, which saves the need for specifying `!inner` on every request. In this case, you can go back to the previous behavior per request by specifying `!left` on the embedded resource, e.g `/projects?select=*,clients!left(*)&clients.id=eq.12`
### Fixed ### Fixed
+1
View File
@@ -176,6 +176,7 @@ test-suite spec
Feature.DeleteSpec Feature.DeleteSpec
Feature.DisabledOpenApiSpec Feature.DisabledOpenApiSpec
Feature.EmbedDisambiguationSpec Feature.EmbedDisambiguationSpec
Feature.EmbedInnerJoinSpec
Feature.ExtraSearchPathSpec Feature.ExtraSearchPathSpec
Feature.HtmlRawOutputSpec Feature.HtmlRawOutputSpec
Feature.InsertSpec Feature.InsertSpec
+1 -1
View File
@@ -572,7 +572,7 @@ returnsScalar _ = False
readRequest :: Monad m => QualifiedIdentifier -> RequestContext -> Handler m ReadRequest readRequest :: Monad m => QualifiedIdentifier -> RequestContext -> Handler m ReadRequest
readRequest QualifiedIdentifier{..} (RequestContext AppConfig{..} dbStructure apiRequest _) = readRequest QualifiedIdentifier{..} (RequestContext AppConfig{..} dbStructure apiRequest _) =
liftEither $ liftEither $
ReqBuilder.readRequest qiSchema qiName configDbMaxRows ReqBuilder.readRequest qiSchema qiName configDbMaxRows configDbEmbedDefaultJoin
(dbRelationships dbStructure) (dbRelationships dbStructure)
apiRequest apiRequest
+12
View File
@@ -57,6 +57,7 @@ import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
import PostgREST.Config.Proxy (Proxy (..), import PostgREST.Config.Proxy (Proxy (..),
isMalformedProxyUri, toURI) isMalformedProxyUri, toURI)
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier, toQi) import PostgREST.DbStructure.Identifiers (QualifiedIdentifier, toQi)
import PostgREST.Request.Types (JoinType (..))
import Protolude hiding (Proxy, toList, toS) import Protolude hiding (Proxy, toList, toS)
import Protolude.Conv (toS) import Protolude.Conv (toS)
@@ -79,6 +80,7 @@ data AppConfig = AppConfig
, configDbTxAllowOverride :: Bool , configDbTxAllowOverride :: Bool
, configDbTxRollbackAll :: Bool , configDbTxRollbackAll :: Bool
, configDbUri :: Text , configDbUri :: Text
, configDbEmbedDefaultJoin :: JoinType
, configFilePath :: Maybe FilePath , configFilePath :: Maybe FilePath
, configJWKS :: Maybe JWKSet , configJWKS :: Maybe JWKSet
, configJwtAudience :: Maybe StringOrURI , configJwtAudience :: Maybe StringOrURI
@@ -132,6 +134,7 @@ toText conf =
,("db-config", q . T.toLower . show . configDbConfig) ,("db-config", q . T.toLower . show . configDbConfig)
,("db-tx-end", q . showTxEnd) ,("db-tx-end", q . showTxEnd)
,("db-uri", q . configDbUri) ,("db-uri", q . configDbUri)
,("db-embed-default-join", q . show . configDbEmbedDefaultJoin)
,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience) ,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience)
,("jwt-role-claim-key", q . T.intercalate mempty . fmap show . configJwtRoleClaimKey) ,("jwt-role-claim-key", q . T.intercalate mempty . fmap show . configJwtRoleClaimKey)
,("jwt-secret", q . toS . showJwtSecret) ,("jwt-secret", q . toS . showJwtSecret)
@@ -222,6 +225,7 @@ parser optPath env dbSettings =
<*> parseTxEnd "db-tx-end" snd <*> parseTxEnd "db-tx-end" snd
<*> parseTxEnd "db-tx-end" fst <*> parseTxEnd "db-tx-end" fst
<*> reqString "db-uri" <*> reqString "db-uri"
<*> parseEmbedDefaultJoin "db-embed-default-join"
<*> pure optPath <*> pure optPath
<*> pure Nothing <*> pure Nothing
<*> parseJwtAudience "jwt-aud" <*> parseJwtAudience "jwt-aud"
@@ -304,6 +308,14 @@ parser optPath env dbSettings =
Just "rollback-allow-override" -> pure $ f (True, True) Just "rollback-allow-override" -> pure $ f (True, True)
Just _ -> fail "Invalid transaction termination. Check your configuration." Just _ -> fail "Invalid transaction termination. Check your configuration."
parseEmbedDefaultJoin :: C.Key -> C.Parser C.Config JoinType
parseEmbedDefaultJoin k =
optString k >>= \case
Nothing -> pure JTLeft
Just "left" -> pure JTLeft
Just "inner" -> pure JTInner
Just _ -> fail "Invalid db-embed-default-join. Check your configuration."
parseRoleClaimKey :: C.Key -> C.Key -> C.Parser C.Config JSPath parseRoleClaimKey :: C.Key -> C.Key -> C.Parser C.Config JSPath
parseRoleClaimKey k al = parseRoleClaimKey k al =
optWithAlias (optString k) (optString al) >>= \case optWithAlias (optString k) (optString al) >>= \case
+18 -9
View File
@@ -50,22 +50,31 @@ readRequestToQuery (Node (Select colSelects mainQi tblAlias implJoins logicFores
(joins, selects) = foldr getJoinsSelects ([],[]) forest (joins, selects) = foldr getJoinsSelects ([],[]) forest
getJoinsSelects :: ReadRequest -> ([H.Snippet], [H.Snippet]) -> ([H.Snippet], [H.Snippet]) getJoinsSelects :: ReadRequest -> ([H.Snippet], [H.Snippet]) -> ([H.Snippet], [H.Snippet])
getJoinsSelects rr@(Node (_, (name, Just Relationship{relCardinality=card,relTable=Table{tableName=table}}, alias, _, _)) _) (j,s) = getJoinsSelects rr@(Node (_, (name, Just Relationship{relCardinality=card,relTable=Table{tableName=table}}, alias, _, Just joinType, _)) _) (j,s) =
let subquery = readRequestToQuery rr in let subquery = readRequestToQuery rr in
case card of case card of
M2O _ -> M2O _ ->
let aliasOrName = fromMaybe name alias let aliasOrName = fromMaybe name alias
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
sel = H.sql ("row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName) sel = H.sql ("row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName)
joi = " LEFT JOIN LATERAL( " <> subquery <> " ) AS " <> H.sql localTableName <> " ON TRUE " in joi = (if joinType == JTInner then " INNER" else " LEFT")
<> " JOIN LATERAL( " <> subquery <> " ) AS " <> H.sql localTableName <> " ON TRUE " in
(joi:j,sel:s) (joi:j,sel:s)
_ -> _ -> case joinType of
let sel = "COALESCE ((" JTInner ->
<> "SELECT json_agg(" <> H.sql (pgFmtIdent table) <> ".*) " let aliasOrName = fromMaybe name alias
<> "FROM (" <> subquery <> ") " <> H.sql (pgFmtIdent table) <> " " localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
<> "), '[]') AS " <> H.sql (pgFmtIdent (fromMaybe name alias)) in sel = H.sql $ localTableName <> "._ AS " <> pgFmtIdent aliasOrName
(j,sel:s) joi = "INNER JOIN LATERAL( SELECT json_agg(_) AS _ FROM (" <> subquery <> " ) _) AS " <>
getJoinsSelects (Node (_, (_, Nothing, _, _, _)) _) _ = ([], []) H.sql localTableName <> " ON " <> H.sql localTableName <> "IS NOT NULL" in
(joi:j,sel:s)
JTLeft ->
let sel = "COALESCE (("
<> "SELECT json_agg(" <> H.sql (pgFmtIdent table) <> ".*) "
<> "FROM (" <> subquery <> ") " <> H.sql (pgFmtIdent table) <> " "
<> "), '[]') AS " <> H.sql (pgFmtIdent (fromMaybe name alias)) in
(j,sel:s)
getJoinsSelects _ _ = ([], [])
mutateRequestToQuery :: MutateRequest -> H.Snippet mutateRequestToQuery :: MutateRequest -> H.Snippet
mutateRequestToQuery (Insert mainQi iCols body onConflct putConditions returnings) = mutateRequestToQuery (Insert mainQi iCols body onConflct putConditions returnings) =
+2 -2
View File
@@ -206,11 +206,11 @@ pgFmtField :: QualifiedIdentifier -> Field -> H.Snippet
pgFmtField table (c, jp) = H.sql (pgFmtColumn table c) <> pgFmtJsonPath jp pgFmtField table (c, jp) = H.sql (pgFmtColumn table c) <> pgFmtJsonPath jp
pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> H.Snippet pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> H.Snippet
pgFmtSelectItem table (f@(fName, jp), Nothing, alias, _) = pgFmtField table f <> H.sql (pgFmtAs fName jp alias) pgFmtSelectItem table (f@(fName, jp), Nothing, alias, _, _) = pgFmtField table f <> H.sql (pgFmtAs fName jp alias)
-- Ideally we'd quote the cast with "pgFmtIdent cast". However, that would invalidate common casts such as "int", "bigint", etc. -- Ideally we'd quote the cast with "pgFmtIdent cast". However, that would invalidate common casts such as "int", "bigint", etc.
-- Try doing: `select 1::"bigint"` - it'll err, using "int8" will work though. There's some parser magic that pg does that's invalidated when quoting. -- Try doing: `select 1::"bigint"` - it'll err, using "int8" will work though. There's some parser magic that pg does that's invalidated when quoting.
-- Not quoting should be fine, we validate the input on Parsers. -- Not quoting should be fine, we validate the input on Parsers.
pgFmtSelectItem table (f@(fName, jp), Just cast, alias, _) = "CAST (" <> pgFmtField table f <> " AS " <> H.sql (encodeUtf8 cast) <> " )" <> H.sql (pgFmtAs fName jp alias) pgFmtSelectItem table (f@(fName, jp), Just cast, alias, _, _) = "CAST (" <> pgFmtField table f <> " AS " <> H.sql (encodeUtf8 cast) <> " )" <> H.sql (pgFmtAs fName jp alias)
pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> H.Snippet pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> H.Snippet
pgFmtOrderTerm qi ot = pgFmtOrderTerm qi ot =
+18 -18
View File
@@ -61,11 +61,11 @@ import Protolude hiding (from)
-- | Builds the ReadRequest tree on a number of stages. -- | Builds the ReadRequest tree on a number of stages.
-- | Adds filters, order, limits on its respective nodes. -- | Adds filters, order, limits on its respective nodes.
-- | Adds joins conditions obtained from resource embedding. -- | Adds joins conditions obtained from resource embedding.
readRequest :: Schema -> TableName -> Maybe Integer -> [Relationship] -> ApiRequest -> Either Error ReadRequest readRequest :: Schema -> TableName -> Maybe Integer -> JoinType -> [Relationship] -> ApiRequest -> Either Error ReadRequest
readRequest schema rootTableName maxRows allRels apiRequest = readRequest schema rootTableName maxRows defJoinType allRels apiRequest =
mapLeft ApiRequestError $ mapLeft ApiRequestError $
treeRestrictRange maxRows =<< treeRestrictRange maxRows =<<
augmentRequestWithJoin schema rootRels =<< augmentRequestWithJoin schema rootRels defJoinType =<<
(addFiltersOrdersRanges apiRequest . initReadRequest rootName =<< pRequestSelect sel) (addFiltersOrdersRanges apiRequest . initReadRequest rootName =<< pRequestSelect sel)
where where
sel = fromMaybe "*" $ iSelect apiRequest -- default to all columns requested (SELECT *) for a non existent ?select querystring param sel = fromMaybe "*" $ iSelect apiRequest -- default to all columns requested (SELECT *) for a non existent ?select querystring param
@@ -100,16 +100,16 @@ initReadRequest rootQi =
rootDepth = 0 rootDepth = 0
rootSchema = qiSchema rootQi rootSchema = qiSchema rootQi
rootName = qiName rootQi rootName = qiName rootQi
initial = Node (Select [] rootQi Nothing [] [] [] [] allRange, (rootName, Nothing, Nothing, Nothing, rootDepth)) [] initial = Node (Select [] rootQi Nothing [] [] [] [] allRange, (rootName, Nothing, Nothing, Nothing, Nothing, rootDepth)) []
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
treeEntry depth (Node fld@((fn, _),_,alias, embedHint) fldForest) (Node (q, i) rForest) = treeEntry depth (Node fld@((fn, _),_,alias, hint, joinType) fldForest) (Node (q, i) rForest) =
let nxtDepth = succ depth in let nxtDepth = succ depth in
case fldForest of case fldForest of
[] -> Node (q {select=fld:select q}, i) rForest [] -> Node (q {select=fld:select q}, i) rForest
_ -> Node (q, i) $ _ -> Node (q, i) $
foldr (treeEntry nxtDepth) foldr (treeEntry nxtDepth)
(Node (Select [] (QualifiedIdentifier rootSchema fn) Nothing [] [] [] [] allRange, (Node (Select [] (QualifiedIdentifier rootSchema fn) Nothing [] [] [] [] allRange,
(fn, Nothing, alias, embedHint, nxtDepth)) []) (fn, Nothing, alias, hint, joinType, nxtDepth)) [])
fldForest:rForest fldForest:rForest
-- | Enforces the `max-rows` config on the result -- | Enforces the `max-rows` config on the result
@@ -119,26 +119,26 @@ treeRestrictRange maxRows request = pure $ nodeRestrictRange maxRows <$> request
nodeRestrictRange :: Maybe Integer -> ReadNode -> ReadNode nodeRestrictRange :: Maybe Integer -> ReadNode -> ReadNode
nodeRestrictRange m (q@Select {range_=r}, i) = (q{range_=restrictRange m r }, i) nodeRestrictRange m (q@Select {range_=r}, i) = (q{range_=restrictRange m r }, i)
augmentRequestWithJoin :: Schema -> [Relationship] -> ReadRequest -> Either ApiRequestError ReadRequest augmentRequestWithJoin :: Schema -> [Relationship] -> JoinType -> ReadRequest -> Either ApiRequestError ReadRequest
augmentRequestWithJoin schema allRels request = augmentRequestWithJoin schema allRels defJoinType request =
addRels schema allRels Nothing request addRels schema allRels Nothing defJoinType request
>>= addJoinConditions Nothing >>= addJoinConditions Nothing
addRels :: Schema -> [Relationship] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest addRels :: Schema -> [Relationship] -> Maybe ReadRequest -> JoinType -> ReadRequest -> Either ApiRequestError ReadRequest
addRels schema allRels parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, hint, depth)) forest) = addRels schema allRels parentNode defJoinType (Node (query@Select{from=tbl}, (nodeName, _, alias, hint, joinType, depth)) forest) =
case parentNode of case parentNode of
Just (Node (Select{from=parentNodeQi}, _) _) -> Just (Node (Select{from=parentNodeQi}, _) _) ->
let newFrom r = if qiName tbl == nodeName then tableQi (relForeignTable r) else tbl let newFrom r = if qiName tbl == nodeName then tableQi (relForeignTable r) else tbl
newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, Nothing, depth))) <$> rel newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, hint, joinType <|> Just defJoinType, depth))) <$> rel
rel = findRel schema allRels (qiName parentNodeQi) nodeName hint rel = findRel schema allRels (qiName parentNodeQi) nodeName hint
in in
Node <$> newReadNode <*> (updateForest . hush $ Node <$> newReadNode <*> pure forest) Node <$> newReadNode <*> (updateForest . hush $ Node <$> newReadNode <*> pure forest)
_ -> _ ->
let rn = (query, (nodeName, Nothing, alias, Nothing, depth)) in let rn = (query, (nodeName, Nothing, alias, Nothing, joinType, depth)) in
Node rn <$> updateForest (Just $ Node rn forest) Node rn <$> updateForest (Just $ Node rn forest)
where where
updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest] updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
updateForest rq = addRels schema allRels rq `traverse` forest updateForest rq = addRels schema allRels rq defJoinType `traverse` forest
-- Finds a relationship between an origin and a target in the request: -- Finds a relationship between an origin and a target in the request:
-- /origin?select=target(*) If more than one relationship is found then the -- /origin?select=target(*) If more than one relationship is found then the
@@ -150,7 +150,7 @@ addRels schema allRels parentNode (Node (query@Select{from=tbl}, (nodeName, _, a
-- target = table / view / constraint / column-from-origin -- target = table / view / constraint / column-from-origin
-- hint = table / view / constraint / column-from-origin / column-from-target -- hint = table / view / constraint / column-from-origin / column-from-target
-- (hint can take table / view values to aid in finding the junction in an m2m relationship) -- (hint can take table / view values to aid in finding the junction in an m2m relationship)
findRel :: Schema -> [Relationship] -> NodeName -> NodeName -> Maybe EmbedHint -> Either ApiRequestError Relationship findRel :: Schema -> [Relationship] -> NodeName -> NodeName -> Maybe Hint -> Either ApiRequestError Relationship
findRel schema allRels origin target hint = findRel schema allRels origin target hint =
case rel of case rel of
[] -> Left $ NoRelBetween origin target [] -> Left $ NoRelBetween origin target
@@ -210,7 +210,7 @@ findRel schema allRels origin target hint =
-- previousAlias is only used for the case of self joins -- previousAlias is only used for the case of self joins
addJoinConditions :: Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest addJoinConditions :: Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest
addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, rel, _, _, depth)) forest) = addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, rel, _, _, _, depth)) forest) =
case rel of case rel of
Just r@Relationship{relCardinality=M2M Junction{junTable}} -> Just r@Relationship{relCardinality=M2M Junction{junTable}} ->
let rq = augmentQuery r in let rq = augmentQuery r in
@@ -307,7 +307,7 @@ addProperty f (targetNodeName:remainingPath, a) (Node rn forest) =
Nothing -> Node rn forest -- the property is silenty dropped in the Request does not contain the required path Nothing -> Node rn forest -- the property is silenty dropped in the Request does not contain the required path
Just tn -> Node rn (addProperty f (remainingPath, a) tn:delete tn forest) Just tn -> Node rn (addProperty f (remainingPath, a) tn:delete tn forest)
where where
pathNode = find (\(Node (_,(nodeName,_,alias,_,_)) _) -> nodeName == targetNodeName || alias == Just targetNodeName) forest pathNode = find (\(Node (_,(nodeName,_,alias,_,_, _)) _) -> nodeName == targetNodeName || alias == Just targetNodeName) forest
mutateRequest :: Schema -> TableName -> ApiRequest -> [FieldName] -> ReadRequest -> Either Error MutateRequest mutateRequest :: Schema -> TableName -> ApiRequest -> [FieldName] -> ReadRequest -> Either Error MutateRequest
mutateRequest schema tName apiRequest pkCols readReq = mapLeft ApiRequestError $ mutateRequest schema tName apiRequest pkCols readReq = mapLeft ApiRequestError $
@@ -379,7 +379,7 @@ returningCols rr@(Node _ forest) pkCols
-- projects. So this adds the foreign key columns to ensure the embedding -- projects. So this adds the foreign key columns to ensure the embedding
-- succeeds, result would be `RETURNING name, client_id`. -- succeeds, result would be `RETURNING name, client_id`.
fkCols = concat $ mapMaybe (\case fkCols = concat $ mapMaybe (\case
Node (_, (_, Just Relationship{relColumns=cols}, _, _, _)) _ -> Just cols Node (_, (_, Just Relationship{relColumns=cols}, _, _, _, _)) _ -> Just cols
_ -> Nothing _ -> Nothing
) forest ) forest
-- However if the "client_id" is present, e.g. mutateRequest to -- However if the "client_id" is present, e.g. mutateRequest to
+18 -4
View File
@@ -158,9 +158,23 @@ pRelationSelect :: Parser SelectItem
pRelationSelect = lexeme $ try ( do pRelationSelect = lexeme $ try ( do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) ) alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField fld <- pField
hint <- optionMaybe $ char '!' *> pFieldName prm1 <- optionMaybe pEmbedParam
return (fld, Nothing, alias, hint) prm2 <- optionMaybe pEmbedParam
return (fld, Nothing, alias, embedParamHint prm1 <|> embedParamHint prm2, embedParamJoin prm1 <|> embedParamJoin prm2)
) )
where
pEmbedParam :: Parser EmbedParam
pEmbedParam =
char '!' *> (
try (string "left" $> EPJoinType JTLeft) <|>
try (string "inner" $> EPJoinType JTInner) <|>
try (EPHint <$> pFieldName))
embedParamHint prm = case prm of
Just (EPHint hint) -> Just hint
_ -> Nothing
embedParamJoin prm = case prm of
Just (EPJoinType jt) -> Just jt
_ -> Nothing
pFieldSelect :: Parser SelectItem pFieldSelect :: Parser SelectItem
pFieldSelect = lexeme $ pFieldSelect = lexeme $
@@ -169,11 +183,11 @@ pFieldSelect = lexeme $
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) ) alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField fld <- pField
cast' <- optionMaybe (string "::" *> many letter) cast' <- optionMaybe (string "::" *> many letter)
return (fld, toS <$> cast', alias, Nothing) return (fld, toS <$> cast', alias, Nothing, Nothing)
) )
<|> do <|> do
s <- pStar s <- pStar
return ((s, []), Nothing, Nothing, Nothing) return ((s, []), Nothing, Nothing, Nothing, Nothing)
pOpExpr :: Parser SingleVal -> Parser OpExpr pOpExpr :: Parser SingleVal -> Parser OpExpr
pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation
+21 -8
View File
@@ -2,14 +2,16 @@
module PostgREST.Request.Types module PostgREST.Request.Types
( Alias ( Alias
, Depth , Depth
, EmbedHint , EmbedParam(..)
, EmbedPath , EmbedPath
, Field , Field
, Filter(..) , Filter(..)
, Hint
, CallQuery(..) , CallQuery(..)
, CallParams(..) , CallParams(..)
, CallRequest , CallRequest
, JoinCondition(..) , JoinCondition(..)
, JoinType(..)
, JsonOperand(..) , JsonOperand(..)
, JsonOperation(..) , JsonOperation(..)
, JsonPath , JsonPath
@@ -54,7 +56,7 @@ type MutateRequest = MutateQuery
type CallRequest = CallQuery type CallRequest = CallQuery
type ReadNode = type ReadNode =
(ReadQuery, (NodeName, Maybe Relationship, Maybe Alias, Maybe EmbedHint, Depth)) (ReadQuery, (NodeName, Maybe Relationship, Maybe Alias, Maybe Hint, Maybe JoinType, Depth))
type NodeName = Text type NodeName = Text
type Depth = Integer type Depth = Integer
@@ -140,16 +142,27 @@ data CallParams
| OnePosParam ProcParam -- ^ Call with positional params(only one supported): func(val) | OnePosParam ProcParam -- ^ Call with positional params(only one supported): func(val)
-- | The select value in `/tbl?select=alias:field::cast` -- | The select value in `/tbl?select=alias:field::cast`
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint) type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe Hint, Maybe JoinType)
type Field = (FieldName, JsonPath) type Field = (FieldName, JsonPath)
type Cast = Text type Cast = Text
type Alias = Text type Alias = Text
type Hint = Text
-- | Disambiguates an embedding operation when there's multiple relationships data EmbedParam
-- between two tables. Can be the name of a foreign key constraint, column -- | Disambiguates an embedding operation when there's multiple relationships
-- name or the junction in an m2m relationship. -- between two tables. Can be the name of a foreign key constraint, column
type EmbedHint = Text -- name or the junction in an m2m relationship.
= EPHint Hint
| EPJoinType JoinType
data JoinType
= JTInner
| JTLeft
deriving Eq
instance Show JoinType where
show JTInner = "inner"
show JTLeft = "left"
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path -- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path
-- ["clients", "projects"] -- ["clients", "projects"]
@@ -176,7 +189,7 @@ data JsonOperand
-- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d)) -- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d))
fstFieldNames :: ReadRequest -> [FieldName] fstFieldNames :: ReadRequest -> [FieldName]
fstFieldNames (Node (sel, _) _) = fstFieldNames (Node (sel, _) _) =
fst . (\(f, _, _, _) -> f) <$> select sel fst . (\(f, _, _, _, _) -> f) <$> select sel
-- | Boolean logic expression tree e.g. "and(name.eq.N,or(id.eq.1,id.eq.2))" is: -- | Boolean logic expression tree e.g. "and(name.eq.N,or(id.eq.1,id.eq.2))" is:
+260
View File
@@ -0,0 +1,260 @@
module Feature.EmbedInnerJoinSpec where
import Network.Wai (Application)
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Protolude hiding (get)
import SpecHelper
spec :: SpecWith ((), Application)
spec =
describe "Embedding with an inner join" $ do
context "many-to-one relationships" $ do
it "ignores null embeddings while the default left join doesn't" $ do
get "/projects?select=id,clients!inner(id)" `shouldRespondWith`
[json|[
{"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}},
{"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/projects?select=id,clients!left(id)" `shouldRespondWith`
[json|[
{"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}},
{"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}},
{"id":5,"clients":null}]|]
{ matchHeaders = [matchContentTypeJson] }
it "filters source tables when the embedded table is filtered" $ do
get "/projects?select=id,clients!inner(id)&clients.id=eq.1" `shouldRespondWith`
[json|[
{"id":1,"clients":{"id":1}},
{"id":2,"clients":{"id":1}}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/projects?select=id,clients!inner(id)&clients.id=eq.2" `shouldRespondWith`
[json|[
{"id":3,"clients":{"id":2}},
{"id":4,"clients":{"id":2}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/projects?select=id,clients!inner(id)&clients.id=eq.0" `shouldRespondWith`
[json|[]|]
{ matchHeaders = [matchContentTypeJson] }
it "filters source tables when a two levels below embedded table is filtered" $ do
get "/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.1" `shouldRespondWith`
[json|[
{"id":1,"projects":{"id":1,"clients":{"id":1}}},
{"id":2,"projects":{"id":1,"clients":{"id":1}}},
{"id":3,"projects":{"id":2,"clients":{"id":1}}},
{"id":4,"projects":{"id":2,"clients":{"id":1}}}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.2" `shouldRespondWith`
[json|[
{"id":5,"projects":{"id":3,"clients":{"id":2}}},
{"id":6,"projects":{"id":3,"clients":{"id":2}}},
{"id":7,"projects":{"id":4,"clients":{"id":2}}},
{"id":8,"projects":{"id":4,"clients":{"id":2}}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "only affects the source table rows if his direct embedding is an inner join" $
get "/tasks?select=id,projects(id,clients!inner(id))&projects.clients.id=eq.2" `shouldRespondWith`
[json|[
{"id":1,"projects":null},
{"id":2,"projects":null},
{"id":3,"projects":null},
{"id":4,"projects":null},
{"id":5,"projects":{"id":3,"clients":{"id":2}}},
{"id":6,"projects":{"id":3,"clients":{"id":2}}},
{"id":7,"projects":{"id":4,"clients":{"id":2}}},
{"id":8,"projects":{"id":4,"clients":{"id":2}}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works with views" $
get "/books?select=title,authors!inner(name)&authors.name=eq.George%20Orwell" `shouldRespondWith`
[json| [{"title":"1984","authors":{"name":"George Orwell"}}] |]
{ matchHeaders = [matchContentTypeJson] }
context "one-to-many relationships" $ do
it "ignores empty array embeddings while the default left join doesn't" $ do
get "/entities?select=id,child_entities!inner(id)" `shouldRespondWith`
[json|[
{"id":1,"child_entities":[{"id":1}, {"id":2}, {"id":4}, {"id":5}]},
{"id":2,"child_entities":[{"id":3}, {"id":6}]}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/entities?select=id,child_entities!left(id)" `shouldRespondWith`
[json| [
{"id":1,"child_entities":[{"id":1}, {"id":2}, {"id":4}, {"id":5}]},
{"id":2,"child_entities":[{"id":3}, {"id":6}]},
{"id":3,"child_entities":[]},
{"id":4,"child_entities":[]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "filters source tables when the embedded table is filtered" $ do
get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.1" `shouldRespondWith`
[json|[{"id":1,"child_entities":[{"id":1}]}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.3" `shouldRespondWith`
[json|[{"id":2,"child_entities":[{"id":3}]}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.0" `shouldRespondWith`
[json|[]|]
{ matchHeaders = [matchContentTypeJson] }
it "filters source tables when a two levels below embedded table is filtered" $ do
get "/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=in.(1,5)"
`shouldRespondWith`
[json|[
{
"id": 1,
"child_entities": [
{ "id": 1, "grandchild_entities": [ { "id": 1 } ] },
{ "id": 2, "grandchild_entities": [ { "id": 5 } ] }]
}
]|]
{ matchHeaders = [matchContentTypeJson] }
get "/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=eq.2" `shouldRespondWith`
[json|[
{
"id": 1,
"child_entities": [
{ "id": 1, "grandchild_entities": [ { "id": 2 } ] } ]
}
]|]
{ matchHeaders = [matchContentTypeJson] }
it "only affects the source table rows if his direct embedding is an inner join" $
get "/entities?select=id,child_entities!inner(id,grandchild_entities(id))&child_entities.grandchild_entities.id=eq.2" `shouldRespondWith`
[json|[
{
"id": 1,
"child_entities": [
{ "id": 1, "grandchild_entities": [ { "id": 2 } ] },
{ "id": 2, "grandchild_entities": [] },
{ "id": 4, "grandchild_entities": [] },
{ "id": 5, "grandchild_entities": [] } ]
},
{
"id": 2,
"child_entities": [
{ "id": 3, "grandchild_entities": [] },
{ "id": 6, "grandchild_entities": [] } ]
}
]|]
{ matchHeaders = [matchContentTypeJson] }
it "works with views" $
get "/authors?select=*,books!inner(*)&books.title=eq.1984" `shouldRespondWith`
[json| [{"id":1,"name":"George Orwell","books":[{"id":1,"title":"1984","publication_year":1949,"author_id":1}]}] |]
{ matchHeaders = [matchContentTypeJson] }
context "many-to-many relationships" $ do
it "ignores empty array embeddings while the default left join doesn't" $ do
get "/products?select=id,suppliers!inner(id)" `shouldRespondWith`
[json| [
{"id":1,"suppliers":[{"id":1}, {"id":2}]},
{"id":2,"suppliers":[{"id":1}, {"id":3}]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/products?select=id,suppliers!left(id)" `shouldRespondWith`
[json| [
{"id":1,"suppliers":[{"id":1}, {"id":2}]},
{"id":2,"suppliers":[{"id":1}, {"id":3}]},
{"id":3,"suppliers":[]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "filters source tables when the embedded table is filtered" $ do
get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.2" `shouldRespondWith`
[json| [{"id":1,"suppliers":[{"id":2}]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.3" `shouldRespondWith`
[json| [{"id":2,"suppliers":[{"id":3}]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.0" `shouldRespondWith`
[json| [] |]
{ matchHeaders = [matchContentTypeJson] }
it "filters source tables when a two levels below embedded table is filtered" $ do
get "/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.3"
`shouldRespondWith`
[json|[{"id":1,"suppliers":[{"id":2,"trade_unions":[{"id":3}]}]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.4"
`shouldRespondWith`
[json|[{"id":1,"suppliers":[{"id":2,"trade_unions":[{"id":4}]}]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "only affects the source table rows if his direct embedding is an inner join" $
get "/products?select=id,suppliers!inner(id,trade_unions(id))&suppliers.trade_unions.id=eq.3" `shouldRespondWith`
[json|[
{"id":1,"suppliers":[{"id":1,"trade_unions":[]}, {"id":2,"trade_unions":[{"id":3}]}]},
{"id":2,"suppliers":[{"id":1,"trade_unions":[]}, {"id":3,"trade_unions":[]}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works with views" $ do
get "/actors?select=*,films!inner(*)&films.title=eq.douze%20commandements" `shouldRespondWith`
[json| [{"id":1,"name":"john","films":[{"id":12,"title":"douze commandements"}]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/films?select=*,actors!inner(*)&actors.name=eq.john" `shouldRespondWith`
[json| [{"id":12,"title":"douze commandements","actors":[{"id":1,"name":"john"}]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "works with m2o and m2m relationships combined" $
get "/projects?select=name,clients!inner(name),users!inner(name)" `shouldRespondWith`
[json| [
{"name":"Windows 7","clients":{"name":"Microsoft"},"users":[{"name":"Angela Martin"}, {"name":"Dwight Schrute"}]},
{"name":"Windows 10","clients":{"name":"Microsoft"},"users":[{"name":"Angela Martin"}]},
{"name":"IOS","clients":{"name":"Apple"},"users":[{"name":"Michael Scott"}, {"name":"Dwight Schrute"}]},
{"name":"OSX","clients":{"name":"Apple"},"users":[{"name":"Michael Scott"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works with rpc" $
get "/rpc/getallprojects?select=id,clients!inner(id)&clients.id=eq.1" `shouldRespondWith`
[json| [{"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "works when using hints" $ do
get "/projects?select=id,clients!client!inner(id)&clients.id=eq.2" `shouldRespondWith`
[json| [{"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/projects?select=id,client!inner(id)&client.id=eq.2" `shouldRespondWith`
[json| [{"id":3,"client":{"id":2}}, {"id":4,"client":{"id":2}}] |]
{ matchHeaders = [matchContentTypeJson] }
notDefaultConfig :: SpecWith ((), Application)
notDefaultConfig =
describe "Embedding with a default inner join(db-embed-default-join = 'inner')" $ do
it "works on many-to-one relationships" $
get "/tasks?select=id,projects(id,clients(id))&projects.clients.id=eq.1" `shouldRespondWith`
[json|[
{"id":1,"projects":{"id":1,"clients":{"id":1}}},
{"id":2,"projects":{"id":1,"clients":{"id":1}}},
{"id":3,"projects":{"id":2,"clients":{"id":1}}},
{"id":4,"projects":{"id":2,"clients":{"id":1}}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works on one-to-many relationships" $
get "/entities?select=id,child_entities(id,grandchild_entities(id))&child_entities.grandchild_entities.id=in.(1,5)"
`shouldRespondWith`
[json|[
{
"id": 1,
"child_entities": [
{ "id": 1, "grandchild_entities": [ { "id": 1 } ] },
{ "id": 2, "grandchild_entities": [ { "id": 5 } ] }]
}
]|]
{ matchHeaders = [matchContentTypeJson] }
it "works on many-to-many relationships" $
get "/products?select=id,suppliers(id,trade_unions(id))&suppliers.trade_unions.id=eq.3"
`shouldRespondWith`
[json|[{"id":1,"suppliers":[{"id":2,"trade_unions":[{"id":3}]}]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can restore default left join behavior" $
get "/projects?select=id,clients!left(id)" `shouldRespondWith`
[json|[
{"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}},
{"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}},
{"id":5,"clients":null}]|]
{ matchHeaders = [matchContentTypeJson] }
+7
View File
@@ -30,6 +30,7 @@ import qualified Feature.CorsSpec
import qualified Feature.DeleteSpec import qualified Feature.DeleteSpec
import qualified Feature.DisabledOpenApiSpec import qualified Feature.DisabledOpenApiSpec
import qualified Feature.EmbedDisambiguationSpec import qualified Feature.EmbedDisambiguationSpec
import qualified Feature.EmbedInnerJoinSpec
import qualified Feature.ExtraSearchPathSpec import qualified Feature.ExtraSearchPathSpec
import qualified Feature.HtmlRawOutputSpec import qualified Feature.HtmlRawOutputSpec
import qualified Feature.IgnorePrivOpenApiSpec import qualified Feature.IgnorePrivOpenApiSpec
@@ -94,6 +95,7 @@ main = do
let withApp = app testCfg let withApp = app testCfg
maxRowsApp = app testMaxRowsCfg maxRowsApp = app testMaxRowsCfg
embedInnerJoinApp = app testEmbedInnerJoinCfg
disabledOpenApi = app testDisabledOpenApiCfg disabledOpenApi = app testDisabledOpenApiCfg
proxyApp = app testProxyCfg proxyApp = app testProxyCfg
noJwtApp = app testCfgNoJWT noJwtApp = app testCfgNoJWT
@@ -125,6 +127,7 @@ main = do
, ("Feature.CorsSpec" , Feature.CorsSpec.spec) , ("Feature.CorsSpec" , Feature.CorsSpec.spec)
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec) , ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
, ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec) , ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec)
, ("Feature.EmbedInnerJoinSpec" , Feature.EmbedInnerJoinSpec.spec)
, ("Feature.InsertSpec" , Feature.InsertSpec.spec actualPgVersion) , ("Feature.InsertSpec" , Feature.InsertSpec.spec actualPgVersion)
, ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion) , ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion)
, ("Feature.OpenApiSpec" , Feature.OpenApiSpec.spec actualPgVersion) , ("Feature.OpenApiSpec" , Feature.OpenApiSpec.spec actualPgVersion)
@@ -207,6 +210,10 @@ main = do
parallel $ before multipleSchemaApp $ parallel $ before multipleSchemaApp $
describe "Feature.MultipleSchemaSpec" $ Feature.MultipleSchemaSpec.spec actualPgVersion describe "Feature.MultipleSchemaSpec" $ Feature.MultipleSchemaSpec.spec actualPgVersion
-- this test runs with db-embed-default-join = inner
before embedInnerJoinApp $
describe "Feature.EmbedInnerJoinSpecNotDefaultConfig" Feature.EmbedInnerJoinSpec.notDefaultConfig
-- Note: the rollback tests can not run in parallel, because they test persistance and -- Note: the rollback tests can not run in parallel, because they test persistance and
-- this results in race conditions -- this results in race conditions
+5
View File
@@ -28,6 +28,7 @@ import PostgREST.Config (AppConfig (..),
OpenAPIMode (..), OpenAPIMode (..),
parseSecret) parseSecret)
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..)) import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..))
import PostgREST.Request.Types (JoinType (..))
import Protolude hiding (toS) import Protolude hiding (toS)
import Protolude.Conv (toS) import Protolude.Conv (toS)
@@ -89,6 +90,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configDbSchemas = fromList ["test"] , configDbSchemas = fromList ["test"]
, configDbConfig = False , configDbConfig = False
, configDbUri = mempty , configDbUri = mempty
, configDbEmbedDefaultJoin = JTLeft
, configFilePath = Nothing , configFilePath = Nothing
, configJWKS = parseSecret <$> secret , configJWKS = parseSecret <$> secret
, configJwtAudience = Nothing , configJwtAudience = Nothing
@@ -125,6 +127,9 @@ testUnicodeCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["
testMaxRowsCfg :: Text -> AppConfig testMaxRowsCfg :: Text -> AppConfig
testMaxRowsCfg testDbConn = (testCfg testDbConn) { configDbMaxRows = Just 2 } testMaxRowsCfg testDbConn = (testCfg testDbConn) { configDbMaxRows = Just 2 }
testEmbedInnerJoinCfg :: Text -> AppConfig
testEmbedInnerJoinCfg testDbConn = (testCfg testDbConn) { configDbEmbedDefaultJoin = JTInner }
testDisabledOpenApiCfg :: Text -> AppConfig testDisabledOpenApiCfg :: Text -> AppConfig
testDisabledOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OADisabled } testDisabledOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OADisabled }
+15
View File
@@ -692,3 +692,18 @@ DO $do$BEGIN
INSERT INTO test.reference_to_partitioned(id, id_a, name_a) VALUES (2, 2, 'first'); INSERT INTO test.reference_to_partitioned(id, id_a, name_a) VALUES (2, 2, 'first');
END IF; END IF;
END$do$; END$do$;
TRUNCATE TABLE test.products CASCADE;
INSERT INTO test.products (id, name) VALUES (1,'product-1'), (2,'product-2'), (3,'product-3');
TRUNCATE TABLE test.suppliers CASCADE;
INSERT INTO test.suppliers (id, name) VALUES (1,'supplier-1'), (2,'supplier-2'), (3, 'supplier-3');
TRUNCATE TABLE test.products_suppliers CASCADE;
INSERT INTO test.products_suppliers (product_id, supplier_id) VALUES (1,1), (1,2), (2,1), (2,3);
TRUNCATE TABLE test.trade_unions CASCADE;
INSERT INTO test.trade_unions (id, name) VALUES (1,'union-1'), (2,'union-2'), (3, 'union-3'), (4, 'union-4');
TRUNCATE TABLE test.suppliers_trade_unions CASCADE;
INSERT INTO test.suppliers_trade_unions (supplier_id, trade_union_id) VALUES (1,1), (1,2), (2,3), (2,4);
+5
View File
@@ -151,6 +151,11 @@ GRANT ALL ON TABLE
, schauspieler , schauspieler
, filme , filme
, rollen , rollen
, products
, suppliers
, products_suppliers
, trade_unions
, suppliers_trade_unions
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
+27
View File
@@ -2284,3 +2284,30 @@ $$ language sql;
create or replace function test.overloaded_unnamed_param(x int, y int) returns int as $$ create or replace function test.overloaded_unnamed_param(x int, y int) returns int as $$
select x + y; select x + y;
$$ language sql; $$ language sql;
create table products(
id int primary key
, name text
);
create table suppliers(
id int primary key
, name text
);
create table products_suppliers(
product_id int references products(id),
supplier_id int references suppliers(id),
primary key (product_id, supplier_id)
);
create table trade_unions(
id int primary key
, name text
);
create table suppliers_trade_unions(
supplier_id int references suppliers(id),
trade_union_id int references trade_unions(id),
primary key (supplier_id, trade_union_id)
);
@@ -12,6 +12,7 @@ db-schemas = "provided_through_alias"
db-config = "false" db-config = "false"
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "required" db-uri = "required"
db-embed-default-join = "left"
jwt-aud = "" jwt-aud = ""
jwt-role-claim-key = ".\"aliased\"" jwt-role-claim-key = ".\"aliased\""
jwt-secret = "" jwt-secret = ""
@@ -12,6 +12,7 @@ db-schemas = "required"
db-config = "false" db-config = "false"
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "required" db-uri = "required"
db-embed-default-join = "left"
jwt-aud = "" jwt-aud = ""
jwt-role-claim-key = ".\"role\"" jwt-role-claim-key = ".\"role\""
jwt-secret = "" jwt-secret = ""
@@ -12,6 +12,7 @@ db-schemas = "required"
db-config = "false" db-config = "false"
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "required" db-uri = "required"
db-embed-default-join = "left"
jwt-aud = "" jwt-aud = ""
jwt-role-claim-key = ".\"role\"" jwt-role-claim-key = ".\"role\""
jwt-secret = "" jwt-secret = ""
@@ -12,6 +12,7 @@ db-schemas = "required"
db-config = "false" db-config = "false"
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "required" db-uri = "required"
db-embed-default-join = "left"
jwt-aud = "" jwt-aud = ""
jwt-role-claim-key = ".\"role\"" jwt-role-claim-key = ".\"role\""
jwt-secret = "" jwt-secret = ""
@@ -12,6 +12,7 @@ db-schemas = "test,other_tenant1,other_tenant2"
db-config = "true" db-config = "true"
db-tx-end = "rollback-allow-override" db-tx-end = "rollback-allow-override"
db-uri = "<REPLACED_WITH_DB_URI>" db-uri = "<REPLACED_WITH_DB_URI>"
db-embed-default-join = "inner"
jwt-aud = "https://otherexample.org" jwt-aud = "https://otherexample.org"
jwt-role-claim-key = ".\"other\".\"role\"" jwt-role-claim-key = ".\"other\".\"role\""
jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE" jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE"
@@ -12,6 +12,7 @@ db-schemas = "test,tenant1,tenant2"
db-config = "true" db-config = "true"
db-tx-end = "commit-allow-override" db-tx-end = "commit-allow-override"
db-uri = "<REPLACED_WITH_DB_URI>" db-uri = "<REPLACED_WITH_DB_URI>"
db-embed-default-join = "inner"
jwt-aud = "https://example.org" jwt-aud = "https://example.org"
jwt-role-claim-key = ".\"a\".\"role\"" jwt-role-claim-key = ".\"a\".\"role\""
jwt-secret = "OVERRIDEREALLYREALLYREALLYREALLYVERYSAFE" jwt-secret = "OVERRIDEREALLYREALLYREALLYREALLYVERYSAFE"
@@ -12,6 +12,7 @@ db-schemas = "multi,tenant,setup"
db-config = "false" db-config = "false"
db-tx-end = "rollback-allow-override" db-tx-end = "rollback-allow-override"
db-uri = "tmp_db" db-uri = "tmp_db"
db-embed-default-join = "inner"
jwt-aud = "https://postgrest.org" jwt-aud = "https://postgrest.org"
jwt-role-claim-key = ".\"user\"[0].\"real-role\"" jwt-role-claim-key = ".\"user\"[0].\"real-role\""
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5" jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
@@ -12,6 +12,7 @@ db-schemas = "required"
db-config = "true" db-config = "true"
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "required" db-uri = "required"
db-embed-default-join = "left"
jwt-aud = "" jwt-aud = ""
jwt-role-claim-key = ".\"role\"" jwt-role-claim-key = ".\"role\""
jwt-secret = "" jwt-secret = ""
@@ -14,6 +14,7 @@ PGRST_DB_SCHEMAS: multi, tenant,setup
PGRST_DB_CONFIG: false PGRST_DB_CONFIG: false
PGRST_DB_TX_END: rollback-allow-override PGRST_DB_TX_END: rollback-allow-override
PGRST_DB_URI: tmp_db PGRST_DB_URI: tmp_db
PGRST_DB_EMBED_DEFAULT_JOIN: inner
PGRST_JWT_AUD: 'https://postgrest.org' PGRST_JWT_AUD: 'https://postgrest.org'
PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"' PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"'
PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5 PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5
+1
View File
@@ -12,6 +12,7 @@ db-schemas = "multi, tenant,setup"
db-config = "false" db-config = "false"
db-tx-end = "rollback-allow-override" db-tx-end = "rollback-allow-override"
db-uri = "tmp_db" db-uri = "tmp_db"
db-embed-default-join = "inner"
jwt-aud = "https://postgrest.org" jwt-aud = "https://postgrest.org"
jwt-role-claim-key = ".user[0].\"real-role\"" jwt-role-claim-key = ".user[0].\"real-role\""
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5" jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
+5
View File
@@ -179,3 +179,8 @@ invalidopenapimodes:
- 'follow-' - 'follow-'
- 'ignore-' - 'ignore-'
- '.#$$%&$%/' - '.#$$%&$%/'
invalidjointypes:
- 'left!'
- 'right'
- '.#$$%&$%/'
+15
View File
@@ -440,6 +440,21 @@ def test_invalid_openapi_mode(invalidopenapimodes, defaultenv):
print(line) print(line)
@pytest.mark.parametrize("invalidjointypes", FIXTURES["invalidjointypes"])
def test_invalid_db_embed_default_join(invalidjointypes, defaultenv):
"Given an invalid db-embed-default-join, Postgrest should exit with a non-zero exit code."
env = {
**defaultenv,
"PGRST_DB_EMBED_DEFAULT_JOIN": invalidjointypes,
}
with pytest.raises(PostgrestError):
dump = dumpconfig(CONFIGSDIR / "defaults.config", env=env)
for line in dump.split("\n"):
if line.startswith("db-embed-default-join"):
print(line)
def test_iat_claim(defaultenv): def test_iat_claim(defaultenv):
""" """
A claim with an 'iat' (issued at) attribute should be successful. A claim with an 'iat' (issued at) attribute should be successful.