Nested action joins

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3613
GitOrigin-RevId: 95fdb317a1052bdc440865f2dc8c5897e8531539
This commit is contained in:
David Overton 2022-03-03 14:43:27 +11:00 committed by hasura-bot
parent 876300c049
commit 1eb7fe5999
22 changed files with 417 additions and 302 deletions

View File

@ -18,6 +18,7 @@
- server: fix parsing timestamp values in BigQuery backends (fix #8076)
- server: add support for customising the GraphQL schema descriptions of table root fields
- server: add a `request_headers` field to the `test_webhook_transform` API.
- server: add support for relationships on nested action fields
- console: include cron trigger with include in metadata as false on cron trigger manage page
- console: show an error notification if Hasura CLI migrations fail
- console: fixed an issue where cron triggers were not removed from the list after deletion

View File

@ -622,6 +622,7 @@ library
, Hasura.RQL.DML.Internal
, Hasura.RQL.DML.Update
, Hasura.RQL.DML.Types
, Hasura.RQL.IR.Action
, Hasura.RQL.IR.BoolExp
, Hasura.RQL.IR.Conflict
, Hasura.RQL.IR.Delete

View File

@ -30,18 +30,15 @@ import Data.Environment qualified as Env
import Data.Has
import Data.HashMap.Strict qualified as Map
import Data.IORef
import Data.IntMap qualified as IntMap
import Data.Set (Set)
import Data.TByteString qualified as TBS
import Data.Text.Extended
import Data.Text.NonEmpty
import Data.Vector qualified as Vec
import Database.PG.Query qualified as Q
import Hasura.Backends.Postgres.Execute.Prepare
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.SQL.Value (PGScalarValue (..))
import Hasura.Backends.Postgres.Translate.Column (toTxtValue)
import Hasura.Backends.Postgres.Translate.Select (asSingleRowJsonResp)
import Hasura.Backends.Postgres.Translate.Select qualified as RS
import Hasura.Base.Error
@ -57,6 +54,7 @@ import Hasura.Prelude
import Hasura.RQL.DDL.Headers
import Hasura.RQL.DDL.Schema.Cache
import Hasura.RQL.DDL.WebhookTransforms
import Hasura.RQL.IR.Action qualified as RA
import Hasura.RQL.IR.Select qualified as RS
import Hasura.RQL.Types
import Hasura.SQL.Types
@ -113,71 +111,15 @@ resolveActionExecution ::
Env.Environment ->
L.Logger L.Hasura ->
UserInfo ->
AnnActionExecution ('Postgres 'Vanilla) Void (UnpreparedValue ('Postgres 'Vanilla)) ->
AnnActionExecution Void ->
ActionExecContext ->
Maybe GQLQueryText ->
ActionExecution
resolveActionExecution env logger userInfo AnnActionExecution {..} ActionExecContext {..} gqlQueryText =
case _aaeSource of
-- Build client response
ASINoSource -> ActionExecution $ first (encJFromOrderedValue . makeActionResponseNoRelations _aaeFields) <$> runWebhook
ASISource _ sourceConfig -> ActionExecution do
(webhookRes, respHeaders) <- runWebhook
let webhookResponse = makeActionResponseNoRelations _aaeFields webhookRes
webhookResponseExpression =
RS.AEInput $
UVLiteral $
toTxtValue $ ColumnValue (ColumnScalar PGJSONB) $ PGValJSONB $ Q.JSONB $ J.toJSON webhookRes
selectAstUnresolved =
processOutputSelectionSet
webhookResponseExpression
_aaeOutputType
_aaeDefinitionList
_aaeFields
_aaeStrfyNum
(RS.AnnSelectG {..}, finalPlanningSt) <- flip runStateT initPlanningSt $ traverse (prepareWithPlan userInfo) selectAstUnresolved
let prepArgs = fmap fst $ IntMap.elems $ withUserVars (_uiSession userInfo) $ _psPrepped finalPlanningSt
astRelations = RS.AnnSelectG {_asnFields = filter (isRelationField . snd) _asnFields, ..}
maybeRelationData <-
if null $ RS._asnFields astRelations
then pure Nothing
else AO.decode . encJToBS <$> executeActionInDb sourceConfig astRelations prepArgs
let mergeNoRelAndRelation noRelResponse relData =
case (noRelResponse, relData) of
(AO.Object nRelObj, AO.Object relObj) ->
let lookupField (fieldName, fieldType) =
let fName = getFieldNameTxt fieldName
in (fName,)
<$> if isRelationField fieldType
then AO.lookup fName relObj
else AO.lookup fName nRelObj
in pure $ AO.Object $ AO.fromList $ mapMaybe lookupField _asnFields
(AO.Array nRelArr, AO.Array relArr) ->
AO.Array <$> Vec.zipWithM mergeNoRelAndRelation nRelArr relArr
_ -> throw500 "Type mismatch in action relation response"
finalResponse <- maybe (pure webhookResponse) (mergeNoRelAndRelation webhookResponse) maybeRelationData
pure (encJFromOrderedValue finalResponse, respHeaders)
resolveActionExecution env logger _userInfo AnnActionExecution {..} ActionExecContext {..} gqlQueryText =
ActionExecution $ first (encJFromOrderedValue . makeActionResponseNoRelations _aaeFields) <$> runWebhook
where
isRelationField = \case
RS.AFObjectRelation _ -> True
RS.AFArrayRelation _ -> True
_ -> False
handlerPayload = ActionWebhookPayload (ActionContext _aaeName) _aecSessionVariables _aaePayload gqlQueryText
executeActionInDb ::
(MonadError QErr m, MonadIO m, MonadBaseControl IO m) =>
SourceConfig ('Postgres 'Vanilla) ->
RS.AnnSimpleSelect ('Postgres 'Vanilla) ->
[Q.PrepArg] ->
m EncJSON
executeActionInDb sourceConfig astResolved prepArgs = do
let jsonAggType = mkJsonAggSelect _aaeOutputType
liftEitherM $
runExceptT $
runTx (_pscExecCtx sourceConfig) Q.ReadOnly $
liftTx $ asSingleRowJsonResp (Q.fromBuilder $ toSQL $ RS.mkSQLSelect jsonAggType astResolved) prepArgs
runWebhook ::
(MonadIO m, MonadError QErr m, Tracing.MonadTrace m) =>
m (ActionWebhookResponse, HTTP.ResponseHeaders)
@ -198,24 +140,23 @@ resolveActionExecution env logger userInfo AnnActionExecution {..} ActionExecCon
_aaeResponseTransform
-- | Build action response from the Webhook JSON response when there are no relationships defined
makeActionResponseNoRelations :: forall b r v. RS.ActionFieldsG b r v -> ActionWebhookResponse -> AO.Value
makeActionResponseNoRelations :: RA.ActionFieldsG Void -> ActionWebhookResponse -> AO.Value
makeActionResponseNoRelations annFields webhookResponse =
let mkResponseObject :: RS.ActionFieldsG b r v -> HashMap Text J.Value -> AO.Value
let mkResponseObject :: RA.ActionFieldsG Void -> HashMap Text J.Value -> AO.Value
mkResponseObject fields obj =
AO.object $
flip mapMaybe fields $ \(fieldName, annField) ->
let fieldText = getFieldNameTxt fieldName
in (fieldText,) <$> case annField of
RS.ACFExpression t -> Just $ AO.String t
RS.ACFScalar fname -> AO.toOrdered <$> Map.lookup (G.unName fname) obj
RS.ACFNestedObject _ nestedFields -> do
RA.ACFExpression t -> Just $ AO.String t
RA.ACFScalar fname -> AO.toOrdered <$> Map.lookup (G.unName fname) obj
RA.ACFNestedObject _ nestedFields -> do
let mkValue :: J.Value -> Maybe AO.Value
mkValue = \case
J.Object o -> Just $ mkResponseObject nestedFields o
J.Array a -> Just $ AO.array $ mapMaybe mkValue $ toList a
_ -> Nothing
Map.lookup fieldText obj >>= mkValue
_ -> AO.toOrdered <$> Map.lookup fieldText obj
in -- NOTE (Sam): This case would still not allow for aliased fields to be
-- a part of the response. Also, seeing that none of the other `annField`
-- types would be caught in the example, I've chosen to leave it as it is.
@ -281,7 +222,7 @@ Resolving async action query happens in two steps;
-- | See Note: [Resolving async action query]
resolveAsyncActionQuery ::
UserInfo ->
AnnActionAsyncQuery ('Postgres 'Vanilla) Void (UnpreparedValue ('Postgres 'Vanilla)) ->
AnnActionAsyncQuery ('Postgres 'Vanilla) Void ->
AsyncActionQueryExecution (UnpreparedValue ('Postgres 'Vanilla))
resolveAsyncActionQuery userInfo annAction =
case actionSource of
@ -663,9 +604,9 @@ processOutputSelectionSet ::
RS.ArgumentExp v ->
GraphQLType ->
[(PGCol, PGScalarType)] ->
RS.ActionFieldsG ('Postgres 'Vanilla) r v ->
RA.ActionFieldsG Void ->
StringifyNumbers ->
RS.AnnSimpleSelectG ('Postgres 'Vanilla) r v
RS.AnnSimpleSelectG ('Postgres 'Vanilla) Void v
processOutputSelectionSet tableRowInput actionOutputType definitionList actionFields =
RS.AnnSelectG annotatedFields selectFrom RS.noTablePermissions RS.noSelectArgs
where
@ -679,13 +620,11 @@ processOutputSelectionSet tableRowInput actionOutputType definitionList actionFi
functionArgs = RS.FunctionArgsExp [tableRowInput] mempty
selectFrom = RS.FromFunction jsonbToPostgresRecordFunction functionArgs $ Just definitionList
actionFieldToAnnField :: RS.ActionFieldG ('Postgres 'Vanilla) r v -> RS.AnnFieldG ('Postgres 'Vanilla) r v
actionFieldToAnnField :: RA.ActionFieldG Void -> RS.AnnFieldG ('Postgres 'Vanilla) Void v
actionFieldToAnnField = \case
RS.ACFScalar asf -> RS.mkAnnColumnField (unsafePGCol $ toTxt asf) (ColumnScalar PGJSON) Nothing Nothing
RS.ACFObjectRelation ors -> RS.AFObjectRelation ors
RS.ACFArrayRelation as -> RS.AFArrayRelation as
RS.ACFExpression txt -> RS.AFExpression txt
RS.ACFNestedObject fieldName _ -> RS.mkAnnColumnField (unsafePGCol $ toTxt fieldName) (ColumnScalar PGJSON) Nothing Nothing
RA.ACFScalar asf -> RS.mkAnnColumnField (unsafePGCol $ toTxt asf) (ColumnScalar PGJSON) Nothing Nothing
RA.ACFExpression txt -> RS.AFExpression txt
RA.ACFNestedObject fieldName _ -> RS.mkAnnColumnField (unsafePGCol $ toTxt fieldName) (ColumnScalar PGJSON) Nothing Nothing
mkJsonAggSelect :: GraphQLType -> JsonAggSelect
mkJsonAggSelect =

View File

@ -46,7 +46,7 @@ convertMutationAction ::
HTTP.Manager ->
HTTP.RequestHeaders ->
Maybe GH.GQLQueryText ->
ActionMutation ('Postgres 'Vanilla) Void (UnpreparedValue ('Postgres 'Vanilla)) ->
ActionMutation Void ->
m ActionExecutionPlan
convertMutationAction env logger userInfo manager reqHeaders gqlQueryText = \case
AMSync s -> pure $ AEPSync $ resolveActionExecution env logger userInfo s actionExecContext gqlQueryText

View File

@ -19,6 +19,7 @@ import Hasura.Prelude
import Hasura.RQL.IR
import Hasura.RQL.Types
import Hasura.SQL.AnyBackend qualified as AB
import Language.GraphQL.Draft.Syntax qualified as G
{- Note [Remote Joins Architecture]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -131,9 +132,8 @@ getRemoteJoinsMutationOutput =
-- local helpers
getRemoteJoinsActionFields ::
Backend b =>
ActionFieldsG b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b) ->
(ActionFieldsG b Void (UnpreparedValue b), Maybe RemoteJoins)
ActionFieldsG (RemoteRelationshipField UnpreparedValue) ->
(ActionFieldsG Void, Maybe RemoteJoins)
getRemoteJoinsActionFields =
runCollector . transformActionFields
@ -164,17 +164,15 @@ getRemoteJoinsMutationDB = \case
in (delete {dqp1Output = output'}, remoteJoins)
getRemoteJoinsSyncAction ::
(Backend b) =>
AnnActionExecution b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b) ->
(AnnActionExecution b Void (UnpreparedValue b), Maybe RemoteJoins)
AnnActionExecution (RemoteRelationshipField UnpreparedValue) ->
(AnnActionExecution Void, Maybe RemoteJoins)
getRemoteJoinsSyncAction actionExecution =
let (fields', remoteJoins) = getRemoteJoinsActionFields $ _aaeFields actionExecution
in (actionExecution {_aaeFields = fields'}, remoteJoins)
getRemoteJoinsActionQuery ::
(Backend b) =>
ActionQuery b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b) ->
(ActionQuery b Void (UnpreparedValue b), Maybe RemoteJoins)
ActionQuery (RemoteRelationshipField UnpreparedValue) ->
(ActionQuery Void, Maybe RemoteJoins)
getRemoteJoinsActionQuery = \case
AQQuery sync ->
first AQQuery $ getRemoteJoinsSyncAction sync
@ -198,9 +196,8 @@ getRemoteJoinsActionQuery = \case
AsyncErrors -> pure AsyncErrors
getRemoteJoinsActionMutation ::
(Backend b) =>
ActionMutation b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b) ->
(ActionMutation b Void (UnpreparedValue b), Maybe RemoteJoins)
ActionMutation (RemoteRelationshipField UnpreparedValue) ->
(ActionMutation Void, Maybe RemoteJoins)
getRemoteJoinsActionMutation = \case
AMAsync async -> (AMAsync async, Nothing)
AMSync sync -> first AMSync $ getRemoteJoinsSyncAction sync
@ -373,11 +370,6 @@ transformAnnFields fields = do
remoteAnnPlaceholder :: AnnFieldG src Void (UnpreparedValue src)
remoteAnnPlaceholder = AFExpression "remote relationship placeholder"
-- Get the fields targeted by some 'Traversal' for an arbitrary list of
-- tuples, discarding any elements whose fields cannot be focused upon.
getFields :: Traversal' super sub -> [(any, super)] -> [(any, sub)]
getFields focus = mapMaybe (traverse $ preview focus)
-- This is a map of column name to its alias of all columns in the
-- selection set.
columnFields :: HashMap (Column src) FieldName
@ -446,6 +438,11 @@ transformAnnFields fields = do
ComputedFieldScalarSelect _scfFunction functionArgs _scfType Nothing
in AFComputedField _scfXField _scfName fieldSelect
-- | Get the fields targeted by some 'Traversal' for an arbitrary list of
-- tuples, discarding any elements whose fields cannot be focused upon.
getFields :: Traversal' super sub -> [(any, super)] -> [(any, sub)]
getFields focus = mapMaybe (traverse $ preview focus)
-- | Annotate an element a remote source join from '_rssJoinMapping' so that
-- a remote join can be constructed.
transformAnnRelation ::
@ -457,40 +454,80 @@ transformAnnRelation transform relation@(AnnRelationSelectG _ _ select) = do
pure $ relation {aarAnnSelect = transformedSelect}
transformActionFields ::
forall src.
Backend src =>
ActionFieldsG src (RemoteRelationshipField UnpreparedValue) (UnpreparedValue src) ->
Collector (ActionFieldsG src Void (UnpreparedValue src))
ActionFieldsG (RemoteRelationshipField UnpreparedValue) ->
Collector (ActionFieldsG Void)
transformActionFields fields = do
-- Produces a list of transformed fields that may or may not have an
-- associated remote join.
for fields \(fieldName, field') -> withField fieldName do
-- FIXME: There's way too much going on in this 'case .. of' block...
let mkCollector ::
ActionFieldG src (RemoteRelationshipField UnpreparedValue) (UnpreparedValue src) ->
Collector
(ActionFieldG src Void (UnpreparedValue src))
mkCollector annField = case annField of
-- ActionFields which do not need to be transformed.
ACFScalar c -> pure (ACFScalar c)
ACFExpression t -> pure (ACFExpression t)
-- ActionFields with no associated remote joins and whose transformations are
-- relatively straightforward.
ACFObjectRelation annRel -> do
transformed <- transformAnnRelation transformObjectSelect annRel
pure (ACFObjectRelation transformed)
ACFArrayRelation (ASSimple annRel) -> do
transformed <- transformAnnRelation transformSelect annRel
pure (ACFArrayRelation . ASSimple $ transformed)
ACFArrayRelation (ASAggregate aggRel) -> do
transformed <- transformAnnRelation transformAggregateSelect aggRel
pure (ACFArrayRelation . ASAggregate $ transformed)
ACFArrayRelation (ASConnection annRel) -> do
transformed <- transformAnnRelation transformConnectionSelect annRel
pure (ACFArrayRelation . ASConnection $ transformed)
ACFNestedObject fn fs ->
ACFNestedObject fn <$> transformActionFields fs
(fieldName,) <$> mkCollector field'
annotatedFields <- for fields \(fieldName, field') -> withField fieldName do
(fieldName,) <$> case field' of
-- ActionFields which do not need to be transformed.
ACFScalar c -> pure (ACFScalar c, Nothing)
ACFExpression t -> pure (ACFExpression t, Nothing)
-- Remote ActionFields, whose elements require annotation so that they can be
-- used to construct a remote join.
ACFRemote ActionRemoteRelationshipSelect {..} ->
pure
( -- We generate this so that the response has a key with the relationship,
-- without which preserving the order of fields in the final response
-- would require a lot of bookkeeping.
remoteActionPlaceholder,
Just $ createRemoteJoin joinColumnAliases _arrsRelationship
)
ACFNestedObject fn fs ->
(,Nothing) . ACFNestedObject fn <$> transformActionFields fs
let transformedFields = (fmap . fmap) fst annotatedFields
remoteJoins =
annotatedFields & mapMaybe \(fieldName, (_, mRemoteJoin)) ->
(fieldName,) <$> mRemoteJoin
case NEMap.fromList remoteJoins of
Nothing -> pure transformedFields
Just neRemoteJoins -> do
collect neRemoteJoins
pure $ transformedFields <> phantomFields
where
-- Placeholder text to annotate a remote relationship field.
remoteActionPlaceholder :: ActionFieldG Void
remoteActionPlaceholder = ACFExpression "remote relationship placeholder"
-- This is a map of column name to its alias of all columns in the
-- selection set.
scalarFields :: HashMap G.Name FieldName
scalarFields =
Map.fromList $
[ (name, alias)
| (alias, name) <- getFields _ACFScalar fields
]
-- Annotate a join field with its field name and an alias so that it may
-- be used to construct a remote join.
annotateJoinField ::
FieldName -> G.Name -> (G.Name, JoinColumnAlias)
annotateJoinField fieldName field =
let alias = getJoinColumnAlias fieldName field scalarFields allAliases
in (field, alias)
where
allAliases = map fst fields
-- goes through all the remote relationships in the selection set and emits
-- 1. a map of join field names to their aliases in the lhs response
-- 2. a list of extra fields that need to be included in the lhs query
-- that are required for the join
(joinColumnAliases, phantomFields :: ([(FieldName, ActionFieldG Void)])) =
let lhsJoinFields =
Map.unions $ map (_arrsLHSJoinFields . snd) $ getFields _ACFRemote fields
annotatedJoinColumns = Map.mapWithKey annotateJoinField $ lhsJoinFields
phantomFields_ :: ([(FieldName, ActionFieldG Void)]) =
toList annotatedJoinColumns & mapMaybe \(joinField, alias) ->
case alias of
JCSelected _ -> Nothing
JCPhantom a ->
let annotatedColumn =
ACFScalar joinField
in Just (a, annotatedColumn)
in (fmap snd annotatedJoinColumns, phantomFields_)
getJoinColumnAlias ::
(Eq field, Hashable field) =>

View File

@ -10,6 +10,7 @@ import Data.Has
import Data.HashMap.Strict qualified as Map
import Data.Text.Extended
import Data.Text.NonEmpty
import Hasura.Backends.Postgres.Instances.Schema ()
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.Types.Column
import Hasura.Base.Error
@ -27,10 +28,10 @@ import Hasura.GraphQL.Schema.Backend
import Hasura.GraphQL.Schema.Common
import Hasura.GraphQL.Schema.Select
import Hasura.Prelude
import Hasura.RQL.DML.Internal qualified as RQL
import Hasura.RQL.IR.Action qualified as RQL
import Hasura.RQL.IR.Root qualified as RQL
import Hasura.RQL.IR.Select qualified as RQL
import Hasura.RQL.Types
import Hasura.SQL.AnyBackend qualified as AB
import Hasura.Session
import Language.GraphQL.Draft.Syntax qualified as G
@ -49,7 +50,7 @@ actionExecute ::
MonadBuildSchema ('Postgres 'Vanilla) r m n =>
AnnotatedCustomTypes ->
ActionInfo ->
m (Maybe (FieldParser n (AnnActionExecution ('Postgres 'Vanilla) (RQL.RemoteRelationshipField UnpreparedValue) (UnpreparedValue ('Postgres 'Vanilla)))))
m (Maybe (FieldParser n (AnnActionExecution (RQL.RemoteRelationshipField UnpreparedValue))))
actionExecute customTypes actionInfo = runMaybeT do
roleName <- askRoleName
guard (roleName == adminRoleName || roleName `Map.member` permissions)
@ -63,7 +64,6 @@ actionExecute customTypes actionInfo = runMaybeT do
AOTScalar ast -> do
let selectionSet = customScalarParser ast
pure $ P.selection fieldName description inputArguments selectionSet <&> (,[])
stringifyNum <- asks $ qcStringifyNum . getter
pure $
parserOutput
<&> \(argsJson, fields) ->
@ -73,13 +73,10 @@ actionExecute customTypes actionInfo = runMaybeT do
_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 = _adRequestTransform definition,
_aaeResponseTransform = _adResponseTransform definition
}
@ -128,7 +125,7 @@ actionAsyncQuery ::
MonadBuildSchema ('Postgres 'Vanilla) r m n =>
AnnotatedObjects ->
ActionInfo ->
m (Maybe (FieldParser n (AnnActionAsyncQuery ('Postgres 'Vanilla) (RQL.RemoteRelationshipField UnpreparedValue) (UnpreparedValue ('Postgres 'Vanilla)))))
m (Maybe (FieldParser n (AnnActionAsyncQuery ('Postgres 'Vanilla) (RQL.RemoteRelationshipField UnpreparedValue))))
actionAsyncQuery objectTypes actionInfo = runMaybeT do
roleName <- askRoleName
guard $ roleName == adminRoleName || roleName `Map.member` permissions
@ -201,11 +198,11 @@ actionIdParser = ActionId <$> P.uuid
actionOutputFields ::
forall r m n.
MonadBuildSchema ('Postgres 'Vanilla) r m n =>
MonadBuildSchemaBase r m n =>
G.GType ->
AnnotatedObjectType ->
AnnotatedObjects ->
m (Parser 'Output n (AnnotatedActionFields ('Postgres 'Vanilla)))
m (Parser 'Output n (AnnotatedActionFields))
actionOutputFields outputType annotatedObject objectTypes = do
let outputObject = _aotDefinition annotatedObject
scalarOrEnumOrObjectFields <- forM (toList $ _otdFields outputObject) outputFieldParser
@ -229,7 +226,7 @@ actionOutputFields outputType annotatedObject objectTypes = do
outputFieldParser ::
ObjectFieldDefinition (G.GType, AnnotatedObjectFieldType) ->
m (FieldParser n (AnnotatedActionField ('Postgres 'Vanilla)))
m (FieldParser n (AnnotatedActionField))
outputFieldParser (ObjectFieldDefinition name _ description (gType, objectFieldType)) = memoizeOn 'actionOutputFields (_otdName $ _aotDefinition annotatedObject, name) do
case objectFieldType of
AOFTScalar def ->
@ -249,44 +246,39 @@ actionOutputFields outputType annotatedObject objectTypes = do
relationshipFieldParser ::
TypeRelationship (TableInfo ('Postgres 'Vanilla)) (ColumnInfo ('Postgres 'Vanilla)) ->
m (Maybe [FieldParser n (AnnotatedActionField ('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
m (Maybe [FieldParser n (AnnotatedActionField)])
relationshipFieldParser (TypeRelationship relationshipName relType sourceName tableInfo fieldMapping) = runMaybeT do
sourceInfo <- MaybeT $ asks $ (unsafeSourceInfo @('Postgres 'Vanilla) <=< Map.lookup sourceName) . getter
relName <- hoistMaybe $ RelName <$> mkNonEmptyText (toTxt relationshipName)
let lhsJoinFields = Map.fromList $ do
(k, v) <- Map.toList fieldMapping
pure (unsafePGCol $ G.unName $ unObjectFieldName k, ciColumn 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 <- MaybeT $ tableSelectionSet sourceName tableInfo
pure $
pure $
P.nonNullableField $
P.subselection_ fieldName desc selectionSetParser
<&> \fields ->
RQL.ACFObjectRelation $
RQL.AnnRelationSelectG tableRelName columnMapping $
RQL.AnnObjectSelectG fields tableName $
(fmap partialSQLExpToUnpreparedValue <$> spiFilter tablePerms)
ArrRel -> do
let desc = Just $ G.Description "An array relationship"
otherTableParser <- MaybeT $ selectTable sourceName tableInfo fieldName desc
let arrayRelField =
otherTableParser <&> \selectExp ->
RQL.ACFArrayRelation $
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
pure $
catMaybes
[ Just arrayRelField,
fmap (RQL.ACFArrayRelation . RQL.ASAggregate . RQL.AnnRelationSelectG tableRelName columnMapping) <$> tableAggField
]
pure (FieldName $ G.unName $ unObjectFieldName k, ciName v)
joinMapping = Map.fromList $ do
(k, v) <- Map.toList fieldMapping
let scalarType = case ciType v of
ColumnScalar scalar -> scalar
-- We don't currently allow enum types as fields of custom types so they should not appear here.
-- If we do allow them in future then they would be represented in Postgres as Text.
ColumnEnumReference _ -> PGText
pure (FieldName $ G.unName $ unObjectFieldName k, (scalarType, ciColumn v))
remoteFieldInfo =
RemoteFieldInfo
{ _rfiLHS = lhsJoinFields,
_rfiRHS =
RFISource $
AB.mkAnyBackend @('Postgres 'Vanilla) $
RemoteSourceFieldInfo
{ _rsfiName = relName,
_rsfiType = relType,
_rsfiSource = sourceName,
_rsfiSourceConfig = _siConfiguration sourceInfo,
_rsfiSourceCustomization = getSourceTypeCustomization $ _siCustomization sourceInfo,
_rsfiTable = tableInfoName tableInfo,
_rsfiMapping = joinMapping
}
}
remoteRelationshipFieldParsers <- MaybeT $ remoteRelationshipField remoteFieldInfo
pure $ remoteRelationshipFieldParsers <&> fmap (RQL.ACFRemote . RQL.ActionRemoteRelationshipSelect lhsJoinFields)
mkDefinitionList :: AnnotatedOutputType -> [(PGCol, ScalarType ('Postgres 'Vanilla))]
mkDefinitionList (AOTScalar _) = []

View File

@ -42,6 +42,7 @@ import Hasura.Base.Error
import Hasura.GraphQL.Execute.Types qualified as ET (GraphQLQueryType)
import Hasura.GraphQL.Parser qualified as P
import Hasura.Prelude
import Hasura.RQL.IR.Action qualified as IR
import Hasura.RQL.IR.Root qualified as IR
import Hasura.RQL.IR.Select qualified as IR
import Hasura.RQL.Types
@ -77,9 +78,9 @@ type ConnectionFields b = IR.ConnectionFields b (IR.RemoteRelationshipField P.Un
type EdgeFields b = IR.EdgeFields b (IR.RemoteRelationshipField P.UnpreparedValue) (P.UnpreparedValue b)
type AnnotatedActionFields b = IR.ActionFieldsG b (IR.RemoteRelationshipField P.UnpreparedValue) (P.UnpreparedValue b)
type AnnotatedActionFields = IR.ActionFieldsG (IR.RemoteRelationshipField P.UnpreparedValue)
type AnnotatedActionField b = IR.ActionFieldG b (IR.RemoteRelationshipField P.UnpreparedValue) (P.UnpreparedValue b)
type AnnotatedActionField = IR.ActionFieldG (IR.RemoteRelationshipField P.UnpreparedValue)
data RemoteRelationshipQueryContext = RemoteRelationshipQueryContext
{ _rrscIntrospectionResultOriginal :: !IntrospectionResult,
@ -126,7 +127,7 @@ parsedSelectionsToFields ::
-- | how to handle @__typename@ fields
(Text -> a) ->
OMap.InsOrdHashMap G.Name (P.ParsedSelection a) ->
IR.Fields a
Fields a
parsedSelectionsToFields mkTypename =
OMap.toList
>>> map (FieldName . G.unName *** P.handleTypename (mkTypename . G.unName))

View File

@ -237,11 +237,10 @@ runSessVarPred = filterSessionVariables . unSessVarPred
-- | Filter out only those session variables used by the query AST provided
filterVariablesFromQuery ::
Backend backend =>
[ RootField
(QueryDBRoot (RemoteRelationshipField UnpreparedValue) UnpreparedValue)
(RemoteSchemaRootField Void RemoteSchemaVariable)
(ActionQuery backend (RemoteRelationshipField UnpreparedValue) (UnpreparedValue backend))
(ActionQuery (RemoteRelationshipField UnpreparedValue))
d
] ->
SessVarPred
@ -252,7 +251,7 @@ filterVariablesFromQuery query = fold $ rootToSessVarPreds =<< query
AB.dispatchAnyBackend @Backend exists \case
SourceConfigWith _ _ (QDBR db) -> toPred <$> toListOf traverse db
RFRemote remote -> match <$> toListOf (traverse . _SessionPresetVariable) remote
RFAction actionQ -> toPred <$> toListOf traverse actionQ
RFAction _ -> [] -- TODO: does this work correctly if there are session variables in a remote join?
_ -> []
_SessionPresetVariable :: Traversal' RemoteSchemaVariable SessionVariable

View File

@ -22,7 +22,6 @@ 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.HashSet qualified as Set
import Data.List.NonEmpty qualified as NEList
import Data.Text.Extended
import Hasura.Base.Error
@ -177,24 +176,6 @@ resolveAction env AnnotatedCustomTypes {..} ActionDefinition {..} allScalars = d
throw400 ConstraintError $
"Async action relations cannot be used with object fields : " <> commaSeparated (dquote . _ofdName <$> nestedObjects)
pure aot
-- checking if there is any relation which is not in output type of action
let checkNestedObjRelationship :: (QErrM m) => HashSet G.Name -> AnnotatedObjectFieldType -> m ()
checkNestedObjRelationship seenObjectTypes = \case
AOFTScalar _ -> pure ()
AOFTEnum _ -> pure ()
AOFTObject objectTypeName -> do
unless (objectTypeName `Set.member` seenObjectTypes) $ do
-- avoid infinite loop for recursive types
ObjectTypeDefinition {..} <-
_aotDefinition <$> Map.lookup objectTypeName _actObjects
`onNothing` throw500 ("Custom object type " <> objectTypeName <<> " not found")
when (isJust _otdRelationships) $
throw400 ConstraintError $ "Relationship cannot be defined for nested object " <> _otdName <<> ". Relationship can be used only for top level object " <> outputBaseType <<> "."
for_ _otdFields $ checkNestedObjRelationship (Set.insert objectTypeName seenObjectTypes) . snd . _ofdType
outFields = case outputObject of
AOTObject aot -> NEList.toList $ _otdFields $ _aotDefinition aot
AOTScalar _ -> []
for_ (outFields) $ checkNestedObjRelationship mempty . snd . _ofdType
resolvedWebhook <- resolveWebhook env _adHandler
pure
( ActionDefinition

View File

@ -3,6 +3,7 @@ module Hasura.RQL.IR
)
where
import Hasura.RQL.IR.Action as IR
import Hasura.RQL.IR.BoolExp as IR
import Hasura.RQL.IR.Delete as IR
import Hasura.RQL.IR.Insert as IR

View File

@ -0,0 +1,42 @@
module Hasura.RQL.IR.Action
( ActionFieldG (..),
ActionFieldsG,
ActionFields,
ActionRemoteRelationshipSelect (..),
_ACFExpression,
_ACFNestedObject,
_ACFRemote,
_ACFScalar,
)
where
import Control.Lens.TH (makePrisms)
import Data.Kind (Type)
import Hasura.Prelude
import Hasura.RQL.Types.Common (FieldName, Fields)
import Language.GraphQL.Draft.Syntax qualified as G
data ActionFieldG (r :: Type)
= ACFScalar G.Name
| ACFRemote (ActionRemoteRelationshipSelect r)
| ACFExpression Text
| ACFNestedObject G.Name !(ActionFieldsG r)
deriving (Eq, Show)
type ActionFieldsG r = Fields (ActionFieldG r)
type ActionFields = ActionFieldsG Void
data ActionRemoteRelationshipSelect r = ActionRemoteRelationshipSelect
{ -- | The fields on the table that are required for the join condition
-- of the remote relationship
_arrsLHSJoinFields :: HashMap FieldName G.Name,
-- | The field that captures the relationship
-- r ~ (RemoteRelationshipField UnpreparedValue) when the AST is emitted by the parser.
-- r ~ Void when an execution tree is constructed so that a backend is
-- absolved of dealing with remote relationships.
_arrsRelationship :: r
}
deriving (Eq, Show)
$(makePrisms ''ActionFieldG)

View File

@ -19,6 +19,7 @@ import Hasura.EncJSON
import Hasura.Prelude
import Hasura.RQL.IR.Select
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Common
import Hasura.SQL.Backend
data MutFldG (b :: BackendType) (r :: Type) v

View File

@ -50,15 +50,13 @@ data MutationDB (b :: BackendType) (r :: Type) v
MDBFunction RQL.JsonAggSelect (AnnSimpleSelectG b r v)
deriving stock (Generic, Functor, Foldable, Traversable)
data ActionQuery (b :: BackendType) (r :: Type) v
= AQQuery !(RQL.AnnActionExecution b r v)
| AQAsync !(RQL.AnnActionAsyncQuery b r v)
deriving (Functor, Foldable, Traversable)
data ActionQuery (r :: Type)
= AQQuery !(RQL.AnnActionExecution r)
| AQAsync !(RQL.AnnActionAsyncQuery ('Postgres 'Vanilla) r)
data ActionMutation (b :: BackendType) (r :: Type) v
= AMSync !(RQL.AnnActionExecution b r v)
data ActionMutation (r :: Type)
= AMSync !(RQL.AnnActionExecution r)
| AMAsync !RQL.AnnActionMutationAsync
deriving (Functor, Foldable, Traversable)
-- The `db` type argument of @RootField@ expects only one type argument, the backend `b`, as not all
-- types stored in a RootField will have a second parameter like @QueryDB@ does: they all only have
@ -80,11 +78,11 @@ data RemoteRelationshipField vf
-- | Represents a query root field to an action
type QueryActionRoot v =
ActionQuery ('Postgres 'Vanilla) (RemoteRelationshipField v) (v ('Postgres 'Vanilla))
ActionQuery (RemoteRelationshipField v)
-- | Represents a mutation root field to an action
type MutationActionRoot v =
ActionMutation ('Postgres 'Vanilla) (RemoteRelationshipField v) (v ('Postgres 'Vanilla))
ActionMutation (RemoteRelationshipField v)
type QueryRootField v =
RootField

View File

@ -24,10 +24,7 @@
-- @UnpreparedValue b@ for their respective backend @b@, and most backends will then transform
-- their AST, cutting all such remote branches, and therefore using @Const Void@ for @r@.
module Hasura.RQL.IR.Select
( ActionFieldG (..),
ActionFieldsG,
ActionFields,
AggregateField (..),
( AggregateField (..),
AggregateFields,
AggregateOp (..),
AnnAggregateSelect,
@ -70,7 +67,6 @@ module Hasura.RQL.IR.Select
ConnectionSplitKind (..),
EdgeField (..),
EdgeFields,
Fields,
FunctionArgExp,
FunctionArgsExpG (..),
FunctionArgsExpTableRow,
@ -146,7 +142,6 @@ import Hasura.RQL.Types.Instances ()
import Hasura.RQL.Types.Relationships.Local
import Hasura.RQL.Types.Relationships.Remote
import Hasura.SQL.Backend
import Language.GraphQL.Draft.Syntax qualified as G
-- Root selection
@ -402,10 +397,6 @@ type AnnotatedOrderByItem b = AnnotatedOrderByItemG b (SQLExpression b)
-- Fields
-- The field name here is the GraphQL alias, i.e, the name with which the field
-- should appear in the response
type Fields a = [(FieldName, a)]
-- | captures a remote relationship's selection and the necessary context
data RemoteRelationshipSelect b r = RemoteRelationshipSelect
{ -- | The fields on the table that are required for the join condition
@ -899,36 +890,6 @@ insertFunctionArg argName idx value (FunctionArgsExp positional named) =
where
insertAt i a = toList . Seq.insertAt i a . Seq.fromList
-- Actions
data ActionFieldG (b :: BackendType) (r :: Type) v
= ACFScalar G.Name
| ACFObjectRelation (ObjectRelationSelectG b r v)
| ACFArrayRelation (ArraySelectG b r v)
| ACFExpression Text
| ACFNestedObject G.Name !(ActionFieldsG b r v)
deriving (Functor, Foldable, Traversable)
deriving instance
( Backend b,
Eq (BooleanOperators b v),
Eq v,
Eq r
) =>
Eq (ActionFieldG b r v)
deriving instance
( Backend b,
Show (BooleanOperators b v),
Show v,
Show r
) =>
Show (ActionFieldG b r v)
type ActionFieldsG b r v = Fields (ActionFieldG b r v)
type ActionFields b = ActionFieldsG b Void (SQLExpression b)
-- | The "distinct" input field inside "count" aggregate field
--
-- count (

View File

@ -78,7 +78,7 @@ import Hasura.Incremental (Cacheable)
import Hasura.Prelude
import Hasura.RQL.DDL.Headers
import Hasura.RQL.DDL.WebhookTransforms (MetadataRequestTransform, MetadataResponseTransform)
import Hasura.RQL.IR.Select
import Hasura.RQL.IR.Action
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.CustomTypes
@ -318,27 +318,23 @@ getActionSourceInfo :: AnnotatedOutputType -> ActionSourceInfo ('Postgres 'Vanil
getActionSourceInfo (AOTObject aot) = maybe ASINoSource (uncurry ASISource) . _aotSource $ aot
getActionSourceInfo (AOTScalar _) = ASINoSource
data AnnActionExecution (b :: BackendType) (r :: Type) v = AnnActionExecution
data AnnActionExecution (r :: Type) = AnnActionExecution
{ _aaeName :: !ActionName,
-- | output type
_aaeOutputType :: !GraphQLType,
-- | output selection
_aaeFields :: !(ActionFieldsG b r v),
_aaeFields :: !(ActionFieldsG r),
-- | jsonified input arguments
_aaePayload :: !J.Value,
-- | to validate the response fields from webhook
_aaeOutputFields :: !ActionOutputFields,
_aaeDefinitionList :: ![(Column b, ScalarType b)],
_aaeWebhook :: !ResolvedWebhook,
_aaeHeaders :: ![HeaderConf],
_aaeForwardClientHeaders :: !Bool,
_aaeStrfyNum :: !StringifyNumbers,
_aaeTimeOut :: !Timeout,
_aaeSource :: !(ActionSourceInfo b),
_aaeRequestTransform :: !(Maybe MetadataRequestTransform),
_aaeResponseTransform :: !(Maybe MetadataResponseTransform)
}
deriving (Functor, Foldable, Traversable)
data AnnActionMutationAsync = AnnActionMutationAsync
{ _aamaName :: !ActionName,
@ -348,27 +344,25 @@ data AnnActionMutationAsync = AnnActionMutationAsync
}
deriving (Show, Eq)
data AsyncActionQueryFieldG (b :: BackendType) (r :: Type) v
data AsyncActionQueryFieldG (r :: Type)
= AsyncTypename !Text
| AsyncOutput !(ActionFieldsG b r v)
| AsyncOutput !(ActionFieldsG r)
| AsyncId
| AsyncCreatedAt
| AsyncErrors
deriving (Functor, Foldable, Traversable)
type AsyncActionQueryFieldsG b r v = Fields (AsyncActionQueryFieldG b r v)
type AsyncActionQueryFieldsG r = Fields (AsyncActionQueryFieldG r)
data AnnActionAsyncQuery (b :: BackendType) (r :: Type) v = AnnActionAsyncQuery
data AnnActionAsyncQuery (b :: BackendType) (r :: Type) = AnnActionAsyncQuery
{ _aaaqName :: !ActionName,
_aaaqActionId :: !ActionId,
_aaaqOutputType :: !GraphQLType,
_aaaqFields :: !(AsyncActionQueryFieldsG b r v),
_aaaqFields :: !(AsyncActionQueryFieldsG r),
_aaaqDefinitionList :: ![(Column b, ScalarType b)],
_aaaqStringifyNum :: !StringifyNumbers,
_aaaqForwardClientHeaders :: !Bool,
_aaaqSource :: !(ActionSourceInfo b)
}
deriving (Functor, Foldable, Traversable)
data ActionExecContext = ActionExecContext
{ _aecManager :: !HTTP.Manager,

View File

@ -6,6 +6,7 @@ module Hasura.RQL.Types.Common
relTypeToTxt,
OID (..),
FieldName (..),
Fields,
InsertOrder (..),
ToAesonPairs (..),
InpValInfo (..),
@ -187,6 +188,10 @@ newtype FieldName = FieldName {getFieldNameTxt :: Text}
instance ToTxt FieldName where
toTxt (FieldName c) = c
-- The field name here is the GraphQL alias, i.e, the name with which the field
-- should appear in the response
type Fields a = [(FieldName, a)]
class ToAesonPairs a where
toAesonPairs :: (KeyValue v) => a -> [v]

View File

@ -1,33 +1,11 @@
- description: Create an action with PG scalars in input arguments
url: /v1/query
status: 400
status: 200
response:
internal:
- definition:
definition:
kind: synchronous
output_type: UserIdObj_Fail
arguments:
- name: someInput
type: String!
description:
headers: []
handler: http://some.random/endpoint
type: mutation
timeout: 30
forward_client_headers: false
name: action_create_fail
comment:
reason: 'Inconsistent object: in action "action_create_fail"; Relationship cannot
be defined for nested object "UserId". Relationship can be used only for top level
object "UserIdObj_Fail".'
name: action action_create_fail
type: action
path: "$.args[1].args"
error: 'Inconsistent object: in action "action_create_fail"; Relationship cannot be
defined for nested object "UserId". Relationship can be used only for top level
object "UserIdObj_Fail".'
code: invalid-configuration
- message: success
- message: success
- message: success
- message: success
query:
type: bulk
args:
@ -44,17 +22,24 @@
remote_table: user
field_mapping:
id: id
- name: UserIdObj_Fail
- name: UserIdObj
fields:
- name: user_id
type: UserId
- type: create_action
args:
name: action_create_fail
name: action_create
definition:
kind: synchronous
arguments:
- name: someInput
type: String!
output_type: UserIdObj_Fail
output_type: UserIdObj
handler: http://some.random/endpoint
- type: drop_action
args:
name: action_create
clear_data: true
# clear custom types
- type: set_custom_types
args: {}

View File

@ -1,5 +1,14 @@
type: run_sql
type: bulk
args:
cascade: true
sql: |
DROP TABLE "user";
- type: untrack_table
args:
cascade: true
table:
name: user
schema: public
- type: run_sql
args:
cascade: true
sql: |
DROP TABLE "user";

View File

@ -0,0 +1,123 @@
- description: Run get_user_by_email_nested query action with valid email, the response should be an object
url: /v1/graphql
status: 200
query:
query: |
query ($email: String!){
get_user_by_email_nested_join(email: $email){
address {
country
}
user_id {
id
user {
id
name
email
}
articles {
id
name
user_id
}
articles_aggregate {
aggregate {
count
}
}
}
}
}
variables:
email: clarke@gmail.com
response:
data:
get_user_by_email_nested_join:
address:
country: USA
user_id:
id: 1
user:
id: 1
name: Clarke
email: clarke@gmail.com
articles:
- id: 1
name: foo
user_id: 1
- id: 2
name: bar
user_id: 1
- id: 3
name: bar
user_id: 1
articles_aggregate:
aggregate:
count: 3
- description: Run get_user_by_email_nested_join query action with valid email, the response should be an object
url: /v1/graphql
status: 200
query:
query: |
query ($email: String!){
get_user_by_email_nested_join(email: $email){
id
address {
city
country
}
}
}
variables:
email: clarke@gmail.com
response:
data:
get_user_by_email_nested_join:
id: 1
address:
city: New York
country: USA
- description: Run get_user_by_email_nested_join query action with valid email, the response should be an object
url: /v1/graphql
status: 200
query:
query: |
query ($email: String!){
get_user_by_email_nested_join(email: $email){
id
user_id {
id
user {
name
}
}
address {
city
country
}
addresses {
city
country
}
}
}
variables:
email: clarke@gmail.com
response:
data:
get_user_by_email_nested_join:
id: 1
user_id:
id: 1
user:
name: Clarke
address:
city: New York
country: USA
addresses:
- city: Bangalore
country: India
- city: Melbourne
country: Australia

View File

@ -143,6 +143,17 @@ args:
- name: other_addresses
type: '[Address]'
- name: NestedJoinObject
fields:
- name: id
type: Int!
- name: user_id
type: UserId
- name: address
type: Address
- name: addresses
type: '[Address]'
- name: DirectRecursiveType
fields:
- name: id
@ -361,6 +372,17 @@ args:
{{ end }}
}
- type: create_action
args:
name: get_user_by_email_nested_join
definition:
type: query
arguments:
- name: email
type: String!
output_type: NestedJoinObject!
handler: http://127.0.0.1:5593/get-user-by-email-nested
- type: create_action
args:
name: get_users_by_email

View File

@ -44,6 +44,10 @@ args:
args:
name: get_users_by_email_nested
clear_data: true
- type: drop_action
args:
name: get_user_by_email_nested_join
clear_data: true
- type: drop_action
args:
name: intentional_error

View File

@ -74,19 +74,19 @@ class TestActionsSync:
def test_expecting_array_response_got_object(self, hge_ctx):
check_query_secret(hge_ctx, self.dir() + '/expecting_array_response.yaml')
def test_expecting_scalar_output_type_success(self, hge_ctx):
check_query_secret(hge_ctx, self.dir() + '/get_scalar_action_output_type_success.yaml')
def test_expecting_scalar_string_output_type_got_object(self, hge_ctx):
check_query_secret(hge_ctx, self.dir() + '/expecting_scalar_response_got_object.yaml')
def test_expecting_object_output_type_success_got_scalar_string(self, hge_ctx):
check_query_secret(hge_ctx, self.dir() + '/expecting_object_response_got_scalar.yaml')
def test_scalar_response_action_transformed_output(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/scalar_response_action_transformed_output.yaml')
def test_object_response_action_transformed_output(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/object_response_action_transformed_output.yaml')
@ -267,6 +267,25 @@ class TestQueryActions:
assert code == 200,resp
check_query_f(hge_ctx, self.dir() + '/get_user_by_email_nested_success.yaml')
def test_query_action_success_output_nested_join(self, hge_ctx):
gql_query = '''
mutation {
insert_user_one(object: {email: "clarke@gmail.com", name:"Clarke"}){
id
}
}
'''
query = {
'query': gql_query
}
headers = {}
admin_secret = hge_ctx.hge_key
if admin_secret is not None:
headers['X-Hasura-Admin-Secret'] = admin_secret
code, resp, _ = hge_ctx.anyq('/v1/graphql', query, headers)
assert code == 200,resp
check_query_f(hge_ctx, self.dir() + '/get_user_by_email_nested_join_success.yaml')
def test_query_action_success_output_list(self, hge_ctx):
gql_query = '''
mutation {
@ -696,7 +715,7 @@ class TestCreateActionNestedTypeWithRelation:
check_query_f(hge_ctx, self.dir() + '/create_async_action_with_nested_output_and_relation.yaml')
# no toplevel, extensions with no error
def test_create_sync_action_with_nested_output_and_nested_relation_fail(self, hge_ctx):
def test_create_sync_action_with_nested_output_and_nested_relation(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/create_sync_action_with_nested_output_and_nested_relation.yaml')
@pytest.mark.usefixtures('per_class_tests_db_state')