mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 04:51:35 +03:00
9ca0bc1e5c
> ### Description > While adding [insert mutation schema parser for MSSQL backend](https://github.com/hasura/graphql-engine-mono/pull/2141) I also included [identity](https://en.wikipedia.org/wiki/Identity_column) notion to table columns across all backends. In MSSQL we cannot insert any value (even `DEFAULT` expression) into Identity columns. This behavior of identity columns is not same in Postgres as we can insert values. This PR drops the notion of identity in the column info. The context of identity columns for MSSQL is carried in `ExtraTableMetadata` type. ### Changelog - [x] `CHANGELOG.md` is updated with user-facing content relevant to this PR. If no changelog is required, then add the `no-changelog-required` label. ### Affected components - [x] Server - [ ] Console - [ ] CLI - [ ] Docs - [ ] Community Content - [ ] Build System - [x] Tests - [ ] Other (list it) ### Related Issues -> Fix https://github.com/hasura/graphql-engine/issues/7557 https://github.com/hasura/graphql-engine-mono/pull/2378 GitOrigin-RevId: c18b5708e2e6107423a0a95a7fc2e9721e8a21a1
357 lines
17 KiB
Haskell
357 lines
17 KiB
Haskell
module Hasura.GraphQL.Schema.Action
|
|
( actionExecute
|
|
, actionAsyncMutation
|
|
, actionAsyncQuery
|
|
) where
|
|
|
|
import Hasura.Prelude
|
|
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.HashMap.Strict as Map
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
|
|
|
import Data.Has
|
|
import Data.Text.Extended
|
|
import Data.Text.NonEmpty
|
|
|
|
import qualified Hasura.GraphQL.Parser as P
|
|
import qualified Hasura.GraphQL.Parser.Internal.Parser as P
|
|
import qualified Hasura.RQL.DML.Internal as RQL
|
|
import qualified Hasura.RQL.IR.Select as RQL
|
|
|
|
import Hasura.Backends.Postgres.SQL.Types
|
|
import Hasura.Backends.Postgres.Types.Column
|
|
import Hasura.Base.Error
|
|
import Hasura.GraphQL.Parser (FieldParser, InputFieldsParser, Kind (..),
|
|
Parser, UnpreparedValue (..))
|
|
import Hasura.GraphQL.Parser.Class
|
|
import Hasura.GraphQL.Schema.Backend
|
|
import Hasura.GraphQL.Schema.Common
|
|
import Hasura.GraphQL.Schema.Select
|
|
import Hasura.RQL.Types
|
|
import Hasura.Session
|
|
|
|
|
|
-- | actionExecute is used to execute either a query action or a synchronous
|
|
-- mutation action. A query action or a synchronous mutation action accepts
|
|
-- the field name and input arguments and a selectionset. The
|
|
-- input argument and selectionset types are defined by the user.
|
|
--
|
|
--
|
|
-- > action_name(action_input_arguments) {
|
|
-- > col1: col1_type
|
|
-- > col2: col2_type
|
|
-- > }
|
|
actionExecute
|
|
:: forall r m n
|
|
. MonadBuildSchema ('Postgres 'Vanilla) r m n
|
|
=> NonObjectTypeMap
|
|
-> ActionInfo
|
|
-> m (Maybe (FieldParser n (AnnActionExecution ('Postgres 'Vanilla) (RQL.RemoteSelect UnpreparedValue) (UnpreparedValue ('Postgres 'Vanilla)))))
|
|
actionExecute nonObjectTypeMap actionInfo = runMaybeT do
|
|
roleName <- askRoleName
|
|
guard (roleName == adminRoleName || roleName `Map.member` permissions)
|
|
let fieldName = unActionName actionName
|
|
description = G.Description <$> comment
|
|
inputArguments <- lift $ actionInputArguments nonObjectTypeMap $ _adArguments definition
|
|
selectionSet <- lift $ actionOutputFields outputType outputObject
|
|
stringifyNum <- asks $ qcStringifyNum . getter
|
|
pure $ P.subselection fieldName description inputArguments selectionSet
|
|
<&> \(argsJson, fields) -> AnnActionExecution
|
|
{ _aaeName = actionName
|
|
, _aaeFields = fields
|
|
, _aaePayload = argsJson
|
|
, _aaeOutputType = _adOutputType definition
|
|
, _aaeOutputFields = getActionOutputFields outputObject
|
|
, _aaeDefinitionList = mkDefinitionList outputObject
|
|
, _aaeWebhook = _adHandler definition
|
|
, _aaeHeaders = _adHeaders definition
|
|
, _aaeForwardClientHeaders = _adForwardClientHeaders definition
|
|
, _aaeStrfyNum = stringifyNum
|
|
, _aaeTimeOut = _adTimeout definition
|
|
, _aaeSource = getActionSourceInfo outputObject
|
|
, _aaeRequestTransform = _aiRequestTransform actionInfo
|
|
}
|
|
where
|
|
ActionInfo actionName (outputType, outputObject) definition permissions _ comment _ = actionInfo
|
|
|
|
-- | actionAsyncMutation is used to execute a asynchronous mutation action. An
|
|
-- asynchronous action expects the field name and the input arguments to the
|
|
-- action. A selectionset is *not* expected. An action ID (UUID) will be
|
|
-- returned after performing the action
|
|
--
|
|
-- > action_name(action_input_arguments)
|
|
actionAsyncMutation
|
|
:: forall m n r. (MonadSchema n m, MonadTableInfo r m, MonadRole r m)
|
|
=> NonObjectTypeMap
|
|
-> ActionInfo
|
|
-> m (Maybe (FieldParser n AnnActionMutationAsync))
|
|
actionAsyncMutation nonObjectTypeMap actionInfo = runMaybeT do
|
|
roleName <- lift askRoleName
|
|
guard $ roleName == adminRoleName || roleName `Map.member` permissions
|
|
inputArguments <- lift $ actionInputArguments nonObjectTypeMap $ _adArguments definition
|
|
let fieldName = unActionName actionName
|
|
description = G.Description <$> comment
|
|
pure $ P.selection fieldName description inputArguments actionIdParser
|
|
<&> AnnActionMutationAsync actionName forwardClientHeaders
|
|
where
|
|
ActionInfo actionName _ definition permissions forwardClientHeaders comment _ = actionInfo
|
|
|
|
-- | actionAsyncQuery is used to query/subscribe to the result of an
|
|
-- asynchronous mutation action. The only input argument to an
|
|
-- asynchronous mutation action is the action ID (UUID) and a selection
|
|
-- set is expected, the selection set contains 4 fields namely 'id',
|
|
-- 'created_at','errors' and 'output'. The result of the action can be queried
|
|
-- through the 'output' field.
|
|
--
|
|
-- > action_name (id: UUID!) {
|
|
-- > id: UUID!
|
|
-- > created_at: timestampz!
|
|
-- > errors: JSON
|
|
-- > output: user_defined_type!
|
|
-- > }
|
|
actionAsyncQuery
|
|
:: forall r m n
|
|
. MonadBuildSchema ('Postgres 'Vanilla) r m n
|
|
=> ActionInfo
|
|
-> m (Maybe (FieldParser n (AnnActionAsyncQuery ('Postgres 'Vanilla) (RQL.RemoteSelect UnpreparedValue) (UnpreparedValue ('Postgres 'Vanilla)))))
|
|
actionAsyncQuery actionInfo = runMaybeT do
|
|
roleName <- askRoleName
|
|
guard $ roleName == adminRoleName || roleName `Map.member` permissions
|
|
actionOutputParser <- lift $ actionOutputFields outputType outputObject
|
|
createdAtFieldParser <-
|
|
lift $ columnParser @('Postgres 'Vanilla) (ColumnScalar PGTimeStampTZ) (G.Nullability False)
|
|
errorsFieldParser <-
|
|
lift $ columnParser @('Postgres 'Vanilla) (ColumnScalar PGJSON) (G.Nullability True)
|
|
|
|
let fieldName = unActionName actionName
|
|
description = G.Description <$> comment
|
|
actionIdInputField =
|
|
P.field idFieldName (Just idFieldDescription) actionIdParser
|
|
allFieldParsers =
|
|
let idField = P.selection_ idFieldName (Just idFieldDescription) actionIdParser $> AsyncId
|
|
createdAtField = P.selection_ $$(G.litName "created_at")
|
|
(Just "the time at which this action was created")
|
|
createdAtFieldParser $> AsyncCreatedAt
|
|
errorsField = P.selection_ $$(G.litName "errors")
|
|
(Just "errors related to the invocation")
|
|
errorsFieldParser $> AsyncErrors
|
|
outputField = P.subselection_ $$(G.litName "output")
|
|
(Just "the output fields of this action")
|
|
actionOutputParser <&> AsyncOutput
|
|
in [idField, createdAtField, errorsField, outputField]
|
|
selectionSet =
|
|
let outputTypeName = unActionName actionName
|
|
desc = G.Description $ "fields of action: " <>> actionName
|
|
in P.selectionSet outputTypeName (Just desc) allFieldParsers
|
|
<&> parsedSelectionsToFields AsyncTypename
|
|
|
|
stringifyNum <- asks $ qcStringifyNum . getter
|
|
pure $ P.subselection fieldName description actionIdInputField selectionSet
|
|
<&> \(idArg, fields) -> AnnActionAsyncQuery
|
|
{ _aaaqName = actionName
|
|
, _aaaqActionId = idArg
|
|
, _aaaqOutputType = _adOutputType definition
|
|
, _aaaqFields = fields
|
|
, _aaaqDefinitionList = mkDefinitionList outputObject
|
|
, _aaaqStringifyNum = stringifyNum
|
|
, _aaaqForwardClientHeaders = forwardClientHeaders
|
|
, _aaaqSource = getActionSourceInfo outputObject
|
|
}
|
|
where
|
|
ActionInfo actionName (outputType, outputObject) definition permissions forwardClientHeaders comment dataTransform = actionInfo
|
|
idFieldName = $$(G.litName "id")
|
|
idFieldDescription = "the unique id of an action"
|
|
|
|
-- | Async action's unique id
|
|
actionIdParser
|
|
:: MonadParse n => Parser 'Both n ActionId
|
|
actionIdParser = ActionId <$> P.uuid
|
|
|
|
actionOutputFields
|
|
:: forall r m n
|
|
. MonadBuildSchema ('Postgres 'Vanilla) r m n
|
|
=> G.GType
|
|
-> AnnotatedObjectType
|
|
-> m (Parser 'Output n (AnnotatedFields ('Postgres 'Vanilla)))
|
|
actionOutputFields outputType annotatedObject = do
|
|
let outputObject = _aotDefinition annotatedObject
|
|
scalarOrEnumFields = map outputFieldParser $ toList $ _otdFields outputObject
|
|
relationshipFields <- forM (_otdRelationships outputObject) $ traverse relationshipFieldParser
|
|
let allFieldParsers = scalarOrEnumFields <>
|
|
maybe [] (concat . catMaybes . toList) relationshipFields
|
|
outputTypeName = unObjectTypeName $ _otdName outputObject
|
|
outputTypeDescription = _otdDescription outputObject
|
|
pure $ outputParserModifier outputType $
|
|
P.selectionSet outputTypeName outputTypeDescription allFieldParsers
|
|
<&> parsedSelectionsToFields RQL.AFExpression
|
|
where
|
|
outputParserModifier :: G.GType -> Parser 'Output n a -> Parser 'Output n a
|
|
outputParserModifier = \case
|
|
G.TypeNamed (G.Nullability True) _ -> P.nullableParser
|
|
G.TypeNamed (G.Nullability False) _ -> P.nonNullableParser
|
|
G.TypeList (G.Nullability True) t -> P.nullableParser . P.multiple . outputParserModifier t
|
|
G.TypeList (G.Nullability False) t -> P.nonNullableParser . P.multiple . outputParserModifier t
|
|
|
|
outputFieldParser
|
|
:: ObjectFieldDefinition (G.GType, AnnotatedObjectFieldType)
|
|
-> FieldParser n (AnnotatedField ('Postgres 'Vanilla))
|
|
outputFieldParser (ObjectFieldDefinition name _ description (gType, objectFieldType)) =
|
|
let fieldName = unObjectFieldName name
|
|
selection = P.selection_ fieldName description $ case objectFieldType of
|
|
AOFTScalar def -> customScalarParser def
|
|
AOFTEnum def -> customEnumParser def
|
|
pgColumnInfo =
|
|
ColumnInfo (unsafePGCol $ G.unName fieldName) fieldName 0 (ColumnScalar PGJSON) (G.isNullable gType) Nothing
|
|
in P.wrapFieldParser gType selection $> RQL.mkAnnColumnField pgColumnInfo Nothing Nothing
|
|
|
|
relationshipFieldParser
|
|
:: TypeRelationship (TableInfo ('Postgres 'Vanilla)) (ColumnInfo ('Postgres 'Vanilla))
|
|
-> m (Maybe [FieldParser n (AnnotatedField ('Postgres 'Vanilla))])
|
|
relationshipFieldParser (TypeRelationship relName relType sourceName tableInfo fieldMapping) = runMaybeT do
|
|
let tableName = _tciName $ _tiCoreInfo tableInfo
|
|
fieldName = unRelationshipName relName
|
|
tableRelName = RelName $ mkNonEmptyTextUnsafe $ G.unName fieldName
|
|
columnMapping = Map.fromList $ do
|
|
(k, v) <- Map.toList fieldMapping
|
|
pure (unsafePGCol $ G.unName $ unObjectFieldName k, pgiColumn v)
|
|
roleName <- lift askRoleName
|
|
tablePerms <- hoistMaybe $ RQL.getPermInfoMaybe roleName PASelect tableInfo
|
|
case relType of
|
|
ObjRel -> do
|
|
let desc = Just $ G.Description "An object relationship"
|
|
selectionSetParser <- lift $ tableSelectionSet sourceName tableInfo tablePerms
|
|
pure $ pure $ P.nonNullableField $
|
|
P.subselection_ fieldName desc selectionSetParser
|
|
<&> \fields -> RQL.AFObjectRelation $ RQL.AnnRelationSelectG tableRelName columnMapping $
|
|
RQL.AnnObjectSelectG fields tableName $
|
|
(fmap partialSQLExpToUnpreparedValue <$> spiFilter tablePerms)
|
|
ArrRel -> do
|
|
let desc = Just $ G.Description "An array relationship"
|
|
otherTableParser <- lift $ selectTable sourceName tableInfo fieldName desc tablePerms
|
|
let arrayRelField = otherTableParser <&> \selectExp -> RQL.AFArrayRelation $
|
|
RQL.ASSimple $ RQL.AnnRelationSelectG tableRelName columnMapping selectExp
|
|
relAggFieldName = fieldName <> $$(G.litName "_aggregate")
|
|
relAggDesc = Just $ G.Description "An aggregate relationship"
|
|
tableAggField <- lift $ selectTableAggregate sourceName tableInfo relAggFieldName relAggDesc tablePerms
|
|
pure $ catMaybes [ Just arrayRelField
|
|
, fmap (RQL.AFArrayRelation . RQL.ASAggregate . RQL.AnnRelationSelectG tableRelName columnMapping) <$> tableAggField
|
|
]
|
|
|
|
|
|
mkDefinitionList :: AnnotatedObjectType -> [(PGCol, ScalarType ('Postgres 'Vanilla))]
|
|
mkDefinitionList AnnotatedObjectType{..} =
|
|
flip map (toList _otdFields) $ \ObjectFieldDefinition{..} ->
|
|
(unsafePGCol . G.unName . unObjectFieldName $ _ofdName,) $
|
|
case Map.lookup _ofdName fieldReferences of
|
|
Nothing -> fieldTypeToScalarType $ snd _ofdType
|
|
Just columnInfo -> unsafePGColumnToBackend $ pgiType columnInfo
|
|
where
|
|
ObjectTypeDefinition{..} = _aotDefinition
|
|
fieldReferences =
|
|
Map.unions $ map _trFieldMapping $ maybe [] toList _otdRelationships
|
|
|
|
|
|
actionInputArguments
|
|
:: forall m n r. (MonadSchema n m, MonadTableInfo r m)
|
|
=> NonObjectTypeMap
|
|
-> [ArgumentDefinition (G.GType, NonObjectCustomType)]
|
|
-> m (InputFieldsParser n J.Value)
|
|
actionInputArguments nonObjectTypeMap arguments = do
|
|
argumentParsers <- for arguments $ \argument -> do
|
|
let ArgumentDefinition argumentName (gType, nonObjectType) argumentDescription = argument
|
|
name = unArgumentName argumentName
|
|
(name,) <$> argumentParser name argumentDescription gType nonObjectType
|
|
pure $ J.Object <$> inputFieldsToObject argumentParsers
|
|
where
|
|
inputFieldsToObject
|
|
:: [(G.Name, InputFieldsParser n (Maybe J.Value))]
|
|
-> InputFieldsParser n J.Object
|
|
inputFieldsToObject inputFields =
|
|
let mkTuple (name, parser) = fmap (G.unName name,) <$> parser
|
|
in Map.fromList . catMaybes <$> traverse mkTuple inputFields
|
|
|
|
argumentParser
|
|
:: G.Name
|
|
-> Maybe G.Description
|
|
-> G.GType
|
|
-> NonObjectCustomType
|
|
-> m (InputFieldsParser n (Maybe J.Value))
|
|
argumentParser name description gType nonObjectType = do
|
|
let mkResult :: forall k. ('Input P.<: k) => Parser k n J.Value -> InputFieldsParser n (Maybe J.Value)
|
|
mkResult = mkArgumentInputFieldParser name description gType
|
|
case nonObjectType of
|
|
-- scalar and enum parsers are not recursive and need not be memoized
|
|
NOCTScalar def -> pure $ mkResult $ customScalarParser def
|
|
NOCTEnum def -> pure $ mkResult $ customEnumParser def
|
|
-- input objects however may recursively contain one another
|
|
NOCTInputObject (InputObjectTypeDefinition (InputObjectTypeName objectName) objectDesc inputFields) ->
|
|
mkResult <$> memoizeOn 'actionInputArguments objectName do
|
|
inputFieldsParsers <- forM (toList inputFields)
|
|
\(InputObjectFieldDefinition (InputObjectFieldName fieldName) fieldDesc (GraphQLType fieldType)) -> do
|
|
nonObjectFieldType <-
|
|
Map.lookup (G.getBaseType fieldType) nonObjectTypeMap
|
|
`onNothing` throw500 "object type for a field found in custom input object type"
|
|
(fieldName,) <$> argumentParser fieldName fieldDesc fieldType nonObjectFieldType
|
|
pure
|
|
$ P.object objectName objectDesc
|
|
$ J.Object <$> inputFieldsToObject inputFieldsParsers
|
|
|
|
|
|
mkArgumentInputFieldParser
|
|
:: forall m k. (MonadParse m, 'Input P.<: k)
|
|
=> G.Name
|
|
-> Maybe G.Description
|
|
-> G.GType
|
|
-> Parser k m J.Value
|
|
-> InputFieldsParser m (Maybe J.Value)
|
|
mkArgumentInputFieldParser name description gType parser =
|
|
if G.isNullable gType
|
|
then P.fieldOptional name description modifiedParser
|
|
else Just <$> P.field name description modifiedParser
|
|
where
|
|
modifiedParser = parserModifier gType parser
|
|
|
|
parserModifier
|
|
:: G.GType -> Parser k m J.Value -> Parser k m J.Value
|
|
parserModifier = \case
|
|
G.TypeNamed nullable _ -> nullableModifier nullable
|
|
G.TypeList nullable ty ->
|
|
nullableModifier nullable . fmap J.toJSON . P.list . parserModifier ty
|
|
where
|
|
nullableModifier =
|
|
bool (fmap J.toJSON) (fmap J.toJSON . P.nullable) . G.unNullability
|
|
|
|
customScalarParser
|
|
:: MonadParse m
|
|
=> AnnotatedScalarType
|
|
-> Parser 'Both m J.Value
|
|
customScalarParser = \case
|
|
ASTCustom ScalarTypeDefinition{..} ->
|
|
if | _stdName == idScalar -> J.toJSON <$> P.identifier
|
|
| _stdName == intScalar -> J.toJSON <$> P.int
|
|
| _stdName == floatScalar -> J.toJSON <$> P.float
|
|
| _stdName == stringScalar -> J.toJSON <$> P.string
|
|
| _stdName == boolScalar -> J.toJSON <$> P.boolean
|
|
| otherwise -> P.jsonScalar _stdName _stdDescription
|
|
ASTReusedScalar name pgScalarType ->
|
|
let schemaType = P.NonNullable $ P.TNamed $ P.mkDefinition name Nothing P.TIScalar
|
|
in P.Parser
|
|
{ pType = schemaType
|
|
, pParser = P.valueToJSON (P.toGraphQLType schemaType) >=>
|
|
either (parseErrorWith ParseFailed . qeError)
|
|
(pure . scalarValueToJSON @('Postgres 'Vanilla)) . parseScalarValue @('Postgres 'Vanilla) pgScalarType
|
|
}
|
|
|
|
customEnumParser
|
|
:: MonadParse m
|
|
=> EnumTypeDefinition
|
|
-> Parser 'Both m J.Value
|
|
customEnumParser (EnumTypeDefinition typeName description enumValues) =
|
|
let enumName = unEnumTypeName typeName
|
|
enumValueDefinitions = enumValues <&> \enumValue ->
|
|
let valueName = G.unEnumValue $ _evdValue enumValue
|
|
in (, J.toJSON valueName) $ P.mkDefinition valueName
|
|
(_evdDescription enumValue) P.EnumValueInfo
|
|
in P.enum enumName description enumValueDefinitions
|