graphql-engine/server/src-lib/Hasura/RQL/DDL/Action.hs
David Overton 1eb7fe5999 Nested action joins
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3613
GitOrigin-RevId: 95fdb317a1052bdc440865f2dc8c5897e8531539
2022-03-03 03:44:20 +00:00

324 lines
11 KiB
Haskell

module Hasura.RQL.DDL.Action
( CreateAction (..),
runCreateAction,
resolveAction,
UpdateAction,
runUpdateAction,
DropAction,
runDropAction,
dropActionInMetadata,
CreateActionPermission (..),
runCreateActionPermission,
DropActionPermission,
runDropActionPermission,
dropActionPermissionInMetadata,
)
where
import Control.Lens ((.~), (^.))
import Data.Aeson qualified as J
import Data.Aeson.TH qualified as J
import Data.Dependent.Map qualified as DMap
import Data.Environment qualified as Env
import Data.HashMap.Strict qualified as Map
import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.List.NonEmpty qualified as NEList
import Data.Text.Extended
import Hasura.Base.Error
import Hasura.EncJSON
import Hasura.Metadata.Class
import Hasura.Prelude
import Hasura.RQL.DDL.CustomTypes (lookupPGScalar)
import Hasura.RQL.Types
import Hasura.SQL.Tag
import Hasura.Session
import Language.GraphQL.Draft.Syntax qualified as G
getActionInfo ::
(QErrM m, CacheRM m) =>
ActionName ->
m ActionInfo
getActionInfo actionName = do
actionMap <- scActions <$> askSchemaCache
onNothing (Map.lookup actionName actionMap) $
throw400 NotExists $ "action with name " <> actionName <<> " does not exist"
data CreateAction = CreateAction
{ _caName :: !ActionName,
_caDefinition :: !ActionDefinitionInput,
_caComment :: !(Maybe Text)
}
$(J.deriveJSON hasuraJSON ''CreateAction)
runCreateAction ::
(QErrM m, CacheRWM m, MetadataM 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"
let metadata =
ActionMetadata
actionName
(_caComment createAction)
(_caDefinition createAction)
[]
buildSchemaCacheFor (MOAction actionName) $
MetadataModifier $
metaActions %~ OMap.insert actionName metadata
pure successMsg
where
actionName = _caName createAction
{- 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.
-}
resolveAction ::
QErrM m =>
Env.Environment ->
AnnotatedCustomTypes ->
ActionDefinitionInput ->
DMap.DMap BackendTag ScalarSet -> -- See Note [Postgres scalars in custom types]
m
( ResolvedActionDefinition,
AnnotatedOutputType
)
resolveAction env AnnotatedCustomTypes {..} ActionDefinition {..} allScalars = do
resolvedArguments <- forM _adArguments $ \argumentDefinition -> do
forM argumentDefinition $ \argumentType -> do
let gType = unGraphQLType argumentType
argumentBaseType = G.getBaseType gType
(gType,)
<$> if
| Just noCTScalar <- lookupPGScalar allScalars argumentBaseType (NOCTScalar . ASTReusedScalar argumentBaseType) ->
pure noCTScalar
| Just nonObjectType <- Map.lookup argumentBaseType _actNonObjects ->
pure nonObjectType
| otherwise ->
throw400 InvalidParams $
"the type: " <> dquote argumentBaseType
<> " is not defined in custom types or it is not a scalar/enum/input_object"
-- Check if the response type is an object
let outputType = unGraphQLType _adOutputType
outputBaseType = G.getBaseType outputType
outputObject <- do
aot <-
if
| Just aoTScalar <- lookupPGScalar allScalars outputBaseType (AOTScalar . ASTReusedScalar outputBaseType) -> do
pure aoTScalar
| Just objectType <- Map.lookup outputBaseType _actObjects ->
pure $ AOTObject objectType
| otherwise ->
throw400 NotExists ("the type: " <> dquote outputBaseType <> " is not an object type defined in custom types")
-- If the Action is sync:
-- 1. Check if the output type has only top level relations (if any)
-- If the Action is async:
-- 1. Check that the output type has no relations if the output type contains nested objects
-- These checks ensure that the SQL we generate for the join does not have to extract nested fields
-- from the action webhook response.
let (nestedObjects, scalarOrEnumFields) = case aot of
AOTObject aot' ->
NEList.partition
( \ObjectFieldDefinition {..} ->
case snd _ofdType of
AOFTScalar _ -> False
AOFTEnum _ -> False
AOFTObject _ -> True
)
(_otdFields $ _aotDefinition aot')
AOTScalar _ -> ([], [])
scalarOrEnumFieldNames = fmap (\ObjectFieldDefinition {..} -> unObjectFieldName _ofdName) scalarOrEnumFields
validateSyncAction = case aot of
AOTObject aot' ->
onJust (_otdRelationships $ _aotDefinition aot') $ \relLst -> do
let relationshipsWithNonTopLevelFields =
NEList.filter
( \TypeRelationship {..} ->
let objsInRel = unObjectFieldName <$> Map.keys _trFieldMapping
in not $ all (`elem` scalarOrEnumFieldNames) objsInRel
)
relLst
unless (null relationshipsWithNonTopLevelFields) $
throw400 ConstraintError $
"Relationships cannot be defined with nested object fields : "
<> commaSeparated (dquote . _trName <$> relationshipsWithNonTopLevelFields)
AOTScalar _ -> pure ()
case _adType of
ActionQuery -> validateSyncAction
ActionMutation ActionSynchronous -> validateSyncAction
ActionMutation ActionAsynchronous ->
let maybeRel = case aot of
AOTObject aot' -> isJust (_otdRelationships $ _aotDefinition aot')
AOTScalar _ -> False
in when (maybeRel && not (null nestedObjects)) $
throw400 ConstraintError $
"Async action relations cannot be used with object fields : " <> commaSeparated (dquote . _ofdName <$> nestedObjects)
pure aot
resolvedWebhook <- resolveWebhook env _adHandler
pure
( ActionDefinition
resolvedArguments
_adOutputType
_adType
_adHeaders
_adForwardClientHeaders
_adTimeout
resolvedWebhook
_adRequestTransform
_adResponseTransform,
outputObject
)
data UpdateAction = UpdateAction
{ _uaName :: !ActionName,
_uaDefinition :: !ActionDefinitionInput,
_uaComment :: !(Maybe Text)
}
$(J.deriveFromJSON hasuraJSON ''UpdateAction)
runUpdateAction ::
forall m.
(QErrM m, CacheRWM m, MetadataM m) =>
UpdateAction ->
m EncJSON
runUpdateAction (UpdateAction actionName actionDefinition actionComment) = do
sc <- askSchemaCache
let actionsMap = scActions sc
void $
onNothing (Map.lookup actionName actionsMap) $
throw400 NotExists $ "action with name " <> actionName <<> " does not exist"
buildSchemaCacheFor (MOAction actionName) $ updateActionMetadataModifier actionDefinition actionComment
pure successMsg
where
updateActionMetadataModifier :: ActionDefinitionInput -> Maybe Text -> MetadataModifier
updateActionMetadataModifier def comment =
MetadataModifier $
(metaActions . ix actionName . amDefinition .~ def)
. (metaActions . ix actionName . amComment .~ comment)
newtype ClearActionData = ClearActionData {unClearActionData :: Bool}
deriving (Show, Eq, 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)
$(J.deriveJSON hasuraJSON ''DropAction)
runDropAction ::
( CacheRWM m,
MetadataM m,
MonadMetadataStorageQueryAPI m
) =>
DropAction ->
m EncJSON
runDropAction (DropAction actionName clearDataM) = do
void $ getActionInfo actionName
withNewInconsistentObjsCheck $
buildSchemaCache $
dropActionInMetadata actionName
when (shouldClearActionData clearData) $ deleteActionData actionName
return successMsg
where
-- When clearData is not present we assume that
-- the data needs to be retained
clearData = fromMaybe defaultClearActionData clearDataM
dropActionInMetadata :: ActionName -> MetadataModifier
dropActionInMetadata name =
MetadataModifier $ metaActions %~ OMap.delete name
newtype ActionMetadataField = ActionMetadataField {unActionMetadataField :: Text}
deriving (Show, Eq, J.FromJSON, J.ToJSON)
doesActionPermissionExist :: Metadata -> ActionName -> RoleName -> Bool
doesActionPermissionExist metadata actionName roleName =
any ((== roleName) . _apmRole) $ metadata ^. (metaActions . ix actionName . amPermissions)
data CreateActionPermission = CreateActionPermission
{ _capAction :: !ActionName,
_capRole :: !RoleName,
_capDefinition :: !(Maybe J.Value),
_capComment :: !(Maybe Text)
}
$(J.deriveFromJSON hasuraJSON ''CreateActionPermission)
runCreateActionPermission ::
(QErrM m, CacheRWM m, MetadataM m) =>
CreateActionPermission ->
m EncJSON
runCreateActionPermission createActionPermission = do
metadata <- getMetadata
when (doesActionPermissionExist metadata actionName roleName) $
throw400 AlreadyExists $
"permission for role " <> roleName
<<> " is already defined on " <>> actionName
buildSchemaCacheFor (MOActionPermission actionName roleName) $
MetadataModifier $
metaActions . ix actionName . amPermissions
%~ (:) (ActionPermissionMetadata roleName comment)
pure successMsg
where
CreateActionPermission actionName roleName _ comment = createActionPermission
data DropActionPermission = DropActionPermission
{ _dapAction :: !ActionName,
_dapRole :: !RoleName
}
deriving (Show, Eq)
$(J.deriveJSON hasuraJSON ''DropActionPermission)
runDropActionPermission ::
(QErrM m, CacheRWM m, MetadataM m) =>
DropActionPermission ->
m EncJSON
runDropActionPermission dropActionPermission = do
metadata <- getMetadata
unless (doesActionPermissionExist metadata actionName roleName) $
throw400 NotExists $
"permission for role: " <> roleName <<> " is not defined on " <>> actionName
buildSchemaCacheFor (MOActionPermission actionName roleName) $
dropActionPermissionInMetadata actionName roleName
return successMsg
where
actionName = _dapAction dropActionPermission
roleName = _dapRole dropActionPermission
dropActionPermissionInMetadata :: ActionName -> RoleName -> MetadataModifier
dropActionPermissionInMetadata name role =
MetadataModifier $
metaActions . ix name . amPermissions %~ filter ((/=) role . _apmRole)