graphql-engine/server/src-lib/Hasura/RQL/DDL/Action.hs
Rakesh Emmadi f80b69e931
few actions' fixes and improvements (fix #3977, #4061 & close #4021) (#4109)
* add 'ID' to default scalars for custom types, fix #4061

* preserve cookie headers from sync action webhook, close #4021

* validate action webhook response to conform to output type, fix #3977

* fix tests, don't run actions' tests on PG version < 10

* update CHANGELOG.md

* no-op refactor, use types from http-network more

Co-authored-by: Vamshi Surabhi <0x777@users.noreply.github.com>
2020-03-20 12:16:45 +05:30

265 lines
9.2 KiB
Haskell

{-# 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
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
import qualified Data.Text as T
import qualified Database.PG.Query as Q
import qualified Language.GraphQL.Draft.Syntax as G
import Data.URL.Template (renderURLTemplate)
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
resolveAction
:: (QErrM m, MonadIO m)
=> (NonObjectTypeMap, AnnotatedObjects)
-> ActionDefinitionInput
-> m (ResolvedActionDefinition, ActionOutputFields)
resolveAction customTypes actionDefinition = do
let responseType = unGraphQLType $ _adOutputType actionDefinition
responseBaseType = G.getBaseType responseType
forM (_adArguments actionDefinition) $ \argument -> do
let argumentBaseType = G.getBaseType $ unGraphQLType $ _argType argument
argTypeInfo <- getNonObjectTypeInfo argumentBaseType
case argTypeInfo of
VT.TIScalar _ -> return ()
VT.TIEnum _ -> return ()
VT.TIInpObj _ -> return ()
_ -> throw400 InvalidParams $ "the argument's base type: "
<> showNamedTy argumentBaseType <>
" should be a scalar/enum/input_object"
-- Check if the response type is an object
annFields <- _aotAnnotatedFields <$> getObjectTypeInfo responseBaseType
let outputFields = Map.fromList $ map (unObjectFieldName *** fst) $ Map.toList annFields
resolvedDef <- traverse resolveWebhook actionDefinition
pure (resolvedDef, outputFields)
where
getNonObjectTypeInfo typeName = do
let nonObjectTypeMap = unNonObjectTypeMap $ fst $ customTypes
inputTypeInfos = nonObjectTypeMap <> mapFromL VT.getNamedTy defaultTypes
onNothing (Map.lookup typeName inputTypeInfos) $
throw400 NotExists $ "the type: " <> showNamedTy typeName <>
" is not defined in custom types"
resolveWebhook (InputWebhook urlTemplate) = do
eitherRenderedTemplate <- renderURLTemplate urlTemplate
either (throw400 Unexpected . T.pack) (pure . ResolvedWebhook) eitherRenderedTemplate
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
void $ onJust (Map.lookup role $ _aiPermissions actionInfo) $ const $
throw400 AlreadyExists $ "permission for role: " <> role
<<> "is already defined on " <>> actionName
persistCreateActionPermission createActionPermission
buildSchemaCacheFor $ MOActionPermission actionName role
pure successMsg
where
actionName = _capAction createActionPermission
role = _capRole createActionPermission
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
void $ onNothing (Map.lookup role $ _aiPermissions actionInfo) $
throw400 NotExists $
"permission for role: " <> role <<> " is not defined on " <>> actionName
liftTx $ deleteActionPermissionFromCatalog actionName role
return successMsg
where
actionName = _dapAction dropActionPermission
role = _dapRole dropActionPermission
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