- Role: {permsState.role} Query: {permsState.query}
+ Role: {permsState.role}
+ Query: {permsState.query}
{getRowSection(queryTypes, permsState)}
{getColumnSection(tableSchema, permsState)}
+ {getLimitSection(permsState)}
{getUpsertSection(permsState)}
{getButtonsSection(tableSchema, permsState)}
diff --git a/server/src-lib/Hasura/GraphQL/Resolve.hs b/server/src-lib/Hasura/GraphQL/Resolve.hs
index f71b08fe56c57..178687f214b35 100644
--- a/server/src-lib/Hasura/GraphQL/Resolve.hs
+++ b/server/src-lib/Hasura/GraphQL/Resolve.hs
@@ -32,8 +32,8 @@ buildTx userInfo gCtx fld = do
opCxt <- getOpCtx $ _fName fld
join $ fmap fst $ runConvert (fldMap, orderByCtx) $ case opCxt of
- OCSelect tn permFilter hdrs ->
- validateHdrs hdrs >> RS.convertSelect tn permFilter fld
+ OCSelect tn permFilter permLimit hdrs ->
+ validateHdrs hdrs >> RS.convertSelect tn permFilter permLimit fld
-- RS.convertSelect tn permFilter fld
OCInsert tn vn cols hdrs ->
validateHdrs hdrs >> RM.convertInsert (tn, vn) cols fld
diff --git a/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs b/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs
index c13c700f2b878..84eaf62ea2ccd 100644
--- a/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs
+++ b/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs
@@ -80,7 +80,7 @@ parseColExp nt n val = do
fldInfo <- getFldInfo nt n
case fldInfo of
Left pgColInfo -> RA.AVCol pgColInfo <$> parseOpExps val
- Right (relInfo, permExp) -> do
+ Right (relInfo, permExp, _) -> do
relBoolExp <- parseBoolExp val
return $ RA.AVRel relInfo relBoolExp permExp
diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs
index 3bc71c6817e6d..b80305fd99d8a 100644
--- a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs
+++ b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs
@@ -15,6 +15,7 @@ module Hasura.GraphQL.Resolve.Context
, getArg
, withArg
, withArgM
+ , nameAsPath
, PrepArgs
, Convert
, runConvert
@@ -41,7 +42,7 @@ import Hasura.SQL.Value
import qualified Hasura.SQL.DML as S
type FieldMap
- = Map.HashMap (G.NamedType, G.Name) (Either PGColInfo (RelInfo, S.BoolExp))
+ = Map.HashMap (G.NamedType, G.Name) (Either PGColInfo (RelInfo, S.BoolExp, Maybe Int))
data OrdTy
= OAsc
@@ -63,7 +64,7 @@ type OrdByResolveCtx
getFldInfo
:: (MonadError QErr m, MonadReader r m, Has FieldMap r)
- => G.NamedType -> G.Name -> m (Either PGColInfo (RelInfo, S.BoolExp))
+ => G.NamedType -> G.Name -> m (Either PGColInfo (RelInfo, S.BoolExp, Maybe Int))
getFldInfo nt n = do
fldMap <- asks getter
onNothing (Map.lookup (nt,n) fldMap) $
@@ -90,13 +91,23 @@ getArg args arg =
onNothing (Map.lookup arg args) $
throw500 $ "missing argument: " <> showName arg
+prependArgsInPath
+ :: (MonadError QErr m)
+ => m a -> m a
+prependArgsInPath = withPathK "args"
+
+nameAsPath
+ :: (MonadError QErr m)
+ => G.Name -> m a -> m a
+nameAsPath name = withPathK (G.unName name)
+
withArg
:: (MonadError QErr m)
=> ArgsMap
-> G.Name
-> (AnnGValue -> m a)
-> m a
-withArg args arg f =
+withArg args arg f = prependArgsInPath $ nameAsPath arg $
getArg args arg >>= f
withArgM
@@ -105,7 +116,7 @@ withArgM
-> G.Name
-> (AnnGValue -> m a)
-> m (Maybe a)
-withArgM args arg f =
+withArgM args arg f = prependArgsInPath $ nameAsPath arg $
mapM f $ Map.lookup arg args
type PrepArgs = Seq.Seq Q.PrepArg
diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs
index a632520d6b1c3..b7c50711e310f 100644
--- a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs
+++ b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs
@@ -1,8 +1,9 @@
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE MultiWayIf #-}
-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
module Hasura.GraphQL.Resolve.Select
( convertSelect
@@ -23,8 +24,10 @@ import Hasura.GraphQL.Resolve.Context
import Hasura.GraphQL.Resolve.InputValue
import Hasura.GraphQL.Validate.Field
import Hasura.GraphQL.Validate.Types
+import Hasura.RQL.DML.Internal (onlyPositiveInt)
import Hasura.RQL.Types
import Hasura.SQL.Types
+import Hasura.SQL.Value
fromSelSet
:: G.NamedType
@@ -40,19 +43,23 @@ fromSelSet fldTy flds =
fldInfo <- getFldInfo fldTy fldName
case fldInfo of
Left (PGColInfo pgCol colTy) -> return (rqlFldName, RS.FCol (pgCol, colTy))
- Right (relInfo, tableFilter) -> do
+ Right (relInfo, tableFilter, tableLimit) -> do
let relTN = riRTable relInfo
- relSelData <- fromField relTN tableFilter fld
+ relSelData <- fromField relTN tableFilter tableLimit fld
let annRel = RS.AnnRel (riName relInfo) (riType relInfo)
(riMapping relInfo) relSelData
return (rqlFldName, RS.FRel annRel)
+fieldAsPath :: (MonadError QErr m) => Field -> m a -> m a
+fieldAsPath fld = nameAsPath $ _fName fld
+
fromField
- :: QualifiedTable -> S.BoolExp -> Field -> Convert RS.SelectData
-fromField tn permFilter fld = do
+ :: QualifiedTable -> S.BoolExp -> Maybe Int -> Field -> Convert RS.SelectData
+fromField tn permFilter permLimit fld = fieldAsPath fld $ do
whereExpM <- withArgM args "where" $ convertBoolExp tn
ordByExpM <- withArgM args "order_by" parseOrderBy
- limitExpM <- withArgM args "limit" $ asPGColVal >=> prepare
+ limitExpM <- RS.applyPermLimit permLimit
+ <$> withArgM args "limit" parseLimit
offsetExpM <- withArgM args "offset" $ asPGColVal >=> prepare
annFlds <- fromSelSet (_fType fld) $ _fSelSet fld
return $ RS.SelectData annFlds tn (permFilter, whereExpM) ordByExpM
@@ -99,9 +106,20 @@ parseOrderBy v = do
NFirst -> S.NFirst
NLast -> S.NLast
+parseLimit :: ( MonadError QErr m ) => AnnGValue -> m Int
+parseLimit v = do
+ (_, pgColVal) <- asPGColVal v
+ limit <- maybe noIntErr return $ pgColValueToInt pgColVal
+ -- validate int value
+ onlyPositiveInt limit
+ return limit
+ where
+ noIntErr = throw400 Unexpected "expecting Integer value for \"limit\""
+
convertSelect
- :: QualifiedTable -> S.BoolExp -> Field -> Convert RespTx
-convertSelect qt permFilter fld = do
- selData <- fromField qt permFilter fld
+ :: QualifiedTable -> S.BoolExp -> Maybe Int -> Field -> Convert RespTx
+convertSelect qt permFilter permLimit fld = do
+ selData <- withPathK "selectionSet" $
+ fromField qt permFilter permLimit fld
prepArgs <- get
return $ RS.selectP2 (selData, prepArgs)
diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs
index 9636ca8035eb1..f60a3adc23e3b 100644
--- a/server/src-lib/Hasura/GraphQL/Schema.hs
+++ b/server/src-lib/Hasura/GraphQL/Schema.hs
@@ -45,8 +45,8 @@ type OpCtxMap = Map.HashMap G.Name OpCtx
data OpCtx
-- tn, vn, cols, req hdrs
= OCInsert QualifiedTable QualifiedTable [PGCol] [T.Text]
- -- tn, filter exp, req hdrs
- | OCSelect QualifiedTable S.BoolExp [T.Text]
+ -- tn, filter exp, limit, req hdrs
+ | OCSelect QualifiedTable S.BoolExp (Maybe Int) [T.Text]
-- tn, filter exp, req hdrs
| OCUpdate QualifiedTable S.BoolExp [T.Text]
-- tn, filter exp, req hdrs
@@ -83,7 +83,7 @@ instance Monoid TyAgg where
mempty = TyAgg Map.empty Map.empty Map.empty
mappend = (<>)
-type SelField = Either PGColInfo (RelInfo, S.BoolExp)
+type SelField = Either PGColInfo (RelInfo, S.BoolExp, Maybe Int)
qualTableToName :: QualifiedTable -> G.Name
qualTableToName = G.Name <$> \case
@@ -239,7 +239,8 @@ mkTableObj
mkTableObj tn allowedFlds =
mkObjTyInfo (Just desc) (mkTableTy tn) $ mapFromL _fiName flds
where
- flds = map (either mkPGColFld (mkRelFld . fst)) allowedFlds
+ flds = map (either mkPGColFld (mkRelFld . fst')) allowedFlds
+ fst' (a, _, _) = a
desc = G.Description $
"columns and relationships of " <>> tn
@@ -342,7 +343,7 @@ mkBoolExpInp tn fields =
mkFldExpInp = \case
Left (PGColInfo colName colTy) ->
mk (G.Name $ getPGColTxt colName) (mkCompExpTy colTy)
- Right (RelInfo relName _ _ remTab _, _) ->
+ Right (RelInfo relName _ _ remTab _, _, _) ->
mk (G.Name $ getRelTxt relName) (mkBoolExpTy remTab)
mkPGColInp :: PGColInfo -> InpValInfo
@@ -830,7 +831,7 @@ mkGCtxRole' tn insColsM selFldsM updColsM delPermM constraints =
nameFromSelFld = \case
Left colInfo -> G.Name $ getPGColTxt $ pgiName colInfo
- Right (relInfo, _) -> G.Name $ getRelTxt $ riName relInfo
+ Right (relInfo, _, _) -> G.Name $ getRelTxt $ riName relInfo
-- helper
mkColFldMap ty = mapFromL ((ty,) . nameFromSelFld) . map Left
@@ -894,7 +895,7 @@ getRootFldsRole'
-> [TableConstraint]
-> FieldInfoMap
-> Maybe (QualifiedTable, [T.Text]) -- insert view
- -> Maybe (S.BoolExp, [T.Text]) -- select filter
+ -> Maybe (S.BoolExp, Maybe Int, [T.Text]) -- select filter
-> Maybe ([PGCol], S.BoolExp, [T.Text]) -- update filter
-> Maybe (S.BoolExp, [T.Text]) -- delete filter
-> RootFlds
@@ -915,8 +916,8 @@ getRootFldsRole' tn constraints fields insM selM updM delM =
)
getDelDet (delFltr, hdrs) =
(OCDelete tn delFltr hdrs, Right $ mkDelMutFld tn)
- getSelDet (selFltr, hdrs) =
- (OCSelect tn selFltr hdrs, Left $ mkSelFld tn)
+ getSelDet (selFltr, pLimit, hdrs) =
+ (OCSelect tn selFltr pLimit hdrs, Left $ mkSelFld tn)
-- getRootFlds
-- :: TableCache
@@ -945,7 +946,8 @@ getSelFlds tableCache fields role selPermInfo =
remTableInfo <- getTabInfo $ riRTable relInfo
let remTableSelPermM =
Map.lookup role (tiRolePermInfoMap remTableInfo) >>= _permSel
- return $ fmap (Right . (relInfo,) . spiFilter) remTableSelPermM
+ return $ flip fmap remTableSelPermM $
+ \rmSelPermM -> Right $ (relInfo, spiFilter rmSelPermM, spiLimit rmSelPermM)
where
allowedCols = spiCols selPermInfo
getTabInfo tn =
@@ -986,7 +988,7 @@ getRootFldsRole tn constraints fields (RolePermInfo insM selM updM delM) =
(mkUpd <$> updM) (mkDel <$> delM)
where
mkIns i = (ipiView i, ipiRequiredHeaders i)
- mkSel s = (spiFilter s, spiRequiredHeaders s)
+ mkSel s = (spiFilter s, spiLimit s, spiRequiredHeaders s)
mkUpd u = ( Set.toList $ upiCols u
, upiFilter u
, upiRequiredHeaders u
@@ -1009,10 +1011,10 @@ mkGCtxMapTable tableCache (TableInfo tn _ fields rolePerms constraints) = do
allCols = map pgiName colInfos
selFlds = flip map (toValidFieldInfos fields) $ \case
FIColumn pgColInfo -> Left pgColInfo
- FIRelationship relInfo -> Right (relInfo, noFilter)
+ FIRelationship relInfo -> Right (relInfo, noFilter, Nothing)
noFilter = S.BELit True
adminRootFlds =
- getRootFldsRole' tn constraints fields (Just (tn, [])) (Just (noFilter, []))
+ getRootFldsRole' tn constraints fields (Just (tn, [])) (Just (noFilter, Nothing, []))
(Just (allCols, noFilter, [])) (Just (noFilter, []))
mkScalarTyInfo :: PGColType -> ScalarTyInfo
diff --git a/server/src-lib/Hasura/RQL/DDL/Permission.hs b/server/src-lib/Hasura/RQL/DDL/Permission.hs
index bd8f3db054b78..7367b5e2d8941 100644
--- a/server/src-lib/Hasura/RQL/DDL/Permission.hs
+++ b/server/src-lib/Hasura/RQL/DDL/Permission.hs
@@ -48,21 +48,22 @@ module Hasura.RQL.DDL.Permission
, addPermP2
) where
+import Hasura.Prelude
import Hasura.RQL.DDL.Permission.Internal
+import Hasura.RQL.DML.Internal (onlyPositiveInt)
import Hasura.RQL.Types
import Hasura.SQL.Types
-import Hasura.Prelude
-import qualified Database.PG.Query as Q
+import qualified Database.PG.Query as Q
import qualified Hasura.SQL.DML as S
import Data.Aeson.Casing
import Data.Aeson.TH
-import Language.Haskell.TH.Syntax (Lift)
+import Language.Haskell.TH.Syntax (Lift)
-import qualified Data.ByteString.Builder as BB
-import qualified Data.HashSet as HS
-import qualified Data.Text as T
+import qualified Data.ByteString.Builder as BB
+import qualified Data.HashSet as HS
+import qualified Data.Text as T
-- Insert permission
data InsPerm
@@ -191,6 +192,7 @@ data SelPerm
= SelPerm
{ spColumns :: !PermColSpec -- Allowed columns
, spFilter :: !BoolExp -- Filter expression
+ , spLimit :: !(Maybe Int) -- Limit value
} deriving (Show, Eq, Lift)
$(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''SelPerm)
@@ -212,8 +214,11 @@ buildSelPermInfo tabInfo sp = do
let deps = mkParentDep tn : beDeps ++ map (mkColDep "untyped" tn) pgCols
depHeaders = getDependentHeaders $ spFilter sp
+ mLimit = spLimit sp
+
+ withPathK "limit" $ mapM_ onlyPositiveInt mLimit
- return $ SelPermInfo (HS.fromList pgCols) tn be deps depHeaders
+ return $ SelPermInfo (HS.fromList pgCols) tn be mLimit deps depHeaders
where
tn = tiName tabInfo
diff --git a/server/src-lib/Hasura/RQL/DDL/QueryTemplate.hs b/server/src-lib/Hasura/RQL/DDL/QueryTemplate.hs
index 9ab8e7586f721..78fe2c293132b 100644
--- a/server/src-lib/Hasura/RQL/DDL/QueryTemplate.hs
+++ b/server/src-lib/Hasura/RQL/DDL/QueryTemplate.hs
@@ -18,11 +18,11 @@ module Hasura.RQL.DDL.QueryTemplate
, SetQueryTemplateComment(..)
) where
+import Hasura.Prelude
import Hasura.RQL.GBoolExp (txtRHSBuilder)
import Hasura.RQL.Types
import Hasura.SQL.Types
import Hasura.SQL.Value
-import Hasura.Prelude
import qualified Database.PG.Query as Q
import qualified Hasura.RQL.DML.Count as R
@@ -74,6 +74,20 @@ validateParam pct val =
validateDefault =
void . runAesonParser (convToBin pct)
+mkSelQ :: SelectQueryT -> P1 SelectQuery
+mkSelQ (DMLQuery tn (SelectG c w o lim offset)) = do
+ intLim <- withPathK "limit" $ maybe returnNothing parseAsInt lim
+ intOffset <- withPathK "offset" $ maybe returnNothing parseAsInt offset
+ return $ DMLQuery tn $ SelectG c w o intLim intOffset
+ where
+ returnNothing = return Nothing
+ parseAsInt v = case v of
+ Object _ -> do
+ tpc <- decodeValue v
+ withPathK "default" $
+ mapM decodeValue $ tpcDefault tpc
+ _ -> Just <$> decodeValue v
+
data QueryTP1
= QTP1Insert R.InsertQueryP1
| QTP1Select R.SelectData
@@ -88,7 +102,7 @@ validateTQuery
-> P1 QueryTP1
validateTQuery qt = withPathK "args" $ case qt of
QTInsert q -> QTP1Insert <$> R.convInsertQuery decodeInsObjs validateParam q
- QTSelect q -> QTP1Select <$> R.convSelectQuery validateParam q
+ QTSelect q -> QTP1Select <$> (mkSelQ q >>= R.convSelectQuery validateParam)
QTUpdate q -> QTP1Update <$> R.convUpdateQuery validateParam q
QTDelete q -> QTP1Delete <$> R.convDeleteQuery validateParam q
QTCount q -> QTP1Count <$> R.countP1 validateParam q
diff --git a/server/src-lib/Hasura/RQL/DML/Internal.hs b/server/src-lib/Hasura/RQL/DML/Internal.hs
index 4df7df1d8fcea..365331075f2d0 100644
--- a/server/src-lib/Hasura/RQL/DML/Internal.hs
+++ b/server/src-lib/Hasura/RQL/DML/Internal.hs
@@ -56,7 +56,7 @@ mkAdminRolePermInfo ti =
tn = tiName ti
i = InsPermInfo tn (S.BELit True) True [] []
- s = SelPermInfo (HS.fromList pgCols) tn (S.BELit True) [] []
+ s = SelPermInfo (HS.fromList pgCols) tn (S.BELit True) Nothing [] []
u = UpdPermInfo (HS.fromList pgCols) tn (S.BELit True) [] []
d = DelPermInfo tn (S.BELit True) [] []
@@ -281,3 +281,9 @@ simplifyError txErr = do
-- invalid parameter value
("22023", msg) -> return msg
_ -> Nothing
+
+
+-- validate limit and offset int values
+onlyPositiveInt :: MonadError QErr m => Int -> m ()
+onlyPositiveInt i = when (i < 0) $ throw400 NotSupported
+ "unexpected negative value"
diff --git a/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs b/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs
index fc5e17b0337a6..01cef87dab4dc 100644
--- a/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs
+++ b/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs
@@ -7,6 +7,7 @@
module Hasura.RQL.DML.QueryTemplate where
+import Hasura.Prelude
import Hasura.RQL.DDL.QueryTemplate
import Hasura.RQL.DML.Internal
import Hasura.RQL.DML.Returning (encodeJSONVector)
@@ -14,7 +15,6 @@ import Hasura.RQL.GBoolExp (txtRHSBuilder)
import Hasura.RQL.Instances ()
import Hasura.RQL.Types
import Hasura.SQL.Types
-import Hasura.Prelude
import qualified Database.PG.Query as Q
import qualified Hasura.RQL.DML.Count as R
@@ -82,6 +82,21 @@ buildPrepArg args pct val =
withParamErrMsg tpc t =
"when processing parameter " <> tpcParam tpc <<> " : " <> t
+decodeIntValue :: TemplateArgs -> Value -> EQTP1 Int
+decodeIntValue args val =
+ case val of
+ Object _ -> do
+ tpc <- decodeValue val
+ v <- getParamValue args tpc
+ decodeValue v
+ _ -> decodeValue val
+
+mkSelQWithArgs :: SelectQueryT -> TemplateArgs -> EQTP1 SelectQuery
+mkSelQWithArgs (DMLQuery tn (SelectG c w o lim offset)) args = do
+ intLim <- mapM (decodeIntValue args) lim
+ intOffset <- mapM (decodeIntValue args) offset
+ return $ DMLQuery tn $ SelectG c w o intLim intOffset
+
convQT
:: (P1C m)
=> TemplateArgs
@@ -90,7 +105,8 @@ convQT
convQT args qt = case qt of
QTInsert q -> fmap QTPInsert $ peelSt $
R.convInsertQuery decodeParam binRHSBuilder q
- QTSelect q -> fmap QTPSelect $ peelSt $ R.convSelectQuery f q
+ QTSelect q -> fmap QTPSelect $ peelSt $
+ mkSelQWithArgs q args >>= R.convSelectQuery f
QTUpdate q -> fmap QTPUpdate $ peelSt $ R.convUpdateQuery f q
QTDelete q -> fmap QTPDelete $ peelSt $ R.convDeleteQuery f q
QTCount q -> fmap QTPCount $ peelSt $ R.countP1 f q
diff --git a/server/src-lib/Hasura/RQL/DML/Select.hs b/server/src-lib/Hasura/RQL/DML/Select.hs
index 409cb0c545fa7..82e53c16c64d3 100644
--- a/server/src-lib/Hasura/RQL/DML/Select.hs
+++ b/server/src-lib/Hasura/RQL/DML/Select.hs
@@ -30,7 +30,7 @@ import qualified Hasura.SQL.DML as S
-- Stage 1 : Convert input query into an annotated AST
-- Stage 2 : Convert annotated AST to SQL Select
-type SelectQExt = SelectG ExtCol BoolExp
+type SelectQExt = SelectG ExtCol BoolExp Int
-- Columns in RQL
data ExtCol
= ECSimple !PGCol
@@ -101,7 +101,7 @@ convWildcard
-> SelPermInfo
-> Wildcard
-> m [ExtCol]
-convWildcard fieldInfoMap (SelPermInfo cols _ _ _ _) wildcard =
+convWildcard fieldInfoMap (SelPermInfo cols _ _ _ _ _) wildcard =
case wildcard of
Star -> return simpleCols
(StarDot wc) -> (simpleCols ++) <$> (catMaybes <$> relExtCols wc)
@@ -243,6 +243,19 @@ partitionExtCols = foldr f ([], [])
f (ECSimple pgCol) ~(l, r) = (pgCol:l, r)
f (ECRel relName mAlias selQ) ~(l, r) = (l, (relName, mAlias, selQ):r)
+-- If query limit > permission limit then consider permission limit Else consider query limit
+applyPermLimit
+ :: Maybe Int -- Permission limit
+ -> Maybe Int -- Query limit
+ -> Maybe S.SQLExp -- Return SQL exp
+applyPermLimit mPermLimit mQueryLimit =
+ S.intToSQLExp <$> maybe mQueryLimit compareWithPermLimit mPermLimit
+ where
+ compareWithPermLimit pLimit =
+ maybe (Just pLimit) (compareLimits pLimit) mQueryLimit
+ compareLimits pLimit qLimit = Just $
+ if qLimit > pLimit then pLimit else qLimit
+
convSelectQ
:: (P1C m)
=> FieldInfoMap -- Table information of current table
@@ -289,15 +302,23 @@ convSelectQ fieldInfoMap selPermInfo selQ prepValBuilder = do
-- Convert order by
sqlOrderBy <- mapM convOrderByExp $ sqOrderBy selQ
- -- convert limit expression
- limitExp <- mapM (prepValBuilder PGBigInt) $ sqLimit selQ
+ -- validate limit and offset values
+ withPathK "limit" $ mapM_ onlyPositiveInt mQueryLimit
+ withPathK "offset" $ mapM_ onlyPositiveInt mQueryOffset
- -- convert offest value
- offsetExp <- mapM (prepValBuilder PGBigInt) $ sqOffset selQ
+ -- convert limit expression
+ let limitExp = applyPermLimit mPermLimit mQueryLimit
+ -- convert offset expression
+ offsetExp = S.intToSQLExp <$> mQueryOffset
return $ SelectData newAnnFlds (spiTable selPermInfo)
(spiFilter selPermInfo, wClause) sqlOrderBy [] limitExp offsetExp
+ where
+ mQueryOffset = sqOffset selQ
+ mQueryLimit = sqLimit selQ
+ mPermLimit = spiLimit selPermInfo
+
convExtSimple
:: (P1C m)
=> FieldInfoMap
diff --git a/server/src-lib/Hasura/RQL/Types/DML.hs b/server/src-lib/Hasura/RQL/Types/DML.hs
index b7c7b8fcca4d0..8fcd69a2169fc 100644
--- a/server/src-lib/Hasura/RQL/Types/DML.hs
+++ b/server/src-lib/Hasura/RQL/Types/DML.hs
@@ -19,7 +19,9 @@ module Hasura.RQL.Types.DML
, Wildcard(..)
, SelCol(..)
, SelectQ
+ , SelectQT
, SelectQuery
+ , SelectQueryT
, InsObj
, InsertQuery(..)
@@ -137,19 +139,19 @@ orderByParser =
<|> (return Nothing)
colP = orderByColFromTxt <$> Atto.takeText
-data SelectG a b
+data SelectG a b c
= SelectG
{ sqColumns :: ![a] -- Postgres columns and relationships
, sqWhere :: !(Maybe b) -- Filter
, sqOrderBy :: !(Maybe OrderByExp) -- Ordering
- , sqLimit :: !(Maybe Value) -- Limit
- , sqOffset :: !(Maybe Value) -- Offset
+ , sqLimit :: !(Maybe c) -- Limit
+ , sqOffset :: !(Maybe c) -- Offset
} deriving (Show, Eq, Lift)
$(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''SelectG)
-selectGToPairs :: (KeyValue kv, ToJSON a, ToJSON b)
- => SelectG a b -> [kv]
+selectGToPairs :: (KeyValue kv, ToJSON a, ToJSON b, ToJSON c)
+ => SelectG a b c -> [kv]
selectGToPairs (SelectG selCols mWh mOb mLt mOf) =
[ "columns" .= selCols
, "where" .= mWh
@@ -205,14 +207,20 @@ instance ToJSON SelCol where
, "alias" .= mrn
] ++ selectGToPairs selq
-type SelectQ = SelectG SelCol BoolExp
+type SelectQ = SelectG SelCol BoolExp Int
+type SelectQT = SelectG SelCol BoolExp Value
type SelectQuery = DMLQuery SelectQ
+type SelectQueryT = DMLQuery SelectQT
instance ToJSON SelectQuery where
toJSON (DMLQuery qt selQ) =
object $ "table" .= qt : selectGToPairs selQ
+instance ToJSON SelectQueryT where
+ toJSON (DMLQuery qt selQ) =
+ object $ "table" .= qt : selectGToPairs selQ
+
type InsObj = M.HashMap PGCol Value
data ConflictAction
@@ -317,7 +325,7 @@ $(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''CountQuery)
data QueryT
= QTInsert !InsertQuery
- | QTSelect !SelectQuery
+ | QTSelect !SelectQueryT
| QTUpdate !UpdateQuery
| QTDelete !DeleteQuery
| QTCount !CountQuery
diff --git a/server/src-lib/Hasura/RQL/Types/SchemaCache.hs b/server/src-lib/Hasura/RQL/Types/SchemaCache.hs
index 1813d96a121f8..1454ed0582db5 100644
--- a/server/src-lib/Hasura/RQL/Types/SchemaCache.hs
+++ b/server/src-lib/Hasura/RQL/Types/SchemaCache.hs
@@ -264,6 +264,7 @@ data SelPermInfo
{ spiCols :: !(HS.HashSet PGCol)
, spiTable :: !QualifiedTable
, spiFilter :: !S.BoolExp
+ , spiLimit :: !(Maybe Int)
, spiDeps :: ![SchemaDependency]
, spiRequiredHeaders :: ![T.Text]
} deriving (Show, Eq)
diff --git a/server/src-lib/Hasura/SQL/DML.hs b/server/src-lib/Hasura/SQL/DML.hs
index 259b0d65935d4..62085d7a4ccb1 100644
--- a/server/src-lib/Hasura/SQL/DML.hs
+++ b/server/src-lib/Hasura/SQL/DML.hs
@@ -294,6 +294,11 @@ instance ToSQL SQLExp where
toSQL (SEArray exps) = BB.string7 "ARRAY" <> BB.char7 '['
<> (", " <+> exps) <> BB.char7 ']'
+intToSQLExp :: Int -> SQLExp
+intToSQLExp i = SETyAnn e intType
+ where
+ e = SELit $ T.pack $ show i
+
data Extractor = Extractor !SQLExp !(Maybe Alias)
deriving (Show, Eq)
diff --git a/server/src-lib/Hasura/SQL/Value.hs b/server/src-lib/Hasura/SQL/Value.hs
index 8e0227bd843d3..054a43ad925ff 100644
--- a/server/src-lib/Hasura/SQL/Value.hs
+++ b/server/src-lib/Hasura/SQL/Value.hs
@@ -10,12 +10,12 @@ import qualified Database.PG.Query as Q
import qualified Database.PG.Query.PTI as PTI
import qualified Hasura.SQL.DML as S
-import Hasura.Prelude
import Data.Aeson
import Data.Aeson.Internal
import Data.Int
import Data.Scientific
import Data.Time
+import Hasura.Prelude
import qualified Data.Aeson.Text as AE
import qualified Data.Aeson.Types as AT
@@ -195,3 +195,9 @@ toPrepParam i pct =
if pct == PGGeometry || pct == PGGeography
then S.SEFnApp "ST_GeomFromGeoJSON" [S.SEPrep i] Nothing
else S.SEPrep i
+
+pgColValueToInt :: PGColValue -> Maybe Int
+pgColValueToInt (PGValInteger i) = Just $ fromIntegral i
+pgColValueToInt (PGValSmallInt i) = Just $ fromIntegral i
+pgColValueToInt (PGValBigInt i) = Just $ fromIntegral i
+pgColValueToInt _ = Nothing
diff --git a/server/test/Spec.hs b/server/test/Spec.hs
index cf4beeafad25e..ab8f81c9ca5e8 100644
--- a/server/test/Spec.hs
+++ b/server/test/Spec.hs
@@ -52,6 +52,9 @@ gqlSpecFiles =
, "insert_mutation/person.yaml"
, "insert_mutation/person_array.yaml"
, "nested_select_query_article.yaml"
+ , "select_query_article_limit_offset.yaml"
+ , "select_query_article_limit_offset_error_01.yaml"
+ , "select_query_article_limit_offset_error_02.yaml"
, "update_mutation/author.yaml"
, "update_mutation/person_set.yaml"
, "update_mutation/person_append.yaml"
diff --git a/server/test/testcases/select_query_article_limit_offset.yaml b/server/test/testcases/select_query_article_limit_offset.yaml
new file mode 100644
index 0000000000000..79e0422ecb192
--- /dev/null
+++ b/server/test/testcases/select_query_article_limit_offset.yaml
@@ -0,0 +1,16 @@
+description: Nested select on article with limit
+url: /v1alpha1/graphql
+status: 200
+query:
+ query: |
+ query {
+ article(limit: 3, offset: 2) {
+ id
+ title
+ content
+ author {
+ id
+ name
+ }
+ }
+ }
diff --git a/server/test/testcases/select_query_article_limit_offset_error_01.yaml b/server/test/testcases/select_query_article_limit_offset_error_01.yaml
new file mode 100644
index 0000000000000..da5c155186955
--- /dev/null
+++ b/server/test/testcases/select_query_article_limit_offset_error_01.yaml
@@ -0,0 +1,16 @@
+description: Nested select on article with limit expecting error
+url: /v1alpha1/graphql
+status: 400
+query:
+ query: |
+ query {
+ article(limit: "3", offset: 1) {
+ id
+ title
+ content
+ author {
+ id
+ name
+ }
+ }
+ }
diff --git a/server/test/testcases/select_query_article_limit_offset_error_02.yaml b/server/test/testcases/select_query_article_limit_offset_error_02.yaml
new file mode 100644
index 0000000000000..2804db017643f
--- /dev/null
+++ b/server/test/testcases/select_query_article_limit_offset_error_02.yaml
@@ -0,0 +1,16 @@
+description: Nested select on article with limit expecting error
+url: /v1alpha1/graphql
+status: 400
+query:
+ query: |
+ query {
+ article(limit: -1, offset: 1) {
+ id
+ title
+ content
+ author {
+ id
+ name
+ }
+ }
+ }