2020-02-13 20:38:23 +03:00
|
|
|
{-# LANGUAGE RecordWildCards #-}
|
|
|
|
module Hasura.RQL.DDL.Action
|
|
|
|
( CreateAction
|
|
|
|
, runCreateAction
|
|
|
|
, persistCreateAction
|
|
|
|
, resolveAction
|
|
|
|
|
|
|
|
, UpdateAction
|
|
|
|
, runUpdateAction
|
|
|
|
|
|
|
|
, DropAction
|
|
|
|
, runDropAction
|
|
|
|
, deleteActionFromCatalog
|
|
|
|
|
|
|
|
, fetchActions
|
|
|
|
|
|
|
|
, CreateActionPermission
|
|
|
|
, runCreateActionPermission
|
|
|
|
, persistCreateActionPermission
|
|
|
|
|
|
|
|
, DropActionPermission
|
|
|
|
, runDropActionPermission
|
|
|
|
, deleteActionPermissionFromCatalog
|
|
|
|
) where
|
|
|
|
|
|
|
|
import Hasura.EncJSON
|
|
|
|
import Hasura.GraphQL.Context (defaultTypes)
|
|
|
|
import Hasura.GraphQL.Utils
|
|
|
|
import Hasura.Prelude
|
|
|
|
import Hasura.RQL.Types
|
2020-04-24 12:10:53 +03:00
|
|
|
import Hasura.Session
|
2020-02-13 20:38:23 +03:00
|
|
|
import Hasura.SQL.Types
|
|
|
|
|
|
|
|
import qualified Hasura.GraphQL.Validate.Types as VT
|
|
|
|
|
|
|
|
import qualified Data.Aeson as J
|
|
|
|
import qualified Data.Aeson.Casing as J
|
|
|
|
import qualified Data.Aeson.TH as J
|
|
|
|
import qualified Data.HashMap.Strict as Map
|
2020-04-15 15:03:13 +03:00
|
|
|
import qualified Data.HashSet as Set
|
2020-02-13 20:38:23 +03:00
|
|
|
import qualified Database.PG.Query as Q
|
|
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
|
|
|
|
|
|
|
import Language.Haskell.TH.Syntax (Lift)
|
|
|
|
|
|
|
|
getActionInfo
|
|
|
|
:: (QErrM m, CacheRM m)
|
|
|
|
=> ActionName -> m ActionInfo
|
|
|
|
getActionInfo actionName = do
|
|
|
|
actionMap <- scActions <$> askSchemaCache
|
|
|
|
case Map.lookup actionName actionMap of
|
|
|
|
Just actionInfo -> return actionInfo
|
|
|
|
Nothing ->
|
|
|
|
throw400 NotExists $
|
|
|
|
"action with name " <> actionName <<> " does not exist"
|
|
|
|
|
|
|
|
runCreateAction
|
|
|
|
:: (QErrM m , CacheRWM m, MonadTx m)
|
|
|
|
=> CreateAction -> m EncJSON
|
|
|
|
runCreateAction createAction = do
|
|
|
|
-- check if action with same name exists already
|
|
|
|
actionMap <- scActions <$> askSchemaCache
|
|
|
|
void $ onJust (Map.lookup actionName actionMap) $ const $
|
|
|
|
throw400 AlreadyExists $
|
|
|
|
"action with name " <> actionName <<> " already exists"
|
|
|
|
persistCreateAction createAction
|
|
|
|
buildSchemaCacheFor $ MOAction actionName
|
|
|
|
pure successMsg
|
|
|
|
where
|
|
|
|
actionName = _caName createAction
|
|
|
|
|
|
|
|
persistCreateAction :: (MonadTx m) => CreateAction -> m ()
|
|
|
|
persistCreateAction (CreateAction actionName actionDefinition comment) = do
|
|
|
|
liftTx $ Q.unitQE defaultTxErrorHandler [Q.sql|
|
|
|
|
INSERT into hdb_catalog.hdb_action
|
|
|
|
(action_name, action_defn, comment)
|
|
|
|
VALUES ($1, $2, $3)
|
|
|
|
|] (actionName, Q.AltJ actionDefinition, comment) True
|
|
|
|
|
2020-04-15 15:03:13 +03:00
|
|
|
{- Note [Postgres scalars in action input arguments]
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
It's very comfortable to be able to reference Postgres scalars in actions
|
|
|
|
input arguments. For example, see the following action mutation:
|
|
|
|
|
|
|
|
extend type mutation_root {
|
|
|
|
create_user (
|
|
|
|
name: String!
|
|
|
|
created_at: timestamptz
|
|
|
|
): User
|
|
|
|
}
|
|
|
|
|
|
|
|
The timestamptz is a Postgres scalar. We need to validate the presence of
|
|
|
|
timestamptz type in the Postgres database. So, the 'resolveAction' function
|
|
|
|
takes all Postgres scalar types as one of the inputs and returns the set of
|
|
|
|
referred scalars.
|
|
|
|
-}
|
|
|
|
|
2020-02-13 20:38:23 +03:00
|
|
|
resolveAction
|
|
|
|
:: (QErrM m, MonadIO m)
|
|
|
|
=> (NonObjectTypeMap, AnnotatedObjects)
|
2020-04-15 15:03:13 +03:00
|
|
|
-> HashSet PGScalarType -- ^ List of all Postgres scalar types.
|
2020-02-13 20:38:23 +03:00
|
|
|
-> ActionDefinitionInput
|
2020-04-15 15:03:13 +03:00
|
|
|
-> m ( ResolvedActionDefinition
|
|
|
|
, AnnotatedObjectType
|
|
|
|
, HashSet PGScalarType -- ^ see Note [Postgres scalars in action input arguments].
|
|
|
|
)
|
|
|
|
resolveAction customTypes allPGScalars actionDefinition = do
|
2020-02-13 20:38:23 +03:00
|
|
|
let responseType = unGraphQLType $ _adOutputType actionDefinition
|
|
|
|
responseBaseType = G.getBaseType responseType
|
2020-04-15 15:03:13 +03:00
|
|
|
|
|
|
|
reusedPGScalars <- execWriterT $
|
|
|
|
forM (_adArguments actionDefinition) $ \argument -> do
|
|
|
|
let argumentBaseType = G.getBaseType $ unGraphQLType $ _argType argument
|
|
|
|
maybeArgTypeInfo = getNonObjectTypeInfo argumentBaseType
|
|
|
|
maybePGScalar = find ((==) argumentBaseType . VT.mkScalarTy) allPGScalars
|
|
|
|
|
|
|
|
if | Just argTypeInfo <- maybeArgTypeInfo ->
|
|
|
|
case argTypeInfo of
|
|
|
|
VT.TIScalar _ -> pure ()
|
|
|
|
VT.TIEnum _ -> pure ()
|
|
|
|
VT.TIInpObj _ -> pure ()
|
|
|
|
_ -> throw400 InvalidParams $ "the argument's base type: "
|
|
|
|
<> showNamedTy argumentBaseType <>
|
|
|
|
" should be a scalar/enum/input_object"
|
|
|
|
-- Collect the referred Postgres scalar. See Note [Postgres scalars in action input arguments].
|
|
|
|
| Just pgScalar <- maybePGScalar -> tell $ Set.singleton pgScalar
|
|
|
|
| Nothing <- maybeArgTypeInfo ->
|
|
|
|
throw400 NotExists $ "the type: " <> showNamedTy argumentBaseType
|
|
|
|
<> " is not defined in custom types"
|
|
|
|
| otherwise -> pure ()
|
|
|
|
|
2020-02-13 20:38:23 +03:00
|
|
|
-- Check if the response type is an object
|
2020-04-15 15:03:13 +03:00
|
|
|
outputObject <- getObjectTypeInfo responseBaseType
|
2020-03-20 09:46:45 +03:00
|
|
|
resolvedDef <- traverse resolveWebhook actionDefinition
|
2020-04-15 15:03:13 +03:00
|
|
|
pure (resolvedDef, outputObject, reusedPGScalars)
|
2020-02-13 20:38:23 +03:00
|
|
|
where
|
2020-04-15 15:03:13 +03:00
|
|
|
getNonObjectTypeInfo typeName =
|
2020-02-13 20:38:23 +03:00
|
|
|
let nonObjectTypeMap = unNonObjectTypeMap $ fst $ customTypes
|
|
|
|
inputTypeInfos = nonObjectTypeMap <> mapFromL VT.getNamedTy defaultTypes
|
2020-04-15 15:03:13 +03:00
|
|
|
in Map.lookup typeName inputTypeInfos
|
2020-02-13 20:38:23 +03:00
|
|
|
|
|
|
|
getObjectTypeInfo typeName =
|
|
|
|
onNothing (Map.lookup (ObjectTypeName typeName) (snd customTypes)) $
|
|
|
|
throw400 NotExists $ "the type: "
|
|
|
|
<> showNamedTy typeName <>
|
|
|
|
" is not an object type defined in custom types"
|
|
|
|
|
|
|
|
runUpdateAction
|
|
|
|
:: forall m. ( QErrM m , CacheRWM m, MonadTx m)
|
|
|
|
=> UpdateAction -> m EncJSON
|
|
|
|
runUpdateAction (UpdateAction actionName actionDefinition) = do
|
|
|
|
sc <- askSchemaCache
|
|
|
|
let actionsMap = scActions sc
|
|
|
|
void $ onNothing (Map.lookup actionName actionsMap) $
|
|
|
|
throw400 NotExists $ "action with name " <> actionName <<> " not exists"
|
|
|
|
updateActionInCatalog
|
|
|
|
buildSchemaCacheFor $ MOAction actionName
|
|
|
|
pure successMsg
|
|
|
|
where
|
|
|
|
updateActionInCatalog :: m ()
|
|
|
|
updateActionInCatalog =
|
|
|
|
liftTx $ Q.unitQE defaultTxErrorHandler [Q.sql|
|
|
|
|
UPDATE hdb_catalog.hdb_action
|
|
|
|
SET action_defn = $2
|
|
|
|
WHERE action_name = $1
|
|
|
|
|] (actionName, Q.AltJ actionDefinition) True
|
|
|
|
|
|
|
|
newtype ClearActionData
|
|
|
|
= ClearActionData { unClearActionData :: Bool }
|
|
|
|
deriving (Show, Eq, Lift, J.FromJSON, J.ToJSON)
|
|
|
|
|
|
|
|
shouldClearActionData :: ClearActionData -> Bool
|
|
|
|
shouldClearActionData = unClearActionData
|
|
|
|
|
|
|
|
defaultClearActionData :: ClearActionData
|
|
|
|
defaultClearActionData = ClearActionData True
|
|
|
|
|
|
|
|
data DropAction
|
|
|
|
= DropAction
|
|
|
|
{ _daName :: !ActionName
|
|
|
|
, _daClearData :: !(Maybe ClearActionData)
|
|
|
|
} deriving (Show, Eq, Lift)
|
|
|
|
$(J.deriveJSON (J.aesonDrop 3 J.snakeCase) ''DropAction)
|
|
|
|
|
|
|
|
runDropAction
|
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m)
|
|
|
|
=> DropAction -> m EncJSON
|
|
|
|
runDropAction (DropAction actionName clearDataM)= do
|
|
|
|
void $ getActionInfo actionName
|
|
|
|
liftTx $ do
|
|
|
|
deleteActionPermissionsFromCatalog
|
|
|
|
deleteActionFromCatalog actionName clearDataM
|
|
|
|
buildSchemaCacheStrict
|
|
|
|
return successMsg
|
|
|
|
where
|
|
|
|
deleteActionPermissionsFromCatalog =
|
|
|
|
Q.unitQE defaultTxErrorHandler [Q.sql|
|
|
|
|
DELETE FROM hdb_catalog.hdb_action_permission
|
|
|
|
WHERE action_name = $1
|
|
|
|
|] (Identity actionName) True
|
|
|
|
|
|
|
|
deleteActionFromCatalog
|
|
|
|
:: ActionName
|
|
|
|
-> Maybe ClearActionData
|
|
|
|
-> Q.TxE QErr ()
|
|
|
|
deleteActionFromCatalog actionName clearDataM = do
|
|
|
|
Q.unitQE defaultTxErrorHandler [Q.sql|
|
|
|
|
DELETE FROM hdb_catalog.hdb_action
|
|
|
|
WHERE action_name = $1
|
|
|
|
|] (Identity actionName) True
|
|
|
|
when (shouldClearActionData clearData) $
|
|
|
|
clearActionDataFromCatalog actionName
|
|
|
|
where
|
|
|
|
-- When clearData is not present we assume that
|
|
|
|
-- the data needs to be retained
|
|
|
|
clearData = fromMaybe defaultClearActionData clearDataM
|
|
|
|
|
|
|
|
clearActionDataFromCatalog :: ActionName -> Q.TxE QErr ()
|
|
|
|
clearActionDataFromCatalog actionName =
|
|
|
|
Q.unitQE defaultTxErrorHandler [Q.sql|
|
|
|
|
DELETE FROM hdb_catalog.hdb_action_log
|
|
|
|
WHERE action_name = $1
|
|
|
|
|] (Identity actionName) True
|
|
|
|
|
|
|
|
fetchActions :: Q.TxE QErr [CreateAction]
|
|
|
|
fetchActions =
|
|
|
|
map fromRow <$> Q.listQE defaultTxErrorHandler
|
|
|
|
[Q.sql|
|
|
|
|
SELECT action_name, action_defn, comment
|
|
|
|
FROM hdb_catalog.hdb_action
|
|
|
|
ORDER BY action_name ASC
|
|
|
|
|] () True
|
|
|
|
where
|
|
|
|
fromRow (actionName, Q.AltJ definition, comment) =
|
|
|
|
CreateAction actionName definition comment
|
|
|
|
|
|
|
|
newtype ActionMetadataField
|
|
|
|
= ActionMetadataField { unActionMetadataField :: Text }
|
|
|
|
deriving (Show, Eq, J.FromJSON, J.ToJSON)
|
|
|
|
|
|
|
|
runCreateActionPermission
|
|
|
|
:: (QErrM m , CacheRWM m, MonadTx m)
|
|
|
|
=> CreateActionPermission -> m EncJSON
|
|
|
|
runCreateActionPermission createActionPermission = do
|
|
|
|
actionInfo <- getActionInfo actionName
|
2020-04-24 12:10:53 +03:00
|
|
|
void $ onJust (Map.lookup roleName $ _aiPermissions actionInfo) $ const $
|
|
|
|
throw400 AlreadyExists $ "permission for role " <> roleName
|
2020-04-22 08:59:14 +03:00
|
|
|
<<> " is already defined on " <>> actionName
|
2020-02-13 20:38:23 +03:00
|
|
|
persistCreateActionPermission createActionPermission
|
2020-04-24 12:10:53 +03:00
|
|
|
buildSchemaCacheFor $ MOActionPermission actionName roleName
|
2020-02-13 20:38:23 +03:00
|
|
|
pure successMsg
|
|
|
|
where
|
|
|
|
actionName = _capAction createActionPermission
|
2020-04-24 12:10:53 +03:00
|
|
|
roleName = _capRole createActionPermission
|
2020-02-13 20:38:23 +03:00
|
|
|
|
|
|
|
persistCreateActionPermission :: (MonadTx m) => CreateActionPermission -> m ()
|
|
|
|
persistCreateActionPermission CreateActionPermission{..}= do
|
|
|
|
liftTx $ Q.unitQE defaultTxErrorHandler [Q.sql|
|
|
|
|
INSERT into hdb_catalog.hdb_action_permission
|
|
|
|
(action_name, role_name, comment)
|
|
|
|
VALUES ($1, $2, $3)
|
|
|
|
|] (_capAction, _capRole, _capComment) True
|
|
|
|
|
|
|
|
data DropActionPermission
|
|
|
|
= DropActionPermission
|
|
|
|
{ _dapAction :: !ActionName
|
|
|
|
, _dapRole :: !RoleName
|
|
|
|
} deriving (Show, Eq, Lift)
|
|
|
|
$(J.deriveJSON (J.aesonDrop 4 J.snakeCase) ''DropActionPermission)
|
|
|
|
|
|
|
|
runDropActionPermission
|
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m)
|
|
|
|
=> DropActionPermission -> m EncJSON
|
|
|
|
runDropActionPermission dropActionPermission = do
|
|
|
|
actionInfo <- getActionInfo actionName
|
2020-04-24 12:10:53 +03:00
|
|
|
void $ onNothing (Map.lookup roleName $ _aiPermissions actionInfo) $
|
2020-02-13 20:38:23 +03:00
|
|
|
throw400 NotExists $
|
2020-04-24 12:10:53 +03:00
|
|
|
"permission for role: " <> roleName <<> " is not defined on " <>> actionName
|
|
|
|
liftTx $ deleteActionPermissionFromCatalog actionName roleName
|
|
|
|
buildSchemaCacheFor $ MOActionPermission actionName roleName
|
2020-02-13 20:38:23 +03:00
|
|
|
return successMsg
|
|
|
|
where
|
|
|
|
actionName = _dapAction dropActionPermission
|
2020-04-24 12:10:53 +03:00
|
|
|
roleName = _dapRole dropActionPermission
|
2020-02-13 20:38:23 +03:00
|
|
|
|
|
|
|
deleteActionPermissionFromCatalog :: ActionName -> RoleName -> Q.TxE QErr ()
|
|
|
|
deleteActionPermissionFromCatalog actionName role =
|
|
|
|
Q.unitQE defaultTxErrorHandler [Q.sql|
|
|
|
|
DELETE FROM hdb_catalog.hdb_action_permission
|
|
|
|
WHERE action_name = $1
|
|
|
|
AND role_name = $2
|
|
|
|
|] (actionName, role) True
|