2020-10-29 19:58:13 +03:00
|
|
|
module Hasura.Backends.Postgres.Translate.Select
|
|
|
|
( selectQuerySQL
|
|
|
|
, selectAggregateQuerySQL
|
|
|
|
, connectionSelectQuerySQL
|
|
|
|
, asSingleRowJsonResp
|
|
|
|
, mkSQLSelect
|
2020-06-08 15:13:01 +03:00
|
|
|
, mkAggregateSelect
|
|
|
|
, mkConnectionSelect
|
2021-05-21 05:46:58 +03:00
|
|
|
, PostgresAnnotatedFieldJSON
|
2020-10-29 19:58:13 +03:00
|
|
|
) where
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2020-08-27 19:36:39 +03:00
|
|
|
import Hasura.Prelude
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2020-11-02 14:50:40 +03:00
|
|
|
import qualified Data.HashMap.Strict as HM
|
|
|
|
import qualified Data.List.NonEmpty as NE
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import qualified Database.PG.Query as Q
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2020-11-02 14:50:40 +03:00
|
|
|
import Control.Lens hiding (op)
|
2020-08-27 19:36:39 +03:00
|
|
|
import Control.Monad.Writer.Strict
|
2020-10-21 19:35:06 +03:00
|
|
|
import Data.Text.Extended
|
2020-08-27 19:36:39 +03:00
|
|
|
|
2020-11-02 14:50:40 +03:00
|
|
|
import qualified Hasura.Backends.Postgres.SQL.DML as S
|
2020-08-27 19:36:39 +03:00
|
|
|
|
2020-10-27 16:53:49 +03:00
|
|
|
import Hasura.Backends.Postgres.SQL.Rewrite
|
|
|
|
import Hasura.Backends.Postgres.SQL.Types
|
2020-11-02 14:50:40 +03:00
|
|
|
import Hasura.Backends.Postgres.Translate.BoolExp
|
2020-11-25 17:18:58 +03:00
|
|
|
import Hasura.Backends.Postgres.Translate.Types
|
2021-05-11 18:18:31 +03:00
|
|
|
import Hasura.Base.Error
|
2020-10-29 19:58:13 +03:00
|
|
|
import Hasura.EncJSON
|
2021-02-03 19:17:20 +03:00
|
|
|
import Hasura.GraphQL.Schema.Common (currentNodeIdVersion, nodeIdVersionInt)
|
2018-10-31 15:51:20 +03:00
|
|
|
import Hasura.RQL.DML.Internal
|
2020-11-12 12:25:48 +03:00
|
|
|
import Hasura.RQL.IR.OrderBy
|
2020-10-29 19:58:13 +03:00
|
|
|
import Hasura.RQL.IR.Select
|
2020-11-25 17:18:58 +03:00
|
|
|
import Hasura.RQL.Types hiding (Identifier)
|
2020-10-29 19:58:13 +03:00
|
|
|
import Hasura.SQL.Types
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
selectQuerySQL
|
|
|
|
:: forall pgKind
|
2021-05-21 05:46:58 +03:00
|
|
|
. (Backend ('Postgres pgKind), PostgresAnnotatedFieldJSON pgKind)
|
2021-04-22 00:44:37 +03:00
|
|
|
=> JsonAggSelect
|
|
|
|
-> AnnSimpleSel ('Postgres pgKind)
|
|
|
|
-> Q.Query
|
2020-10-29 19:58:13 +03:00
|
|
|
selectQuerySQL jsonAggSelect sel =
|
|
|
|
Q.fromBuilder $ toSQL $ mkSQLSelect jsonAggSelect sel
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
selectAggregateQuerySQL
|
|
|
|
:: forall pgKind
|
2021-05-21 05:46:58 +03:00
|
|
|
. (Backend ('Postgres pgKind), PostgresAnnotatedFieldJSON pgKind)
|
2021-04-22 00:44:37 +03:00
|
|
|
=> AnnAggregateSelect ('Postgres pgKind)
|
|
|
|
-> Q.Query
|
2020-10-29 19:58:13 +03:00
|
|
|
selectAggregateQuerySQL =
|
|
|
|
Q.fromBuilder . toSQL . mkAggregateSelect
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
connectionSelectQuerySQL
|
|
|
|
:: forall pgKind
|
2021-05-21 05:46:58 +03:00
|
|
|
. ( Backend ('Postgres pgKind)
|
|
|
|
, PostgresAnnotatedFieldJSON pgKind
|
|
|
|
)
|
2021-04-22 00:44:37 +03:00
|
|
|
=> ConnectionSelect ('Postgres pgKind) S.SQLExp
|
|
|
|
-> Q.Query
|
2020-10-29 19:58:13 +03:00
|
|
|
connectionSelectQuerySQL =
|
|
|
|
Q.fromBuilder . toSQL . mkConnectionSelect
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
asSingleRowJsonResp
|
|
|
|
:: Q.Query
|
|
|
|
-> [Q.PrepArg]
|
|
|
|
-> Q.TxE QErr EncJSON
|
2020-10-29 19:58:13 +03:00
|
|
|
asSingleRowJsonResp query args =
|
|
|
|
encJFromBS . runIdentity . Q.getRow
|
|
|
|
<$> Q.rawQE dmlTxErrorHandler query args True
|
2018-10-31 15:51:20 +03:00
|
|
|
|
|
|
|
|
|
|
|
-- Conversion of SelectQ happens in 2 Stages.
|
|
|
|
-- Stage 1 : Convert input query into an annotated AST
|
|
|
|
-- Stage 2 : Convert annotated AST to SQL Select
|
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
functionToIdentifier :: QualifiedFunction -> Identifier
|
|
|
|
functionToIdentifier = Identifier . qualifiedObjectToText
|
2018-11-16 15:40:23 +03:00
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
selectFromToFromItem :: Identifier -> SelectFrom ('Postgres pgKind) -> S.FromItem
|
2020-06-08 15:13:01 +03:00
|
|
|
selectFromToFromItem pfx = \case
|
2019-10-18 11:29:47 +03:00
|
|
|
FromTable tn -> S.FISimple tn Nothing
|
2020-10-27 13:34:31 +03:00
|
|
|
FromIdentifier i -> S.FIIdentifier i
|
2020-02-13 20:38:23 +03:00
|
|
|
FromFunction qf args defListM ->
|
2019-10-18 11:29:47 +03:00
|
|
|
S.FIFunc $ S.FunctionExp qf (fromTableRowArgs pfx args) $
|
2020-10-27 13:34:31 +03:00
|
|
|
Just $ S.mkFunctionAlias (functionToIdentifier qf) defListM
|
2019-10-18 11:29:47 +03:00
|
|
|
|
2020-02-13 20:38:23 +03:00
|
|
|
-- This function shouldn't be present ideally
|
|
|
|
-- You should be able to retrieve this information
|
2020-06-08 15:13:01 +03:00
|
|
|
-- from the FromItem generated with selectFromToFromItem
|
2020-02-13 20:38:23 +03:00
|
|
|
-- however given from S.FromItem is modelled, it is not
|
|
|
|
-- possible currently
|
2021-04-22 00:44:37 +03:00
|
|
|
selectFromToQual :: SelectFrom ('Postgres pgKind) -> S.Qual
|
2020-06-08 15:13:01 +03:00
|
|
|
selectFromToQual = \case
|
2020-10-21 19:35:06 +03:00
|
|
|
FromTable tn -> S.QualTable tn
|
2020-10-27 13:34:31 +03:00
|
|
|
FromIdentifier i -> S.QualifiedIdentifier i Nothing
|
|
|
|
FromFunction qf _ _ -> S.QualifiedIdentifier (functionToIdentifier qf) Nothing
|
2018-11-16 15:40:23 +03:00
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
aggregateFieldToExp :: AggregateFields ('Postgres pgKind) -> Bool -> S.SQLExp
|
2021-01-18 16:51:36 +03:00
|
|
|
aggregateFieldToExp aggFlds strfyNum = jsonRow
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
|
|
|
jsonRow = S.applyJsonBuildObj (concatMap aggToFlds aggFlds)
|
|
|
|
withAls fldName sqlExp = [S.SELit fldName, sqlExp]
|
2018-12-12 15:58:39 +03:00
|
|
|
aggToFlds (FieldName t, fld) = withAls t $ case fld of
|
2018-11-14 15:59:59 +03:00
|
|
|
AFCount cty -> S.SECount cty
|
|
|
|
AFOp aggOp -> aggOpToObj aggOp
|
|
|
|
AFExp e -> S.SELit e
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
aggOpToObj (AggregateOp opText flds) =
|
|
|
|
S.applyJsonBuildObj $ concatMap (colFldsToExtr opText) flds
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2021-01-18 16:51:36 +03:00
|
|
|
colFldsToExtr opText (FieldName t, CFCol col ty) =
|
2018-10-31 15:51:20 +03:00
|
|
|
[ S.SELit t
|
2021-01-18 16:51:36 +03:00
|
|
|
, toJSONableExp strfyNum ty False
|
|
|
|
$ S.SEFnApp opText [S.SEIdentifier $ toIdentifier col] Nothing
|
2018-10-31 15:51:20 +03:00
|
|
|
]
|
2020-10-22 23:42:27 +03:00
|
|
|
colFldsToExtr _ (FieldName t, CFExp e) =
|
2018-10-31 15:51:20 +03:00
|
|
|
[ S.SELit t , S.SELit e]
|
|
|
|
|
2019-04-26 11:19:59 +03:00
|
|
|
asSingleRowExtr :: S.Alias -> S.SQLExp
|
|
|
|
asSingleRowExtr col =
|
|
|
|
S.SEFnApp "coalesce" [jsonAgg, S.SELit "null"] Nothing
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
|
|
|
jsonAgg = S.SEOpApp (S.SQLOp "->")
|
2020-10-27 13:34:31 +03:00
|
|
|
[ S.SEFnApp "json_agg" [S.SEIdentifier $ toIdentifier col] Nothing
|
2018-10-31 15:51:20 +03:00
|
|
|
, S.SEUnsafe "0"
|
|
|
|
]
|
|
|
|
|
2019-08-01 08:09:52 +03:00
|
|
|
withJsonAggExtr
|
2020-06-08 15:13:01 +03:00
|
|
|
:: PermissionLimitSubQuery -> Maybe S.OrderByExp -> S.Alias -> S.SQLExp
|
|
|
|
withJsonAggExtr permLimitSubQuery ordBy alias =
|
2019-08-01 08:09:52 +03:00
|
|
|
-- if select has aggregations then use subquery to apply permission limit
|
2020-06-08 15:13:01 +03:00
|
|
|
case permLimitSubQuery of
|
|
|
|
PLSQRequired permLimit -> withPermLimit permLimit
|
|
|
|
PLSQNotRequired -> simpleJsonAgg
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
2019-08-01 08:09:52 +03:00
|
|
|
simpleJsonAgg = mkSimpleJsonAgg rowIdenExp ordBy
|
2020-10-27 13:34:31 +03:00
|
|
|
rowIdenExp = S.SEIdentifier $ S.getAlias alias
|
|
|
|
subSelAls = Identifier "sub_query"
|
|
|
|
unnestTable = Identifier "unnest_table"
|
2019-04-26 11:19:59 +03:00
|
|
|
|
|
|
|
mkSimpleJsonAgg rowExp ob =
|
|
|
|
let jsonAggExp = S.SEFnApp "json_agg" [rowExp] ob
|
|
|
|
in S.SEFnApp "coalesce" [jsonAggExp, S.SELit "[]"] Nothing
|
|
|
|
|
|
|
|
withPermLimit limit =
|
|
|
|
let subSelect = mkSubSelect limit
|
2020-10-27 13:34:31 +03:00
|
|
|
rowIdentifier = S.mkQIdenExp subSelAls alias
|
|
|
|
extr = S.Extractor (mkSimpleJsonAgg rowIdentifier newOrderBy) Nothing
|
2019-04-26 11:19:59 +03:00
|
|
|
fromExp = S.FromExp $ pure $
|
|
|
|
S.mkSelFromItem subSelect $ S.Alias subSelAls
|
|
|
|
in S.SESelect $ S.mkSelect { S.selExtr = pure extr
|
|
|
|
, S.selFrom = Just fromExp
|
|
|
|
}
|
|
|
|
|
|
|
|
mkSubSelect limit =
|
|
|
|
let jsonRowExtr = flip S.Extractor (Just alias) $
|
|
|
|
S.mkQIdenExp unnestTable alias
|
|
|
|
obExtrs = flip map newOBAliases $ \a ->
|
|
|
|
S.Extractor (S.mkQIdenExp unnestTable a) $ Just $ S.Alias a
|
|
|
|
in S.mkSelect { S.selExtr = jsonRowExtr : obExtrs
|
|
|
|
, S.selFrom = Just $ S.FromExp $ pure unnestFromItem
|
|
|
|
, S.selLimit = Just $ S.LimitExp $ S.intToSQLExp limit
|
2020-06-08 15:13:01 +03:00
|
|
|
, S.selOrderBy = newOrderBy
|
2019-04-26 11:19:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
unnestFromItem =
|
|
|
|
let arrayAggItems = flip map (rowIdenExp : obCols) $
|
|
|
|
\s -> S.SEFnApp "array_agg" [s] Nothing
|
|
|
|
in S.FIUnnest arrayAggItems (S.Alias unnestTable) $
|
2020-10-27 13:34:31 +03:00
|
|
|
rowIdenExp : map S.SEIdentifier newOBAliases
|
2019-04-26 11:19:59 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
newOrderBy = S.OrderByExp <$> NE.nonEmpty newOBItems
|
2019-04-26 11:19:59 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
(newOBItems, obCols, newOBAliases) = maybe ([], [], []) transformOrderBy ordBy
|
|
|
|
transformOrderBy (S.OrderByExp l) = unzip3 $
|
|
|
|
flip map (zip (toList l) [1..]) $ \(obItem, i::Int) ->
|
2020-12-17 14:37:16 +03:00
|
|
|
let iden = Identifier $ "ob_col_" <> tshow i
|
2020-10-27 13:34:31 +03:00
|
|
|
in ( obItem{S.oColumn = S.SEIdentifier iden}
|
2019-04-26 11:19:59 +03:00
|
|
|
, S.oColumn obItem
|
|
|
|
, iden
|
|
|
|
)
|
|
|
|
|
|
|
|
asJsonAggExtr
|
2020-06-08 15:13:01 +03:00
|
|
|
:: JsonAggSelect -> S.Alias -> PermissionLimitSubQuery -> Maybe S.OrderByExp -> S.Extractor
|
|
|
|
asJsonAggExtr jsonAggSelect als permLimitSubQuery ordByExpM =
|
2020-02-13 20:38:23 +03:00
|
|
|
flip S.Extractor (Just als) $ case jsonAggSelect of
|
2020-06-08 15:13:01 +03:00
|
|
|
JASMultipleRows -> withJsonAggExtr permLimitSubQuery ordByExpM als
|
2020-02-13 20:38:23 +03:00
|
|
|
JASSingleObject -> asSingleRowExtr als
|
2018-10-31 15:51:20 +03:00
|
|
|
|
|
|
|
-- array relationships are not grouped, so have to be prefixed by
|
|
|
|
-- parent's alias
|
2020-10-27 13:34:31 +03:00
|
|
|
mkUniqArrayRelationAlias :: FieldName -> [FieldName] -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkUniqArrayRelationAlias parAls flds =
|
|
|
|
let sortedFields = sort flds
|
2020-10-27 13:34:31 +03:00
|
|
|
in Identifier $
|
2020-06-08 15:13:01 +03:00
|
|
|
getFieldNameTxt parAls <> "."
|
|
|
|
<> T.intercalate "." (map getFieldNameTxt sortedFields)
|
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkArrayRelationTableAlias :: Identifier -> FieldName -> [FieldName] -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkArrayRelationTableAlias pfx parAls flds =
|
2020-10-27 13:34:31 +03:00
|
|
|
pfx <> Identifier ".ar." <> uniqArrRelAls
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
uniqArrRelAls = mkUniqArrayRelationAlias parAls flds
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkObjectRelationTableAlias :: Identifier -> RelName -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkObjectRelationTableAlias pfx relName =
|
2020-10-27 13:34:31 +03:00
|
|
|
pfx <> Identifier ".or." <> toIdentifier relName
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkComputedFieldTableAlias :: Identifier -> FieldName -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkComputedFieldTableAlias pfx fldAls =
|
2020-10-27 13:34:31 +03:00
|
|
|
pfx <> Identifier ".cf." <> toIdentifier fldAls
|
2019-10-18 11:29:47 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkBaseTableAlias :: Identifier -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkBaseTableAlias pfx =
|
2020-10-27 13:34:31 +03:00
|
|
|
pfx <> Identifier ".base"
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkBaseTableColumnAlias :: Identifier -> PGCol -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkBaseTableColumnAlias pfx pgColumn =
|
2020-10-27 13:34:31 +03:00
|
|
|
pfx <> Identifier ".pg." <> toIdentifier pgColumn
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2019-10-05 07:00:53 +03:00
|
|
|
mkOrderByFieldName :: RelName -> FieldName
|
|
|
|
mkOrderByFieldName relName =
|
|
|
|
FieldName $ relNameToTxt relName <> "." <> "order_by"
|
2018-12-12 15:58:39 +03:00
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
mkAggregateOrderByAlias :: AnnAggregateOrderBy ('Postgres pgKind) -> S.Alias
|
2020-10-27 13:34:31 +03:00
|
|
|
mkAggregateOrderByAlias = (S.Alias . Identifier) . \case
|
2020-10-21 19:35:06 +03:00
|
|
|
AAOCount -> "count"
|
2020-06-08 15:13:01 +03:00
|
|
|
AAOOp opText col -> opText <> "." <> getPGColTxt (pgiColumn col)
|
|
|
|
|
|
|
|
mkArrayRelationSourcePrefix
|
2020-10-27 13:34:31 +03:00
|
|
|
:: Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
-> FieldName
|
|
|
|
-> HM.HashMap FieldName [FieldName]
|
|
|
|
-> FieldName
|
2020-10-27 13:34:31 +03:00
|
|
|
-> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkArrayRelationSourcePrefix parentSourcePrefix parentFieldName similarFieldsMap fieldName =
|
|
|
|
mkArrayRelationTableAlias parentSourcePrefix parentFieldName $
|
|
|
|
HM.lookupDefault [fieldName] fieldName similarFieldsMap
|
|
|
|
|
|
|
|
mkArrayRelationAlias
|
|
|
|
:: FieldName
|
|
|
|
-> HM.HashMap FieldName [FieldName]
|
|
|
|
-> FieldName
|
|
|
|
-> S.Alias
|
|
|
|
mkArrayRelationAlias parentFieldName similarFieldsMap fieldName =
|
|
|
|
S.Alias $ mkUniqArrayRelationAlias parentFieldName $
|
|
|
|
HM.lookupDefault [fieldName] fieldName similarFieldsMap
|
|
|
|
|
2019-10-18 11:29:47 +03:00
|
|
|
fromTableRowArgs
|
2021-04-22 00:44:37 +03:00
|
|
|
:: Identifier -> FunctionArgsExpTableRow ('Postgres pgKind) S.SQLExp -> S.FunctionArgs
|
2019-10-18 11:29:47 +03:00
|
|
|
fromTableRowArgs pfx = toFunctionArgs . fmap toSQLExp
|
|
|
|
where
|
|
|
|
toFunctionArgs (FunctionArgsExp positional named) =
|
|
|
|
S.FunctionArgs positional named
|
2020-10-27 13:34:31 +03:00
|
|
|
toSQLExp (AETableRow Nothing) = S.SERowIdentifier $ mkBaseTableAlias pfx
|
2020-06-08 15:13:01 +03:00
|
|
|
toSQLExp (AETableRow (Just acc)) = S.mkQIdenExp (mkBaseTableAlias pfx) acc
|
2020-04-27 18:07:03 +03:00
|
|
|
toSQLExp (AESession s) = s
|
2020-02-13 20:38:23 +03:00
|
|
|
toSQLExp (AEInput s) = s
|
2019-10-18 11:29:47 +03:00
|
|
|
|
2018-10-31 15:51:20 +03:00
|
|
|
-- uses row_to_json to build a json object
|
|
|
|
withRowToJSON
|
|
|
|
:: FieldName -> [S.Extractor] -> (S.Alias, S.SQLExp)
|
|
|
|
withRowToJSON parAls extrs =
|
|
|
|
(S.toAlias parAls, jsonRow)
|
|
|
|
where
|
|
|
|
jsonRow = S.applyRowToJson extrs
|
|
|
|
|
|
|
|
-- uses json_build_object to build a json object
|
|
|
|
withJsonBuildObj
|
|
|
|
:: FieldName -> [S.SQLExp] -> (S.Alias, S.SQLExp)
|
|
|
|
withJsonBuildObj parAls exps =
|
|
|
|
(S.toAlias parAls, jsonRow)
|
|
|
|
where
|
|
|
|
jsonRow = S.applyJsonBuildObj exps
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
-- | Forces aggregation
|
|
|
|
withForceAggregation :: S.TypeAnn -> S.SQLExp -> S.SQLExp
|
|
|
|
withForceAggregation tyAnn e =
|
|
|
|
-- bool_or to force aggregation
|
|
|
|
S.SEFnApp "coalesce" [e, S.SETyAnn (S.SEUnsafe "bool_or('true')") tyAnn] Nothing
|
|
|
|
|
|
|
|
mkAggregateOrderByExtractorAndFields
|
2021-04-22 00:44:37 +03:00
|
|
|
:: forall pgKind
|
|
|
|
. Backend ('Postgres pgKind)
|
|
|
|
=> AnnAggregateOrderBy ('Postgres pgKind)
|
|
|
|
-> (S.Extractor, AggregateFields ('Postgres pgKind))
|
2020-06-08 15:13:01 +03:00
|
|
|
mkAggregateOrderByExtractorAndFields annAggOrderBy =
|
|
|
|
case annAggOrderBy of
|
|
|
|
AAOCount ->
|
|
|
|
( S.Extractor S.countStar alias
|
|
|
|
, [(FieldName "count", AFCount S.CTStar)]
|
|
|
|
)
|
|
|
|
AAOOp opText pgColumnInfo ->
|
|
|
|
let pgColumn = pgiColumn pgColumnInfo
|
2021-01-18 16:51:36 +03:00
|
|
|
pgType = pgiType pgColumnInfo
|
2020-10-27 13:34:31 +03:00
|
|
|
in ( S.Extractor (S.SEFnApp opText [S.SEIdentifier $ toIdentifier pgColumn] Nothing) alias
|
2021-01-18 16:51:36 +03:00
|
|
|
, [ ( FieldName opText
|
|
|
|
, AFOp $ AggregateOp opText
|
2021-04-22 00:44:37 +03:00
|
|
|
[ ( fromCol @('Postgres pgKind) pgColumn
|
2021-01-18 16:51:36 +03:00
|
|
|
, CFCol pgColumn pgType
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
2020-06-08 15:13:01 +03:00
|
|
|
)
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
alias = Just $ mkAggregateOrderByAlias annAggOrderBy
|
|
|
|
|
|
|
|
mkAnnOrderByAlias
|
2021-04-22 00:44:37 +03:00
|
|
|
:: Identifier -> FieldName -> SimilarArrayFields -> AnnOrderByElementG ('Postgres pgKind) v -> S.Alias
|
2020-06-08 15:13:01 +03:00
|
|
|
mkAnnOrderByAlias pfx parAls similarFields = \case
|
|
|
|
AOCColumn pgColumnInfo ->
|
|
|
|
let pgColumn = pgiColumn pgColumnInfo
|
|
|
|
obColAls = mkBaseTableColumnAlias pfx pgColumn
|
|
|
|
in S.Alias obColAls
|
2018-10-31 15:51:20 +03:00
|
|
|
-- "pfx.or.relname"."pfx.ob.or.relname.rest" AS "pfx.ob.or.relname.rest"
|
2020-06-08 15:13:01 +03:00
|
|
|
AOCObjectRelation relInfo _ rest ->
|
|
|
|
let rn = riName relInfo
|
|
|
|
relPfx = mkObjectRelationTableAlias pfx rn
|
2019-10-05 07:00:53 +03:00
|
|
|
ordByFldName = mkOrderByFieldName rn
|
2020-06-08 15:13:01 +03:00
|
|
|
nesAls = mkAnnOrderByAlias relPfx ordByFldName mempty rest
|
|
|
|
in nesAls
|
|
|
|
AOCArrayAggregation relInfo _ aggOrderBy ->
|
|
|
|
let rn = riName relInfo
|
|
|
|
arrPfx = mkArrayRelationSourcePrefix pfx parAls similarFields $
|
|
|
|
mkOrderByFieldName rn
|
2020-10-27 13:34:31 +03:00
|
|
|
obAls = arrPfx <> Identifier "." <> toIdentifier (mkAggregateOrderByAlias aggOrderBy)
|
2020-06-08 15:13:01 +03:00
|
|
|
in S.Alias obAls
|
|
|
|
|
|
|
|
processDistinctOnColumns
|
2020-10-27 13:34:31 +03:00
|
|
|
:: Identifier
|
2018-11-23 04:53:56 +03:00
|
|
|
-> NE.NonEmpty PGCol
|
|
|
|
-> ( S.DistinctExpr
|
2020-06-08 15:13:01 +03:00
|
|
|
, [(S.Alias, S.SQLExp)] -- additional column extractors
|
2018-11-23 04:53:56 +03:00
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
processDistinctOnColumns pfx neCols = (distOnExp, colExtrs)
|
2018-11-23 04:53:56 +03:00
|
|
|
where
|
|
|
|
cols = toList neCols
|
2020-10-27 13:34:31 +03:00
|
|
|
distOnExp = S.DistinctOn $ map (S.SEIdentifier . toIdentifier . mkQColAls) cols
|
|
|
|
mkQCol c = S.mkQIdenExp (mkBaseTableAlias pfx) $ toIdentifier c
|
2020-06-08 15:13:01 +03:00
|
|
|
mkQColAls = S.Alias . mkBaseTableColumnAlias pfx
|
2018-11-23 04:53:56 +03:00
|
|
|
colExtrs = flip map cols $ mkQColAls &&& mkQCol
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
type SimilarArrayFields = HM.HashMap FieldName [FieldName]
|
2018-11-23 04:53:56 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
mkSimilarArrayFields
|
2021-04-22 00:44:37 +03:00
|
|
|
:: forall pgKind v
|
|
|
|
. (Backend ('Postgres pgKind), Eq v)
|
|
|
|
=> AnnFieldsG ('Postgres pgKind) v
|
|
|
|
-> Maybe (NE.NonEmpty (AnnOrderByItemG ('Postgres pgKind) v))
|
2020-06-08 15:13:01 +03:00
|
|
|
-> SimilarArrayFields
|
|
|
|
mkSimilarArrayFields annFields maybeOrderBys =
|
|
|
|
HM.fromList $ flip map allTuples $
|
|
|
|
\(relNameAndArgs, fieldName) -> (fieldName, getSimilarFields relNameAndArgs)
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
getSimilarFields relNameAndArgs = map snd $ filter ((== relNameAndArgs) . fst) allTuples
|
|
|
|
allTuples = arrayRelationTuples <> aggOrderByRelationTuples
|
|
|
|
arrayRelationTuples =
|
|
|
|
let arrayFields = mapMaybe getAnnArr annFields
|
|
|
|
in flip map arrayFields $
|
|
|
|
\(f, relSel) -> (getArrayRelNameAndSelectArgs relSel, f)
|
|
|
|
|
|
|
|
aggOrderByRelationTuples =
|
|
|
|
let mkItem (relName, fieldName) = ( (relName, noSelectArgs)
|
|
|
|
, fieldName
|
|
|
|
)
|
|
|
|
in map mkItem $ maybe []
|
|
|
|
(mapMaybe (fetchAggOrderByRels . obiColumn) . toList) maybeOrderBys
|
|
|
|
|
|
|
|
fetchAggOrderByRels (AOCArrayAggregation ri _ _) =
|
|
|
|
Just (riName ri, mkOrderByFieldName $ riName ri)
|
|
|
|
fetchAggOrderByRels _ = Nothing
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
getArrayRelNameAndSelectArgs :: ArraySelectG ('Postgres pgKind) v -> (RelName, SelectArgsG ('Postgres pgKind) v)
|
2020-06-08 15:13:01 +03:00
|
|
|
getArrayRelNameAndSelectArgs = \case
|
2020-10-21 19:35:06 +03:00
|
|
|
ASSimple r -> (aarRelationshipName r, _asnArgs $ aarAnnSelect r)
|
|
|
|
ASAggregate r -> (aarRelationshipName r, _asnArgs $ aarAnnSelect r)
|
2020-06-08 15:13:01 +03:00
|
|
|
ASConnection r -> (aarRelationshipName r, _asnArgs $ _csSelect $ aarAnnSelect r)
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
getAnnArr :: (a, AnnFieldG ('Postgres pgKind) v) -> Maybe (a, ArraySelectG ('Postgres pgKind) v)
|
2020-06-08 15:13:01 +03:00
|
|
|
getAnnArr (f, annFld) = case annFld of
|
|
|
|
AFArrayRelation (ASConnection _) -> Nothing
|
|
|
|
AFArrayRelation ar -> Just (f, ar)
|
|
|
|
_ -> Nothing
|
|
|
|
|
|
|
|
|
|
|
|
withWriteJoinTree
|
2021-03-25 20:50:08 +03:00
|
|
|
:: (MonadWriter JoinTree m)
|
|
|
|
=> (JoinTree -> b -> JoinTree)
|
2020-06-08 15:13:01 +03:00
|
|
|
-> m (a, b)
|
|
|
|
-> m a
|
|
|
|
withWriteJoinTree joinTreeUpdater action =
|
|
|
|
pass $ do
|
|
|
|
(out, result) <- action
|
|
|
|
let fromJoinTree joinTree =
|
|
|
|
joinTreeUpdater joinTree result
|
|
|
|
pure (out, fromJoinTree)
|
|
|
|
|
|
|
|
withWriteObjectRelation
|
2021-03-25 20:50:08 +03:00
|
|
|
:: (MonadWriter JoinTree m)
|
|
|
|
=> m ( ObjectRelationSource
|
2020-06-08 15:13:01 +03:00
|
|
|
, HM.HashMap S.Alias S.SQLExp
|
|
|
|
, a
|
|
|
|
)
|
|
|
|
-> m a
|
|
|
|
withWriteObjectRelation action =
|
|
|
|
withWriteJoinTree updateJoinTree $ do
|
|
|
|
(source, nodeExtractors, out) <- action
|
|
|
|
pure (out, (source, nodeExtractors))
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
updateJoinTree joinTree (source, nodeExtractors) =
|
|
|
|
let selectNode = SelectNode nodeExtractors joinTree
|
|
|
|
in mempty{_jtObjectRelations = HM.singleton source selectNode}
|
|
|
|
|
|
|
|
withWriteArrayRelation
|
2021-03-25 20:50:08 +03:00
|
|
|
:: (MonadWriter JoinTree m)
|
|
|
|
=> m ( ArrayRelationSource
|
2020-06-08 15:13:01 +03:00
|
|
|
, S.Extractor
|
|
|
|
, HM.HashMap S.Alias S.SQLExp
|
|
|
|
, a
|
|
|
|
)
|
|
|
|
-> m a
|
|
|
|
withWriteArrayRelation action =
|
|
|
|
withWriteJoinTree updateJoinTree $ do
|
|
|
|
(source, topExtractor, nodeExtractors, out) <- action
|
|
|
|
pure (out, (source, topExtractor, nodeExtractors))
|
2019-08-01 08:09:52 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
updateJoinTree joinTree (source, topExtractor, nodeExtractors) =
|
|
|
|
let arraySelectNode = ArraySelectNode [topExtractor] $
|
|
|
|
SelectNode nodeExtractors joinTree
|
|
|
|
in mempty{_jtArrayRelations = HM.singleton source arraySelectNode}
|
|
|
|
|
|
|
|
withWriteArrayConnection
|
2021-03-25 20:50:08 +03:00
|
|
|
:: (MonadWriter JoinTree m)
|
|
|
|
=> m ( ArrayConnectionSource
|
2020-06-08 15:13:01 +03:00
|
|
|
, S.Extractor
|
|
|
|
, HM.HashMap S.Alias S.SQLExp
|
|
|
|
, a
|
|
|
|
)
|
|
|
|
-> m a
|
|
|
|
withWriteArrayConnection action =
|
|
|
|
withWriteJoinTree updateJoinTree $ do
|
|
|
|
(source, topExtractor, nodeExtractors, out) <- action
|
|
|
|
pure (out, (source, topExtractor, nodeExtractors))
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
updateJoinTree joinTree (source, topExtractor, nodeExtractors) =
|
|
|
|
let arraySelectNode = ArraySelectNode [topExtractor] $
|
|
|
|
SelectNode nodeExtractors joinTree
|
|
|
|
in mempty{_jtArrayConnections = HM.singleton source arraySelectNode}
|
|
|
|
|
|
|
|
withWriteComputedFieldTableSet
|
2021-03-25 20:50:08 +03:00
|
|
|
:: (MonadWriter JoinTree m)
|
|
|
|
=> m ( ComputedFieldTableSetSource
|
2020-06-08 15:13:01 +03:00
|
|
|
, HM.HashMap S.Alias S.SQLExp
|
|
|
|
, a
|
|
|
|
)
|
|
|
|
-> m a
|
|
|
|
withWriteComputedFieldTableSet action =
|
|
|
|
withWriteJoinTree updateJoinTree $ do
|
|
|
|
(source, nodeExtractors, out) <- action
|
|
|
|
pure (out, (source, nodeExtractors))
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
updateJoinTree joinTree (source, nodeExtractors) =
|
|
|
|
let selectNode = SelectNode nodeExtractors joinTree
|
|
|
|
in mempty{_jtComputedFieldTableSets = HM.singleton source selectNode}
|
2018-12-12 15:58:39 +03:00
|
|
|
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
processAnnSimpleSelect
|
2021-04-22 00:44:37 +03:00
|
|
|
:: forall pgKind m
|
|
|
|
. ( MonadReader Bool m
|
|
|
|
, MonadWriter JoinTree m
|
|
|
|
, Backend ('Postgres pgKind)
|
2021-05-21 05:46:58 +03:00
|
|
|
, PostgresAnnotatedFieldJSON pgKind
|
2021-04-22 00:44:37 +03:00
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
=> SourcePrefixes
|
|
|
|
-> FieldName
|
|
|
|
-> PermissionLimitSubQuery
|
2021-04-22 00:44:37 +03:00
|
|
|
-> AnnSimpleSel ('Postgres pgKind)
|
2021-03-25 20:50:08 +03:00
|
|
|
-> m ( SelectSource
|
2020-06-08 15:13:01 +03:00
|
|
|
, HM.HashMap S.Alias S.SQLExp
|
|
|
|
)
|
|
|
|
processAnnSimpleSelect sourcePrefixes fieldAlias permLimitSubQuery annSimpleSel = do
|
|
|
|
(selectSource, orderByAndDistinctExtrs, _) <-
|
|
|
|
processSelectParams sourcePrefixes fieldAlias similarArrayFields tableFrom
|
|
|
|
permLimitSubQuery tablePermissions tableArgs
|
|
|
|
annFieldsExtr <- processAnnFields (_pfThis sourcePrefixes) fieldAlias similarArrayFields annSelFields
|
|
|
|
let allExtractors = HM.fromList $ annFieldsExtr : orderByAndDistinctExtrs
|
|
|
|
pure (selectSource, allExtractors)
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
AnnSelectG annSelFields tableFrom tablePermissions tableArgs _ = annSimpleSel
|
|
|
|
similarArrayFields =
|
|
|
|
mkSimilarArrayFields annSelFields $ _saOrderBy tableArgs
|
|
|
|
|
|
|
|
processAnnAggregateSelect
|
2021-04-22 00:44:37 +03:00
|
|
|
:: forall pgKind m
|
|
|
|
. ( MonadReader Bool m
|
|
|
|
, MonadWriter JoinTree m
|
|
|
|
, Backend ('Postgres pgKind)
|
2021-05-21 05:46:58 +03:00
|
|
|
, PostgresAnnotatedFieldJSON pgKind
|
2021-04-22 00:44:37 +03:00
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
=> SourcePrefixes
|
|
|
|
-> FieldName
|
2021-04-22 00:44:37 +03:00
|
|
|
-> AnnAggregateSelect ('Postgres pgKind)
|
2021-03-25 20:50:08 +03:00
|
|
|
-> m ( SelectSource
|
2020-06-08 15:13:01 +03:00
|
|
|
, HM.HashMap S.Alias S.SQLExp
|
|
|
|
, S.Extractor
|
|
|
|
)
|
|
|
|
processAnnAggregateSelect sourcePrefixes fieldAlias annAggSel = do
|
|
|
|
(selectSource, orderByAndDistinctExtrs, _) <-
|
|
|
|
processSelectParams sourcePrefixes fieldAlias similarArrayFields tableFrom
|
|
|
|
permLimitSubQuery tablePermissions tableArgs
|
|
|
|
let thisSourcePrefix = _pfThis sourcePrefixes
|
|
|
|
processedFields <- forM aggSelFields $ \(fieldName, field) ->
|
|
|
|
(fieldName,) <$>
|
|
|
|
case field of
|
|
|
|
TAFAgg aggFields ->
|
|
|
|
pure ( aggregateFieldsToExtractorExps thisSourcePrefix aggFields
|
2021-01-18 16:51:36 +03:00
|
|
|
, aggregateFieldToExp aggFields strfyNum
|
2020-06-08 15:13:01 +03:00
|
|
|
)
|
2021-02-03 19:17:20 +03:00
|
|
|
TAFNodes _ annFields -> do
|
2020-06-08 15:13:01 +03:00
|
|
|
annFieldExtr <- processAnnFields thisSourcePrefix fieldName similarArrayFields annFields
|
|
|
|
pure ( [annFieldExtr]
|
|
|
|
, withJsonAggExtr permLimitSubQuery (_ssOrderBy selectSource) $
|
2020-10-27 13:34:31 +03:00
|
|
|
S.Alias $ toIdentifier fieldName
|
2020-06-08 15:13:01 +03:00
|
|
|
)
|
|
|
|
TAFExp e ->
|
|
|
|
pure ( []
|
|
|
|
, withForceAggregation S.textTypeAnn $ S.SELit e
|
|
|
|
)
|
2018-12-12 15:58:39 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
let topLevelExtractor =
|
2020-10-27 13:34:31 +03:00
|
|
|
flip S.Extractor (Just $ S.Alias $ toIdentifier fieldAlias) $
|
2020-06-08 15:13:01 +03:00
|
|
|
S.applyJsonBuildObj $ flip concatMap (map (second snd) processedFields) $
|
|
|
|
\(FieldName fieldText, fieldExp) -> [S.SELit fieldText, fieldExp]
|
|
|
|
nodeExtractors = HM.fromList $
|
|
|
|
concatMap (fst . snd) processedFields <> orderByAndDistinctExtrs
|
2018-12-12 15:58:39 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
pure (selectSource, nodeExtractors, topLevelExtractor)
|
|
|
|
where
|
2021-01-18 16:51:36 +03:00
|
|
|
AnnSelectG aggSelFields tableFrom tablePermissions tableArgs strfyNum = annAggSel
|
2020-06-08 15:13:01 +03:00
|
|
|
permLimit = _tpLimit tablePermissions
|
|
|
|
orderBy = _saOrderBy tableArgs
|
|
|
|
permLimitSubQuery = mkPermissionLimitSubQuery permLimit aggSelFields orderBy
|
|
|
|
similarArrayFields = HM.unions $
|
|
|
|
flip map (map snd aggSelFields) $ \case
|
|
|
|
TAFAgg _ -> mempty
|
2021-02-03 19:17:20 +03:00
|
|
|
TAFNodes _ annFlds ->
|
2020-06-08 15:13:01 +03:00
|
|
|
mkSimilarArrayFields annFlds orderBy
|
|
|
|
TAFExp _ -> mempty
|
|
|
|
|
|
|
|
mkPermissionLimitSubQuery
|
|
|
|
:: Maybe Int
|
2021-04-22 00:44:37 +03:00
|
|
|
-> TableAggregateFields ('Postgres pgKind)
|
|
|
|
-> Maybe (NE.NonEmpty (AnnOrderByItem ('Postgres pgKind)))
|
2020-06-08 15:13:01 +03:00
|
|
|
-> PermissionLimitSubQuery
|
|
|
|
mkPermissionLimitSubQuery permLimit aggFields orderBys =
|
|
|
|
case permLimit of
|
|
|
|
Nothing -> PLSQNotRequired
|
|
|
|
Just limit ->
|
|
|
|
if hasAggregateField || hasAggOrderBy then PLSQRequired limit
|
|
|
|
else PLSQNotRequired
|
|
|
|
where
|
|
|
|
hasAggregateField = flip any (map snd aggFields) $
|
|
|
|
\case
|
|
|
|
TAFAgg _ -> True
|
|
|
|
_ -> False
|
|
|
|
|
|
|
|
hasAggOrderBy = case orderBys of
|
|
|
|
Nothing -> False
|
|
|
|
Just l -> flip any (concatMap toList $ toList l) $
|
|
|
|
\case
|
|
|
|
AOCArrayAggregation{} -> True
|
2020-10-21 19:35:06 +03:00
|
|
|
_ -> False
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
processArrayRelation
|
2021-04-22 00:44:37 +03:00
|
|
|
:: forall pgKind m
|
|
|
|
. ( MonadReader Bool m
|
|
|
|
, MonadWriter JoinTree m
|
|
|
|
, Backend ('Postgres pgKind)
|
2021-05-21 05:46:58 +03:00
|
|
|
, PostgresAnnotatedFieldJSON pgKind
|
2021-04-22 00:44:37 +03:00
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
=> SourcePrefixes
|
2019-08-01 08:09:52 +03:00
|
|
|
-> FieldName
|
2020-06-08 15:13:01 +03:00
|
|
|
-> S.Alias
|
2021-04-22 00:44:37 +03:00
|
|
|
-> ArraySelect ('Postgres pgKind)
|
2020-06-08 15:13:01 +03:00
|
|
|
-> m ()
|
|
|
|
processArrayRelation sourcePrefixes fieldAlias relAlias arrSel =
|
|
|
|
case arrSel of
|
|
|
|
ASSimple annArrRel -> withWriteArrayRelation $ do
|
|
|
|
let AnnRelationSelectG _ colMapping sel = annArrRel
|
|
|
|
permLimitSubQuery =
|
|
|
|
maybe PLSQNotRequired PLSQRequired $ _tpLimit $ _asnPerm sel
|
|
|
|
(source, nodeExtractors) <-
|
|
|
|
processAnnSimpleSelect sourcePrefixes fieldAlias permLimitSubQuery sel
|
|
|
|
let topExtr = asJsonAggExtr JASMultipleRows (S.toAlias fieldAlias)
|
|
|
|
permLimitSubQuery $ _ssOrderBy source
|
|
|
|
pure ( ArrayRelationSource relAlias colMapping source
|
|
|
|
, topExtr
|
|
|
|
, nodeExtractors
|
|
|
|
, ()
|
|
|
|
)
|
|
|
|
ASAggregate aggSel -> withWriteArrayRelation $ do
|
|
|
|
let AnnRelationSelectG _ colMapping sel = aggSel
|
|
|
|
(source, nodeExtractors, topExtr) <-
|
|
|
|
processAnnAggregateSelect sourcePrefixes fieldAlias sel
|
|
|
|
pure ( ArrayRelationSource relAlias colMapping source
|
|
|
|
, topExtr
|
|
|
|
, nodeExtractors
|
|
|
|
, ()
|
|
|
|
)
|
|
|
|
ASConnection connSel -> withWriteArrayConnection $ do
|
|
|
|
let AnnRelationSelectG _ colMapping sel = connSel
|
|
|
|
(source, topExtractor, nodeExtractors) <-
|
|
|
|
processConnectionSelect sourcePrefixes fieldAlias relAlias colMapping sel
|
|
|
|
pure ( source
|
|
|
|
, topExtractor
|
|
|
|
, nodeExtractors
|
|
|
|
, ()
|
|
|
|
)
|
|
|
|
|
|
|
|
processSelectParams
|
2021-04-22 00:44:37 +03:00
|
|
|
:: forall pgKind m
|
|
|
|
. ( MonadReader Bool m
|
|
|
|
, MonadWriter JoinTree m
|
|
|
|
, Backend ('Postgres pgKind)
|
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
=> SourcePrefixes
|
|
|
|
-> FieldName
|
|
|
|
-> SimilarArrayFields
|
2021-04-22 00:44:37 +03:00
|
|
|
-> SelectFrom ('Postgres pgKind)
|
2020-06-08 15:13:01 +03:00
|
|
|
-> PermissionLimitSubQuery
|
2021-04-22 00:44:37 +03:00
|
|
|
-> TablePerm ('Postgres pgKind)
|
|
|
|
-> SelectArgs ('Postgres pgKind)
|
2021-03-25 20:50:08 +03:00
|
|
|
-> m ( SelectSource
|
2020-06-08 15:13:01 +03:00
|
|
|
, [(S.Alias, S.SQLExp)]
|
|
|
|
, Maybe S.SQLExp -- Order by cursor
|
|
|
|
)
|
|
|
|
processSelectParams sourcePrefixes fieldAlias similarArrFields selectFrom
|
|
|
|
permLimitSubQ tablePermissions tableArgs = do
|
|
|
|
maybeOrderBy <- mapM
|
|
|
|
(processOrderByItems thisSourcePrefix fieldAlias similarArrFields)
|
|
|
|
orderByM
|
|
|
|
let fromItem = selectFromToFromItem (_pfBase sourcePrefixes) selectFrom
|
|
|
|
(maybeDistinct, distinctExtrs) =
|
2021-02-03 19:17:20 +03:00
|
|
|
maybe (Nothing, []) (first Just) $ processDistinctOnColumns thisSourcePrefix . snd <$> distM
|
2020-06-08 15:13:01 +03:00
|
|
|
finalWhere = toSQLBoolExp (selectFromToQual selectFrom) $
|
|
|
|
maybe permFilter (andAnnBoolExps permFilter) whereM
|
|
|
|
selectSource = SelectSource thisSourcePrefix fromItem maybeDistinct finalWhere
|
|
|
|
((^. _2) <$> maybeOrderBy) finalLimit offsetM
|
|
|
|
orderByExtrs = maybe [] (^. _1) maybeOrderBy
|
|
|
|
pure ( selectSource
|
|
|
|
, orderByExtrs <> distinctExtrs
|
|
|
|
, (^. _3) <$> maybeOrderBy
|
|
|
|
)
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
thisSourcePrefix = _pfThis sourcePrefixes
|
|
|
|
SelectArgs whereM orderByM inpLimitM offsetM distM = tableArgs
|
|
|
|
TablePerm permFilter permLimit = tablePermissions
|
|
|
|
finalLimit =
|
|
|
|
-- if sub query is required, then only use input limit
|
2019-08-01 08:09:52 +03:00
|
|
|
-- because permission limit is being applied in subquery
|
|
|
|
-- else compare input and permission limits
|
2020-06-08 15:13:01 +03:00
|
|
|
case permLimitSubQ of
|
|
|
|
PLSQRequired _ -> inpLimitM
|
|
|
|
PLSQNotRequired -> compareLimits
|
2019-08-01 08:09:52 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
compareLimits =
|
2019-08-01 08:09:52 +03:00
|
|
|
case (inpLimitM, permLimit) of
|
|
|
|
(inpLim, Nothing) -> inpLim
|
|
|
|
(Nothing, permLim) -> permLim
|
|
|
|
(Just inp, Just perm) -> Just $ if inp < perm then inp else perm
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
processOrderByItems
|
2021-04-22 00:44:37 +03:00
|
|
|
:: forall pgKind m
|
|
|
|
. ( MonadReader Bool m
|
|
|
|
, MonadWriter JoinTree m
|
|
|
|
, Backend ('Postgres pgKind)
|
|
|
|
)
|
2020-10-27 13:34:31 +03:00
|
|
|
=> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
-> FieldName
|
|
|
|
-> SimilarArrayFields
|
2021-04-22 00:44:37 +03:00
|
|
|
-> NE.NonEmpty (AnnOrderByItem ('Postgres pgKind))
|
2020-06-08 15:13:01 +03:00
|
|
|
-> m ( [(S.Alias, S.SQLExp)] -- Order by Extractors
|
|
|
|
, S.OrderByExp
|
|
|
|
, S.SQLExp -- The cursor expression
|
|
|
|
)
|
|
|
|
processOrderByItems sourcePrefix' fieldAlias' similarArrayFields orderByItems = do
|
|
|
|
orderByItemExps <- forM orderByItems processAnnOrderByItem
|
|
|
|
let orderByExp = S.OrderByExp $ toOrderByExp <$> orderByItemExps
|
|
|
|
orderByExtractors = concat $ toList $ map snd . toList <$> orderByItemExps
|
|
|
|
cursor = mkCursorExp $ toList orderByItemExps
|
|
|
|
pure (orderByExtractors, orderByExp, cursor)
|
|
|
|
where
|
2021-04-22 00:44:37 +03:00
|
|
|
processAnnOrderByItem :: AnnOrderByItem ('Postgres pgKind) -> m (OrderByItemExp ('Postgres pgKind))
|
2020-06-08 15:13:01 +03:00
|
|
|
processAnnOrderByItem orderByItem =
|
|
|
|
forM orderByItem $ \ordByCol -> (ordByCol,) <$>
|
|
|
|
processAnnOrderByElement sourcePrefix' fieldAlias' ordByCol
|
|
|
|
|
|
|
|
processAnnOrderByElement
|
2021-04-22 00:44:37 +03:00
|
|
|
:: Identifier -> FieldName -> AnnOrderByElement ('Postgres pgKind) S.SQLExp -> m (S.Alias, S.SQLExp)
|
2020-06-08 15:13:01 +03:00
|
|
|
processAnnOrderByElement sourcePrefix fieldAlias annObCol = do
|
|
|
|
let ordByAlias = mkAnnOrderByAlias sourcePrefix fieldAlias similarArrayFields annObCol
|
|
|
|
(ordByAlias, ) <$> case annObCol of
|
|
|
|
AOCColumn pgColInfo -> pure $
|
2020-10-27 13:34:31 +03:00
|
|
|
S.mkQIdenExp (mkBaseTableAlias sourcePrefix) $ toIdentifier $ pgiColumn pgColInfo
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
AOCObjectRelation relInfo relFilter rest -> withWriteObjectRelation $ do
|
2021-03-03 16:02:00 +03:00
|
|
|
let RelInfo relName _ colMapping relTable _ _ _ = relInfo
|
2020-06-08 15:13:01 +03:00
|
|
|
relSourcePrefix = mkObjectRelationTableAlias sourcePrefix relName
|
|
|
|
fieldName = mkOrderByFieldName relName
|
|
|
|
(relOrderByAlias, relOrdByExp) <-
|
|
|
|
processAnnOrderByElement relSourcePrefix fieldName rest
|
2020-06-25 06:33:37 +03:00
|
|
|
let selectSource = ObjectSelectSource relSourcePrefix
|
|
|
|
(S.FISimple relTable Nothing)
|
2020-06-08 15:13:01 +03:00
|
|
|
(toSQLBoolExp (S.QualTable relTable) relFilter)
|
|
|
|
relSource = ObjectRelationSource relName colMapping selectSource
|
|
|
|
pure ( relSource
|
|
|
|
, HM.singleton relOrderByAlias relOrdByExp
|
|
|
|
, S.mkQIdenExp relSourcePrefix relOrderByAlias
|
|
|
|
)
|
|
|
|
|
|
|
|
AOCArrayAggregation relInfo relFilter aggOrderBy -> withWriteArrayRelation $ do
|
2021-03-03 16:02:00 +03:00
|
|
|
let RelInfo relName _ colMapping relTable _ _ _ = relInfo
|
2020-06-08 15:13:01 +03:00
|
|
|
fieldName = mkOrderByFieldName relName
|
|
|
|
relSourcePrefix = mkArrayRelationSourcePrefix sourcePrefix fieldAlias
|
|
|
|
similarArrayFields fieldName
|
|
|
|
relAlias = mkArrayRelationAlias fieldAlias similarArrayFields fieldName
|
|
|
|
(topExtractor, fields) = mkAggregateOrderByExtractorAndFields aggOrderBy
|
|
|
|
selectSource = SelectSource relSourcePrefix
|
|
|
|
(S.FISimple relTable Nothing) Nothing
|
|
|
|
(toSQLBoolExp (S.QualTable relTable) relFilter)
|
|
|
|
Nothing Nothing Nothing
|
|
|
|
relSource = ArrayRelationSource relAlias colMapping selectSource
|
|
|
|
pure ( relSource
|
|
|
|
, topExtractor
|
|
|
|
, HM.fromList $ aggregateFieldsToExtractorExps relSourcePrefix fields
|
|
|
|
, S.mkQIdenExp relSourcePrefix (mkAggregateOrderByAlias aggOrderBy)
|
|
|
|
)
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
toOrderByExp :: OrderByItemExp ('Postgres pgKind) -> S.OrderByItem
|
2020-06-08 15:13:01 +03:00
|
|
|
toOrderByExp orderByItemExp =
|
|
|
|
let OrderByItemG obTyM expAlias obNullsM = fst . snd <$> orderByItemExp
|
2020-11-12 12:25:48 +03:00
|
|
|
in S.OrderByItem (S.SEIdentifier $ toIdentifier expAlias) obTyM obNullsM
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
mkCursorExp :: [OrderByItemExp ('Postgres pgKind)] -> S.SQLExp
|
2020-06-08 15:13:01 +03:00
|
|
|
mkCursorExp orderByItemExps =
|
|
|
|
S.applyJsonBuildObj $ flip concatMap orderByItemExps $
|
|
|
|
\orderByItemExp ->
|
|
|
|
let OrderByItemG _ (annObCol, (_, valExp)) _ = orderByItemExp
|
|
|
|
in annObColToJSONField valExp annObCol
|
|
|
|
where
|
|
|
|
annObColToJSONField valExp = \case
|
|
|
|
AOCColumn pgCol -> [S.SELit $ getPGColTxt $ pgiColumn pgCol, valExp]
|
|
|
|
AOCObjectRelation relInfo _ obCol ->
|
|
|
|
[ S.SELit $ relNameToTxt $ riName relInfo
|
|
|
|
, S.applyJsonBuildObj $ annObColToJSONField valExp obCol
|
|
|
|
]
|
|
|
|
AOCArrayAggregation relInfo _ aggOrderBy ->
|
|
|
|
[ S.SELit $ relNameToTxt (riName relInfo) <> "_aggregate"
|
|
|
|
, S.applyJsonBuildObj $
|
|
|
|
case aggOrderBy of
|
|
|
|
AAOCount -> [S.SELit "count", valExp]
|
|
|
|
AAOOp opText colInfo ->
|
|
|
|
[ S.SELit opText
|
|
|
|
, S.applyJsonBuildObj [S.SELit $ getPGColTxt $ pgiColumn colInfo, valExp]
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
aggregateFieldsToExtractorExps
|
2021-04-22 00:44:37 +03:00
|
|
|
:: Identifier -> AggregateFields ('Postgres pgKind) -> [(S.Alias, S.SQLExp)]
|
2020-06-08 15:13:01 +03:00
|
|
|
aggregateFieldsToExtractorExps sourcePrefix aggregateFields =
|
|
|
|
flip concatMap aggregateFields $ \(_, field) ->
|
|
|
|
case field of
|
|
|
|
AFCount cty -> case cty of
|
|
|
|
S.CTStar -> []
|
|
|
|
S.CTSimple cols -> colsToExps cols
|
|
|
|
S.CTDistinct cols -> colsToExps cols
|
|
|
|
AFOp aggOp -> aggOpToExps aggOp
|
|
|
|
AFExp _ -> []
|
|
|
|
where
|
2021-01-18 16:51:36 +03:00
|
|
|
colsToExps = fmap mkColExp
|
|
|
|
|
|
|
|
aggOpToExps = mapMaybe colToMaybeExp . _aoFields
|
|
|
|
colToMaybeExp = \case
|
|
|
|
(_, CFCol col _) -> Just $ mkColExp col
|
|
|
|
_ -> Nothing
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2021-01-18 16:51:36 +03:00
|
|
|
mkColExp c =
|
2020-10-27 13:34:31 +03:00
|
|
|
let qualCol = S.mkQIdenExp (mkBaseTableAlias sourcePrefix) (toIdentifier c)
|
|
|
|
colAls = toIdentifier c
|
2021-01-18 16:51:36 +03:00
|
|
|
in (S.Alias colAls, qualCol)
|
2018-10-31 15:51:20 +03:00
|
|
|
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
{- Note: [SQL generation for inherited roles]
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
When a query is executed by an inherited role, each column may contain a predicate
|
2021-04-22 00:44:37 +03:00
|
|
|
(AnnColumnCaseBoolExp ('Postgres pgKind) SQLExp) along with it. The predicate is then
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
converted to a BoolExp, which will be used to check if the said column should
|
|
|
|
be nullified. For example,
|
|
|
|
|
|
|
|
Suppose there are two roles, role1 gives access only to the `addr` column with
|
|
|
|
row filter P1 and role2 gives access to both addr and phone column with row
|
|
|
|
filter P2. The `OR`ing of the predicates will have already been done while
|
|
|
|
the schema has been generated. The SQL generated will look like this:
|
|
|
|
|
|
|
|
select
|
|
|
|
(case when (P1 or P2) then addr else null end) as addr,
|
|
|
|
(case when P2 then phone else null end) as phone
|
|
|
|
from employee
|
|
|
|
where (P1 or P2)
|
|
|
|
|
|
|
|
-}
|
|
|
|
|
2021-05-21 05:46:58 +03:00
|
|
|
class PostgresAnnotatedFieldJSON (pgKind :: PostgresKind) where
|
|
|
|
annRowToJson :: FieldName -> [(FieldName, S.SQLExp)] -> (S.Alias, S.SQLExp)
|
|
|
|
|
|
|
|
instance PostgresAnnotatedFieldJSON 'Vanilla where
|
|
|
|
annRowToJson fieldAlias fieldExps =
|
|
|
|
-- postgres ignores anything beyond 63 chars for an iden
|
|
|
|
-- in this case, we'll need to use json_build_object function
|
|
|
|
-- json_build_object is slower than row_to_json hence it is only
|
|
|
|
-- used when needed
|
|
|
|
if any ( (> 63) . T.length . getFieldNameTxt . fst ) fieldExps then
|
|
|
|
withJsonBuildObj fieldAlias $ concatMap toJsonBuildObjectExps fieldExps
|
|
|
|
else
|
|
|
|
withRowToJSON fieldAlias $ map toRowToJsonExtr fieldExps
|
|
|
|
|
|
|
|
where
|
|
|
|
toJsonBuildObjectExps (fieldName, fieldExp) =
|
|
|
|
[S.SELit $ getFieldNameTxt fieldName, fieldExp]
|
|
|
|
|
|
|
|
toRowToJsonExtr (fieldName, fieldExp) =
|
|
|
|
S.Extractor fieldExp $ Just $ S.toAlias fieldName
|
|
|
|
|
|
|
|
instance PostgresAnnotatedFieldJSON 'Citus where
|
|
|
|
annRowToJson fieldAlias fieldExps =
|
|
|
|
-- Due to the restrictions Citus imposes on joins between tables of various
|
|
|
|
-- distribution types we cannot use row_to_json and have to only rely on
|
|
|
|
-- json_build_object.
|
|
|
|
withJsonBuildObj fieldAlias $ concatMap toJsonBuildObjectExps fieldExps
|
|
|
|
|
|
|
|
where
|
|
|
|
toJsonBuildObjectExps (fieldName, fieldExp) =
|
|
|
|
[S.SELit $ getFieldNameTxt fieldName, fieldExp]
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
processAnnFields
|
2021-04-22 00:44:37 +03:00
|
|
|
:: forall pgKind m
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
. ( MonadReader Bool m
|
2021-03-25 20:50:08 +03:00
|
|
|
, MonadWriter JoinTree m
|
2021-04-22 00:44:37 +03:00
|
|
|
, Backend ('Postgres pgKind)
|
2021-05-21 05:46:58 +03:00
|
|
|
, PostgresAnnotatedFieldJSON pgKind
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
)
|
2020-10-27 13:34:31 +03:00
|
|
|
=> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
-> FieldName
|
|
|
|
-> SimilarArrayFields
|
2021-04-22 00:44:37 +03:00
|
|
|
-> AnnFields ('Postgres pgKind)
|
2020-06-08 15:13:01 +03:00
|
|
|
-> m (S.Alias, S.SQLExp)
|
|
|
|
processAnnFields sourcePrefix fieldAlias similarArrFields annFields = do
|
|
|
|
fieldExps <- forM annFields $ \(fieldName, field) ->
|
|
|
|
(fieldName,) <$>
|
|
|
|
case field of
|
|
|
|
AFExpression t -> pure $ S.SELit t
|
|
|
|
|
2021-02-03 19:17:20 +03:00
|
|
|
AFNodeId _ tn pKeys -> pure $ mkNodeId tn pKeys
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
AFColumn c -> toSQLCol c
|
|
|
|
|
2021-06-11 06:26:50 +03:00
|
|
|
-- this will be gone once the code which collects remote joins from the IR
|
|
|
|
-- emits a modified IR where remote relationships can't be reached
|
|
|
|
AFRemote _ -> pure $ S.SELit "null: remote field selected"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
AFObjectRelation objSel -> withWriteObjectRelation $ do
|
2020-06-25 06:33:37 +03:00
|
|
|
let AnnRelationSelectG relName relMapping annObjSel = objSel
|
|
|
|
AnnObjectSelectG objAnnFields tableFrom tableFilter = annObjSel
|
2020-06-08 15:13:01 +03:00
|
|
|
objRelSourcePrefix = mkObjectRelationTableAlias sourcePrefix relName
|
2020-06-25 06:33:37 +03:00
|
|
|
sourcePrefixes = mkSourcePrefixes objRelSourcePrefix
|
|
|
|
annFieldsExtr <- processAnnFields (_pfThis sourcePrefixes) fieldName HM.empty objAnnFields
|
|
|
|
let selectSource = ObjectSelectSource (_pfThis sourcePrefixes)
|
|
|
|
(S.FISimple tableFrom Nothing)
|
|
|
|
(toSQLBoolExp (S.QualTable tableFrom) tableFilter)
|
|
|
|
objRelSource = ObjectRelationSource relName relMapping selectSource
|
2020-06-08 15:13:01 +03:00
|
|
|
pure ( objRelSource
|
2020-06-25 06:33:37 +03:00
|
|
|
, HM.fromList [annFieldsExtr]
|
2020-06-08 15:13:01 +03:00
|
|
|
, S.mkQIdenExp objRelSourcePrefix fieldName
|
|
|
|
)
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
AFArrayRelation arrSel -> do
|
|
|
|
let arrRelSourcePrefix = mkArrayRelationSourcePrefix sourcePrefix fieldAlias similarArrFields fieldName
|
|
|
|
arrRelAlias = mkArrayRelationAlias fieldAlias similarArrFields fieldName
|
|
|
|
processArrayRelation (mkSourcePrefixes arrRelSourcePrefix) fieldName arrRelAlias arrSel
|
|
|
|
pure $ S.mkQIdenExp arrRelSourcePrefix fieldName
|
|
|
|
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
AFComputedField _ (CFSScalar scalar caseBoolExpMaybe) -> do
|
|
|
|
computedFieldSQLExp <- fromScalarComputedField scalar
|
|
|
|
-- The computed field is conditionally outputed depending
|
|
|
|
-- on the presence of `caseBoolExpMaybe` and the value it
|
|
|
|
-- evaluates to. `caseBoolExpMaybe` will be set only in the
|
|
|
|
-- case of an inherited role.
|
|
|
|
-- See [SQL generation for inherited role]
|
|
|
|
case caseBoolExpMaybe of
|
|
|
|
Nothing -> pure computedFieldSQLExp
|
|
|
|
Just caseBoolExp ->
|
|
|
|
let boolExp = S.simplifyBoolExp $ toSQLBoolExp (S.QualifiedIdentifier baseTableIdentifier Nothing)
|
|
|
|
$ _accColCaseBoolExpField <$> caseBoolExp
|
|
|
|
in pure $ S.SECond boolExp computedFieldSQLExp S.SENull
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-02-03 19:17:20 +03:00
|
|
|
AFComputedField _ (CFSTable selectTy sel) -> withWriteComputedFieldTableSet $ do
|
2020-06-08 15:13:01 +03:00
|
|
|
let computedFieldSourcePrefix =
|
|
|
|
mkComputedFieldTableAlias sourcePrefix fieldName
|
|
|
|
(selectSource, nodeExtractors) <-
|
|
|
|
processAnnSimpleSelect (mkSourcePrefixes computedFieldSourcePrefix)
|
|
|
|
fieldName PLSQNotRequired sel
|
|
|
|
let computedFieldTableSetSource =
|
|
|
|
ComputedFieldTableSetSource fieldName selectTy selectSource
|
|
|
|
pure ( computedFieldTableSetSource
|
|
|
|
, nodeExtractors
|
|
|
|
, S.mkQIdenExp computedFieldSourcePrefix fieldName
|
|
|
|
)
|
2018-12-12 15:58:39 +03:00
|
|
|
|
2021-05-21 05:46:58 +03:00
|
|
|
pure $ annRowToJson @pgKind fieldAlias fieldExps
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
|
|
|
mkSourcePrefixes newPrefix = SourcePrefixes newPrefix sourcePrefix
|
|
|
|
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
baseTableIdentifier = mkBaseTableAlias sourcePrefix
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
toSQLCol :: AnnColumnField ('Postgres pgKind) S.SQLExp -> m S.SQLExp
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
toSQLCol (AnnColumnField col asText colOpM caseBoolExpMaybe) = do
|
2020-06-08 15:13:01 +03:00
|
|
|
strfyNum <- ask
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
let sqlExpression =
|
|
|
|
withColumnOp colOpM $
|
|
|
|
S.mkQIdenExp baseTableIdentifier $ pgiColumn col
|
|
|
|
finalSQLExpression =
|
|
|
|
-- Check out [SQL generation for inherited role]
|
|
|
|
case caseBoolExpMaybe of
|
|
|
|
Nothing -> sqlExpression
|
|
|
|
Just caseBoolExp ->
|
|
|
|
let boolExp =
|
|
|
|
S.simplifyBoolExp $ toSQLBoolExp (S.QualifiedIdentifier baseTableIdentifier Nothing) $
|
|
|
|
_accColCaseBoolExpField <$> caseBoolExp
|
|
|
|
in S.SECond boolExp sqlExpression S.SENull
|
|
|
|
pure $ toJSONableExp strfyNum (pgiType col) asText finalSQLExpression
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
fromScalarComputedField :: ComputedFieldScalarSelect ('Postgres pgKind) S.SQLExp -> m S.SQLExp
|
2020-06-08 15:13:01 +03:00
|
|
|
fromScalarComputedField computedFieldScalar = do
|
|
|
|
strfyNum <- ask
|
2020-11-25 17:18:58 +03:00
|
|
|
pure $ toJSONableExp strfyNum (ColumnScalar ty) False $ withColumnOp colOpM $
|
2020-06-08 15:13:01 +03:00
|
|
|
S.SEFunction $ S.FunctionExp fn (fromTableRowArgs sourcePrefix args) Nothing
|
|
|
|
where
|
|
|
|
ComputedFieldScalarSelect fn args ty colOpM = computedFieldScalar
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
withColumnOp :: Maybe (ColumnOp ('Postgres pgKind)) -> S.SQLExp -> S.SQLExp
|
2020-06-08 15:13:01 +03:00
|
|
|
withColumnOp colOpM sqlExp = case colOpM of
|
|
|
|
Nothing -> sqlExp
|
|
|
|
Just (ColumnOp opText cExp) -> S.mkSQLOpExp opText sqlExp cExp
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
mkNodeId :: QualifiedTable -> PrimaryKeyColumns ('Postgres pgKind) -> S.SQLExp
|
2020-06-08 15:13:01 +03:00
|
|
|
mkNodeId (QualifiedObject tableSchema tableName) pkeyColumns =
|
2020-06-16 17:25:49 +03:00
|
|
|
let columnInfoToSQLExp pgColumnInfo =
|
|
|
|
toJSONableExp False (pgiType pgColumnInfo) False $
|
|
|
|
S.mkQIdenExp (mkBaseTableAlias sourcePrefix) $ pgiColumn pgColumnInfo
|
|
|
|
|
|
|
|
-- See Note [Relay Node id].
|
|
|
|
in encodeBase64 $ flip S.SETyAnn S.textTypeAnn $ S.applyJsonBuildArray $
|
|
|
|
[ S.intToSQLExp $ nodeIdVersionInt currentNodeIdVersion
|
|
|
|
, S.SELit (getSchemaTxt tableSchema)
|
|
|
|
, S.SELit (toTxt tableName)
|
|
|
|
] <> map columnInfoToSQLExp (toList pkeyColumns)
|
2018-10-31 15:51:20 +03:00
|
|
|
|
|
|
|
injectJoinCond :: S.BoolExp -- ^ Join condition
|
|
|
|
-> S.BoolExp -- ^ Where condition
|
|
|
|
-> S.WhereFrag -- ^ New where frag
|
|
|
|
injectJoinCond joinCond whereCond =
|
|
|
|
S.WhereFrag $ S.simplifyBoolExp $ S.BEBin S.AndOp joinCond whereCond
|
|
|
|
|
2019-12-13 00:46:33 +03:00
|
|
|
mkJoinCond :: S.Alias -> HashMap PGCol PGCol -> S.BoolExp
|
2019-10-18 11:29:47 +03:00
|
|
|
mkJoinCond baseTablepfx colMapn =
|
2019-12-13 00:46:33 +03:00
|
|
|
foldl' (S.BEBin S.AndOp) (S.BELit True) $ flip map (HM.toList colMapn) $ \(lCol, rCol) ->
|
2019-10-18 11:29:47 +03:00
|
|
|
S.BECompare S.SEQ (S.mkQIdenExp baseTablepfx lCol) (S.mkSIdenExp rCol)
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
generateSQLSelect
|
|
|
|
:: S.BoolExp -- ^ Pre join condition
|
2021-03-25 20:50:08 +03:00
|
|
|
-> SelectSource
|
|
|
|
-> SelectNode
|
2020-06-08 15:13:01 +03:00
|
|
|
-> S.Select
|
|
|
|
generateSQLSelect joinCondition selectSource selectNode =
|
2018-10-31 15:51:20 +03:00
|
|
|
S.mkSelect
|
2020-06-08 15:13:01 +03:00
|
|
|
{ S.selExtr = [S.Extractor e $ Just a | (a, e) <- HM.toList extractors]
|
|
|
|
, S.selFrom = Just $ S.FromExp [joinedFrom]
|
|
|
|
, S.selOrderBy = maybeOrderby
|
|
|
|
, S.selLimit = S.LimitExp . S.intToSQLExp <$> maybeLimit
|
2021-06-10 19:13:20 +03:00
|
|
|
, S.selOffset = S.OffsetExp . S.int64ToSQLExp <$> maybeOffset
|
2020-06-08 15:13:01 +03:00
|
|
|
, S.selDistinct = maybeDistinct
|
2018-10-31 15:51:20 +03:00
|
|
|
}
|
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
SelectSource sourcePrefix fromItem maybeDistinct whereExp
|
|
|
|
maybeOrderby maybeLimit maybeOffset = selectSource
|
|
|
|
SelectNode extractors joinTree = selectNode
|
|
|
|
JoinTree objectRelations arrayRelations arrayConnections computedFields = joinTree
|
|
|
|
-- this is the table which is aliased as "sourcePrefix.base"
|
|
|
|
baseSelect = S.mkSelect
|
2020-01-16 07:53:28 +03:00
|
|
|
{ S.selExtr = [S.Extractor (S.SEStar Nothing) Nothing]
|
2018-10-31 15:51:20 +03:00
|
|
|
, S.selFrom = Just $ S.FromExp [fromItem]
|
2020-06-08 15:13:01 +03:00
|
|
|
, S.selWhere = Just $ injectJoinCond joinCondition whereExp
|
2018-10-31 15:51:20 +03:00
|
|
|
}
|
2020-06-08 15:13:01 +03:00
|
|
|
baseSelectAlias = S.Alias $ mkBaseTableAlias sourcePrefix
|
|
|
|
baseFromItem = S.mkSelFromItem baseSelect baseSelectAlias
|
2018-10-31 15:51:20 +03:00
|
|
|
|
|
|
|
-- function to create a joined from item from two from items
|
|
|
|
leftOuterJoin current new =
|
|
|
|
S.FIJoin $ S.JoinExpr current S.LeftOuter new $
|
|
|
|
S.JoinOn $ S.BELit True
|
|
|
|
|
|
|
|
-- this is the from eexp for the final select
|
|
|
|
joinedFrom :: S.FromItem
|
|
|
|
joinedFrom = foldl' leftOuterJoin baseFromItem $
|
2020-06-08 15:13:01 +03:00
|
|
|
map objectRelationToFromItem (HM.toList objectRelations) <>
|
|
|
|
map arrayRelationToFromItem (HM.toList arrayRelations) <>
|
|
|
|
map arrayConnectionToFromItem (HM.toList arrayConnections) <>
|
|
|
|
map computedFieldToFromItem (HM.toList computedFields)
|
|
|
|
|
|
|
|
|
|
|
|
objectRelationToFromItem
|
2021-03-25 20:50:08 +03:00
|
|
|
:: (ObjectRelationSource, SelectNode) -> S.FromItem
|
2020-06-08 15:13:01 +03:00
|
|
|
objectRelationToFromItem (objectRelationSource, node) =
|
2020-06-25 06:33:37 +03:00
|
|
|
let ObjectRelationSource _ colMapping objectSelectSource = objectRelationSource
|
|
|
|
alias = S.Alias $ _ossPrefix objectSelectSource
|
|
|
|
source = objectSelectSourceToSelectSource objectSelectSource
|
2020-06-08 15:13:01 +03:00
|
|
|
select = generateSQLSelect (mkJoinCond baseSelectAlias colMapping) source node
|
|
|
|
in S.mkLateralFromItem select alias
|
|
|
|
|
|
|
|
arrayRelationToFromItem
|
2021-03-25 20:50:08 +03:00
|
|
|
:: (ArrayRelationSource, ArraySelectNode) -> S.FromItem
|
2020-06-08 15:13:01 +03:00
|
|
|
arrayRelationToFromItem (arrayRelationSource, arraySelectNode) =
|
|
|
|
let ArrayRelationSource _ colMapping source = arrayRelationSource
|
|
|
|
alias = S.Alias $ _ssPrefix source
|
|
|
|
select = generateSQLSelectFromArrayNode source arraySelectNode $
|
|
|
|
mkJoinCond baseSelectAlias colMapping
|
|
|
|
in S.mkLateralFromItem select alias
|
|
|
|
|
|
|
|
arrayConnectionToFromItem
|
2021-03-25 20:50:08 +03:00
|
|
|
:: (ArrayConnectionSource, ArraySelectNode) -> S.FromItem
|
2020-06-08 15:13:01 +03:00
|
|
|
arrayConnectionToFromItem (arrayConnectionSource, arraySelectNode) =
|
|
|
|
let selectWith = connectionToSelectWith baseSelectAlias arrayConnectionSource arraySelectNode
|
|
|
|
alias = S.Alias $ _ssPrefix $ _acsSource arrayConnectionSource
|
|
|
|
in S.FISelectWith (S.Lateral True) selectWith alias
|
|
|
|
|
|
|
|
computedFieldToFromItem
|
2021-03-25 20:50:08 +03:00
|
|
|
:: (ComputedFieldTableSetSource, SelectNode) -> S.FromItem
|
2020-06-08 15:13:01 +03:00
|
|
|
computedFieldToFromItem (computedFieldTableSource, node) =
|
|
|
|
let ComputedFieldTableSetSource fieldName selectTy source = computedFieldTableSource
|
|
|
|
internalSelect = generateSQLSelect (S.BELit True) source node
|
|
|
|
extractor = asJsonAggExtr selectTy (S.toAlias fieldName) PLSQNotRequired $
|
|
|
|
_ssOrderBy source
|
|
|
|
alias = S.Alias $ _ssPrefix source
|
|
|
|
select = S.mkSelect
|
|
|
|
{ S.selExtr = [extractor]
|
|
|
|
, S.selFrom = Just $ S.FromExp [S.mkSelFromItem internalSelect alias]
|
2019-10-18 11:29:47 +03:00
|
|
|
}
|
2020-06-08 15:13:01 +03:00
|
|
|
in S.mkLateralFromItem select alias
|
|
|
|
|
|
|
|
generateSQLSelectFromArrayNode
|
2021-03-25 20:50:08 +03:00
|
|
|
:: SelectSource
|
|
|
|
-> ArraySelectNode
|
2020-06-08 15:13:01 +03:00
|
|
|
-> S.BoolExp
|
|
|
|
-> S.Select
|
|
|
|
generateSQLSelectFromArrayNode selectSource arraySelectNode joinCondition =
|
|
|
|
S.mkSelect
|
|
|
|
{ S.selExtr = topExtractors
|
|
|
|
, S.selFrom = Just $ S.FromExp [selectFrom]
|
|
|
|
}
|
|
|
|
where
|
|
|
|
ArraySelectNode topExtractors selectNode = arraySelectNode
|
|
|
|
selectFrom = S.mkSelFromItem
|
|
|
|
(generateSQLSelect joinCondition selectSource selectNode) $
|
|
|
|
S.Alias $ _ssPrefix selectSource
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
mkAggregateSelect
|
|
|
|
:: forall pgKind
|
2021-05-21 05:46:58 +03:00
|
|
|
. ( Backend ('Postgres pgKind)
|
|
|
|
, PostgresAnnotatedFieldJSON pgKind
|
|
|
|
)
|
2021-04-22 00:44:37 +03:00
|
|
|
=> AnnAggregateSelect ('Postgres pgKind)
|
|
|
|
-> S.Select
|
2020-06-08 15:13:01 +03:00
|
|
|
mkAggregateSelect annAggSel =
|
|
|
|
let ((selectSource, nodeExtractors, topExtractor), joinTree) =
|
|
|
|
runWriter $ flip runReaderT strfyNum $
|
|
|
|
processAnnAggregateSelect sourcePrefixes rootFieldName annAggSel
|
|
|
|
selectNode = SelectNode nodeExtractors joinTree
|
|
|
|
arrayNode = ArraySelectNode [topExtractor] selectNode
|
|
|
|
in prefixNumToAliases $
|
|
|
|
generateSQLSelectFromArrayNode selectSource arrayNode $ S.BELit True
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
strfyNum = _asnStrfyNum annAggSel
|
|
|
|
rootFieldName = FieldName "root"
|
2020-10-27 13:34:31 +03:00
|
|
|
rootIdentifier = toIdentifier rootFieldName
|
|
|
|
sourcePrefixes = SourcePrefixes rootIdentifier rootIdentifier
|
2018-12-12 15:58:39 +03:00
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
mkSQLSelect
|
|
|
|
:: forall pgKind
|
2021-05-21 05:46:58 +03:00
|
|
|
. ( Backend ('Postgres pgKind)
|
|
|
|
, PostgresAnnotatedFieldJSON pgKind
|
|
|
|
)
|
2021-04-22 00:44:37 +03:00
|
|
|
=> JsonAggSelect
|
|
|
|
-> AnnSimpleSel ('Postgres pgKind)
|
|
|
|
-> S.Select
|
2020-02-13 20:38:23 +03:00
|
|
|
mkSQLSelect jsonAggSelect annSel =
|
2020-06-08 15:13:01 +03:00
|
|
|
let permLimitSubQuery = PLSQNotRequired
|
|
|
|
((selectSource, nodeExtractors), joinTree) =
|
|
|
|
runWriter $ flip runReaderT strfyNum $
|
|
|
|
processAnnSimpleSelect sourcePrefixes rootFldName permLimitSubQuery annSel
|
|
|
|
selectNode = SelectNode nodeExtractors joinTree
|
|
|
|
topExtractor = asJsonAggExtr jsonAggSelect rootFldAls permLimitSubQuery
|
|
|
|
$ _ssOrderBy selectSource
|
|
|
|
arrayNode = ArraySelectNode [topExtractor] selectNode
|
|
|
|
in prefixNumToAliases $
|
|
|
|
generateSQLSelectFromArrayNode selectSource arrayNode $ S.BELit True
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
strfyNum = _asnStrfyNum annSel
|
2020-10-27 13:34:31 +03:00
|
|
|
rootFldIdentifier = toIdentifier rootFldName
|
|
|
|
sourcePrefixes = SourcePrefixes rootFldIdentifier rootFldIdentifier
|
2018-12-12 15:58:39 +03:00
|
|
|
rootFldName = FieldName "root"
|
2020-10-27 13:34:31 +03:00
|
|
|
rootFldAls = S.Alias $ toIdentifier rootFldName
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
mkConnectionSelect
|
|
|
|
:: forall pgKind
|
2021-05-21 05:46:58 +03:00
|
|
|
. ( Backend ('Postgres pgKind)
|
|
|
|
, PostgresAnnotatedFieldJSON pgKind
|
|
|
|
)
|
2021-04-22 00:44:37 +03:00
|
|
|
=> ConnectionSelect ('Postgres pgKind) S.SQLExp
|
|
|
|
-> S.SelectWithG S.Select
|
2020-06-08 15:13:01 +03:00
|
|
|
mkConnectionSelect connectionSelect =
|
|
|
|
let ((connectionSource, topExtractor, nodeExtractors), joinTree) =
|
|
|
|
runWriter $ flip runReaderT strfyNum $
|
|
|
|
processConnectionSelect sourcePrefixes rootFieldName
|
2020-10-27 13:34:31 +03:00
|
|
|
(S.Alias rootIdentifier) mempty connectionSelect
|
2020-06-08 15:13:01 +03:00
|
|
|
selectNode = ArraySelectNode [topExtractor] $
|
|
|
|
SelectNode nodeExtractors joinTree
|
|
|
|
in prefixNumToAliasesSelectWith $
|
2020-10-27 13:34:31 +03:00
|
|
|
connectionToSelectWith (S.Alias rootIdentifier) connectionSource selectNode
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
|
|
|
strfyNum = _asnStrfyNum $ _csSelect connectionSelect
|
|
|
|
rootFieldName = FieldName "root"
|
2020-10-27 13:34:31 +03:00
|
|
|
rootIdentifier = toIdentifier rootFieldName
|
|
|
|
sourcePrefixes = SourcePrefixes rootIdentifier rootIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
-- | First element extractor expression from given record set
|
|
|
|
-- For example:- To get first "id" column from given row set,
|
|
|
|
-- the function generates the SQL expression AS `(array_agg("id"))[1]`
|
|
|
|
mkFirstElementExp :: S.SQLExp -> S.SQLExp
|
2020-10-27 13:34:31 +03:00
|
|
|
mkFirstElementExp expIdentifier =
|
2020-06-08 15:13:01 +03:00
|
|
|
-- For Example
|
2020-10-27 13:34:31 +03:00
|
|
|
S.SEArrayIndex (S.SEFnApp "array_agg" [expIdentifier] Nothing) (S.intToSQLExp 1)
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
-- | Last element extractor expression from given record set.
|
|
|
|
-- For example:- To get first "id" column from given row set,
|
|
|
|
-- the function generates the SQL expression AS `(array_agg("id"))[array_length(array_agg("id"), 1)]`
|
|
|
|
mkLastElementExp :: S.SQLExp -> S.SQLExp
|
2020-10-27 13:34:31 +03:00
|
|
|
mkLastElementExp expIdentifier =
|
|
|
|
let arrayExp = S.SEFnApp "array_agg" [expIdentifier] Nothing
|
2020-06-08 15:13:01 +03:00
|
|
|
in S.SEArrayIndex arrayExp $
|
|
|
|
S.SEFnApp "array_length" [arrayExp, S.intToSQLExp 1] Nothing
|
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
cursorIdentifier :: Identifier
|
|
|
|
cursorIdentifier = Identifier "__cursor"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
startCursorIdentifier :: Identifier
|
|
|
|
startCursorIdentifier = Identifier "__start_cursor"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
endCursorIdentifier :: Identifier
|
|
|
|
endCursorIdentifier = Identifier "__end_cursor"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
hasPreviousPageIdentifier :: Identifier
|
|
|
|
hasPreviousPageIdentifier = Identifier "__has_previous_page"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
hasNextPageIdentifier :: Identifier
|
|
|
|
hasNextPageIdentifier = Identifier "__has_next_page"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
pageInfoSelectAliasIdentifier :: Identifier
|
|
|
|
pageInfoSelectAliasIdentifier = Identifier "__page_info"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
cursorsSelectAliasIdentifier :: Identifier
|
|
|
|
cursorsSelectAliasIdentifier = Identifier "__cursors_select"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
encodeBase64 :: S.SQLExp -> S.SQLExp
|
|
|
|
encodeBase64 =
|
|
|
|
removeNewline . bytesToBase64Text . convertToBytes
|
|
|
|
where
|
|
|
|
convertToBytes e =
|
|
|
|
S.SEFnApp "convert_to" [e, S.SELit "UTF8"] Nothing
|
|
|
|
bytesToBase64Text e =
|
|
|
|
S.SEFnApp "encode" [e, S.SELit "base64"] Nothing
|
|
|
|
removeNewline e =
|
|
|
|
S.SEFnApp "regexp_replace" [e, S.SELit "\\n", S.SELit "", S.SELit "g"] Nothing
|
|
|
|
|
|
|
|
|
|
|
|
processConnectionSelect
|
2021-05-21 05:46:58 +03:00
|
|
|
:: forall pgKind m
|
|
|
|
. ( MonadReader Bool m
|
2021-03-25 20:50:08 +03:00
|
|
|
, MonadWriter JoinTree m
|
2021-04-22 00:44:37 +03:00
|
|
|
, Backend ('Postgres pgKind)
|
2021-05-21 05:46:58 +03:00
|
|
|
, PostgresAnnotatedFieldJSON pgKind
|
2020-06-08 15:13:01 +03:00
|
|
|
)
|
|
|
|
=> SourcePrefixes
|
|
|
|
-> FieldName
|
|
|
|
-> S.Alias
|
|
|
|
-> HM.HashMap PGCol PGCol
|
2021-04-22 00:44:37 +03:00
|
|
|
-> ConnectionSelect ('Postgres pgKind) S.SQLExp
|
2021-03-25 20:50:08 +03:00
|
|
|
-> m ( ArrayConnectionSource
|
2020-06-08 15:13:01 +03:00
|
|
|
, S.Extractor
|
|
|
|
, HM.HashMap S.Alias S.SQLExp
|
|
|
|
)
|
|
|
|
processConnectionSelect sourcePrefixes fieldAlias relAlias colMapping connectionSelect = do
|
|
|
|
(selectSource, orderByAndDistinctExtrs, maybeOrderByCursor) <-
|
|
|
|
processSelectParams sourcePrefixes fieldAlias similarArrayFields selectFrom
|
|
|
|
permLimitSubQuery tablePermissions tableArgs
|
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
let mkCursorExtractor = (S.Alias cursorIdentifier,) . (`S.SETyAnn` S.textTypeAnn)
|
2020-06-08 15:13:01 +03:00
|
|
|
cursorExtractors = case maybeOrderByCursor of
|
|
|
|
Just orderByCursor -> [mkCursorExtractor orderByCursor]
|
|
|
|
Nothing ->
|
|
|
|
-- Extract primary key columns from base select along with cursor expression.
|
|
|
|
-- Those columns are required to perform connection split via a WHERE clause.
|
2020-06-16 17:25:49 +03:00
|
|
|
mkCursorExtractor primaryKeyColumnsObjectExp : primaryKeyColumnExtractors
|
2020-06-08 15:13:01 +03:00
|
|
|
orderByExp = _ssOrderBy selectSource
|
|
|
|
(topExtractorExp, exps) <- flip runStateT [] $ processFields orderByExp
|
2020-10-27 13:34:31 +03:00
|
|
|
let topExtractor = S.Extractor topExtractorExp $ Just $ S.Alias fieldIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
allExtractors = HM.fromList $ cursorExtractors <> exps <> orderByAndDistinctExtrs
|
|
|
|
arrayConnectionSource = ArrayConnectionSource relAlias colMapping
|
|
|
|
(mkSplitBoolExp <$> maybeSplit) maybeSlice selectSource
|
|
|
|
pure ( arrayConnectionSource
|
|
|
|
, topExtractor
|
|
|
|
, allExtractors
|
|
|
|
)
|
|
|
|
where
|
2021-02-03 19:17:20 +03:00
|
|
|
ConnectionSelect _ primaryKeyColumns maybeSplit maybeSlice select = connectionSelect
|
2020-06-08 15:13:01 +03:00
|
|
|
AnnSelectG fields selectFrom tablePermissions tableArgs _ = select
|
2020-10-27 13:34:31 +03:00
|
|
|
fieldIdentifier = toIdentifier fieldAlias
|
2020-06-08 15:13:01 +03:00
|
|
|
thisPrefix = _pfThis sourcePrefixes
|
|
|
|
permLimitSubQuery = PLSQNotRequired
|
|
|
|
|
2020-06-16 17:25:49 +03:00
|
|
|
primaryKeyColumnsObjectExp =
|
|
|
|
S.applyJsonBuildObj $ flip concatMap (toList primaryKeyColumns) $
|
|
|
|
\pgColumnInfo ->
|
|
|
|
[ S.SELit $ getPGColTxt $ pgiColumn pgColumnInfo
|
|
|
|
, toJSONableExp False (pgiType pgColumnInfo) False $
|
|
|
|
S.mkQIdenExp (mkBaseTableAlias thisPrefix) $ pgiColumn pgColumnInfo
|
|
|
|
]
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
primaryKeyColumnExtractors =
|
|
|
|
flip map (toList primaryKeyColumns) $
|
|
|
|
\pgColumnInfo ->
|
|
|
|
let pgColumn = pgiColumn pgColumnInfo
|
|
|
|
in ( S.Alias $ mkBaseTableColumnAlias thisPrefix pgColumn
|
|
|
|
, S.mkQIdenExp (mkBaseTableAlias thisPrefix) pgColumn
|
|
|
|
)
|
|
|
|
|
|
|
|
mkSplitBoolExp (firstSplit NE.:| rest) =
|
|
|
|
S.BEBin S.OrOp (mkSplitCompareExp firstSplit) $ mkBoolExpFromRest firstSplit rest
|
|
|
|
where
|
|
|
|
mkBoolExpFromRest previousSplit =
|
|
|
|
S.BEBin S.AndOp (mkEqualityCompareExp previousSplit) . \case
|
|
|
|
[] -> S.BELit False
|
|
|
|
(thisSplit:remainingSplit) -> mkSplitBoolExp (thisSplit NE.:| remainingSplit)
|
|
|
|
|
|
|
|
mkSplitCompareExp (ConnectionSplit kind v (OrderByItemG obTyM obCol _)) =
|
|
|
|
let obAlias = mkAnnOrderByAlias thisPrefix fieldAlias similarArrayFields obCol
|
2020-11-12 12:25:48 +03:00
|
|
|
obTy = fromMaybe S.OTAsc obTyM
|
2020-06-08 15:13:01 +03:00
|
|
|
compareOp = case (kind, obTy) of
|
|
|
|
(CSKAfter, S.OTAsc) -> S.SGT
|
|
|
|
(CSKAfter, S.OTDesc) -> S.SLT
|
|
|
|
(CSKBefore, S.OTAsc) -> S.SLT
|
|
|
|
(CSKBefore, S.OTDesc) -> S.SGT
|
2020-10-27 13:34:31 +03:00
|
|
|
in S.BECompare compareOp (S.SEIdentifier $ toIdentifier obAlias) v
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
mkEqualityCompareExp (ConnectionSplit _ v orderByItem) =
|
|
|
|
let obAlias = mkAnnOrderByAlias thisPrefix fieldAlias similarArrayFields $
|
|
|
|
obiColumn orderByItem
|
2020-10-27 13:34:31 +03:00
|
|
|
in S.BECompare S.SEQ (S.SEIdentifier $ toIdentifier obAlias) v
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
similarArrayFields = HM.unions $
|
|
|
|
flip map (map snd fields) $ \case
|
|
|
|
ConnectionTypename{} -> mempty
|
|
|
|
ConnectionPageInfo{} -> mempty
|
|
|
|
ConnectionEdges edges -> HM.unions $
|
|
|
|
flip map (map snd edges) $ \case
|
|
|
|
EdgeTypename{} -> mempty
|
|
|
|
EdgeCursor{} -> mempty
|
|
|
|
EdgeNode annFields ->
|
|
|
|
mkSimilarArrayFields annFields $ _saOrderBy tableArgs
|
|
|
|
|
|
|
|
mkSimpleJsonAgg rowExp ob =
|
|
|
|
let jsonAggExp = S.SEFnApp "json_agg" [rowExp] ob
|
|
|
|
in S.SEFnApp "coalesce" [jsonAggExp, S.SELit "[]"] Nothing
|
|
|
|
|
|
|
|
processFields
|
2021-05-21 05:46:58 +03:00
|
|
|
:: forall n
|
|
|
|
. ( MonadReader Bool n
|
|
|
|
, MonadWriter JoinTree n
|
|
|
|
, MonadState [(S.Alias, S.SQLExp)] n
|
2020-06-08 15:13:01 +03:00
|
|
|
)
|
2021-05-21 05:46:58 +03:00
|
|
|
=> Maybe S.OrderByExp -> n S.SQLExp
|
2020-06-08 15:13:01 +03:00
|
|
|
processFields orderByExp =
|
|
|
|
fmap (S.applyJsonBuildObj . concat) $
|
|
|
|
forM fields $
|
|
|
|
\(FieldName fieldText, field) -> (S.SELit fieldText:) . pure <$>
|
|
|
|
case field of
|
|
|
|
ConnectionTypename t -> pure $ withForceAggregation S.textTypeAnn $ S.SELit t
|
|
|
|
ConnectionPageInfo pageInfoFields -> pure $ processPageInfoFields pageInfoFields
|
|
|
|
ConnectionEdges edges ->
|
|
|
|
fmap (flip mkSimpleJsonAgg orderByExp . S.applyJsonBuildObj . concat) $ forM edges $
|
|
|
|
\(FieldName edgeText, edge) -> (S.SELit edgeText:) . pure <$>
|
|
|
|
case edge of
|
|
|
|
EdgeTypename t -> pure $ S.SELit t
|
2020-10-27 13:34:31 +03:00
|
|
|
EdgeCursor -> pure $ encodeBase64 $ S.SEIdentifier (toIdentifier cursorIdentifier)
|
2020-06-08 15:13:01 +03:00
|
|
|
EdgeNode annFields -> do
|
|
|
|
let edgeFieldName = FieldName $
|
|
|
|
getFieldNameTxt fieldAlias <> "." <> fieldText <> "." <> edgeText
|
2020-10-27 13:34:31 +03:00
|
|
|
edgeFieldIdentifier = toIdentifier edgeFieldName
|
2020-06-08 15:13:01 +03:00
|
|
|
annFieldsExtrExp <- processAnnFields thisPrefix edgeFieldName similarArrayFields annFields
|
|
|
|
modify' (<> [annFieldsExtrExp])
|
2020-10-27 13:34:31 +03:00
|
|
|
pure $ S.SEIdentifier edgeFieldIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
processPageInfoFields infoFields =
|
|
|
|
S.applyJsonBuildObj $ flip concatMap infoFields $
|
|
|
|
\(FieldName fieldText, field) -> (:) (S.SELit fieldText) $ pure case field of
|
|
|
|
PageInfoTypename t -> withForceAggregation S.textTypeAnn $ S.SELit t
|
|
|
|
PageInfoHasNextPage -> withForceAggregation S.boolTypeAnn $
|
2020-10-27 13:34:31 +03:00
|
|
|
mkSingleFieldSelect (S.SEIdentifier hasNextPageIdentifier) pageInfoSelectAliasIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
PageInfoHasPreviousPage -> withForceAggregation S.boolTypeAnn $
|
2020-10-27 13:34:31 +03:00
|
|
|
mkSingleFieldSelect (S.SEIdentifier hasPreviousPageIdentifier) pageInfoSelectAliasIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
PageInfoStartCursor -> withForceAggregation S.textTypeAnn $
|
2020-10-27 13:34:31 +03:00
|
|
|
encodeBase64 $ mkSingleFieldSelect (S.SEIdentifier startCursorIdentifier) cursorsSelectAliasIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
PageInfoEndCursor -> withForceAggregation S.textTypeAnn $
|
2020-10-27 13:34:31 +03:00
|
|
|
encodeBase64 $ mkSingleFieldSelect (S.SEIdentifier endCursorIdentifier) cursorsSelectAliasIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
2020-10-27 13:34:31 +03:00
|
|
|
mkSingleFieldSelect field fromIdentifier = S.SESelect
|
2020-06-08 15:13:01 +03:00
|
|
|
S.mkSelect { S.selExtr = [S.Extractor field Nothing]
|
2020-10-27 13:34:31 +03:00
|
|
|
, S.selFrom = Just $ S.FromExp [S.FIIdentifier fromIdentifier]
|
2020-06-08 15:13:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
connectionToSelectWith
|
|
|
|
:: S.Alias
|
2021-03-25 20:50:08 +03:00
|
|
|
-> ArrayConnectionSource
|
|
|
|
-> ArraySelectNode
|
2020-06-08 15:13:01 +03:00
|
|
|
-> S.SelectWithG S.Select
|
|
|
|
connectionToSelectWith baseSelectAlias arrayConnectionSource arraySelectNode =
|
|
|
|
let extractionSelect = S.mkSelect
|
|
|
|
{ S.selExtr = topExtractors
|
2020-10-27 13:34:31 +03:00
|
|
|
, S.selFrom = Just $ S.FromExp [S.FIIdentifier finalSelectIdentifier]
|
2020-06-08 15:13:01 +03:00
|
|
|
}
|
|
|
|
in S.SelectWith fromBaseSelections extractionSelect
|
|
|
|
where
|
|
|
|
ArrayConnectionSource _ columnMapping maybeSplit maybeSlice selectSource =
|
|
|
|
arrayConnectionSource
|
|
|
|
ArraySelectNode topExtractors selectNode = arraySelectNode
|
2020-10-27 13:34:31 +03:00
|
|
|
baseSelectIdentifier = Identifier "__base_select"
|
|
|
|
splitSelectIdentifier = Identifier "__split_select"
|
|
|
|
sliceSelectIdentifier = Identifier "__slice_select"
|
|
|
|
finalSelectIdentifier = Identifier "__final_select"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
rowNumberIdentifier = Identifier "__row_number"
|
2020-06-08 15:13:01 +03:00
|
|
|
rowNumberExp = S.SEUnsafe "(row_number() over (partition by 1))"
|
2020-10-27 13:34:31 +03:00
|
|
|
startRowNumberIdentifier = Identifier "__start_row_number"
|
|
|
|
endRowNumberIdentifier = Identifier "__end_row_number"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
startCursorExp = mkFirstElementExp $ S.SEIdentifier cursorIdentifier
|
|
|
|
endCursorExp = mkLastElementExp $ S.SEIdentifier cursorIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
startRowNumberExp = mkFirstElementExp $ S.SEIdentifier rowNumberIdentifier
|
|
|
|
endRowNumberExp = mkLastElementExp $ S.SEIdentifier rowNumberIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fromBaseSelections =
|
|
|
|
let joinCond = mkJoinCond baseSelectAlias columnMapping
|
|
|
|
baseSelectFrom = S.mkSelFromItem
|
|
|
|
(generateSQLSelect joinCond selectSource selectNode)
|
|
|
|
$ S.Alias $ _ssPrefix selectSource
|
|
|
|
select =
|
|
|
|
S.mkSelect { S.selExtr = [ S.selectStar
|
2020-10-27 13:34:31 +03:00
|
|
|
, S.Extractor rowNumberExp $ Just $ S.Alias rowNumberIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
]
|
|
|
|
, S.selFrom = Just $ S.FromExp [baseSelectFrom]
|
|
|
|
}
|
2020-10-27 13:34:31 +03:00
|
|
|
in (S.Alias baseSelectIdentifier, select):fromSplitSelection
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkStarSelect fromIdentifier =
|
2020-06-08 15:13:01 +03:00
|
|
|
S.mkSelect { S.selExtr = [S.selectStar]
|
2020-10-27 13:34:31 +03:00
|
|
|
, S.selFrom = Just $ S.FromExp [S.FIIdentifier fromIdentifier]
|
2020-06-08 15:13:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
fromSplitSelection = case maybeSplit of
|
2020-10-27 13:34:31 +03:00
|
|
|
Nothing -> fromSliceSelection baseSelectIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
Just splitBool ->
|
|
|
|
let select =
|
2020-10-27 13:34:31 +03:00
|
|
|
(mkStarSelect baseSelectIdentifier){S.selWhere = Just $ S.WhereFrag splitBool}
|
|
|
|
in (S.Alias splitSelectIdentifier, select):fromSliceSelection splitSelectIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fromSliceSelection prevSelect = case maybeSlice of
|
|
|
|
Nothing -> fromFinalSelect prevSelect
|
|
|
|
Just slice ->
|
|
|
|
let select = case slice of
|
|
|
|
SliceFirst limit ->
|
|
|
|
(mkStarSelect prevSelect)
|
|
|
|
{S.selLimit = (Just . S.LimitExp . S.intToSQLExp) limit}
|
|
|
|
SliceLast limit ->
|
|
|
|
let mkRowNumberOrderBy obType =
|
|
|
|
let orderByItem =
|
2020-10-27 13:34:31 +03:00
|
|
|
S.OrderByItem (S.SEIdentifier rowNumberIdentifier) (Just obType) Nothing
|
2020-06-08 15:13:01 +03:00
|
|
|
in S.OrderByExp $ orderByItem NE.:| []
|
|
|
|
|
|
|
|
sliceLastSelect = (mkStarSelect prevSelect)
|
|
|
|
{ S.selLimit = (Just . S.LimitExp . S.intToSQLExp) limit
|
|
|
|
, S.selOrderBy = Just $ mkRowNumberOrderBy S.OTDesc
|
|
|
|
}
|
|
|
|
sliceLastSelectFrom =
|
2020-10-27 13:34:31 +03:00
|
|
|
S.mkSelFromItem sliceLastSelect $ S.Alias sliceSelectIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
in S.mkSelect { S.selExtr = [S.selectStar]
|
|
|
|
, S.selFrom = Just $ S.FromExp [sliceLastSelectFrom]
|
|
|
|
, S.selOrderBy = Just $ mkRowNumberOrderBy S.OTAsc
|
|
|
|
}
|
2020-10-27 13:34:31 +03:00
|
|
|
in (S.Alias sliceSelectIdentifier, select):fromFinalSelect sliceSelectIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fromFinalSelect prevSelect =
|
|
|
|
let select = mkStarSelect prevSelect
|
2020-10-27 13:34:31 +03:00
|
|
|
in (S.Alias finalSelectIdentifier, select):fromCursorSelection
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fromCursorSelection =
|
2020-10-27 13:34:31 +03:00
|
|
|
let extrs = [ S.Extractor startCursorExp $ Just $ S.Alias startCursorIdentifier
|
|
|
|
, S.Extractor endCursorExp $ Just $ S.Alias endCursorIdentifier
|
|
|
|
, S.Extractor startRowNumberExp $ Just $ S.Alias startRowNumberIdentifier
|
|
|
|
, S.Extractor endRowNumberExp $ Just $ S.Alias endRowNumberIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
]
|
|
|
|
select =
|
|
|
|
S.mkSelect { S.selExtr = extrs
|
2020-10-27 13:34:31 +03:00
|
|
|
, S.selFrom = Just $ S.FromExp [S.FIIdentifier finalSelectIdentifier]
|
2020-06-08 15:13:01 +03:00
|
|
|
}
|
2020-10-27 13:34:31 +03:00
|
|
|
in (S.Alias cursorsSelectAliasIdentifier, select):fromPageInfoSelection
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fromPageInfoSelection =
|
|
|
|
let hasPrevPage = S.SEBool $
|
2020-10-27 13:34:31 +03:00
|
|
|
S.mkExists (S.FIIdentifier baseSelectIdentifier) $
|
|
|
|
S.BECompare S.SLT (S.SEIdentifier rowNumberIdentifier) $
|
|
|
|
S.SESelect $ S.mkSelect { S.selFrom = Just $ S.FromExp [S.FIIdentifier cursorsSelectAliasIdentifier]
|
|
|
|
, S.selExtr = [S.Extractor (S.SEIdentifier startRowNumberIdentifier) Nothing]
|
2020-06-08 15:13:01 +03:00
|
|
|
}
|
|
|
|
hasNextPage = S.SEBool $
|
2020-10-27 13:34:31 +03:00
|
|
|
S.mkExists (S.FIIdentifier baseSelectIdentifier) $
|
|
|
|
S.BECompare S.SGT (S.SEIdentifier rowNumberIdentifier) $
|
|
|
|
S.SESelect $ S.mkSelect { S.selFrom = Just $ S.FromExp [S.FIIdentifier cursorsSelectAliasIdentifier]
|
|
|
|
, S.selExtr = [S.Extractor (S.SEIdentifier endRowNumberIdentifier) Nothing]
|
2020-06-08 15:13:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
select =
|
2020-10-27 13:34:31 +03:00
|
|
|
S.mkSelect { S.selExtr = [ S.Extractor hasPrevPage $ Just $ S.Alias hasPreviousPageIdentifier
|
|
|
|
, S.Extractor hasNextPage $ Just $ S.Alias hasNextPageIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
]
|
|
|
|
}
|
2020-10-27 13:34:31 +03:00
|
|
|
in pure (S.Alias pageInfoSelectAliasIdentifier, select)
|