mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 04:51:35 +03:00
b84db36ebb
* 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>
279 lines
10 KiB
Haskell
279 lines
10 KiB
Haskell
module Hasura.GraphQL.Schema.Action
|
|
( mkActionsSchema
|
|
) where
|
|
|
|
import qualified Data.HashMap.Strict as Map
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
|
|
|
import Data.Coerce (coerce)
|
|
|
|
import Hasura.GraphQL.Schema.Builder
|
|
import Hasura.GraphQL.Schema.Common (mkDescriptionWith)
|
|
|
|
import Hasura.GraphQL.Resolve.Types
|
|
import Hasura.GraphQL.Validate.Types
|
|
import Hasura.Prelude
|
|
import Hasura.RQL.Types
|
|
import Hasura.SQL.Types
|
|
|
|
mkAsyncActionSelectionType :: ActionName -> G.NamedType
|
|
mkAsyncActionSelectionType = G.NamedType . unActionName
|
|
|
|
mkAsyncActionQueryResponseObj
|
|
:: ActionName
|
|
-- Name of the action
|
|
-> GraphQLType
|
|
-- output type
|
|
-> ObjTyInfo
|
|
mkAsyncActionQueryResponseObj actionName outputType =
|
|
mkHsraObjTyInfo
|
|
(Just description)
|
|
(mkAsyncActionSelectionType actionName) -- "(action_name)"
|
|
mempty -- no arguments
|
|
(mapFromL _fiName fieldDefinitions)
|
|
where
|
|
description = G.Description $ "fields of action: " <>> actionName
|
|
|
|
mkFieldDefinition (fieldName, fieldDescription, fieldType) =
|
|
mkHsraObjFldInfo
|
|
(Just fieldDescription)
|
|
fieldName
|
|
mempty
|
|
fieldType
|
|
|
|
fieldDefinitions = map mkFieldDefinition
|
|
[ ( "id", "the unique id of an action"
|
|
, G.toGT $ mkScalarTy PGUUID)
|
|
, ( "created_at", "the time at which this action was created"
|
|
, G.toGT $ mkScalarTy PGTimeStampTZ)
|
|
, ( "errors", "errors related to the invocation"
|
|
, G.toGT $ mkScalarTy PGJSON)
|
|
, ( "output", "the output fields of this action"
|
|
, unGraphQLType outputType)
|
|
]
|
|
|
|
mkMutationField
|
|
:: ActionName
|
|
-> ActionInfo
|
|
-> [(PGCol, PGScalarType)]
|
|
-> (ActionExecutionContext, ObjFldInfo)
|
|
mkMutationField actionName actionInfo definitionList =
|
|
( actionExecutionContext
|
|
, fieldInfo
|
|
)
|
|
where
|
|
definition = _aiDefinition actionInfo
|
|
actionExecutionContext =
|
|
case _adKind definition of
|
|
ActionSynchronous ->
|
|
ActionExecutionSyncWebhook $ SyncActionExecutionContext actionName
|
|
(_adOutputType definition)
|
|
definitionList
|
|
(_adHandler definition)
|
|
(_adHeaders definition)
|
|
(_adForwardClientHeaders definition)
|
|
ActionAsynchronous -> ActionExecutionAsync
|
|
|
|
description = mkDescriptionWith (PGDescription <$> (_aiComment actionInfo)) $
|
|
"perform the action: " <>> actionName
|
|
|
|
fieldInfo =
|
|
mkHsraObjFldInfo
|
|
(Just description)
|
|
(unActionName actionName)
|
|
(mapFromL _iviName $ map mkActionArgument $ _adArguments definition)
|
|
actionFieldResponseType
|
|
|
|
mkActionArgument argument =
|
|
InpValInfo (_argDescription argument) (unArgumentName $ _argName argument)
|
|
Nothing $ unGraphQLType $ _argType argument
|
|
|
|
actionFieldResponseType =
|
|
case _adKind definition of
|
|
ActionSynchronous -> unGraphQLType $ _adOutputType definition
|
|
ActionAsynchronous -> G.toGT $ G.toNT $ mkScalarTy PGUUID
|
|
|
|
mkQueryField
|
|
:: ActionName
|
|
-> Maybe Text
|
|
-> ResolvedActionDefinition
|
|
-> [(PGCol, PGScalarType)]
|
|
-> Maybe (ActionSelectOpContext, ObjFldInfo, TypeInfo)
|
|
mkQueryField actionName comment definition definitionList =
|
|
case _adKind definition of
|
|
ActionAsynchronous ->
|
|
Just ( ActionSelectOpContext (_adOutputType definition) definitionList
|
|
|
|
, mkHsraObjFldInfo (Just description) (unActionName actionName)
|
|
(mapFromL _iviName [idArgument])
|
|
(G.toGT $ G.toGT $ mkAsyncActionSelectionType actionName)
|
|
|
|
, TIObj $ mkAsyncActionQueryResponseObj actionName $
|
|
_adOutputType definition
|
|
)
|
|
ActionSynchronous -> Nothing
|
|
where
|
|
description = mkDescriptionWith (PGDescription <$> comment) $
|
|
"retrieve the result of action: " <>> actionName
|
|
|
|
idArgument =
|
|
InpValInfo (Just idDescription) "id" Nothing $ G.toNT $ mkScalarTy PGUUID
|
|
where
|
|
idDescription = G.Description $ "id of the action: " <>> actionName
|
|
|
|
mkActionFieldsAndTypes
|
|
:: (QErrM m)
|
|
=> ActionInfo
|
|
-> AnnotatedObjectType
|
|
-> ActionPermissionInfo
|
|
-> m ( Maybe (ActionSelectOpContext, ObjFldInfo, TypeInfo)
|
|
-- context, field, response type info
|
|
, (ActionExecutionContext, ObjFldInfo) -- mutation field
|
|
, FieldMap
|
|
)
|
|
mkActionFieldsAndTypes actionInfo annotatedOutputType permission =
|
|
return ( mkQueryField actionName comment definition definitionList
|
|
, mkMutationField actionName actionInfo definitionList
|
|
, fieldMap
|
|
)
|
|
where
|
|
actionName = _aiName actionInfo
|
|
definition = _aiDefinition actionInfo
|
|
roleName = _apiRole permission
|
|
comment = _aiComment actionInfo
|
|
|
|
-- all the possible field references
|
|
fieldReferences =
|
|
Map.unions $ map _trFieldMapping $ Map.elems $
|
|
_aotRelationships annotatedOutputType
|
|
|
|
mkPGFieldType fieldName (fieldType, fieldTypeInfo) =
|
|
case (G.isListType fieldType, fieldTypeInfo) of
|
|
-- for scalar lists, we treat them as json columns
|
|
(True, _) -> PGJSON
|
|
-- enums the same
|
|
(False, OutputFieldEnum _) -> PGJSON
|
|
-- default to PGJSON unless you have to join with a postgres table
|
|
-- i.e, if this field is specified as part of some relationship's
|
|
-- mapping, we can cast this column's value as the remote column's type
|
|
(False, OutputFieldScalar _) ->
|
|
case Map.lookup fieldName fieldReferences of
|
|
Just columnInfo -> unsafePGColumnToRepresentation $ pgiType columnInfo
|
|
Nothing -> PGJSON
|
|
|
|
definitionList =
|
|
[ (unsafePGCol $ coerce k, mkPGFieldType k v)
|
|
| (k, v) <- Map.toList $ _aotAnnotatedFields annotatedOutputType
|
|
]
|
|
-- mkFieldMap annotatedOutputType =
|
|
fieldMap =
|
|
Map.fromList $ fields <> catMaybes relationships
|
|
where
|
|
fields =
|
|
flip map (Map.toList $ _aotAnnotatedFields annotatedOutputType) $
|
|
\(fieldName, (fieldType, fieldTypeInfo)) ->
|
|
( (actionOutputBaseType, unObjectFieldName fieldName)
|
|
, RFPGColumn $ PGColumnInfo
|
|
(unsafePGCol $ coerce fieldName)
|
|
(coerce fieldName)
|
|
0
|
|
(PGColumnScalar $ mkPGFieldType fieldName (fieldType, fieldTypeInfo))
|
|
(G.isNullable fieldType)
|
|
Nothing
|
|
)
|
|
relationships =
|
|
flip map (Map.toList $ _aotRelationships annotatedOutputType) $
|
|
\(relationshipName, relationship) ->
|
|
let remoteTableInfo = _trRemoteTable relationship
|
|
remoteTable = _tciName $ _tiCoreInfo remoteTableInfo
|
|
filterAndLimitM = getFilterAndLimit remoteTableInfo
|
|
columnMapping = Map.fromList $
|
|
[ (unsafePGCol $ coerce k, pgiColumn v)
|
|
| (k, v) <- Map.toList $ _trFieldMapping relationship
|
|
]
|
|
in case filterAndLimitM of
|
|
Just (tableFilter, tableLimit) ->
|
|
Just ( ( actionOutputBaseType
|
|
, unRelationshipName relationshipName
|
|
)
|
|
, RFRelationship $ RelationshipField
|
|
(RelInfo
|
|
-- RelationshipName, which is newtype wrapper over G.Name is always
|
|
-- non-empty text so as to conform GraphQL spec
|
|
(RelName $ mkNonEmptyTextUnsafe $ coerce relationshipName)
|
|
(_trType relationship)
|
|
columnMapping remoteTable True)
|
|
False mempty
|
|
tableFilter
|
|
tableLimit
|
|
)
|
|
Nothing -> Nothing
|
|
getFilterAndLimit remoteTableInfo =
|
|
if roleName == adminRole
|
|
then Just (annBoolExpTrue, Nothing)
|
|
else do
|
|
selectPermisisonInfo <-
|
|
getSelectPermissionInfoM remoteTableInfo roleName
|
|
return (spiFilter selectPermisisonInfo, spiLimit selectPermisisonInfo)
|
|
actionOutputBaseType =
|
|
G.getBaseType $ unGraphQLType $ _adOutputType $ _aiDefinition actionInfo
|
|
|
|
mkActionSchemaOne
|
|
:: (QErrM m)
|
|
=> AnnotatedObjects
|
|
-> ActionInfo
|
|
-> m (Map.HashMap RoleName
|
|
( Maybe (ActionSelectOpContext, ObjFldInfo, TypeInfo)
|
|
, (ActionExecutionContext, ObjFldInfo)
|
|
, FieldMap
|
|
)
|
|
)
|
|
mkActionSchemaOne annotatedObjects actionInfo = do
|
|
annotatedOutputType <- onNothing
|
|
(Map.lookup (ObjectTypeName actionOutputBaseType) annotatedObjects) $
|
|
throw500 $ "missing annotated type for: " <> showNamedTy actionOutputBaseType
|
|
forM permissions $ \permission ->
|
|
mkActionFieldsAndTypes actionInfo annotatedOutputType permission
|
|
where
|
|
adminPermission = ActionPermissionInfo adminRole
|
|
permissions = Map.insert adminRole adminPermission $ _aiPermissions actionInfo
|
|
actionOutputBaseType =
|
|
G.getBaseType $ unGraphQLType $ _adOutputType $ _aiDefinition actionInfo
|
|
|
|
mkActionsSchema
|
|
:: (QErrM m)
|
|
=> AnnotatedObjects
|
|
-> ActionCache
|
|
-> m (Map.HashMap RoleName (RootFields, TyAgg))
|
|
mkActionsSchema annotatedObjects =
|
|
foldM
|
|
(\aggregate actionInfo ->
|
|
Map.foldrWithKey f aggregate <$>
|
|
mkActionSchemaOne annotatedObjects actionInfo
|
|
)
|
|
mempty
|
|
where
|
|
-- we'll need to add uuid and timestamptz for actions
|
|
newRoleState = (mempty, addScalarToTyAgg PGJSON $
|
|
addScalarToTyAgg PGTimeStampTZ $
|
|
addScalarToTyAgg PGUUID mempty)
|
|
f roleName (queryFieldM, mutationField, fields) =
|
|
Map.alter (Just . addToState . fromMaybe newRoleState) roleName
|
|
where
|
|
addToState = case queryFieldM of
|
|
Just (fldCtx, fldDefinition, responseTypeInfo) ->
|
|
addToStateAsync (fldCtx, fldDefinition) responseTypeInfo
|
|
Nothing -> addToStateSync
|
|
addToStateSync (rootFields, tyAgg) =
|
|
( addMutationField (first MCAction mutationField) rootFields
|
|
, addFieldsToTyAgg fields tyAgg
|
|
)
|
|
addToStateAsync queryField responseTypeInfo (rootFields, tyAgg) =
|
|
( addMutationField (first MCAction mutationField) $
|
|
addQueryField
|
|
(first QCActionFetch queryField)
|
|
rootFields
|
|
, addTypeInfoToTyAgg responseTypeInfo $
|
|
addFieldsToTyAgg fields tyAgg
|
|
)
|