1
0
mirror of https://github.com/hasura/graphql-engine.git synced 2024-12-16 09:51:59 +03:00
graphql-engine/server/src-lib/Hasura/RQL/DDL/Action.hs

263 lines
9.0 KiB
Haskell
Raw Normal View History

allow custom mutations through actions (#3042) * basic doc for actions * custom_types, sync and async actions * switch to graphql-parser-hs on github * update docs * metadata import/export * webhook calls are now supported * relationships in sync actions * initialise.sql is now in sync with the migration file * fix metadata tests * allow specifying arguments of actions * fix blacklist check on check_build_worthiness job * track custom_types and actions related tables * handlers are now triggered on async actions * default to pgjson unless a field is involved in relationships, for generating definition list * use 'true' for action filter for non admin role * fix create_action_permission sql query * drop permissions when dropping an action * add a hdb_role view (and relationships) to fetch all roles in the system * rename 'webhook' key in action definition to 'handler' * allow templating actions wehook URLs with env vars * add 'update_action' /v1/query type * allow forwarding client headers by setting `forward_client_headers` in action definition * add 'headers' configuration in action definition * handle webhook error response based on status codes * support array relationships for custom types * implement single row mutation, see https://github.com/hasura/graphql-engine/issues/3731 * single row mutation: rename 'pk_columns' -> 'columns' and no-op refactor * use top level primary key inputs for delete_by_pk & account select permissions for single row mutations * use only REST semantics to resolve the webhook response * use 'pk_columns' instead of 'columns' for update_by_pk input * add python basic tests for single row mutations * add action context (name) in webhook payload * Async action response is accessible for non admin roles only if the request session vars equals to action's * clean nulls, empty arrays for actions, custom types in export metadata * async action mutation returns only the UUID of the action * unit tests for URL template parser * Basic sync actions python tests * fix output in async query & add async tests * add admin secret header in async actions python test * document async action architecture in Resolve/Action.hs file * support actions returning array of objects * tests for list type response actions * update docs with actions and custom types metadata API reference * update actions python tests as per #f8e1330 Co-authored-by: Tirumarai Selvan <tirumarai.selvan@gmail.com> Co-authored-by: Aravind Shankar <face11301@gmail.com> Co-authored-by: Rakesh Emmadi <12475069+rakeshkky@users.noreply.github.com>
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
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
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
getObjectTypeInfo responseBaseType
traverse resolveWebhook actionDefinition
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