Files
Karthikeyan Chinnakonda 5116e16e18 server(actions): add support for queries (close #4032) (#4309)
* add support for action queries

* a new parameter `type` is added in the ArgumentDefinition, its value
  can be either `query` or `mutation` and it defaults to the latter

* throw 400 when a query action is tried to explain

* update the actions docs to include query actions

* refactor the ToJSON and ToOrdJSON of ActionDefinition

Co-authored-by: Rishichandra Wawhal <rishi@hasura.io>
Co-authored-by: Tirumarai Selvan <tiru@hasura.io>
2020-04-16 12:55:19 +05:30

381 lines
13 KiB
Haskell

module Hasura.GraphQL.Schema.Action
( mkActionsSchema
) where
import qualified Data.HashMap.Strict as Map
import qualified Data.HashSet as Set
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)
]
mkQueryActionField
:: ActionName
-> ActionInfo
-> [(PGCol, PGScalarType)]
-> (ActionExecutionContext, ObjFldInfo)
mkQueryActionField actionName actionInfo definitionList =
( actionExecutionContext
, fieldInfo
)
where
definition = _aiDefinition actionInfo
actionExecutionContext =
ActionExecutionContext
actionName
(_adOutputType definition)
(getActionOutputFields $ _aiOutputObject actionInfo)
definitionList
(_adHandler definition)
(_adHeaders definition)
(_adForwardClientHeaders definition)
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 = unGraphQLType $ _adOutputType definition
mkMutationActionField
:: ActionName
-> ActionInfo
-> [(PGCol, PGScalarType)]
-> ActionMutationKind
-> (ActionMutationExecutionContext, ObjFldInfo)
mkMutationActionField actionName actionInfo definitionList kind =
( actionExecutionContext
, fieldInfo
)
where
definition = _aiDefinition actionInfo
actionExecutionContext =
case kind of
ActionSynchronous ->
ActionMutationSyncWebhook $ ActionExecutionContext actionName
(_adOutputType definition)
(getActionOutputFields $ _aiOutputObject actionInfo)
definitionList
(_adHandler definition)
(_adHeaders definition)
(_adForwardClientHeaders definition)
ActionAsynchronous -> ActionMutationAsync
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 kind of
ActionSynchronous -> unGraphQLType $ _adOutputType definition
ActionAsynchronous -> G.toGT $ G.toNT $ mkScalarTy PGUUID
mkQueryField
:: ActionName
-> Maybe Text
-> ResolvedActionDefinition
-> [(PGCol, PGScalarType)]
-> ActionMutationKind
-> Maybe (ActionSelectOpContext, ObjFldInfo, TypeInfo)
mkQueryField actionName comment definition definitionList kind =
case kind 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
mkPGFieldType
:: ObjectFieldName
-> (G.GType, OutputFieldTypeInfo)
-> HashMap ObjectFieldName PGColumnInfo
-> PGScalarType
mkPGFieldType fieldName (fieldType, fieldTypeInfo) fieldReferences =
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
mkDefinitionList :: AnnotatedObjectType -> HashMap ObjectFieldName PGColumnInfo -> [(PGCol, PGScalarType)]
mkDefinitionList annotatedOutputType fieldReferences =
[ (unsafePGCol $ coerce k, mkPGFieldType k v fieldReferences)
| (k, v) <- Map.toList $ _aotAnnotatedFields annotatedOutputType
]
mkFieldMap
:: AnnotatedObjectType
-> ActionInfo
-> HashMap ObjectFieldName PGColumnInfo
-> RoleName
-> HashMap (G.NamedType,G.Name) ResolveField
mkFieldMap annotatedOutputType actionInfo fieldReferences roleName =
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) fieldReferences)
(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
mkFieldReferences :: AnnotatedObjectType -> HashMap ObjectFieldName PGColumnInfo
mkFieldReferences annotatedOutputType=
Map.unions $ map _trFieldMapping $ Map.elems $
_aotRelationships annotatedOutputType
mkMutationActionFieldsAndTypes
:: ActionInfo
-> ActionPermissionInfo
-> ActionMutationKind
-> ( Maybe (ActionSelectOpContext, ObjFldInfo, TypeInfo)
-- context, field, response type info
, (ActionMutationExecutionContext, ObjFldInfo) -- mutation field
, FieldMap
)
mkMutationActionFieldsAndTypes actionInfo permission kind =
( mkQueryField actionName comment definition definitionList kind
, mkMutationActionField actionName actionInfo definitionList kind
, fieldMap
)
where
actionName = _aiName actionInfo
annotatedOutputType = _aiOutputObject actionInfo
definition = _aiDefinition actionInfo
roleName = _apiRole permission
comment = _aiComment actionInfo
-- all the possible field references
fieldReferences = mkFieldReferences annotatedOutputType
definitionList = mkDefinitionList annotatedOutputType fieldReferences
fieldMap = mkFieldMap annotatedOutputType actionInfo fieldReferences roleName
mkQueryActionFieldsAndTypes
:: ActionInfo
-> ActionPermissionInfo
-> ((ActionExecutionContext, ObjFldInfo)
, FieldMap
)
mkQueryActionFieldsAndTypes actionInfo permission =
( mkQueryActionField actionName actionInfo definitionList
, fieldMap
)
where
actionName = _aiName actionInfo
roleName = _apiRole permission
annotatedOutputType = _aiOutputObject actionInfo
fieldReferences = mkFieldReferences annotatedOutputType
definitionList = mkDefinitionList annotatedOutputType fieldReferences
fieldMap = mkFieldMap annotatedOutputType actionInfo fieldReferences roleName
mkMutationActionSchemaOne
:: ActionInfo
-> ActionMutationKind
-> Map.HashMap RoleName
( Maybe (ActionSelectOpContext, ObjFldInfo, TypeInfo)
, (ActionMutationExecutionContext, ObjFldInfo)
, FieldMap
)
mkMutationActionSchemaOne actionInfo kind = do
flip Map.map permissions $ \permission ->
mkMutationActionFieldsAndTypes actionInfo permission kind
where
adminPermission = ActionPermissionInfo adminRole
permissions = Map.insert adminRole adminPermission $ _aiPermissions actionInfo
mkQueryActionSchemaOne
:: ActionInfo
-> Map.HashMap RoleName
( (ActionExecutionContext, ObjFldInfo)
, FieldMap
)
mkQueryActionSchemaOne actionInfo =
flip Map.map permissions $ \permission ->
mkQueryActionFieldsAndTypes actionInfo permission
where
adminPermission = ActionPermissionInfo adminRole
permissions = Map.insert adminRole adminPermission $ _aiPermissions actionInfo
mkActionsSchema
:: ActionCache
-> Map.HashMap RoleName (RootFields, TyAgg)
mkActionsSchema =
foldl'
(\aggregate actionInfo ->
case _adType $ _aiDefinition actionInfo of
ActionQuery ->
Map.foldrWithKey (accumulateQuery (_aiPgScalars actionInfo)) aggregate $ mkQueryActionSchemaOne actionInfo
ActionMutation kind ->
Map.foldrWithKey (accumulateMutation (_aiPgScalars actionInfo)) aggregate $ mkMutationActionSchemaOne actionInfo kind
)
mempty
where
-- we'll need to add uuid and timestamptz for actions
mkNewRoleState pgScalars =
( mempty
, foldr addScalarToTyAgg mempty $
pgScalars <> Set.fromList [PGJSON, PGTimeStampTZ, PGUUID]
)
accumulateQuery pgScalars roleName (actionField, fields) =
Map.alter (Just . addToStateSync . fromMaybe (mkNewRoleState pgScalars)) roleName
where
addToStateSync (rootFields, tyAgg) =
( addQueryField (first QCAction actionField) rootFields
, addFieldsToTyAgg fields tyAgg
)
accumulateMutation pgScalars roleName (queryFieldM, actionField, fields) =
Map.alter (Just . addToState . fromMaybe (mkNewRoleState pgScalars)) roleName
where
addToState = case queryFieldM of
Just (fldCtx, fldDefinition, responseTypeInfo) ->
addToStateAsync (fldCtx, fldDefinition) responseTypeInfo
Nothing -> addToStateSync
addToStateSync (rootFields, tyAgg) =
( addMutationField (first MCAction actionField) rootFields
, addFieldsToTyAgg fields tyAgg
)
addToStateAsync queryField responseTypeInfo (rootFields, tyAgg) =
( addMutationField (first MCAction actionField) $
addQueryField
(first QCAsyncActionFetch queryField)
rootFields
, addTypeInfoToTyAgg responseTypeInfo $
addFieldsToTyAgg fields tyAgg
)