Add and/or params for complex boolean logic
* Add support for and/or params in GET, POST, PATCH and DELETE * Restrict usage of IN in and/or to be inside parens e.g. in.(1,2,3) * Allow quoting operators for values that have ',)' chars inside and/or e.g. eq."(entity,1)"
This commit is contained in:
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- #742, Add connection retrying on startup and SIGHUP - @steve-chavez
|
- #742, Add connection retrying on startup and SIGHUP - @steve-chavez
|
||||||
|
- #652, Add and/or params for complex boolean logic - @steve-chavez
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ Test-Suite spec
|
|||||||
, Feature.SingularSpec
|
, Feature.SingularSpec
|
||||||
, Feature.StructureSpec
|
, Feature.StructureSpec
|
||||||
, Feature.UnicodeSpec
|
, Feature.UnicodeSpec
|
||||||
|
, Feature.AndOrParamsSpec
|
||||||
, SpecHelper
|
, SpecHelper
|
||||||
, TestTypes
|
, TestTypes
|
||||||
Build-Depends: aeson
|
Build-Depends: aeson
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ data ApiRequest = ApiRequest {
|
|||||||
, iPreferCount :: Bool
|
, iPreferCount :: Bool
|
||||||
-- | Filters on the result ("id", "eq.10")
|
-- | Filters on the result ("id", "eq.10")
|
||||||
, iFilters :: [(Text, Text)]
|
, iFilters :: [(Text, Text)]
|
||||||
|
-- | &and and &or parameters used for complex boolean logic
|
||||||
|
, iLogic :: [(Text, Text)]
|
||||||
-- | &select parameter used to shape the response
|
-- | &select parameter used to shape the response
|
||||||
, iSelect :: Text
|
, iSelect :: Text
|
||||||
-- | &order parameters for each level
|
-- | &order parameters for each level
|
||||||
@@ -116,7 +118,8 @@ userApiRequest schema req reqBody
|
|||||||
, iPreferRepresentation = representation
|
, iPreferRepresentation = representation
|
||||||
, iPreferSingleObjectParameter = singleObject
|
, iPreferSingleObjectParameter = singleObject
|
||||||
, iPreferCount = hasPrefer "count=exact"
|
, iPreferCount = hasPrefer "count=exact"
|
||||||
, iFilters = [ (toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, k /= "select", not (endingIn ["order", "limit", "offset"] k) ]
|
, iFilters = [ (toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, k /= "select", not (endingIn ["order", "limit", "offset", "and", "or"] k) ]
|
||||||
|
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
|
||||||
, iSelect = toS $ fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
|
, iSelect = toS $ fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
|
||||||
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
||||||
, iCanonicalQS = toS $ urlEncodeVars
|
, iCanonicalQS = toS $ urlEncodeVars
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
module PostgREST.DbRequestBuilder (
|
module PostgREST.DbRequestBuilder (
|
||||||
readRequest
|
readRequest
|
||||||
, mutateRequest
|
, mutateRequest
|
||||||
@@ -6,6 +7,7 @@ module PostgREST.DbRequestBuilder (
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
|
import Control.Arrow ((***))
|
||||||
import Control.Lens.Getter (view)
|
import Control.Lens.Getter (view)
|
||||||
import Control.Lens.Tuple (_1)
|
import Control.Lens.Tuple (_1)
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
@@ -173,7 +175,8 @@ addFiltersOrdersRanges :: ApiRequest -> Either ApiRequestError (ReadRequest -> R
|
|||||||
addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
|
addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
|
||||||
flip (foldr addFilter) <$> filters,
|
flip (foldr addFilter) <$> filters,
|
||||||
flip (foldr addOrder) <$> orders,
|
flip (foldr addOrder) <$> orders,
|
||||||
flip (foldr addRange) <$> ranges
|
flip (foldr addRange) <$> ranges,
|
||||||
|
flip (foldr addLogicTree) <$> logicForest
|
||||||
]
|
]
|
||||||
{-
|
{-
|
||||||
The esence of what is going on above is that we are composing tree functions
|
The esence of what is going on above is that we are composing tree functions
|
||||||
@@ -182,12 +185,13 @@ addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
|
|||||||
where
|
where
|
||||||
filters :: Either ApiRequestError [(EmbedPath, Filter)]
|
filters :: Either ApiRequestError [(EmbedPath, Filter)]
|
||||||
filters = mapM pRequestFilter flts
|
filters = mapM pRequestFilter flts
|
||||||
where
|
logicForest :: Either ApiRequestError [(EmbedPath, LogicTree)]
|
||||||
|
logicForest = mapM pRequestLogicTree logFrst
|
||||||
action = iAction apiRequest
|
action = iAction apiRequest
|
||||||
flts
|
-- there can be no filters on the root table when we are doing insert/update/delete
|
||||||
| action == ActionRead = iFilters apiRequest
|
(flts, logFrst)
|
||||||
| action == ActionInvoke = iFilters apiRequest
|
| action == ActionRead || action == ActionInvoke = (iFilters apiRequest, iLogic apiRequest)
|
||||||
| otherwise = filter (( "." `isInfixOf` ) . fst) $ iFilters apiRequest -- there can be no filters on the root table whre we are doing insert/update
|
| otherwise = join (***) (filter (( "." `isInfixOf` ) . fst)) (iFilters apiRequest, iLogic apiRequest)
|
||||||
orders :: Either ApiRequestError [(EmbedPath, [OrderTerm])]
|
orders :: Either ApiRequestError [(EmbedPath, [OrderTerm])]
|
||||||
orders = mapM pRequestOrder $ iOrder apiRequest
|
orders = mapM pRequestOrder $ iOrder apiRequest
|
||||||
ranges :: Either ApiRequestError [(EmbedPath, NonnegRange)]
|
ranges :: Either ApiRequestError [(EmbedPath, NonnegRange)]
|
||||||
@@ -211,6 +215,12 @@ addRangeToNode r (Node (q,i) f) = Node (q{range_=r}, i) f
|
|||||||
addRange :: (EmbedPath, NonnegRange) -> ReadRequest -> ReadRequest
|
addRange :: (EmbedPath, NonnegRange) -> ReadRequest -> ReadRequest
|
||||||
addRange = addProperty addRangeToNode
|
addRange = addProperty addRangeToNode
|
||||||
|
|
||||||
|
addLogicTreeToNode :: LogicTree -> ReadRequest -> ReadRequest
|
||||||
|
addLogicTreeToNode t (Node (q@Select{logic=l},i) f) = Node (q{logic=t:l}::ReadQuery, i) f
|
||||||
|
|
||||||
|
addLogicTree :: (EmbedPath, LogicTree) -> ReadRequest -> ReadRequest
|
||||||
|
addLogicTree = addProperty addLogicTreeToNode
|
||||||
|
|
||||||
addProperty :: (a -> ReadRequest -> ReadRequest) -> (EmbedPath, a) -> ReadRequest -> ReadRequest
|
addProperty :: (a -> ReadRequest -> ReadRequest) -> (EmbedPath, a) -> ReadRequest -> ReadRequest
|
||||||
addProperty f ([], a) n = f a n
|
addProperty f ([], a) n = f a n
|
||||||
addProperty f (path, a) (Node rn forest) =
|
addProperty f (path, a) (Node rn forest) =
|
||||||
@@ -248,8 +258,8 @@ mutateRequest :: ApiRequest -> [FieldName] -> Either Response MutateRequest
|
|||||||
mutateRequest apiRequest fldNames = mapLeft apiRequestError $
|
mutateRequest apiRequest fldNames = mapLeft apiRequestError $
|
||||||
case action of
|
case action of
|
||||||
ActionCreate -> Right $ Insert rootTableName payload returnings
|
ActionCreate -> Right $ Insert rootTableName payload returnings
|
||||||
ActionUpdate -> Update rootTableName <$> pure payload <*> filters <*> pure returnings
|
ActionUpdate -> Update rootTableName <$> pure payload <*> filters <*> logic_ <*> pure returnings
|
||||||
ActionDelete -> Delete rootTableName <$> filters <*> pure returnings
|
ActionDelete -> Delete rootTableName <$> filters <*> logic_ <*> pure returnings
|
||||||
_ -> Left UnsupportedVerb
|
_ -> Left UnsupportedVerb
|
||||||
where
|
where
|
||||||
action = iAction apiRequest
|
action = iAction apiRequest
|
||||||
@@ -261,7 +271,11 @@ mutateRequest apiRequest fldNames = mapLeft apiRequestError $
|
|||||||
_ -> undefined
|
_ -> undefined
|
||||||
returnings = if iPreferRepresentation apiRequest == None then [] else fldNames
|
returnings = if iPreferRepresentation apiRequest == None then [] else fldNames
|
||||||
filters = map snd <$> mapM pRequestFilter mutateFilters
|
filters = map snd <$> mapM pRequestFilter mutateFilters
|
||||||
where mutateFilters = filter (not . ( "." `isInfixOf` ) . fst) $ iFilters apiRequest -- update/delete filters can be only on the root table
|
logic_ = map snd <$> mapM pRequestLogicTree logicFilters
|
||||||
|
-- update/delete filters can be only on the root table
|
||||||
|
mutateFilters = onlyRoot $ iFilters apiRequest
|
||||||
|
logicFilters = onlyRoot $ iLogic apiRequest
|
||||||
|
onlyRoot = filter (not . ( "." `isInfixOf` ) . fst)
|
||||||
|
|
||||||
fieldNames :: ReadRequest -> [FieldName]
|
fieldNames :: ReadRequest -> [FieldName]
|
||||||
fieldNames (Node (sel, _) forest) =
|
fieldNames (Node (sel, _) forest) =
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pRequestFilter :: (Text, Text) -> Either ApiRequestError (EmbedPath, Filter)
|
|||||||
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
||||||
where
|
where
|
||||||
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
||||||
oper = parse pOperation ("failed to parse filter (" ++ toS v ++ ")") $ toS v
|
oper = parse (pOperation pVText pVTextL) ("failed to parse filter (" ++ toS v ++ ")") $ toS v
|
||||||
path = fst <$> treePath
|
path = fst <$> treePath
|
||||||
fld = snd <$> treePath
|
fld = snd <$> treePath
|
||||||
|
|
||||||
@@ -38,6 +38,15 @@ pRequestRange (k, v) = mapError $ (,) <$> path <*> pure v
|
|||||||
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
||||||
path = fst <$> treePath
|
path = fst <$> treePath
|
||||||
|
|
||||||
|
pRequestLogicTree :: (Text, Text) -> Either ApiRequestError (EmbedPath, LogicTree)
|
||||||
|
pRequestLogicTree (k, v) = mapError $ (,) <$> embedPath <*> logicTree
|
||||||
|
where
|
||||||
|
path = parse pLogicPath ("failed to parser logic path (" ++ toS k ++ ")") $ toS k
|
||||||
|
embedPath = fst <$> path
|
||||||
|
op = snd <$> path
|
||||||
|
-- Concat op and v to make pLogicTree argument regular, in the form of "op(.,.)"
|
||||||
|
logicTree = join $ parse pLogicTree ("failed to parse logic tree (" ++ toS v ++ ")") . toS <$> ((<>) <$> op <*> pure v)
|
||||||
|
|
||||||
ws :: Parser Text
|
ws :: Parser Text
|
||||||
ws = toS <$> many (oneOf " \t")
|
ws = toS <$> many (oneOf " \t")
|
||||||
|
|
||||||
@@ -49,7 +58,7 @@ pReadRequest rootNodeName = do
|
|||||||
fieldTree <- pFieldForest
|
fieldTree <- pFieldForest
|
||||||
return $ foldr treeEntry (Node (readQuery, (rootNodeName, Nothing, Nothing)) []) fieldTree
|
return $ foldr treeEntry (Node (readQuery, (rootNodeName, Nothing, Nothing)) []) fieldTree
|
||||||
where
|
where
|
||||||
readQuery = Select [] [rootNodeName] [] Nothing allRange
|
readQuery = Select [] [rootNodeName] [] [] Nothing allRange
|
||||||
treeEntry :: Tree SelectItem -> ReadRequest -> ReadRequest
|
treeEntry :: Tree SelectItem -> ReadRequest -> ReadRequest
|
||||||
treeEntry (Node fld@((fn, _),_,alias) fldForest) (Node (q, i) rForest) =
|
treeEntry (Node fld@((fn, _),_,alias) fldForest) (Node (q, i) rForest) =
|
||||||
case fldForest of
|
case fldForest of
|
||||||
@@ -57,7 +66,7 @@ pReadRequest rootNodeName = do
|
|||||||
_ -> Node (q, i) newForest
|
_ -> Node (q, i) newForest
|
||||||
where
|
where
|
||||||
newForest =
|
newForest =
|
||||||
foldr treeEntry (Node (Select [] [fn] [] Nothing allRange, (fn, Nothing, alias)) []) fldForest:rForest
|
foldr treeEntry (Node (Select [] [fn] [] [] Nothing allRange, (fn, Nothing, alias)) []) fldForest:rForest
|
||||||
|
|
||||||
pTreePath :: Parser (EmbedPath, Field)
|
pTreePath :: Parser (EmbedPath, Field)
|
||||||
pTreePath = do
|
pTreePath = do
|
||||||
@@ -119,13 +128,13 @@ pSelect = lexeme $
|
|||||||
s <- pStar
|
s <- pStar
|
||||||
return ((s, Nothing), Nothing, Nothing)
|
return ((s, Nothing), Nothing, Nothing)
|
||||||
|
|
||||||
pOperation :: Parser Operation
|
pOperation :: Parser Operand -> Parser Operand -> Parser Operation
|
||||||
pOperation = try ( string "not" *> pDelimiter *> (Operation True <$> pExpr)) <|> Operation False <$> pExpr
|
pOperation parserVText parserVTextL = try ( string "not" *> pDelimiter *> (Operation True <$> pExpr)) <|> Operation False <$> pExpr
|
||||||
where
|
where
|
||||||
pExpr :: Parser (Operator, Operand)
|
pExpr :: Parser (Operator, Operand)
|
||||||
pExpr =
|
pExpr =
|
||||||
((,) <$> (toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys notInOps)) <*> pVText)
|
((,) <$> (toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys notInOps)) <*> parserVText)
|
||||||
<|> ((,) <$> (toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys inOps)) <*> pVTextL)
|
<|> ((,) <$> (toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys inOps)) <*> parserVTextL)
|
||||||
<?> "operator (eq, gt, ...)"
|
<?> "operator (eq, gt, ...)"
|
||||||
inOps = M.filterWithKey (const . flip elem ["in", "notin"]) operators
|
inOps = M.filterWithKey (const . flip elem ["in", "notin"]) operators
|
||||||
notInOps = M.difference operators inOps
|
notInOps = M.difference operators inOps
|
||||||
@@ -137,7 +146,10 @@ pVTextL :: Parser Operand
|
|||||||
pVTextL = VTextL <$> lexeme pLValue `sepBy1` char ','
|
pVTextL = VTextL <$> lexeme pLValue `sepBy1` char ','
|
||||||
where
|
where
|
||||||
pLValue :: Parser Text
|
pLValue :: Parser Text
|
||||||
pLValue = toS <$> (try (char '"' *> many (noneOf "\"") <* char '"' <* notFollowedBy (noneOf ",") ) <|> many (noneOf ","))
|
pLValue = try pQuotedValue <|> (toS <$> many (noneOf ","))
|
||||||
|
|
||||||
|
pQuotedValue :: Parser Text
|
||||||
|
pQuotedValue = toS <$> (char '"' *> many (noneOf "\"") <* char '"' <* notFollowedBy (noneOf ",)"))
|
||||||
|
|
||||||
pDelimiter :: Parser Char
|
pDelimiter :: Parser Char
|
||||||
pDelimiter = char '.' <?> "delimiter (.)"
|
pDelimiter = char '.' <?> "delimiter (.)"
|
||||||
@@ -161,6 +173,44 @@ pOrderTerm =
|
|||||||
)
|
)
|
||||||
<|> OrderTerm <$> pField <*> pure Nothing <*> pure Nothing
|
<|> OrderTerm <$> pField <*> pure Nothing <*> pure Nothing
|
||||||
|
|
||||||
|
pLogicTree :: Parser LogicTree
|
||||||
|
pLogicTree = Stmnt <$> try pLogicFilter
|
||||||
|
<|> Expr <$> pNot <*> pLogicOp <*> (lexeme (char '(') *> pLogicTree) <*> (lexeme (char ',') *> pLogicTree <* lexeme (char ')'))
|
||||||
|
where
|
||||||
|
pLogicFilter :: Parser Filter
|
||||||
|
pLogicFilter = Filter <$> pField <* pDelimiter <*> pOperation pLogicVText pLogicVTextL
|
||||||
|
pNot :: Parser Bool
|
||||||
|
pNot = try (string "not" *> pDelimiter *> pure True)
|
||||||
|
<|> pure False
|
||||||
|
<?> "negation operator (not)"
|
||||||
|
pLogicOp :: Parser LogicOperator
|
||||||
|
pLogicOp = try (string "and" *> pure And)
|
||||||
|
<|> string "or" *> pure Or
|
||||||
|
<?> "logic operator (and, or)"
|
||||||
|
|
||||||
|
pLogicVText :: Parser Operand
|
||||||
|
pLogicVText = VText <$> (try pQuotedValue <|> try pPgArray <|> (toS <$> many (noneOf ",)")))
|
||||||
|
where
|
||||||
|
pPgArray :: Parser Text
|
||||||
|
pPgArray = do
|
||||||
|
a <- string "{"
|
||||||
|
b <- many (noneOf "{}")
|
||||||
|
c <- string "}"
|
||||||
|
toS <$> pure (a ++ b ++ c)
|
||||||
|
|
||||||
|
pLogicVTextL :: Parser Operand
|
||||||
|
pLogicVTextL = VTextL <$> (lexeme (char '(') *> pLValue `sepBy1` char ',' <* lexeme (char ')'))
|
||||||
|
where
|
||||||
|
pLValue :: Parser Text
|
||||||
|
pLValue = try pQuotedValue <|> (toS <$> many (noneOf ",)"))
|
||||||
|
|
||||||
|
pLogicPath :: Parser (EmbedPath, Text)
|
||||||
|
pLogicPath = do
|
||||||
|
path <- pFieldName `sepBy1` pDelimiter
|
||||||
|
let op = last path
|
||||||
|
notOp = "not." <> op
|
||||||
|
return (filter (/= "not") (init path), if "not" `elem` path then notOp else op)
|
||||||
|
|
||||||
mapError :: Either ParseError a -> Either ApiRequestError a
|
mapError :: Either ParseError a -> Either ApiRequestError a
|
||||||
mapError = mapLeft translateError
|
mapError = mapLeft translateError
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -194,21 +194,25 @@ pgFmtLit x =
|
|||||||
|
|
||||||
requestToCountQuery :: Schema -> DbRequest -> SqlQuery
|
requestToCountQuery :: Schema -> DbRequest -> SqlQuery
|
||||||
requestToCountQuery _ (DbMutate _) = undefined
|
requestToCountQuery _ (DbMutate _) = undefined
|
||||||
requestToCountQuery schema (DbRead (Node (Select _ _ conditions _ _, (mainTbl, _, _)) _)) =
|
requestToCountQuery schema (DbRead (Node (Select _ _ conditions logic_ _ _, (mainTbl, _, _)) _)) =
|
||||||
unwords [
|
unwords [
|
||||||
"SELECT pg_catalog.count(*)",
|
"SELECT pg_catalog.count(*)",
|
||||||
"FROM ", fromQi qi,
|
"FROM ", fromQi qi,
|
||||||
("WHERE " <> intercalate " AND " ( map (pgFmtFilter qi) localConditions )) `emptyOnNull` localConditions
|
-- logic_ doesn't not need localFilter filtering because it doesn't have VForeignKey vals
|
||||||
|
("WHERE " <> intercalate " AND " (map (pgFmtFilter qi) localConditions ++ map (pgFmtLogicTree qi) logic_))
|
||||||
|
`emptyOnFalse` (null conditions && null logic_)
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
qi = removeSourceCTESchema schema mainTbl
|
qi = removeSourceCTESchema schema mainTbl
|
||||||
fn Filter{operation=Operation{expr=(_, VText _)}} = True
|
localFilter :: Filter -> Bool
|
||||||
fn Filter{operation=Operation{expr=(_, VTextL _)}} = True
|
localFilter Filter{operation=Operation{expr=(_, val)}} = case val of
|
||||||
fn Filter{operation=Operation{expr=(_, VForeignKey _ _)}} = False
|
VText _ -> True
|
||||||
localConditions = filter fn conditions
|
VTextL _ -> True
|
||||||
|
VForeignKey _ _ -> False
|
||||||
|
localConditions = filter localFilter conditions
|
||||||
|
|
||||||
requestToQuery :: Schema -> Bool -> DbRequest -> SqlQuery
|
requestToQuery :: Schema -> Bool -> DbRequest -> SqlQuery
|
||||||
requestToQuery schema isParent (DbRead (Node (Select colSelects tbls conditions ord range, (nodeName, maybeRelation, _)) forest)) =
|
requestToQuery schema isParent (DbRead (Node (Select colSelects tbls conditions logic_ ord range, (nodeName, maybeRelation, _)) forest)) =
|
||||||
query
|
query
|
||||||
where
|
where
|
||||||
mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation)
|
mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation)
|
||||||
@@ -218,7 +222,8 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls conditions
|
|||||||
"SELECT ", intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects),
|
"SELECT ", intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects),
|
||||||
"FROM ", intercalate ", " (map (fromQi . toQi) tbls),
|
"FROM ", intercalate ", " (map (fromQi . toQi) tbls),
|
||||||
unwords joins,
|
unwords joins,
|
||||||
("WHERE " <> intercalate " AND " ( map (pgFmtFilter qi ) conditions )) `emptyOnNull` conditions,
|
("WHERE " <> intercalate " AND " (map (pgFmtFilter qi) conditions ++ map (pgFmtLogicTree qi) logic_))
|
||||||
|
`emptyOnFalse` (null conditions && null logic_),
|
||||||
orderF (fromMaybe [] ord),
|
orderF (fromMaybe [] ord),
|
||||||
if isParent then "" else limitF range
|
if isParent then "" else limitF range
|
||||||
]
|
]
|
||||||
@@ -279,7 +284,7 @@ requestToQuery schema _ (DbMutate (Insert mainTbl (PayloadJSON rows) returnings)
|
|||||||
ret = if null returnings
|
ret = if null returnings
|
||||||
then ""
|
then ""
|
||||||
else unwords [" RETURNING ", intercalate ", " (map (pgFmtColumn qi) returnings)]
|
else unwords [" RETURNING ", intercalate ", " (map (pgFmtColumn qi) returnings)]
|
||||||
requestToQuery schema _ (DbMutate (Update mainTbl (PayloadJSON rows) conditions returnings)) =
|
requestToQuery schema _ (DbMutate (Update mainTbl (PayloadJSON rows) conditions logic_ returnings)) =
|
||||||
case rows V.!? 0 of
|
case rows V.!? 0 of
|
||||||
Just obj ->
|
Just obj ->
|
||||||
let assignments = map
|
let assignments = map
|
||||||
@@ -287,20 +292,22 @@ requestToQuery schema _ (DbMutate (Update mainTbl (PayloadJSON rows) conditions
|
|||||||
unwords [
|
unwords [
|
||||||
"UPDATE ", fromQi qi,
|
"UPDATE ", fromQi qi,
|
||||||
" SET " <> intercalate "," assignments <> " ",
|
" SET " <> intercalate "," assignments <> " ",
|
||||||
("WHERE " <> intercalate " AND " ( map (pgFmtFilter qi ) conditions )) `emptyOnNull` conditions,
|
("WHERE " <> intercalate " AND " (map (pgFmtFilter qi) conditions ++ map (pgFmtLogicTree qi) logic_))
|
||||||
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnNull` returnings
|
`emptyOnFalse` (null conditions && null logic_),
|
||||||
|
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnFalse` null returnings
|
||||||
]
|
]
|
||||||
Nothing -> undefined
|
Nothing -> undefined
|
||||||
where
|
where
|
||||||
qi = QualifiedIdentifier schema mainTbl
|
qi = QualifiedIdentifier schema mainTbl
|
||||||
requestToQuery schema _ (DbMutate (Delete mainTbl conditions returnings)) =
|
requestToQuery schema _ (DbMutate (Delete mainTbl conditions logic_ returnings)) =
|
||||||
query
|
query
|
||||||
where
|
where
|
||||||
qi = QualifiedIdentifier schema mainTbl
|
qi = QualifiedIdentifier schema mainTbl
|
||||||
query = unwords [
|
query = unwords [
|
||||||
"DELETE FROM ", fromQi qi,
|
"DELETE FROM ", fromQi qi,
|
||||||
("WHERE " <> intercalate " AND " ( map (pgFmtFilter qi ) conditions )) `emptyOnNull` conditions,
|
("WHERE " <> intercalate " AND " (map (pgFmtFilter qi) conditions ++ map (pgFmtLogicTree qi) logic_))
|
||||||
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnNull` returnings
|
`emptyOnFalse` (null conditions && null logic_),
|
||||||
|
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnFalse` null returnings
|
||||||
]
|
]
|
||||||
|
|
||||||
sourceCTEName :: SqlFragment
|
sourceCTEName :: SqlFragment
|
||||||
@@ -384,8 +391,8 @@ getJoinConditions (Relation t cols ft fcs typ lt lc1 lc2) =
|
|||||||
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Query a b
|
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Query a b
|
||||||
unicodeStatement = H.statement . T.encodeUtf8
|
unicodeStatement = H.statement . T.encodeUtf8
|
||||||
|
|
||||||
emptyOnNull :: Text -> [a] -> Text
|
emptyOnFalse :: Text -> Bool -> Text
|
||||||
emptyOnNull val x = if null x then "" else val
|
emptyOnFalse val cond = if cond then "" else val
|
||||||
|
|
||||||
insertableValue :: JSON.Value -> SqlFragment
|
insertableValue :: JSON.Value -> SqlFragment
|
||||||
insertableValue JSON.Null = "null"
|
insertableValue JSON.Null = "null"
|
||||||
@@ -439,6 +446,11 @@ pgFmtFilter table (Filter fld (Operation hasNot_ ex)) = notOp <> " " <> case ex
|
|||||||
else pgFmtFieldOp op <> "(" <> intercalate ", " (map unknownLiteral vals) <> ") "
|
else pgFmtFieldOp op <> "(" <> intercalate ", " (map unknownLiteral vals) <> ") "
|
||||||
Nothing -> emptyValForIn op
|
Nothing -> emptyValForIn op
|
||||||
|
|
||||||
|
pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment
|
||||||
|
pgFmtLogicTree qi (Expr hasNot_ op lt rt) = notOp <> " (" <> pgFmtLogicTree qi lt <> " " <> show op <> " " <> pgFmtLogicTree qi rt <> ")"
|
||||||
|
where notOp = if hasNot_ then "NOT" else ""
|
||||||
|
pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt
|
||||||
|
|
||||||
pgFmtJsonPath :: Maybe JsonPath -> SqlFragment
|
pgFmtJsonPath :: Maybe JsonPath -> SqlFragment
|
||||||
pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
|
pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
|
||||||
pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit x <> pgFmtJsonPath ( Just xs )
|
pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit x <> pgFmtJsonPath ( Just xs )
|
||||||
|
|||||||
+19
-3
@@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
module PostgREST.Types where
|
module PostgREST.Types where
|
||||||
import Protolude
|
import Protolude
|
||||||
import qualified GHC.Show
|
import qualified GHC.Show
|
||||||
@@ -157,6 +158,21 @@ operators = M.fromList [
|
|||||||
data Operation = Operation{ hasNot::Bool, expr::(Operator, Operand) } deriving (Eq, Show)
|
data Operation = Operation{ hasNot::Bool, expr::(Operator, Operand) } deriving (Eq, Show)
|
||||||
data Operand = VText Text | VTextL [Text] | VForeignKey QualifiedIdentifier ForeignKey deriving (Show, Eq)
|
data Operand = VText Text | VTextL [Text] | VForeignKey QualifiedIdentifier ForeignKey deriving (Show, Eq)
|
||||||
|
|
||||||
|
data LogicOperator = And | Or deriving Eq
|
||||||
|
instance Show LogicOperator where
|
||||||
|
show And = "AND"
|
||||||
|
show Or = "OR"
|
||||||
|
{-|
|
||||||
|
Boolean logic expression tree e.g. "and(name.eq.N,or(id.eq.1,id.eq.2))" is:
|
||||||
|
|
||||||
|
And
|
||||||
|
/ \
|
||||||
|
name.eq.N Or
|
||||||
|
/ \
|
||||||
|
id.eq.1 id.eq.2
|
||||||
|
-}
|
||||||
|
data LogicTree = Expr Bool LogicOperator LogicTree LogicTree | Stmnt Filter deriving (Show, Eq)
|
||||||
|
|
||||||
type FieldName = Text
|
type FieldName = Text
|
||||||
type JsonPath = [Text]
|
type JsonPath = [Text]
|
||||||
type Field = (FieldName, Maybe JsonPath)
|
type Field = (FieldName, Maybe JsonPath)
|
||||||
@@ -168,10 +184,10 @@ type SelectItem = (Field, Maybe Cast, Maybe Alias)
|
|||||||
type EmbedPath = [Text]
|
type EmbedPath = [Text]
|
||||||
data Filter = Filter { field::Field, operation::Operation } deriving (Show, Eq)
|
data Filter = Filter { field::Field, operation::Operation } deriving (Show, Eq)
|
||||||
|
|
||||||
data ReadQuery = Select { select::[SelectItem], from::[TableName], flt_::[Filter], order::Maybe [OrderTerm], range_::NonnegRange } deriving (Show, Eq)
|
data ReadQuery = Select { select::[SelectItem], from::[TableName], flt_::[Filter], logic::[LogicTree], order::Maybe [OrderTerm], range_::NonnegRange } deriving (Show, Eq)
|
||||||
data MutateQuery = Insert { in_::TableName, qPayload::PayloadJSON, returning::[FieldName] }
|
data MutateQuery = Insert { in_::TableName, qPayload::PayloadJSON, returning::[FieldName] }
|
||||||
| Delete { in_::TableName, where_::[Filter], returning::[FieldName] }
|
| Delete { in_::TableName, where_::[Filter], logic::[LogicTree], returning::[FieldName] }
|
||||||
| Update { in_::TableName, qPayload::PayloadJSON, where_::[Filter], returning::[FieldName] } deriving (Show, Eq)
|
| Update { in_::TableName, qPayload::PayloadJSON, where_::[Filter], logic::[LogicTree], returning::[FieldName] } deriving (Show, Eq)
|
||||||
type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias))
|
type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias))
|
||||||
type ReadRequest = Tree ReadNode
|
type ReadRequest = Tree ReadNode
|
||||||
type MutateRequest = MutateQuery
|
type MutateRequest = MutateQuery
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
module Feature.AndOrParamsSpec where
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
import Network.HTTP.Types
|
||||||
|
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
|
import SpecHelper
|
||||||
|
import Protolude hiding (get)
|
||||||
|
|
||||||
|
|
||||||
|
spec :: SpecWith Application
|
||||||
|
spec =
|
||||||
|
describe "and/or params used for complex boolean logic" $ do
|
||||||
|
context "used with GET" $ do
|
||||||
|
context "or param" $ do
|
||||||
|
it "can do simple logic" $
|
||||||
|
get "/entities?or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can negate simple logic" $
|
||||||
|
get "/entities?not.or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can be combined with traditional filters" $
|
||||||
|
get "/entities?or=(id.eq.1,id.eq.2)&name=eq.entity 1&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "embedded levels" $ do
|
||||||
|
it "can do logic on the second level" $
|
||||||
|
get "/entities?child_entities.or=(id.eq.1,name.eq.child entity 2)&select=id,child_entities{id}" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id": 1, "child_entities": [ { "id": 1 }, { "id": 2 } ] }, { "id": 2, "child_entities": []},
|
||||||
|
{"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}
|
||||||
|
]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can do logic on the third level" $
|
||||||
|
get "/entities?child_entities.grandchild_entities.or=(id.eq.1,id.eq.2)&select=id,child_entities{id,grandchild_entities{id}}" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id": 1, "child_entities": [ { "id": 1, "grandchild_entities": [ { "id": 1 }, { "id": 2 } ]}, { "id": 2, "grandchild_entities": []}]},
|
||||||
|
{"id": 2, "child_entities": [ { "id": 3, "grandchild_entities": []} ]},
|
||||||
|
{"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}
|
||||||
|
]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "and/or params combined" $ do
|
||||||
|
it "can be nested inside the same expression" $
|
||||||
|
get "/entities?or=(and(name.eq.entity 2,id.eq.2),and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can be negated while nested" $
|
||||||
|
get "/entities?or=(not.and(name.eq.entity 2,id.eq.2),not.and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can be combined unnested" $
|
||||||
|
get "/entities?and=(id.eq.1,name.eq.entity 1)&or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "operators inside and/or" $ do
|
||||||
|
it "can handle eq and neq" $
|
||||||
|
get "/entities?and=(id.eq.1,id.neq.2))&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can handle lt and gt" $
|
||||||
|
get "/entities?or=(id.lt.2,id.gt.3)&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can handle lte and gte" $
|
||||||
|
get "/entities?or=(id.lte.2,id.gte.3)&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can handle like and ilike" $
|
||||||
|
get "/entities?or=(name.like.*1,name.ilike.*ENTITY 2)&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can handle in" $
|
||||||
|
get "/entities?or=(id.in.(1,2),id.in.(3,4))&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can handle is" $
|
||||||
|
get "/entities?and=(name.is.null,arr.is.null)&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can handle @@" $
|
||||||
|
get "/entities?or=(text_search_vector.@@.bar,text_search_vector.@@.baz)&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "can handle @> and <@" $
|
||||||
|
get "/entities?or=(arr.@>.{1,2,3},arr.<@.{1})&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 },{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
context "operators with not" $ do
|
||||||
|
it "eq, @>, like can be negated" $
|
||||||
|
get "/entities?and=(arr.not.@>.{1,2,3},and(id.not.eq.2,name.not.like.*3))&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1}]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "in, is, @@ can be negated" $
|
||||||
|
get "/entities?and=(id.not.in.(1,3),and(name.not.is.null,text_search_vector.not.@@.foo))&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 2}]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "lt, gte, <@ can be negated" $
|
||||||
|
get "/entities?and=(arr.not.<@.{1},or(id.not.lt.1,id.not.gte.3))&select=id" `shouldRespondWith`
|
||||||
|
[json|[{"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "gt, lte, ilike can be negated" $
|
||||||
|
get "/entities?and=(name.not.ilike.*ITY2,or(id.not.gt.4,id.not.lte.1))&select=id" `shouldRespondWith`
|
||||||
|
[json|[{"id": 1}, {"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "and/or params with quotes" $ do
|
||||||
|
it "eq can have quotes" $
|
||||||
|
get "/grandchild_entities?or=(name.eq.\"(grandchild,entity,4)\",name.eq.\"(grandchild,entity,5)\")&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "like and ilike can have quotes" $
|
||||||
|
get "/grandchild_entities?or=(name.like.\"*ity,4*\",name.ilike.\"*ITY,5)\")&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "in can have quotes" $
|
||||||
|
get "/grandchild_entities?or=(id.in.(\"1\",\"2\"),id.in.(\"3\",\"4\"))&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "allows whitespace" $
|
||||||
|
get "/entities?and=( and ( id.in.( 1, 2, 3 ) , id.eq.3 ) , or ( id.eq.2 , id.eq.3 ) )&select=id" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "used with POST" $
|
||||||
|
it "includes related data with filters" $
|
||||||
|
request methodPost "/child_entities?entities.or=(id.eq.2,id.eq.3)&select=id,entities{id}"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json|[{"id":4,"name":"entity 4","parent_id":1},
|
||||||
|
{"id":5,"name":"entity 5","parent_id":2},
|
||||||
|
{"id":6,"name":"entity 6","parent_id":3}]|] `shouldRespondWith`
|
||||||
|
[json|[{"id": 4, "entities":null}, {"id": 5, "entities": {"id": 2}}, {"id": 6, "entities": {"id": 3}}]|]
|
||||||
|
{ matchStatus = 201, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "used with PATCH" $
|
||||||
|
it "succeeds when using and/or params" $
|
||||||
|
request methodPatch "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json|{ name : "updated grandchild entity"}|] `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "used with DELETE" $
|
||||||
|
it "succeeds when using and/or params" $
|
||||||
|
request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
|
||||||
|
[("Prefer", "return=representation")] "" `shouldRespondWith`
|
||||||
|
[json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can query columns that begin with and/or reserved words" $
|
||||||
|
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
|
||||||
|
|
||||||
|
it "can query jsonb columns" $
|
||||||
|
get "/grandchild_entities?or=(jsonb_col->a->>b.eq.foo, jsonb_col->>b.eq.bar)&select=id" `shouldRespondWith`
|
||||||
|
[json|[{id: 4}, {id: 5}]|] { matchStatus = 200, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "fails when using IN without () and provides meaningful error message" $
|
||||||
|
get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"details": "unexpected \"1\" expecting \"(\"",
|
||||||
|
"message": "\"failed to parse logic tree ((id.in.1,2,id.eq.3))\" (line 1, column 10)"
|
||||||
|
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "fails on malformed query params and provides meaningful error message" $ do
|
||||||
|
get "/entities?or=()" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"details": "unexpected \")\" expecting field name (* or [a..z0..9_]), negation operator (not) or logic operator (and, or)",
|
||||||
|
"message": "\"failed to parse logic tree (())\" (line 1, column 4)"
|
||||||
|
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/entities?or=)(" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"details": "unexpected \")\" expecting \"(\"",
|
||||||
|
"message": "\"failed to parse logic tree ()()\" (line 1, column 3)"
|
||||||
|
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/entities?or=(id.eq.1)" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"details": "unexpected \")\" expecting \",\"",
|
||||||
|
"message": "\"failed to parse logic tree ((id.eq.1))\" (line 1, column 11)"
|
||||||
|
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/entities?and=(ord(id.eq.1,id.eq.1),id.eq.2)" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"details": "unexpected \"d\" expecting \"(\"",
|
||||||
|
"message": "\"failed to parse logic tree ((ord(id.eq.1,id.eq.1),id.eq.2))\" (line 1, column 7)"
|
||||||
|
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/entities?or=(id.eq.1,not.xor(id.eq.2,id.eq.3))" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"details": "unexpected \"x\" expecting logic operator (and, or)",
|
||||||
|
"message": "\"failed to parse logic tree ((id.eq.1,not.xor(id.eq.2,id.eq.3)))\" (line 1, column 16)"
|
||||||
|
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
@@ -26,6 +26,7 @@ import qualified Feature.StructureSpec
|
|||||||
import qualified Feature.SingularSpec
|
import qualified Feature.SingularSpec
|
||||||
import qualified Feature.UnicodeSpec
|
import qualified Feature.UnicodeSpec
|
||||||
import qualified Feature.ProxySpec
|
import qualified Feature.ProxySpec
|
||||||
|
import qualified Feature.AndOrParamsSpec
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -84,4 +85,5 @@ main = do
|
|||||||
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
|
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
|
||||||
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
|
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
|
||||||
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
|
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
|
||||||
|
, ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec)
|
||||||
]
|
]
|
||||||
|
|||||||
Vendored
+18
@@ -297,6 +297,24 @@ INSERT INTO w_or_wo_comma_names VALUES ('Larry Thompson');
|
|||||||
TRUNCATE TABLE items_with_different_col_types CASCADE;
|
TRUNCATE TABLE items_with_different_col_types CASCADE;
|
||||||
INSERT INTO items_with_different_col_types VALUES (1, null, null, null, null, null, null, null);
|
INSERT INTO items_with_different_col_types VALUES (1, null, null, null, null, null, null, null);
|
||||||
|
|
||||||
|
TRUNCATE TABLE entities CASCADE;
|
||||||
|
INSERT INTO entities VALUES (1, 'entity 1', '{1}', '''bar'':2 ''foo'':1');
|
||||||
|
INSERT INTO entities VALUES (2, 'entity 2', '{1,2}', '''baz'':1 ''qux'':2');
|
||||||
|
INSERT INTO entities VALUES (3, 'entity 3', '{1,2,3}', null);
|
||||||
|
INSERT INTO entities VALUES (4, null, null, null);
|
||||||
|
|
||||||
|
TRUNCATE TABLE child_entities CASCADE;
|
||||||
|
INSERT INTO child_entities VALUES (1, 'child entity 1', 1);
|
||||||
|
INSERT INTO child_entities VALUES (2, 'child entity 2', 1);
|
||||||
|
INSERT INTO child_entities VALUES (3, 'child entity 3', 2);
|
||||||
|
|
||||||
|
TRUNCATE TABLE grandchild_entities CASCADE;
|
||||||
|
INSERT INTO grandchild_entities VALUES (1, 'grandchild entity 1', 1, null, null, null);
|
||||||
|
INSERT INTO grandchild_entities VALUES (2, 'grandchild entity 2', 1, null, null, null);
|
||||||
|
INSERT INTO grandchild_entities VALUES (3, 'grandchild entity 3', 2, null, null, null);
|
||||||
|
INSERT INTO grandchild_entities VALUES (4, '(grandchild,entity,4)', 2, null, null, '{"a": {"b":"foo"}}');
|
||||||
|
INSERT INTO grandchild_entities VALUES (5, '(grandchild,entity,5)', 2, null, null, '{"b":"bar"}');
|
||||||
|
|
||||||
--
|
--
|
||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|||||||
Vendored
+3
@@ -53,6 +53,9 @@ GRANT ALL ON TABLE
|
|||||||
, images_base64
|
, images_base64
|
||||||
, w_or_wo_comma_names
|
, w_or_wo_comma_names
|
||||||
, items_with_different_col_types
|
, items_with_different_col_types
|
||||||
|
, entities
|
||||||
|
, child_entities
|
||||||
|
, grandchild_entities
|
||||||
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;
|
||||||
|
|||||||
Vendored
+24
@@ -1174,6 +1174,30 @@ create table items_with_different_col_types (
|
|||||||
time_data time
|
time_data time
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Tables used for testing complex boolean logic with and/or query params
|
||||||
|
|
||||||
|
create table entities (
|
||||||
|
id integer primary key,
|
||||||
|
name text,
|
||||||
|
arr integer[],
|
||||||
|
text_search_vector tsvector
|
||||||
|
);
|
||||||
|
|
||||||
|
create table child_entities (
|
||||||
|
id integer primary key,
|
||||||
|
name text,
|
||||||
|
parent_id integer references entities(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table grandchild_entities (
|
||||||
|
id integer primary key,
|
||||||
|
name text,
|
||||||
|
parent_id integer references child_entities(id),
|
||||||
|
or_starting_col text,
|
||||||
|
and_starting_col text,
|
||||||
|
jsonb_col jsonb
|
||||||
|
);
|
||||||
|
|
||||||
--
|
--
|
||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|||||||
Reference in New Issue
Block a user