mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-13 19:33:55 +03:00
Nested action joins
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3613 GitOrigin-RevId: 95fdb317a1052bdc440865f2dc8c5897e8531539
This commit is contained in:
parent
876300c049
commit
1eb7fe5999
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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
|
||||
|
@ -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) =>
|
||||
|
@ -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 _) = []
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
42
server/src-lib/Hasura/RQL/IR/Action.hs
Normal file
42
server/src-lib/Hasura/RQL/IR/Action.hs
Normal 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)
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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: {}
|
||||
|
@ -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";
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user