mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-10-05 14:28:08 +03:00
Feature/request transform string interpolation
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2443 Co-authored-by: Tirumarai Selvan <8663570+tirumaraiselvan@users.noreply.github.com> GitOrigin-RevId: d7d68984d0ae1403bb414572e9704c01ed27deab
This commit is contained in:
parent
20f7c85382
commit
4e05bdcaec
@ -3,6 +3,7 @@
|
||||
## Next release
|
||||
(Add entries below in the order of server, console, cli, docs, others)
|
||||
|
||||
- server: add webhook transformations for Actions and EventTriggers
|
||||
- server: optimize SQL query generation with LIMITs
|
||||
- server: add GraphQL request query in the payload for synchronous actions
|
||||
- server: improve the event trigger logging on errors
|
||||
@ -12,7 +13,6 @@
|
||||
- server: support `extensions` field in error responses from action webhook endpoints (fix #4001)
|
||||
- server: fix custom-check based permissions for MSSQL (#7429)
|
||||
- server: remove identity notion for table columns (fix #7557)
|
||||
- server: add webhook transformations for Actions and EventTriggers
|
||||
- server: support MSSQL transactions
|
||||
- server: log individual operation details in the http-log during a batch graphQL query execution
|
||||
- server: update `create_scheduled_event` API to return `event_id` in response
|
||||
|
@ -37,7 +37,7 @@ package graphql-engine
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/hasura/kriti-lang.git
|
||||
tag: c6944c4fe0f6ca85c3512deb7d10174bd29e2385
|
||||
tag: v0.1.0
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
@ -352,3 +352,28 @@ drop_inconsistent_metadata
|
||||
"type": "drop_inconsistent_metadata",
|
||||
"args": {}
|
||||
}
|
||||
|
||||
.. _test_webhook_transform:
|
||||
|
||||
test_webhook_transform
|
||||
--------------------------
|
||||
|
||||
``test_webhook_transform`` can be used to test out request transformations using mock data.
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
POST /v1/metadata HTTP/1.1
|
||||
Content-Type: application/json
|
||||
X-Hasura-Role: admin
|
||||
|
||||
{
|
||||
"type" : "test_webhook_transform",
|
||||
"args" : {
|
||||
"webhook_url": "http://localhost:1234",
|
||||
"body": { "hello": "world" },
|
||||
"request_transform": {
|
||||
"body": "{{ $body.world }}",
|
||||
"template_engine": "Kriti"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1897,6 +1897,10 @@ RequestTransformation
|
||||
- false
|
||||
- String
|
||||
- Replace the Content-Type with this value. Only "application/json" and "application/x-www-form-urlencoded" are allowed. Default: "application/json"
|
||||
* - query_params
|
||||
- false
|
||||
- Object (String : String)
|
||||
- Replace the query params on the URL with this value.
|
||||
* - request_headers
|
||||
- false
|
||||
- :ref:`TransformHeaders`
|
||||
@ -1906,6 +1910,18 @@ RequestTransformation
|
||||
- :ref:`TemplateEngine`
|
||||
- Template language to be used for this transformation. Default: "Kriti"
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Key
|
||||
- required
|
||||
- Schema
|
||||
- Description
|
||||
* - Key
|
||||
- required
|
||||
- Schema
|
||||
- Description
|
||||
|
||||
.. _TransformHeaders
|
||||
|
||||
TransformHeaders
|
||||
|
@ -347,11 +347,11 @@ with the json data and return the results.
|
||||
|
||||
```
|
||||
{
|
||||
"type" : "validate_webhook_transform",
|
||||
"type" : "test_webhook_transform",
|
||||
"args" : {
|
||||
"webhook_url": "https://localhost:1234",
|
||||
"payload": { "hello": "world" },
|
||||
"transformer": {
|
||||
"body": { "hello": "world" },
|
||||
"request_transform": {
|
||||
"body": "{{ $.hello }}",
|
||||
"template_engine": "Kriti"
|
||||
}
|
||||
|
@ -115,6 +115,7 @@ library
|
||||
, deepseq
|
||||
, dependent-map >=0.4 && <0.5
|
||||
, dependent-sum
|
||||
, either
|
||||
, exceptions
|
||||
, fast-logger
|
||||
, ghc-heap-view
|
||||
|
@ -10,6 +10,7 @@ module Hasura.Backends.Postgres.DDL.EventTrigger
|
||||
setRetry,
|
||||
recordSuccess,
|
||||
recordError,
|
||||
recordError',
|
||||
unlockEventsInSource,
|
||||
updateColumnInEventTrigger,
|
||||
)
|
||||
@ -135,9 +136,20 @@ recordError ::
|
||||
Maybe MaintenanceModeVersion ->
|
||||
m (Either QErr ())
|
||||
recordError sourceConfig event invocation processEventError maintenanceModeVersion =
|
||||
recordError' sourceConfig event (Just invocation) processEventError maintenanceModeVersion
|
||||
|
||||
recordError' ::
|
||||
(MonadIO m) =>
|
||||
SourceConfig ('Postgres pgKind) ->
|
||||
Event ('Postgres pgKind) ->
|
||||
Maybe (Invocation 'EventType) ->
|
||||
ProcessEventError ->
|
||||
Maybe MaintenanceModeVersion ->
|
||||
m (Either QErr ())
|
||||
recordError' sourceConfig event invocation processEventError maintenanceModeVersion =
|
||||
liftIO $
|
||||
runPgSourceWriteTx sourceConfig $ do
|
||||
insertInvocation invocation
|
||||
onJust invocation insertInvocation
|
||||
case processEventError of
|
||||
PESetRetry retryTime -> setRetryTx event retryTime maintenanceModeVersion
|
||||
PESetError -> setErrorTx event maintenanceModeVersion
|
||||
|
@ -1,3 +1,5 @@
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
|
||||
-- |
|
||||
-- = Event Triggers
|
||||
--
|
||||
@ -48,7 +50,7 @@ import Control.Concurrent.STM.TVar
|
||||
import Control.Monad.Catch (MonadMask, bracket_, finally, mask_)
|
||||
import Control.Monad.STM
|
||||
import Control.Monad.Trans.Control (MonadBaseControl)
|
||||
import Data.Aeson
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Aeson.TH
|
||||
import Data.Has
|
||||
import Data.HashMap.Strict qualified as M
|
||||
@ -85,7 +87,7 @@ newtype EventInternalErr
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance L.ToEngineLog EventInternalErr L.Hasura where
|
||||
toEngineLog (EventInternalErr qerr) = (L.LevelError, L.eventTriggerLogType, toJSON qerr)
|
||||
toEngineLog (EventInternalErr qerr) = (L.LevelError, L.eventTriggerLogType, J.toJSON qerr)
|
||||
|
||||
{- Note [Maintenance mode]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -134,18 +136,18 @@ newtype QualifiedTableStrict = QualifiedTableStrict
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance ToJSON QualifiedTableStrict where
|
||||
instance J.ToJSON QualifiedTableStrict where
|
||||
toJSON (QualifiedTableStrict (QualifiedObject sn tn)) =
|
||||
object
|
||||
[ "schema" .= sn,
|
||||
"name" .= tn
|
||||
J.object
|
||||
[ "schema" J..= sn,
|
||||
"name" J..= tn
|
||||
]
|
||||
|
||||
data EventPayload (b :: BackendType) = EventPayload
|
||||
{ epId :: EventId,
|
||||
epTable :: TableName b,
|
||||
epTrigger :: TriggerMetadata,
|
||||
epEvent :: Value,
|
||||
epEvent :: J.Value,
|
||||
epDeliveryInfo :: DeliveryInfo,
|
||||
epCreatedAt :: Time.UTCTime
|
||||
}
|
||||
@ -155,8 +157,8 @@ deriving instance Backend b => Show (EventPayload b)
|
||||
|
||||
deriving instance Backend b => Eq (EventPayload b)
|
||||
|
||||
instance Backend b => ToJSON (EventPayload b) where
|
||||
toJSON = genericToJSON hasuraJSON {omitNothingFields = True}
|
||||
instance Backend b => J.ToJSON (EventPayload b) where
|
||||
toJSON = J.genericToJSON hasuraJSON {omitNothingFields = True}
|
||||
|
||||
defaultMaxEventThreads :: Int
|
||||
defaultMaxEventThreads = 100
|
||||
@ -193,6 +195,17 @@ type BackendEventWithSource = AB.AnyBackend EventWithSource
|
||||
|
||||
type FetchEventArguments = ([BackendEventWithSource], Int, Bool)
|
||||
|
||||
pattern HttpErr :: e -> Either e (Either e' a)
|
||||
pattern HttpErr e = Left e
|
||||
|
||||
pattern TransErr :: e' -> Either e (Either e' a)
|
||||
pattern TransErr e = Right (Left e)
|
||||
|
||||
pattern Resp :: a -> Either e (Either e' a)
|
||||
pattern Resp a = Right (Right a)
|
||||
|
||||
{-# COMPLETE HttpErr, TransErr, Resp #-}
|
||||
|
||||
-- | Service events from our in-DB queue.
|
||||
--
|
||||
-- There are a few competing concerns and constraints here; we want to...
|
||||
@ -389,31 +402,37 @@ processEventQueue logger logBehavior httpMgr getSchemaCache EventEngineCtx {..}
|
||||
httpTimeout = HTTP.responseTimeoutMicro (timeoutSeconds * 1000000)
|
||||
(headers, logHeaders) = prepareHeaders logBehavior (etiHeaders eti)
|
||||
ep = createEventPayload retryConf e
|
||||
payload = encode $ toJSON ep
|
||||
payload = J.encode $ J.toJSON ep
|
||||
extraLogCtx = ExtraLogContext (epId ep) (Just $ etiName eti)
|
||||
dataTransform = mkRequestTransform <$> etiMetadataTransform eti
|
||||
dataTransform = mkRequestTransform <$> etiRequestTransform eti
|
||||
|
||||
res <- runExceptT $ do
|
||||
-- reqDetails contains the pre and post transformation
|
||||
-- request for logging purposes.
|
||||
reqDetails <- mkRequest headers httpTimeout payload dataTransform webhook
|
||||
let logger' res details = logHTTPForET res extraLogCtx details logBehavior
|
||||
-- Event Triggers have a configuration parameter called
|
||||
-- HASURA_GRAPHQL_EVENTS_HTTP_WORKERS, which is used
|
||||
-- to control the concurrency of http delivery.
|
||||
-- This bracket is used to increment and decrement an
|
||||
-- HTTP Worker EKG Gauge for the duration of the
|
||||
-- request invocation
|
||||
bracket_
|
||||
(liftIO $ EKG.Gauge.inc $ smNumEventHTTPWorkers serverMetrics)
|
||||
(liftIO $ EKG.Gauge.dec $ smNumEventHTTPWorkers serverMetrics)
|
||||
(hoistEither =<< lift (invokeRequest reqDetails logger'))
|
||||
eitherRes <-
|
||||
runExceptT $
|
||||
mkRequest headers httpTimeout payload dataTransform webhook >>= \case
|
||||
Left err -> pure $ Left err
|
||||
Right reqDetails -> do
|
||||
let logger' res details = logHTTPForET res extraLogCtx details logBehavior
|
||||
-- Event Triggers have a configuration parameter called
|
||||
-- HASURA_GRAPHQL_EVENTS_HTTP_WORKERS, which is used
|
||||
-- to control the concurrency of http delivery.
|
||||
-- This bracket is used to increment and decrement an
|
||||
-- HTTP Worker EKG Gauge for the duration of the
|
||||
-- request invocation
|
||||
resp <-
|
||||
bracket_
|
||||
(liftIO $ EKG.Gauge.inc $ smNumEventHTTPWorkers serverMetrics)
|
||||
(liftIO $ EKG.Gauge.dec $ smNumEventHTTPWorkers serverMetrics)
|
||||
(hoistEither =<< lift (invokeRequest reqDetails logger'))
|
||||
pure $ Right resp
|
||||
case eitherRes of
|
||||
Resp resp -> processSuccess sourceConfig e logHeaders ep maintenanceModeVersion resp >>= flip onLeft logQErr
|
||||
HttpErr err -> processError @b sourceConfig e retryConf logHeaders ep maintenanceModeVersion err >>= flip onLeft logQErr
|
||||
TransErr err -> do
|
||||
-- Log The Transformation Error
|
||||
L.unLogger logger $ L.UnstructuredLog L.LevelError (TBS.fromLBS $ J.encode err)
|
||||
|
||||
either
|
||||
(processError @b sourceConfig e retryConf logHeaders ep maintenanceModeVersion)
|
||||
(processSuccess sourceConfig e logHeaders ep maintenanceModeVersion)
|
||||
res
|
||||
>>= flip onLeft logQErr
|
||||
-- Record an Event Error
|
||||
recordError' @b sourceConfig e Nothing PESetError maintenanceModeVersion >>= flip onLeft logQErr
|
||||
-- removing an event from the _eeCtxLockedEvents after the event has been processed:
|
||||
removeEventTriggerEventFromLockedEvents sourceName (eId e) leEvents
|
||||
|
||||
@ -466,14 +485,14 @@ processError sourceConfig e retryConf reqHeaders ep maintenanceModeVersion err =
|
||||
let invocation = case err of
|
||||
HClient httpException ->
|
||||
let statusMaybe = getHTTPExceptionStatus httpException
|
||||
in mkInvocation ep statusMaybe reqHeaders (TBS.fromLBS (encode httpException)) []
|
||||
in mkInvocation ep statusMaybe reqHeaders (TBS.fromLBS (J.encode httpException)) []
|
||||
HStatus errResp -> do
|
||||
let respPayload = hrsBody errResp
|
||||
respHeaders = hrsHeaders errResp
|
||||
respStatus = hrsStatus errResp
|
||||
mkInvocation ep (Just respStatus) reqHeaders respPayload respHeaders
|
||||
HOther detail -> do
|
||||
let errMsg = TBS.fromLBS $ encode detail
|
||||
let errMsg = TBS.fromLBS $ J.encode detail
|
||||
mkInvocation ep (Just 500) reqHeaders errMsg []
|
||||
retryOrError <- retryOrSetError e retryConf err
|
||||
recordError @b sourceConfig e invocation retryOrError maintenanceModeVersion
|
||||
@ -524,7 +543,7 @@ mkInvocation eventPayload statusMaybe reqHeaders respBody respHeaders =
|
||||
in Invocation
|
||||
(epId eventPayload)
|
||||
statusMaybe
|
||||
(mkWebhookReq (toJSON eventPayload) reqHeaders invocationVersionET)
|
||||
(mkWebhookReq (J.toJSON eventPayload) reqHeaders invocationVersionET)
|
||||
resp
|
||||
|
||||
logQErr :: (MonadReader r m, Has (L.Logger L.Hasura) r, MonadIO m) => QErr -> m ()
|
||||
|
@ -42,8 +42,9 @@ module Hasura.Eventing.HTTP
|
||||
where
|
||||
|
||||
import Control.Exception (try)
|
||||
import Control.Lens (set)
|
||||
import Data.Aeson
|
||||
import Control.Lens (preview, set)
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Aeson.Lens
|
||||
import Data.Aeson.TH
|
||||
import Data.ByteString qualified as BS
|
||||
import Data.ByteString.Lazy qualified as LBS
|
||||
@ -60,11 +61,12 @@ import Hasura.HTTP (HttpException (..), addDefaultHeaders)
|
||||
import Hasura.Logging
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.Headers
|
||||
import Hasura.RQL.DDL.RequestTransform (RequestTransform, applyRequestTransform)
|
||||
import Hasura.RQL.DDL.RequestTransform (RequestTransform, TransformErrorBundle (..), applyRequestTransform)
|
||||
import Hasura.RQL.Types.Common (ResolvedWebhook (..))
|
||||
import Hasura.RQL.Types.EventTrigger
|
||||
import Hasura.RQL.Types.Eventing
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Session (SessionVariables)
|
||||
import Hasura.Tracing
|
||||
import Network.HTTP.Client.Transformable qualified as HTTP
|
||||
|
||||
@ -99,10 +101,10 @@ data HTTPResp (a :: TriggerTypes) = HTTPResp
|
||||
$(deriveToJSON hasuraJSON {omitNothingFields = True} ''HTTPResp)
|
||||
|
||||
instance ToEngineLog (HTTPResp 'EventType) Hasura where
|
||||
toEngineLog resp = (LevelInfo, eventTriggerLogType, toJSON resp)
|
||||
toEngineLog resp = (LevelInfo, eventTriggerLogType, J.toJSON resp)
|
||||
|
||||
instance ToEngineLog (HTTPResp 'ScheduledType) Hasura where
|
||||
toEngineLog resp = (LevelInfo, scheduledTriggerLogType, toJSON resp)
|
||||
toEngineLog resp = (LevelInfo, scheduledTriggerLogType, J.toJSON resp)
|
||||
|
||||
data HTTPErr (a :: TriggerTypes)
|
||||
= HClient !HttpException
|
||||
@ -110,26 +112,26 @@ data HTTPErr (a :: TriggerTypes)
|
||||
| HOther !String
|
||||
deriving (Show)
|
||||
|
||||
instance ToJSON (HTTPErr a) where
|
||||
instance J.ToJSON (HTTPErr a) where
|
||||
toJSON err = toObj $ case err of
|
||||
(HClient httpException) ->
|
||||
("client", toJSON httpException)
|
||||
("client", J.toJSON httpException)
|
||||
(HStatus resp) ->
|
||||
("status", toJSON resp)
|
||||
(HOther e) -> ("internal", toJSON e)
|
||||
("status", J.toJSON resp)
|
||||
(HOther e) -> ("internal", J.toJSON e)
|
||||
where
|
||||
toObj :: (Text, Value) -> Value
|
||||
toObj :: (Text, J.Value) -> J.Value
|
||||
toObj (k, v) =
|
||||
object
|
||||
[ "type" .= k,
|
||||
"detail" .= v
|
||||
J.object
|
||||
[ "type" J..= k,
|
||||
"detail" J..= v
|
||||
]
|
||||
|
||||
instance ToEngineLog (HTTPErr 'EventType) Hasura where
|
||||
toEngineLog err = (LevelError, eventTriggerLogType, toJSON err)
|
||||
toEngineLog err = (LevelError, eventTriggerLogType, J.toJSON err)
|
||||
|
||||
instance ToEngineLog (HTTPErr 'ScheduledType) Hasura where
|
||||
toEngineLog err = (LevelError, scheduledTriggerLogType, toJSON err)
|
||||
toEngineLog err = (LevelError, scheduledTriggerLogType, J.toJSON err)
|
||||
|
||||
mkHTTPResp :: HTTP.Response LBS.ByteString -> HTTPResp a
|
||||
mkHTTPResp resp =
|
||||
@ -163,41 +165,41 @@ data HTTPRespExtra (a :: TriggerTypes) = HTTPRespExtra
|
||||
_hreLogResponse :: !ResponseLogBehavior
|
||||
}
|
||||
|
||||
instance ToJSON (HTTPRespExtra a) where
|
||||
instance J.ToJSON (HTTPRespExtra a) where
|
||||
toJSON (HTTPRespExtra resp ctxt req logResp) =
|
||||
case resp of
|
||||
Left errResp ->
|
||||
object $
|
||||
[ "response" .= toJSON errResp,
|
||||
"request" .= toJSON req,
|
||||
"event_id" .= elEventId ctxt
|
||||
J.object $
|
||||
[ "response" J..= J.toJSON errResp,
|
||||
"request" J..= J.toJSON req,
|
||||
"event_id" J..= elEventId ctxt
|
||||
]
|
||||
++ eventName
|
||||
Right okResp ->
|
||||
object $
|
||||
[ "response" .= case logResp of
|
||||
LogEntireResponse -> toJSON okResp
|
||||
J.object $
|
||||
[ "response" J..= case logResp of
|
||||
LogEntireResponse -> J.toJSON okResp
|
||||
LogSanitisedResponse -> sanitisedRespJSON okResp,
|
||||
"request" .= toJSON req,
|
||||
"event_id" .= elEventId ctxt
|
||||
"request" J..= J.toJSON req,
|
||||
"event_id" J..= elEventId ctxt
|
||||
]
|
||||
++ eventName
|
||||
where
|
||||
eventName = case elEventName ctxt of
|
||||
Just name -> ["event_name" .= name]
|
||||
Just name -> ["event_name" J..= name]
|
||||
Nothing -> []
|
||||
sanitisedRespJSON v =
|
||||
Object $
|
||||
J.Object $
|
||||
HML.fromList
|
||||
[ "size" .= hrsSize v,
|
||||
"status" .= hrsStatus v
|
||||
[ "size" J..= hrsSize v,
|
||||
"status" J..= hrsStatus v
|
||||
]
|
||||
|
||||
instance ToEngineLog (HTTPRespExtra 'EventType) Hasura where
|
||||
toEngineLog resp = (LevelInfo, eventTriggerLogType, toJSON resp)
|
||||
toEngineLog resp = (LevelInfo, eventTriggerLogType, J.toJSON resp)
|
||||
|
||||
instance ToEngineLog (HTTPRespExtra 'ScheduledType) Hasura where
|
||||
toEngineLog resp = (LevelInfo, scheduledTriggerLogType, toJSON resp)
|
||||
toEngineLog resp = (LevelInfo, scheduledTriggerLogType, J.toJSON resp)
|
||||
|
||||
isNetworkError :: HTTPErr a -> Bool
|
||||
isNetworkError = \case
|
||||
@ -224,7 +226,7 @@ anyBodyParser resp = do
|
||||
data HTTPReq = HTTPReq
|
||||
{ _hrqMethod :: !String,
|
||||
_hrqUrl :: !String,
|
||||
_hrqPayload :: !(Maybe Value),
|
||||
_hrqPayload :: !(Maybe J.Value),
|
||||
_hrqTry :: !Int,
|
||||
_hrqDelay :: !(Maybe Int)
|
||||
}
|
||||
@ -233,7 +235,7 @@ data HTTPReq = HTTPReq
|
||||
$(deriveJSON hasuraJSON {omitNothingFields = True} ''HTTPReq)
|
||||
|
||||
instance ToEngineLog HTTPReq Hasura where
|
||||
toEngineLog req = (LevelInfo, eventTriggerLogType, toJSON req)
|
||||
toEngineLog req = (LevelInfo, eventTriggerLogType, J.toJSON req)
|
||||
|
||||
logHTTPForET ::
|
||||
( MonadReader r m,
|
||||
@ -278,7 +280,7 @@ mkRequest ::
|
||||
LBS.ByteString ->
|
||||
Maybe RequestTransform ->
|
||||
ResolvedWebhook ->
|
||||
m RequestDetails
|
||||
m (Either TransformErrorBundle RequestDetails)
|
||||
mkRequest headers timeout payload mRequestTransform (ResolvedWebhook webhook) =
|
||||
case HTTP.mkRequestEither webhook of
|
||||
Left excp -> throwError $ HClient $ HttpException excp
|
||||
@ -288,11 +290,20 @@ mkRequest headers timeout payload mRequestTransform (ResolvedWebhook webhook) =
|
||||
& set HTTP.headers headers
|
||||
& set HTTP.body (Just payload)
|
||||
& set HTTP.timeout timeout
|
||||
|
||||
-- TODO(Solomon) Add SessionVariables
|
||||
transformedReq = (\rt -> applyRequestTransform rt req Nothing) <$> mRequestTransform
|
||||
transformedReqSize = fmap HTTP.getReqSize transformedReq
|
||||
in pure $ RequestDetails req (LBS.length payload) transformedReq transformedReqSize
|
||||
in case mRequestTransform of
|
||||
Nothing -> pure $ Right $ RequestDetails req (LBS.length payload) Nothing Nothing
|
||||
Just reqTransform ->
|
||||
let sessionVars = do
|
||||
val <- J.decode @J.Value payload
|
||||
varVal <- preview (key "event" . key "session_variables") val
|
||||
case J.fromJSON @SessionVariables varVal of
|
||||
J.Success sessionVars' -> pure sessionVars'
|
||||
_ -> Nothing
|
||||
in case applyRequestTransform reqTransform req sessionVars of
|
||||
Left err -> pure $ Left err
|
||||
Right transformedReq ->
|
||||
let transformedReqSize = HTTP.getReqSize transformedReq
|
||||
in pure $ Right $ RequestDetails req (LBS.length payload) (Just transformedReq) (Just transformedReqSize)
|
||||
|
||||
invokeRequest ::
|
||||
( MonadReader r m,
|
||||
@ -322,7 +333,7 @@ mkClientErr message =
|
||||
let cerr = ClientError message
|
||||
in ResponseError cerr
|
||||
|
||||
mkWebhookReq :: Value -> [HeaderConf] -> InvocationVersion -> WebhookRequest
|
||||
mkWebhookReq :: J.Value -> [HeaderConf] -> InvocationVersion -> WebhookRequest
|
||||
mkWebhookReq payload headers = WebhookRequest payload headers
|
||||
|
||||
mkInvocationResp :: Maybe Int -> TBS.TByteString -> [HeaderConf] -> Response a
|
||||
|
@ -1,3 +1,5 @@
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
|
||||
-- |
|
||||
-- = Scheduled Triggers
|
||||
--
|
||||
@ -340,6 +342,17 @@ processScheduledTriggers env logger logBehavior httpMgr getSC LockedEventsCtx {.
|
||||
where
|
||||
logInternalError err = liftIO . L.unLogger logger $ ScheduledTriggerInternalErr err
|
||||
|
||||
pattern HttpErr :: e -> Either e (Either e' a)
|
||||
pattern HttpErr e = Left e
|
||||
|
||||
pattern TransErr :: e' -> Either e (Either e' a)
|
||||
pattern TransErr e = Right (Left e)
|
||||
|
||||
pattern Resp :: a -> Either e (Either e' a)
|
||||
pattern Resp a = Right (Right a)
|
||||
|
||||
{-# COMPLETE HttpErr, TransErr, Resp #-}
|
||||
|
||||
processScheduledEvent ::
|
||||
( MonadReader r m,
|
||||
Has HTTP.Manager r,
|
||||
@ -375,15 +388,24 @@ processScheduledEvent logBehavior eventId eventHeaders retryCtx payload webhookU
|
||||
extraLogCtx = ExtraLogContext eventId (sewpName payload)
|
||||
webhookReqBodyJson = J.toJSON payload
|
||||
webhookReqBody = J.encode webhookReqBodyJson
|
||||
eitherRes <- runExceptT $ do
|
||||
-- reqDetails contains the pre and post transformation
|
||||
-- request for logging purposes.
|
||||
reqDetails <- mkRequest headers httpTimeout webhookReqBody Nothing webhookUrl
|
||||
let logger e d = logHTTPForST e extraLogCtx d logBehavior
|
||||
hoistEither =<< lift (invokeRequest reqDetails logger)
|
||||
eitherRes <-
|
||||
runExceptT $
|
||||
mkRequest headers httpTimeout webhookReqBody Nothing webhookUrl >>= \case
|
||||
Left err -> pure $ Left err
|
||||
Right reqDetails -> do
|
||||
let logger e d = logHTTPForST e extraLogCtx d logBehavior
|
||||
resp <- hoistEither =<< lift (invokeRequest reqDetails logger)
|
||||
pure $ Right resp
|
||||
case eitherRes of
|
||||
Left e -> processError eventId retryCtx decodedHeaders type' webhookReqBodyJson e
|
||||
Right r -> processSuccess eventId decodedHeaders type' webhookReqBodyJson r
|
||||
Resp r -> processSuccess eventId decodedHeaders type' webhookReqBodyJson r
|
||||
HttpErr e -> processError eventId retryCtx decodedHeaders type' webhookReqBodyJson e
|
||||
TransErr e -> do
|
||||
-- Log The Transformation Error
|
||||
logger :: L.Logger L.Hasura <- asks getter
|
||||
L.unLogger logger $ L.UnstructuredLog L.LevelError (TBS.fromLBS $ J.encode e)
|
||||
|
||||
-- Set event state to Error
|
||||
setScheduledEventOp eventId (SEOpStatus SESError) type'
|
||||
where
|
||||
traceNote = "Scheduled trigger" <> foldMap ((": " <>) . triggerNameToTxt) (sewpName payload)
|
||||
|
||||
|
@ -32,6 +32,7 @@ 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 Database.PG.Query qualified as Q
|
||||
@ -486,12 +487,23 @@ callWebhook
|
||||
& set HTTP.body (Just requestBody)
|
||||
& set HTTP.timeout responseTimeout
|
||||
|
||||
transformedReq = (\rt -> applyRequestTransform rt req sessionVars) . mkRequestTransform <$> metadataTransform
|
||||
transformedPayloadSize = pure $ HTTP.getReqSize req
|
||||
actualReq = fromMaybe req transformedReq
|
||||
(transformedReq, transformedReqSize) <- case metadataTransform of
|
||||
Nothing -> pure (Nothing, Nothing)
|
||||
Just transform' ->
|
||||
case applyRequestTransform (mkRequestTransform transform') req sessionVars of
|
||||
Left err -> do
|
||||
-- Log The Transformation Error
|
||||
logger :: L.Logger L.Hasura <- asks getter
|
||||
L.unLogger logger $ L.UnstructuredLog L.LevelError (TBS.fromLBS $ J.encode err)
|
||||
|
||||
-- Throw an exception with the Transformation Error
|
||||
throw500WithDetail "Request Transformation Failed" $ J.toJSON err
|
||||
Right transformedReq ->
|
||||
let transformedPayloadSize = HTTP.getReqSize transformedReq
|
||||
in pure (Just transformedReq, Just transformedPayloadSize)
|
||||
|
||||
httpResponse <-
|
||||
Tracing.tracedHttpRequest actualReq $ \request ->
|
||||
Tracing.tracedHttpRequest (fromMaybe req transformedReq) $ \request ->
|
||||
liftIO . try $ HTTP.performRequest request manager
|
||||
|
||||
let requestInfo =
|
||||
@ -512,7 +524,7 @@ callWebhook
|
||||
|
||||
-- log the request and response to/from the action handler
|
||||
logger :: (L.Logger L.Hasura) <- asks getter
|
||||
L.unLogger logger $ ActionHandlerLog req transformedReq requestBodySize transformedPayloadSize responseBodySize actionName
|
||||
L.unLogger logger $ ActionHandlerLog req transformedReq requestBodySize transformedReqSize responseBodySize actionName
|
||||
|
||||
case J.eitherDecode responseBody of
|
||||
Left e -> do
|
||||
|
@ -47,7 +47,7 @@ data CreateAction = CreateAction
|
||||
{ _caName :: !ActionName,
|
||||
_caDefinition :: !ActionDefinitionInput,
|
||||
_caComment :: !(Maybe Text),
|
||||
_caTransform :: !(Maybe MetadataTransform)
|
||||
_caRequestTransform :: !(Maybe MetadataTransform)
|
||||
}
|
||||
|
||||
$(J.deriveJSON hasuraJSON ''CreateAction)
|
||||
@ -70,7 +70,7 @@ runCreateAction createAction = do
|
||||
(_caComment createAction)
|
||||
(_caDefinition createAction)
|
||||
[]
|
||||
(_caTransform createAction)
|
||||
(_caRequestTransform createAction)
|
||||
buildSchemaCacheFor (MOAction actionName) $
|
||||
MetadataModifier $
|
||||
metaActions %~ OMap.insert actionName metadata
|
||||
@ -169,7 +169,7 @@ runUpdateAction (UpdateAction actionName actionDefinition actionComment transfor
|
||||
MetadataModifier $
|
||||
(metaActions . ix actionName . amDefinition .~ def)
|
||||
. (metaActions . ix actionName . amComment .~ comment)
|
||||
. (metaActions . ix actionName . amMetadataTransform .~ transform)
|
||||
. (metaActions . ix actionName . amRequestTransform .~ transform)
|
||||
|
||||
newtype ClearActionData = ClearActionData {unClearActionData :: Bool}
|
||||
deriving (Show, Eq, J.FromJSON, J.ToJSON)
|
||||
|
@ -48,7 +48,7 @@ data CreateEventTriggerQuery (b :: BackendType) = CreateEventTriggerQuery
|
||||
_cetqWebhookFromEnv :: !(Maybe Text),
|
||||
_cetqHeaders :: !(Maybe [HeaderConf]),
|
||||
_cetqReplace :: !Bool,
|
||||
_cetqMetadataTransform :: !(Maybe MetadataTransform)
|
||||
_cetqRequestTransform :: !(Maybe MetadataTransform)
|
||||
}
|
||||
|
||||
instance Backend b => FromJSON (CreateEventTriggerQuery b) where
|
||||
@ -65,7 +65,7 @@ instance Backend b => FromJSON (CreateEventTriggerQuery b) where
|
||||
webhookFromEnv <- o .:? "webhook_from_env"
|
||||
headers <- o .:? "headers"
|
||||
replace <- o .:? "replace" .!= False
|
||||
transform <- o .:? "transform"
|
||||
requestTransform <- o .:? "request_transform"
|
||||
let regex = "^[A-Za-z]+[A-Za-z0-9_\\-]*$" :: LBS.ByteString
|
||||
compiledRegex = TDFA.makeRegex regex :: TDFA.Regex
|
||||
isMatch = TDFA.match compiledRegex . T.unpack $ triggerNameToTxt name
|
||||
@ -81,7 +81,7 @@ instance Backend b => FromJSON (CreateEventTriggerQuery b) where
|
||||
(Just _, Just _) -> fail "only one of webhook or webhook_from_env should be given"
|
||||
_ -> fail "must provide webhook or webhook_from_env"
|
||||
mapM_ checkEmptyCols [insert, update, delete]
|
||||
return $ CreateEventTriggerQuery sourceName name table insert update delete (Just enableManual) retryConf webhook webhookFromEnv headers replace transform
|
||||
return $ CreateEventTriggerQuery sourceName name table insert update delete (Just enableManual) retryConf webhook webhookFromEnv headers replace requestTransform
|
||||
where
|
||||
checkEmptyCols spec =
|
||||
case spec of
|
||||
|
@ -10,7 +10,7 @@ module Hasura.RQL.DDL.Metadata
|
||||
runDropInconsistentMetadata,
|
||||
runGetCatalogState,
|
||||
runSetCatalogState,
|
||||
runValidateWebhookTransform,
|
||||
runTestWebhookTransform,
|
||||
runSetMetricsConfig,
|
||||
runRemoveMetricsConfig,
|
||||
module Hasura.RQL.DDL.Metadata.Types,
|
||||
@ -18,9 +18,8 @@ module Hasura.RQL.DDL.Metadata
|
||||
where
|
||||
|
||||
import Control.Lens ((.~), (^.), (^?))
|
||||
import Data.Aeson
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Aeson.Ordered qualified as AO
|
||||
import Data.Bifunctor (bimap)
|
||||
import Data.CaseInsensitive qualified as CI
|
||||
import Data.Has (Has, getter)
|
||||
import Data.HashMap.Strict qualified as Map
|
||||
@ -397,11 +396,11 @@ runGetInconsistentMetadata _ = do
|
||||
inconsObjs <- scInconsistentObjs <$> askSchemaCache
|
||||
return $ encJFromJValue $ formatInconsistentObjs inconsObjs
|
||||
|
||||
formatInconsistentObjs :: [InconsistentMetadata] -> Value
|
||||
formatInconsistentObjs :: [InconsistentMetadata] -> J.Value
|
||||
formatInconsistentObjs inconsObjs =
|
||||
object
|
||||
[ "is_consistent" .= null inconsObjs,
|
||||
"inconsistent_objects" .= inconsObjs
|
||||
J.object
|
||||
[ "is_consistent" J..= null inconsObjs,
|
||||
"inconsistent_objects" J..= inconsObjs
|
||||
]
|
||||
|
||||
runDropInconsistentMetadata ::
|
||||
@ -426,7 +425,7 @@ runDropInconsistentMetadata _ = do
|
||||
unless (null droppableInconsistentObjects) $
|
||||
throwError
|
||||
(err400 Unexpected "cannot continue due to new inconsistent metadata")
|
||||
{ qeInternal = Just $ ExtraInternal $ toJSON newInconsistentObjects
|
||||
{ qeInternal = Just $ ExtraInternal $ J.toJSON newInconsistentObjects
|
||||
}
|
||||
return successMsg
|
||||
|
||||
@ -490,26 +489,36 @@ runRemoveMetricsConfig = do
|
||||
metaMetricsConfig .~ emptyMetricsConfig
|
||||
pure successMsg
|
||||
|
||||
runValidateWebhookTransform ::
|
||||
runTestWebhookTransform ::
|
||||
forall m.
|
||||
( QErrM m,
|
||||
MonadIO m
|
||||
) =>
|
||||
ValidateWebhookTransform ->
|
||||
TestWebhookTransform ->
|
||||
m EncJSON
|
||||
runValidateWebhookTransform (ValidateWebhookTransform url payload mt) = do
|
||||
runTestWebhookTransform (TestWebhookTransform url payload mt sv) = do
|
||||
initReq <- liftIO $ HTTP.mkRequestThrow url
|
||||
let req = initReq & HTTP.body .~ pure (encode payload)
|
||||
dataTransform = mkRequestTransformDebug mt
|
||||
let req = initReq & HTTP.body .~ pure (J.encode payload)
|
||||
dataTransform = mkRequestTransform mt
|
||||
-- TODO(Solomon) Add SessionVariables
|
||||
transformed = applyRequestTransform dataTransform req Nothing
|
||||
payload' = decode @Value =<< (transformed ^. HTTP.body)
|
||||
headers' = bimap CI.foldedCase id <$> (transformed ^. HTTP.headers)
|
||||
pure $
|
||||
encJFromJValue $
|
||||
object
|
||||
[ "webhook_url" .= (transformed ^. HTTP.url),
|
||||
"method" .= (transformed ^. HTTP.method),
|
||||
"headers" .= headers',
|
||||
"payload" .= payload'
|
||||
]
|
||||
transformedE = applyRequestTransform dataTransform req sv
|
||||
|
||||
case transformedE of
|
||||
Right transformed ->
|
||||
pure $
|
||||
encJFromJValue $
|
||||
J.object
|
||||
[ "webhook_url" J..= (transformed ^. HTTP.url),
|
||||
"method" J..= (transformed ^. HTTP.method),
|
||||
"headers" J..= (first CI.foldedCase <$> (transformed ^. HTTP.headers)),
|
||||
"body" J..= (J.decode @J.Value =<< (transformed ^. HTTP.body))
|
||||
]
|
||||
Left err ->
|
||||
pure $
|
||||
encJFromJValue $
|
||||
J.object
|
||||
[ "webhook_url" J..= (req ^. HTTP.url),
|
||||
"method" J..= (req ^. HTTP.method),
|
||||
"headers" J..= (first CI.foldedCase <$> (req ^. HTTP.headers)),
|
||||
"body" J..= J.toJSON err
|
||||
]
|
||||
|
@ -13,7 +13,7 @@ module Hasura.RQL.DDL.Metadata.Types
|
||||
ReplaceMetadataV1 (..),
|
||||
ReplaceMetadataV2 (..),
|
||||
AllowInconsistentMetadata (..),
|
||||
ValidateWebhookTransform (..),
|
||||
TestWebhookTransform (..),
|
||||
)
|
||||
where
|
||||
|
||||
@ -23,6 +23,7 @@ import Data.HashMap.Strict qualified as H
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.RequestTransform (MetadataTransform)
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Session (SessionVariables)
|
||||
|
||||
data ClearMetadata
|
||||
= ClearMetadata
|
||||
@ -169,7 +170,7 @@ data ReplaceMetadata
|
||||
|
||||
instance FromJSON ReplaceMetadata where
|
||||
parseJSON = withObject "ReplaceMetadata" $ \o -> do
|
||||
if (H.member "metadata" o)
|
||||
if H.member "metadata" o
|
||||
then RMReplaceMetadataV2 <$> parseJSON (Object o)
|
||||
else RMReplaceMetadataV1 <$> parseJSON (Object o)
|
||||
|
||||
@ -178,24 +179,27 @@ instance ToJSON ReplaceMetadata where
|
||||
RMReplaceMetadataV1 v1 -> toJSON v1
|
||||
RMReplaceMetadataV2 v2 -> toJSON v2
|
||||
|
||||
data ValidateWebhookTransform = ValidateWebhookTransform
|
||||
{ _vwtWebhookUrl :: Text,
|
||||
_vwtPayload :: Value,
|
||||
_vwtTransformer :: MetadataTransform
|
||||
data TestWebhookTransform = TestWebhookTransform
|
||||
{ _twtWebhookUrl :: Text,
|
||||
_twtPayload :: Value,
|
||||
_twtTransformer :: MetadataTransform,
|
||||
_twtSessionVariables :: Maybe SessionVariables
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
instance FromJSON ValidateWebhookTransform where
|
||||
parseJSON = withObject "ValidateWebhookTransform" $ \o -> do
|
||||
instance FromJSON TestWebhookTransform where
|
||||
parseJSON = withObject "TestWebhookTransform" $ \o -> do
|
||||
url <- o .: "webhook_url"
|
||||
payload <- o .: "payload"
|
||||
transformer <- o .: "transformer"
|
||||
pure $ ValidateWebhookTransform url payload transformer
|
||||
payload <- o .: "body"
|
||||
transformer <- o .: "request_transform"
|
||||
sessionVars <- o .:? "session_variables"
|
||||
pure $ TestWebhookTransform url payload transformer sessionVars
|
||||
|
||||
instance ToJSON ValidateWebhookTransform where
|
||||
toJSON (ValidateWebhookTransform url payload mt) =
|
||||
instance ToJSON TestWebhookTransform where
|
||||
toJSON (TestWebhookTransform url payload mt sv) =
|
||||
object
|
||||
[ "webhook_url" .= url,
|
||||
"payload" .= payload,
|
||||
"transfromer" .= mt
|
||||
"body" .= payload,
|
||||
"request_transform" .= mt,
|
||||
"session_variables" .= sv
|
||||
]
|
||||
|
@ -1,30 +1,35 @@
|
||||
module Hasura.RQL.DDL.RequestTransform
|
||||
( applyRequestTransform,
|
||||
mkRequestTransform,
|
||||
mkRequestTransformDebug,
|
||||
RequestMethod (..),
|
||||
TemplatingEngine (..),
|
||||
TemplateText (..),
|
||||
ContentType (..),
|
||||
TransformHeaders (..),
|
||||
TransformErrorBundle (..),
|
||||
MetadataTransform (..),
|
||||
RequestTransform (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Lens (over)
|
||||
import Control.Lens (traverseOf, view)
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Bifunctor (bimap)
|
||||
import Data.Bifunctor (bimap, first)
|
||||
import Data.Bitraversable
|
||||
import Data.ByteString.Lazy qualified as BL
|
||||
import Data.CaseInsensitive qualified as CI
|
||||
import Data.Either.Validation
|
||||
import Data.HashMap.Strict qualified as M
|
||||
import Data.List (nubBy)
|
||||
import Data.Text qualified as T
|
||||
import Data.Text.Encoding qualified as TE
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
import Hasura.Prelude hiding (first)
|
||||
import Hasura.Session (SessionVariables)
|
||||
import Kriti (runKriti)
|
||||
import Kriti (RenderedError (..), runKriti)
|
||||
import Kriti.Parser (parserAndLexer)
|
||||
import Network.HTTP.Client.Transformable qualified as HTTP
|
||||
import Network.URI qualified as URI
|
||||
|
||||
{-
|
||||
|
||||
@ -44,23 +49,98 @@ will return the original request body.
|
||||
|
||||
-}
|
||||
|
||||
-- | A set of data transformation functions and substitutions generated from a
|
||||
-- MetadataTransform
|
||||
-------------
|
||||
--- Types ---
|
||||
-------------
|
||||
|
||||
data TransformContext = TransformContext
|
||||
{ tcUrl :: J.Value,
|
||||
tcBody :: J.Value,
|
||||
tcSessionVars :: J.Value,
|
||||
tcQueryParams :: J.Value
|
||||
}
|
||||
|
||||
-- | A set of data transformation functions and substitutions
|
||||
-- generated from a MetadataTransform. Nothing values mean use the
|
||||
-- original request value, unless otherwise indicated.
|
||||
data RequestTransform = RequestTransform
|
||||
{ -- | Change the request method to one provided here. Nothing means POST.
|
||||
rtRequestMethod :: Maybe RequestMethod,
|
||||
-- | Change the request URL to one provided here. Nothing means original URL.
|
||||
rtRequestURL :: Maybe Text,
|
||||
-- | A function for transforming the request body using a Kriti template.
|
||||
rtBodyTransform :: J.Value -> Maybe SessionVariables -> J.Value,
|
||||
-- | Change content type to one provided here. Nothing means use original.
|
||||
-- | A function which contructs a new URL given the request context.
|
||||
rtRequestURL :: Maybe (TransformContext -> Either TransformErrorBundle Text),
|
||||
-- | A function for transforming the request body.
|
||||
rtBody :: Maybe (TransformContext -> Either TransformErrorBundle J.Value),
|
||||
-- | Change content type to one provided here.
|
||||
rtContentType :: Maybe ContentType,
|
||||
-- | Change Request query params to those provided here. Nothing means use original.
|
||||
rtQueryParams :: Maybe HTTP.Query,
|
||||
-- | A transformation function for modifying the Request Headers.
|
||||
rtRequestHeaders :: [HTTP.Header] -> [HTTP.Header]
|
||||
-- | A function which contructs new query parameters given the request context.
|
||||
rtQueryParams :: Maybe (TransformContext -> Either TransformErrorBundle HTTP.Query),
|
||||
-- | A function which contructs new Headers given the request context.
|
||||
rtRequestHeaders :: Maybe (TransformContext -> Either TransformErrorBundle ([HTTP.Header] -> [HTTP.Header]))
|
||||
}
|
||||
|
||||
-- | A de/serializable request transformation template which can be stored in
|
||||
-- the metadata associated with an action/event trigger/etc. and used to produce
|
||||
-- a 'RequestTransform'.
|
||||
--
|
||||
-- NOTE: This data type is _only_ intended to be parsed from and stored as user
|
||||
-- metadata; no direct logical transformations should be done upon it.
|
||||
--
|
||||
-- NOTE: Users should convert this to 'RequestTransform' as close as possible to
|
||||
-- the call site that performs a transformed HTTP request, as 'RequestTransform'
|
||||
-- has a representation that makes it more difficult to deserialize for
|
||||
-- debugging.
|
||||
--
|
||||
-- Nothing values mean use the original request value, unless
|
||||
-- otherwise indicated.
|
||||
data MetadataTransform = MetadataTransform
|
||||
{ -- | Change the request method to one provided here. Nothing means POST.
|
||||
mtRequestMethod :: Maybe RequestMethod,
|
||||
-- | Template script for transforming the URL.
|
||||
mtRequestURL :: Maybe TemplateText,
|
||||
-- | Template script for transforming the request body.
|
||||
mtBodyTransform :: Maybe TemplateText,
|
||||
-- | Replace the Content-Type with this value. Only
|
||||
-- "application/json" and "application/x-www-form-urlencoded" are
|
||||
-- allowed.
|
||||
mtContentType :: Maybe ContentType,
|
||||
-- | A list of template scripts for constructing new Query Params.
|
||||
mtQueryParams :: Maybe [(TemplateText, Maybe TemplateText)],
|
||||
-- | Transform headers as defined here.
|
||||
mtRequestHeaders :: Maybe TransformHeaders,
|
||||
-- | The template engine to use for transformations. Default: Kriti
|
||||
mtTemplatingEngine :: TemplatingEngine
|
||||
}
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
instance NFData MetadataTransform
|
||||
|
||||
instance Cacheable MetadataTransform
|
||||
|
||||
instance J.ToJSON MetadataTransform where
|
||||
toJSON MetadataTransform {..} =
|
||||
J.object
|
||||
[ "method" J..= mtRequestMethod,
|
||||
"url" J..= mtRequestURL,
|
||||
"body" J..= mtBodyTransform,
|
||||
"content_type" J..= mtContentType,
|
||||
"query_params" J..= fmap M.fromList mtQueryParams,
|
||||
"request_headers" J..= mtRequestHeaders,
|
||||
"template_engine" J..= mtTemplatingEngine
|
||||
]
|
||||
|
||||
instance J.FromJSON MetadataTransform where
|
||||
parseJSON = J.withObject "Object" $ \o -> do
|
||||
method <- o J..:? "method"
|
||||
url <- o J..:? "url"
|
||||
body <- o J..:? "body"
|
||||
contentType <- o J..:? "content_type"
|
||||
queryParams' <- o J..:? "query_params"
|
||||
let queryParams = fmap M.toList queryParams'
|
||||
headers <- o J..:? "request_headers"
|
||||
templateEngine <- o J..:? "template_engine"
|
||||
let templateEngine' = fromMaybe Kriti templateEngine
|
||||
pure $ MetadataTransform method url body contentType queryParams headers templateEngine'
|
||||
|
||||
data RequestMethod = GET | POST | PUT | PATCH | DELETE
|
||||
deriving (Show, Eq, Enum, Bounded, Generic)
|
||||
|
||||
@ -88,63 +168,6 @@ instance NFData RequestMethod
|
||||
|
||||
instance Cacheable RequestMethod
|
||||
|
||||
-- | A de/serializable request transformation template which can be stored in
|
||||
-- the metadata associated with an action/event trigger/etc. and used to produce
|
||||
-- a 'RequestTransform'.
|
||||
--
|
||||
-- NOTE: This data type is _only_ intended to be parsed from and stored as user
|
||||
-- metadata; no direct logical transformations should be done upon it.
|
||||
--
|
||||
-- NOTE: Users should convert this to 'RequestTransform' as close as possible to
|
||||
-- the call site that performs a transformed HTTP request, as 'RequestTransform'
|
||||
-- has a representation that makes it more difficult to deserialize for
|
||||
-- debugging.
|
||||
data MetadataTransform = MetadataTransform
|
||||
{ -- | Change the request method to one provided here. Nothing means POST.
|
||||
mtRequestMethod :: Maybe RequestMethod,
|
||||
-- | Change the request URL to one provided here. Nothing means original URL.
|
||||
mtRequestURL :: Maybe Text,
|
||||
-- | Go-Basic template script for transforming the request body
|
||||
mtBodyTransform :: Maybe TemplateText,
|
||||
-- | Only the following Content-Types are allowed (default: application/json):
|
||||
mtContentType :: Maybe ContentType,
|
||||
-- | Replace any existing query params with those provided here
|
||||
mtQueryParams :: Maybe (M.HashMap Text (Maybe Text)),
|
||||
-- | Transform headers as defined here.
|
||||
mtRequestHeaders :: Maybe TransformHeaders,
|
||||
-- | The template engine to use for transformations. Default: Kriti
|
||||
mtTemplatingEngine :: TemplatingEngine
|
||||
}
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
instance NFData MetadataTransform
|
||||
|
||||
instance Cacheable MetadataTransform
|
||||
|
||||
instance J.ToJSON MetadataTransform where
|
||||
toJSON MetadataTransform {..} =
|
||||
J.object
|
||||
[ "method" J..= mtRequestMethod,
|
||||
"url" J..= mtRequestURL,
|
||||
"body" J..= mtBodyTransform,
|
||||
"content_type" J..= mtContentType,
|
||||
"query_params" J..= mtQueryParams,
|
||||
"request_headers" J..= mtRequestHeaders,
|
||||
"template_engine" J..= mtTemplatingEngine
|
||||
]
|
||||
|
||||
instance J.FromJSON MetadataTransform where
|
||||
parseJSON = J.withObject "Object" $ \o -> do
|
||||
method <- o J..:? "method"
|
||||
url <- o J..:? "url"
|
||||
body <- o J..:? "body"
|
||||
contentType <- o J..:? "content_type"
|
||||
queryParams <- o J..:? "query_params"
|
||||
headers <- o J..:? "request_headers"
|
||||
templateEngine <- o J..:? "template_engine"
|
||||
let templateEngine' = maybe Kriti id templateEngine
|
||||
pure $ MetadataTransform method url body contentType queryParams headers templateEngine'
|
||||
|
||||
-- | Available Template Languages
|
||||
data TemplatingEngine = Kriti
|
||||
deriving (Show, Eq, Enum, Bounded, Generic)
|
||||
@ -164,15 +187,20 @@ instance NFData TemplatingEngine
|
||||
|
||||
instance Cacheable TemplatingEngine
|
||||
|
||||
newtype TemplateText = TemplateText T.Text
|
||||
deriving (Show, Eq, Generic)
|
||||
newtype TemplateText = TemplateText {unTemplateText :: T.Text}
|
||||
deriving (Show, Eq, Ord, Hashable, Generic, J.ToJSONKey, J.FromJSONKey)
|
||||
|
||||
instance NFData TemplateText
|
||||
|
||||
instance Cacheable TemplateText
|
||||
|
||||
instance J.FromJSON TemplateText where
|
||||
parseJSON = J.withText "TemplateText" (pure . TemplateText)
|
||||
parseJSON = J.withText "TemplateText" \t ->
|
||||
case parserAndLexer t of
|
||||
-- TODO: Use the parsed ValueExt in MetadataTransform so that we
|
||||
-- don't have to parse at every request.
|
||||
Right _ -> pure $ TemplateText t
|
||||
Left RenderedError {_message} -> fail $ T.unpack _message
|
||||
|
||||
instance J.ToJSON TemplateText where
|
||||
toJSON = J.String . coerce
|
||||
@ -213,7 +241,7 @@ instance J.FromJSON HeaderKey where
|
||||
key -> pure $ HeaderKey key
|
||||
|
||||
data TransformHeaders = TransformHeaders
|
||||
{ addHeaders :: [(CI.CI Text, Text)],
|
||||
{ addHeaders :: [(CI.CI Text, TemplateText)],
|
||||
removeHeaders :: [CI.CI Text]
|
||||
}
|
||||
deriving (Show, Eq, Ord, Generic)
|
||||
@ -231,54 +259,181 @@ instance J.ToJSON TransformHeaders where
|
||||
|
||||
instance J.FromJSON TransformHeaders where
|
||||
parseJSON = J.withObject "TransformHeaders" $ \o -> do
|
||||
addHeaders :: M.HashMap Text Text <- fromMaybe mempty <$> o J..:? "add_headers"
|
||||
addHeaders :: M.HashMap Text TemplateText <- fromMaybe mempty <$> o J..:? "add_headers"
|
||||
let headers = M.toList $ mapKeys CI.mk addHeaders
|
||||
removeHeaders <- o J..:? "remove_headers"
|
||||
let removeHeaders' = unHeaderKey <$> fromMaybe mempty removeHeaders
|
||||
pure $ TransformHeaders headers removeHeaders'
|
||||
|
||||
newtype TransformErrorBundle = TransformErrorBundle {teMessages :: [J.Value]}
|
||||
deriving stock (Show, Eq, Generic)
|
||||
deriving newtype (Semigroup, Monoid, J.ToJSON)
|
||||
|
||||
instance NFData TransformErrorBundle
|
||||
|
||||
instance Cacheable TransformErrorBundle
|
||||
|
||||
---------------------------------------
|
||||
--- Constructing Request Transforms ---
|
||||
---------------------------------------
|
||||
|
||||
-- | Construct a `RequestTransform` from its metadata representation.
|
||||
mkRequestTransform :: MetadataTransform -> RequestTransform
|
||||
mkRequestTransform MetadataTransform {..} =
|
||||
let transformParams = fmap (bimap TE.encodeUtf8 (fmap TE.encodeUtf8)) . M.toList
|
||||
queryParams = transformParams <$> mtQueryParams
|
||||
headerTransform = maybe id mkHeaderTransform mtRequestHeaders
|
||||
in RequestTransform mtRequestMethod mtRequestURL (mkBodyTransform mtBodyTransform mtTemplatingEngine) mtContentType queryParams headerTransform
|
||||
let urlTransform = mkUrlTransform mtTemplatingEngine <$> mtRequestURL
|
||||
queryTransform = mkQueryParamsTransform mtTemplatingEngine <$> mtQueryParams
|
||||
headerTransform = mkHeaderTransform mtTemplatingEngine <$> mtRequestHeaders
|
||||
bodyTransform = mkTemplateTransform mtTemplatingEngine <$> mtBodyTransform
|
||||
in RequestTransform mtRequestMethod urlTransform bodyTransform mtContentType queryTransform headerTransform
|
||||
|
||||
mkRequestTransformDebug :: MetadataTransform -> RequestTransform
|
||||
mkRequestTransformDebug mt@MetadataTransform {mtBodyTransform, mtTemplatingEngine} =
|
||||
(mkRequestTransform mt) {rtBodyTransform = mkBodyTransform mtBodyTransform mtTemplatingEngine}
|
||||
mkQueryParamsTransform :: TemplatingEngine -> [(TemplateText, Maybe TemplateText)] -> TransformContext -> Either TransformErrorBundle HTTP.Query
|
||||
mkQueryParamsTransform engine templates transformCtx =
|
||||
let transform t =
|
||||
case mkTemplateTransform engine t transformCtx of
|
||||
Left err -> Left err
|
||||
Right (J.String str) -> Right $ str
|
||||
Right (J.Number num) -> Right $ tshow num
|
||||
Right (J.Bool True) -> Right $ "true"
|
||||
Right (J.Bool False) -> Right $ "false"
|
||||
Right val ->
|
||||
Left $
|
||||
TransformErrorBundle $
|
||||
pure $
|
||||
J.object
|
||||
[ "error_code" J..= J.String "TransformationError",
|
||||
"message" J..= J.String "Query Param Transforms must produce a String, Number, or Boolean value",
|
||||
"value" J..= val
|
||||
]
|
||||
toValidation :: [(Either TransformErrorBundle T.Text, Maybe (Either TransformErrorBundle T.Text))] -> [(Validation TransformErrorBundle T.Text, Maybe (Validation TransformErrorBundle T.Text))]
|
||||
toValidation = fmap (bimap eitherToValidation (fmap eitherToValidation))
|
||||
|
||||
-- | Construct a Header Transformation function from a `TransformHeaders` value.
|
||||
mkHeaderTransform :: TransformHeaders -> [HTTP.Header] -> [HTTP.Header]
|
||||
mkHeaderTransform TransformHeaders {..} headers =
|
||||
let toBeRemoved = (fmap . CI.map) (TE.encodeUtf8) removeHeaders
|
||||
filteredHeaders = filter ((`notElem` toBeRemoved) . fst) headers
|
||||
newHeaders = fmap (bimap (CI.map TE.encodeUtf8) TE.encodeUtf8) addHeaders
|
||||
in newHeaders <> filteredHeaders
|
||||
collectErrors :: [(Either TransformErrorBundle T.Text, Maybe (Either TransformErrorBundle T.Text))] -> Validation TransformErrorBundle [(T.Text, Maybe T.Text)]
|
||||
collectErrors xs = traverse (bitraverse id sequenceA) (toValidation xs)
|
||||
|
||||
-- | Construct a Body Transformation function
|
||||
mkBodyTransform :: Maybe TemplateText -> TemplatingEngine -> J.Value -> Maybe SessionVariables -> J.Value
|
||||
mkBodyTransform Nothing _ source _ = source
|
||||
mkBodyTransform (Just (TemplateText template)) engine source sessionVars =
|
||||
let context = [("$", source)] <> catMaybes [("$session",) . J.toJSON <$> sessionVars]
|
||||
results = fmap (bimap transform (fmap transform)) templates
|
||||
collectedResults = validationToEither $ collectErrors results
|
||||
in (fmap . fmap) (bimap TE.encodeUtf8 (fmap TE.encodeUtf8)) collectedResults
|
||||
|
||||
-- | Given a `TransformHeaders` and the `TransformContext`, Construct a
|
||||
-- function to transform the existing headers.
|
||||
mkHeaderTransform :: TemplatingEngine -> TransformHeaders -> TransformContext -> Either TransformErrorBundle ([HTTP.Header] -> [HTTP.Header])
|
||||
mkHeaderTransform engine TransformHeaders {..} transformCtx =
|
||||
let transform t =
|
||||
case mkTemplateTransform engine t transformCtx of
|
||||
Left err -> Failure err
|
||||
Right (J.String str) -> Success $ TE.encodeUtf8 str
|
||||
Right (J.Number num) -> Success $ TE.encodeUtf8 $ tshow num
|
||||
Right (J.Bool True) -> Success "true"
|
||||
Right (J.Bool False) -> Success "false"
|
||||
Right val ->
|
||||
Failure $
|
||||
TransformErrorBundle $
|
||||
pure $
|
||||
J.object
|
||||
[ "error_code" J..= J.String "TransformationError",
|
||||
"message" J..= J.String ("Header Transforms must produce a String, Number, or Boolean value: " <> tshow val)
|
||||
]
|
||||
|
||||
failIfCT key =
|
||||
if CI.foldedCase key == "content-type"
|
||||
then
|
||||
Failure $
|
||||
TransformErrorBundle $
|
||||
pure $
|
||||
J.object
|
||||
[ "error_code" J..= J.String "TransformationError",
|
||||
"message" J..= J.String ("Header Transforms cannot add Content-Type" <> CI.original key)
|
||||
]
|
||||
else Success $ CI.map TE.encodeUtf8 key
|
||||
|
||||
toBeAdded = traverse (bitraverse failIfCT transform) addHeaders
|
||||
toBeRemoved = (fmap . CI.map) TE.encodeUtf8 removeHeaders
|
||||
|
||||
filterHeaders h = filter ((`notElem` toBeRemoved) . fst) h
|
||||
in case toBeAdded of
|
||||
Failure err -> throwError err
|
||||
Success toBeAdded' -> pure (filterHeaders . (toBeAdded' <>))
|
||||
|
||||
mkUrlTransform :: TemplatingEngine -> TemplateText -> TransformContext -> Either TransformErrorBundle Text
|
||||
mkUrlTransform engine template transformCtx =
|
||||
case mkTemplateTransform engine template transformCtx of
|
||||
Left err -> Left err
|
||||
Right (J.String url) ->
|
||||
case URI.parseURI (T.unpack url) of
|
||||
Just _ -> Right url
|
||||
Nothing ->
|
||||
Left $
|
||||
TransformErrorBundle $
|
||||
pure $
|
||||
J.object
|
||||
[ "error_code" J..= J.String "TransformationError",
|
||||
-- TODO: This error message is not very
|
||||
-- helpful. We should find a way to identity what
|
||||
-- is wrong with the URL.
|
||||
"message" J..= J.String ("Invalid URL: " <> url)
|
||||
]
|
||||
Right val ->
|
||||
Left $
|
||||
TransformErrorBundle $
|
||||
pure $
|
||||
J.object
|
||||
[ "error_code" J..= J.String "TransformationError",
|
||||
"message" J..= J.String ("Url Transforms must produce a String value: " <> tshow val)
|
||||
]
|
||||
|
||||
-- | Construct a Template Transformation function
|
||||
mkTemplateTransform :: TemplatingEngine -> TemplateText -> TransformContext -> Either TransformErrorBundle J.Value
|
||||
mkTemplateTransform engine (TemplateText template) TransformContext {..} =
|
||||
let context = [("$url", tcUrl), ("$body", tcBody), ("$session_vars", tcSessionVars), ("$query_params", tcQueryParams)]
|
||||
in case engine of
|
||||
Kriti -> case runKriti template context of
|
||||
Left err -> J.toJSON err
|
||||
Right res -> res
|
||||
Kriti -> first (TransformErrorBundle . pure . J.toJSON) $ runKriti template context
|
||||
|
||||
buildTransformContext :: HTTP.Request -> Maybe SessionVariables -> TransformContext
|
||||
buildTransformContext reqData sessionVars =
|
||||
TransformContext
|
||||
{ tcUrl = J.toJSON $ view HTTP.url reqData,
|
||||
tcBody = fromMaybe J.Null $ J.decode @J.Value =<< view HTTP.body reqData,
|
||||
tcSessionVars = J.toJSON sessionVars,
|
||||
tcQueryParams = J.toJSON $ bimap TE.decodeUtf8 (fmap TE.decodeUtf8) <$> view HTTP.queryParams reqData
|
||||
}
|
||||
|
||||
-- | Transform an `HTTP.Request` with a `RequestTransform`.
|
||||
applyRequestTransform :: RequestTransform -> HTTP.Request -> Maybe SessionVariables -> HTTP.Request
|
||||
applyRequestTransform :: RequestTransform -> HTTP.Request -> Maybe SessionVariables -> Either TransformErrorBundle HTTP.Request
|
||||
applyRequestTransform RequestTransform {..} reqData sessionVars =
|
||||
let method = fmap (TE.encodeUtf8 . renderRequestMethod) rtRequestMethod
|
||||
bodyFunc (Just b) = case J.decode @J.Value b of
|
||||
Just val -> pure $ J.encode $ rtBodyTransform val sessionVars
|
||||
Nothing -> pure b
|
||||
bodyFunc Nothing = Nothing
|
||||
contentType = maybe "application/json" (TE.encodeUtf8 . renderContentType) rtContentType
|
||||
headerFunc = nubBy (\a b -> fst a == fst b) . (:) ("Content-Type", contentType) . rtRequestHeaders
|
||||
in reqData & over HTTP.url (`fromMaybe` rtRequestURL)
|
||||
& over HTTP.method (`fromMaybe` method)
|
||||
& over HTTP.queryParams (`fromMaybe` rtQueryParams)
|
||||
& over HTTP.headers headerFunc
|
||||
& over HTTP.body bodyFunc
|
||||
let transformCtx = buildTransformContext reqData sessionVars
|
||||
method = fmap (TE.encodeUtf8 . renderRequestMethod) rtRequestMethod
|
||||
|
||||
bodyFunc :: Maybe BL.ByteString -> Either TransformErrorBundle (Maybe BL.ByteString)
|
||||
bodyFunc body =
|
||||
case rtBody of
|
||||
Nothing -> pure body
|
||||
Just f -> (pure . J.encode) <$> f transformCtx
|
||||
|
||||
urlFunc :: Text -> Either TransformErrorBundle Text
|
||||
urlFunc url =
|
||||
case rtRequestURL of
|
||||
Nothing -> pure url
|
||||
Just f -> f transformCtx
|
||||
|
||||
queryFunc :: HTTP.Query -> Either TransformErrorBundle HTTP.Query
|
||||
queryFunc query =
|
||||
case rtQueryParams of
|
||||
Nothing -> pure query
|
||||
Just f -> f transformCtx
|
||||
|
||||
headerFunc :: [HTTP.Header] -> Either TransformErrorBundle [HTTP.Header]
|
||||
headerFunc headers =
|
||||
case rtRequestHeaders of
|
||||
Nothing -> pure headers
|
||||
Just f -> f transformCtx >>= \g -> pure $ g headers
|
||||
|
||||
contentTypeFunc :: [HTTP.Header] -> Either TransformErrorBundle [HTTP.Header]
|
||||
contentTypeFunc = pure . nubBy (\a b -> fst a == fst b) . (:) ("Content-Type", contentType)
|
||||
where
|
||||
contentType = maybe "application/json" (TE.encodeUtf8 . renderContentType) rtContentType
|
||||
in reqData & traverseOf HTTP.url urlFunc
|
||||
>>= traverseOf HTTP.body bodyFunc
|
||||
>>= traverseOf HTTP.queryParams queryFunc
|
||||
>>= traverseOf HTTP.method (pure . (`fromMaybe` method))
|
||||
>>= traverseOf HTTP.headers headerFunc
|
||||
>>= traverseOf HTTP.headers contentTypeFunc
|
||||
|
@ -107,7 +107,7 @@ saveMetadataToHdbTables
|
||||
withPathK "actions" $
|
||||
indexedForM_ actions $ \action -> do
|
||||
let createAction =
|
||||
CreateAction (_amName action) (_amDefinition action) (_amComment action) (_amMetadataTransform action)
|
||||
CreateAction (_amName action) (_amDefinition action) (_amComment action) (_amRequestTransform action)
|
||||
addActionToCatalog createAction
|
||||
withPathK "permissions" $
|
||||
indexedForM_ (_amPermissions action) $ \permission -> do
|
||||
|
@ -38,7 +38,7 @@ module Hasura.RQL.Types.Action
|
||||
amComment,
|
||||
amDefinition,
|
||||
amPermissions,
|
||||
amMetadataTransform,
|
||||
amRequestTransform,
|
||||
ActionPermissionMetadata (..),
|
||||
ActionSourceInfo (..),
|
||||
getActionSourceInfo,
|
||||
@ -280,7 +280,7 @@ data ActionMetadata = ActionMetadata
|
||||
_amComment :: !(Maybe Text),
|
||||
_amDefinition :: !ActionDefinitionInput,
|
||||
_amPermissions :: ![ActionPermissionMetadata],
|
||||
_amMetadataTransform :: !(Maybe MetadataTransform)
|
||||
_amRequestTransform :: !(Maybe MetadataTransform)
|
||||
}
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
@ -298,7 +298,7 @@ instance J.FromJSON ActionMetadata where
|
||||
<*> o J..:? "comment"
|
||||
<*> o J..: "definition"
|
||||
<*> o J..:? "permissions" J..!= []
|
||||
<*> o J..:? "transform"
|
||||
<*> o J..:? "request_transform"
|
||||
|
||||
----------------- Resolve Types ----------------
|
||||
|
||||
|
@ -198,7 +198,7 @@ data EventTriggerConf (b :: BackendType) = EventTriggerConf
|
||||
etcWebhookFromEnv :: !(Maybe Text),
|
||||
etcRetryConf :: !RetryConf,
|
||||
etcHeaders :: !(Maybe [HeaderConf]),
|
||||
etcTransform :: !(Maybe MetadataTransform)
|
||||
etcRequestTransform :: !(Maybe MetadataTransform)
|
||||
}
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
@ -270,7 +270,7 @@ data EventTriggerInfo (b :: BackendType) = EventTriggerInfo
|
||||
-- | Custom headers can be added to an event trigger. Each webhook request will have these
|
||||
-- headers added.
|
||||
etiHeaders :: ![EventHeaderInfo],
|
||||
etiMetadataTransform :: !(Maybe MetadataTransform)
|
||||
etiRequestTransform :: !(Maybe MetadataTransform)
|
||||
}
|
||||
deriving (Generic, Eq)
|
||||
|
||||
|
@ -111,6 +111,22 @@ class Backend b => BackendEventTrigger (b :: BackendType) where
|
||||
Maybe MaintenanceModeVersion ->
|
||||
m (Either QErr ())
|
||||
|
||||
-- | @recordError'@ records an erronous event invocation, it does a couple
|
||||
-- of things,
|
||||
--
|
||||
-- 1. If present, insert the invocation in the invocation logs table
|
||||
-- 2. Depending on the value of `ProcessEventError`, it will either,
|
||||
-- - Set a retry for the given event
|
||||
-- - Mark the event as 'error'
|
||||
recordError' ::
|
||||
MonadIO m =>
|
||||
SourceConfig b ->
|
||||
Event b ->
|
||||
Maybe (Invocation 'EventType) ->
|
||||
ProcessEventError ->
|
||||
Maybe MaintenanceModeVersion ->
|
||||
m (Either QErr ())
|
||||
|
||||
-- | @dropTriggerAndArchiveEvents@ drops the database trigger and
|
||||
-- marks all the events related to the event trigger as archived.
|
||||
-- See Note [Cleanup for dropped triggers]
|
||||
@ -155,6 +171,7 @@ instance BackendEventTrigger ('Postgres 'Vanilla) where
|
||||
getMaintenanceModeVersion = PG.getMaintenanceModeVersion
|
||||
recordSuccess = PG.recordSuccess
|
||||
recordError = PG.recordError
|
||||
recordError' = PG.recordError'
|
||||
dropTriggerAndArchiveEvents = PG.dropTriggerAndArchiveEvents
|
||||
redeliverEvent = PG.redeliverEvent
|
||||
unlockEventsInSource = PG.unlockEventsInSource
|
||||
@ -167,6 +184,7 @@ instance BackendEventTrigger ('Postgres 'Citus) where
|
||||
recordSuccess _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for Citus sources"
|
||||
getMaintenanceModeVersion _ = throw400 NotSupported "Event triggers are not supported for Citus sources"
|
||||
recordError _ _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for Citus sources"
|
||||
recordError' _ _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for Citus sources"
|
||||
dropTriggerAndArchiveEvents _ _ = throw400 NotSupported "Event triggers are not supported for Citus sources"
|
||||
redeliverEvent _ _ = throw400 NotSupported "Event triggers are not supported for Citus sources"
|
||||
unlockEventsInSource _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for Citus sources"
|
||||
@ -179,6 +197,7 @@ instance BackendEventTrigger 'MSSQL where
|
||||
recordSuccess _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for MS-SQL sources"
|
||||
getMaintenanceModeVersion _ = throw400 NotSupported "Event triggers are not supported for MS-SQL sources"
|
||||
recordError _ _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for MS-SQL sources"
|
||||
recordError' _ _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for MS-SQL sources"
|
||||
dropTriggerAndArchiveEvents _ _ = throw400 NotSupported "Event triggers are not supported for MS-SQL sources"
|
||||
redeliverEvent _ _ = throw400 NotSupported "Event triggers are not supported for MS-SQL sources"
|
||||
unlockEventsInSource _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for MS-SQL sources"
|
||||
@ -191,6 +210,7 @@ instance BackendEventTrigger 'BigQuery where
|
||||
recordSuccess _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for BigQuery sources"
|
||||
getMaintenanceModeVersion _ = throw400 NotSupported "Event triggers are not supported for BigQuery sources"
|
||||
recordError _ _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for BigQuery sources"
|
||||
recordError' _ _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for BigQuery sources"
|
||||
dropTriggerAndArchiveEvents _ _ = throw400 NotSupported "Event triggers are not supported for BigQuery sources"
|
||||
redeliverEvent _ _ = throw400 NotSupported "Event triggers are not supported for BigQuery sources"
|
||||
unlockEventsInSource _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for BigQuery sources"
|
||||
@ -203,6 +223,7 @@ instance BackendEventTrigger 'MySQL where
|
||||
recordSuccess _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for MySQL sources"
|
||||
getMaintenanceModeVersion _ = throw400 NotSupported "Event triggers are not supported for MySQL sources"
|
||||
recordError _ _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for MySQL sources"
|
||||
recordError' _ _ _ _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for MySQL sources"
|
||||
dropTriggerAndArchiveEvents _ _ = throw400 NotSupported "Event triggers are not supported for MySQL sources"
|
||||
redeliverEvent _ _ = throw400 NotSupported "Event triggers are not supported for MySQL sources"
|
||||
unlockEventsInSource _ _ = runExceptT $ throw400 NotSupported "Event triggers are not supported for MySQL sources"
|
||||
|
@ -819,7 +819,7 @@ metadataToOrdJSON
|
||||
[ ("name", AO.toOrdered name),
|
||||
("definition", AO.toOrdered definition),
|
||||
("retry_conf", AO.toOrdered retryConf),
|
||||
("transform", maybe AO.Null AO.toOrdered metadataTransform)
|
||||
("request_transform", maybe AO.Null AO.toOrdered metadataTransform)
|
||||
]
|
||||
<> catMaybes
|
||||
[ maybeAnyToMaybeOrdPair "webhook" AO.toOrdered webhook,
|
||||
@ -984,7 +984,7 @@ metadataToOrdJSON
|
||||
AO.object $
|
||||
[ ("name", AO.toOrdered name),
|
||||
("definition", actionDefinitionToOrdJSON definition),
|
||||
("transform", AO.toOrdered metaTransform)
|
||||
("request_transform", AO.toOrdered metaTransform)
|
||||
]
|
||||
<> catMaybes
|
||||
[ maybeCommentToMaybeOrdPair comment,
|
||||
|
@ -157,7 +157,7 @@ data RQLMetadataV1
|
||||
RMDumpInternalState !DumpInternalState
|
||||
| RMGetCatalogState !GetCatalogState
|
||||
| RMSetCatalogState !SetCatalogState
|
||||
| RMValidateWebhookTransform !ValidateWebhookTransform
|
||||
| RMTestWebhookTransform !TestWebhookTransform
|
||||
| -- Bulk metadata queries
|
||||
RMBulk [RQLMetadataRequest]
|
||||
|
||||
@ -214,7 +214,7 @@ instance FromJSON RQLMetadataV1 where
|
||||
"get_catalog_state" -> RMGetCatalogState <$> args
|
||||
"set_catalog_state" -> RMSetCatalogState <$> args
|
||||
"set_graphql_schema_introspection_options" -> RMSetGraphqlSchemaIntrospectionOptions <$> args
|
||||
"validate_webhook_transform" -> RMValidateWebhookTransform <$> args
|
||||
"test_webhook_transform" -> RMTestWebhookTransform <$> args
|
||||
"set_query_tags" -> RMSetQueryTagsConfig <$> args
|
||||
"bulk" -> RMBulk <$> args
|
||||
-- backend specific
|
||||
@ -468,7 +468,7 @@ runMetadataQueryV1M env currentResourceVersion = \case
|
||||
RMDumpInternalState q -> runDumpInternalState q
|
||||
RMGetCatalogState q -> runGetCatalogState q
|
||||
RMSetCatalogState q -> runSetCatalogState q
|
||||
RMValidateWebhookTransform q -> runValidateWebhookTransform q
|
||||
RMTestWebhookTransform q -> runTestWebhookTransform q
|
||||
RMSetQueryTagsConfig q -> runSetQueryTagsConfig q
|
||||
RMBulk q -> encJFromList <$> indexedMapM (runMetadataQueryM env currentResourceVersion) q
|
||||
where
|
||||
|
@ -121,7 +121,7 @@ data RQLMetadataV1
|
||||
RMDumpInternalState !DumpInternalState
|
||||
| RMGetCatalogState !GetCatalogState
|
||||
| RMSetCatalogState !SetCatalogState
|
||||
| RMValidateWebhookTransform !ValidateWebhookTransform
|
||||
| RMTestWebhookTransform !TestWebhookTransform
|
||||
| -- Bulk metadata queries
|
||||
RMBulk [RQLMetadataRequest]
|
||||
|
||||
|
@ -2,9 +2,8 @@ module Hasura.RQL.RequestTransformSpec (spec) where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.CaseInsensitive qualified as CI
|
||||
import Data.HashMap.Strict qualified as M
|
||||
import Data.List (nubBy)
|
||||
import Data.Set qualified as S
|
||||
import Data.Text qualified as T
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.RequestTransform
|
||||
import Hedgehog.Gen qualified as Gen
|
||||
@ -37,7 +36,7 @@ spec = do
|
||||
hedgehog $ do
|
||||
transform <- forAll genMetadataTransform
|
||||
let sortH TransformHeaders {..} = TransformHeaders (sort addHeaders) (sort removeHeaders)
|
||||
sortMT mt@MetadataTransform {mtRequestHeaders} = mt {mtRequestHeaders = sortH <$> mtRequestHeaders}
|
||||
sortMT mt@MetadataTransform {mtRequestHeaders, mtQueryParams} = mt {mtRequestHeaders = sortH <$> mtRequestHeaders, mtQueryParams = sort <$> mtQueryParams}
|
||||
transformMaybe = decode $ encode transform
|
||||
Just (sortMT transform) === fmap sortMT transformMaybe
|
||||
|
||||
@ -53,7 +52,9 @@ genTemplatingEngine = Gen.enumBounded @_ @TemplatingEngine
|
||||
-- NOTE: This generator is strictly useful for roundtrip aeson testing
|
||||
-- and does not produce valid template snippets.
|
||||
genTemplateText :: Gen TemplateText
|
||||
genTemplateText = TemplateText <$> Gen.text (Range.constant 3 20) Gen.alphaNum
|
||||
genTemplateText = (TemplateText . wrap) <$> Gen.text (Range.constant 3 20) Gen.alphaNum
|
||||
where
|
||||
wrap txt = "\"" <> txt <> "\""
|
||||
|
||||
genContentType :: Gen ContentType
|
||||
genContentType = Gen.enumBounded @_ @ContentType
|
||||
@ -63,7 +64,7 @@ genTransformHeaders = do
|
||||
numHeaders <- Gen.integral $ Range.constant 1 20
|
||||
|
||||
let genHeaderKey = CI.mk <$> Gen.text (Range.constant 1 20) Gen.alphaNum
|
||||
genHeaderValue = Gen.text (Range.constant 3 20) Gen.alphaNum
|
||||
genHeaderValue = genTemplateText
|
||||
|
||||
genKeys = S.toList <$> Gen.set (Range.singleton numHeaders) genHeaderKey
|
||||
genValues = S.toList <$> Gen.set (Range.singleton numHeaders) genHeaderValue
|
||||
@ -72,21 +73,28 @@ genTransformHeaders = do
|
||||
addHeaders <- liftA2 zip genKeys genValues
|
||||
pure $ TransformHeaders addHeaders removeHeaders
|
||||
|
||||
genQueryParams :: Gen (M.HashMap T.Text (Maybe T.Text))
|
||||
genQueryParams :: Gen [(TemplateText, Maybe TemplateText)]
|
||||
genQueryParams = do
|
||||
numParams <- Gen.integral $ Range.constant 1 20
|
||||
let keyGen = Gen.text (Range.constant 1 20) Gen.alphaNum
|
||||
valueGen = Gen.maybe $ Gen.text (Range.constant 1 20) Gen.alphaNum
|
||||
let keyGen = genTemplateText
|
||||
valueGen = Gen.maybe genTemplateText
|
||||
keys <- Gen.list (Range.singleton numParams) keyGen
|
||||
values <- Gen.list (Range.singleton numParams) valueGen
|
||||
pure $ M.fromList $ zip keys values
|
||||
pure $ nubBy (\a b -> fst a == fst b) $ zip keys values
|
||||
|
||||
genUrl :: Gen TemplateText
|
||||
genUrl = do
|
||||
host <- Gen.text (Range.constant 3 20) Gen.alphaNum
|
||||
|
||||
let wrap txt = "\"" <> txt <> "\""
|
||||
pure $ TemplateText $ wrap $ "http://www." <> host <> ".com"
|
||||
|
||||
genMetadataTransform :: Gen MetadataTransform
|
||||
genMetadataTransform = do
|
||||
method <- Gen.maybe genRequestMethod
|
||||
-- NOTE: At the moment no need to generate valid urls or templates
|
||||
-- but such instances maybe useful in the future.
|
||||
url <- Gen.maybe $ Gen.text (Range.constant 3 20) Gen.alphaNum
|
||||
url <- Gen.maybe $ genUrl
|
||||
bodyTransform <- Gen.maybe $ genTemplateText
|
||||
contentType <- Gen.maybe $ genContentType
|
||||
queryParams <- Gen.maybe $ genQueryParams
|
||||
|
@ -63,7 +63,7 @@ args:
|
||||
type: String!
|
||||
output_type: UserId
|
||||
handler: http://127.0.0.1:5593/create-user
|
||||
transform:
|
||||
request_transform:
|
||||
template_engine: Kriti
|
||||
body: |
|
||||
{
|
||||
|
@ -147,8 +147,8 @@ args:
|
||||
{
|
||||
"input": {
|
||||
"arg": {
|
||||
"id": {{ $.input.arg.name }},
|
||||
"name": {{ $.input.arg.id }}
|
||||
"id": {{ $body.input.arg.name }},
|
||||
"name": {{ $body.input.arg.id }}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
url: /v1/metadata
|
||||
status: 400
|
||||
response:
|
||||
path: $.args.transform.content_type
|
||||
path: $.args.request_transform.content_type
|
||||
error: "Error when parsing command create_event_trigger.\nSee our documentation at https://hasura.io/docs/latest/graphql/core/api-reference/metadata-api/index.html#metadata-apis.\nInternal error message: Invalid ContentType"
|
||||
code: parse-failed
|
||||
query:
|
||||
@ -21,6 +21,6 @@
|
||||
- first_name
|
||||
- last_name
|
||||
replace: false
|
||||
transform:
|
||||
request_transform:
|
||||
template_engine: Kriti
|
||||
content_type: multipart/form-data
|
||||
|
@ -2,7 +2,7 @@
|
||||
url: /v1/metadata
|
||||
status: 400
|
||||
response:
|
||||
path: $.args.transform.request_headers.remove_headers[0]
|
||||
path: $.args.request_transform.request_headers.remove_headers[0]
|
||||
error: "Error when parsing command create_event_trigger.\nSee our documentation at https://hasura.io/docs/latest/graphql/core/api-reference/metadata-api/index.html#metadata-apis.\nInternal error message: Restricted Header: Content-Type"
|
||||
code: parse-failed
|
||||
query:
|
||||
@ -21,10 +21,10 @@
|
||||
- first_name
|
||||
- last_name
|
||||
replace: false
|
||||
transform:
|
||||
request_transform:
|
||||
template_engine: Kriti
|
||||
request_headers:
|
||||
add_headers:
|
||||
foo: bar
|
||||
foo: "\"bar\""
|
||||
remove_headers:
|
||||
- Content-Type
|
||||
|
@ -1,24 +0,0 @@
|
||||
- description: PG create event trigger
|
||||
url: /v1/metadata
|
||||
status: 200
|
||||
response:
|
||||
message: success
|
||||
query:
|
||||
type: pg_create_event_trigger
|
||||
args:
|
||||
name: sample_trigger
|
||||
table:
|
||||
name: test_t1
|
||||
schema: hge_tests
|
||||
source: default
|
||||
webhook: http://127.0.0.1:5592
|
||||
insert:
|
||||
columns: '*'
|
||||
payload:
|
||||
- id
|
||||
- first_name
|
||||
- last_name
|
||||
replace: false
|
||||
transform:
|
||||
template_engine: Kriti
|
||||
body: "$.event.data.new }}"
|
@ -19,13 +19,14 @@
|
||||
- first_name
|
||||
- last_name
|
||||
replace: false
|
||||
transform:
|
||||
request_transform:
|
||||
content_type: application/json
|
||||
template_engine: Kriti
|
||||
body: "{{ $.event.data.new }}"
|
||||
query_params:
|
||||
foo: bar
|
||||
body: "{{ $body.event.data.new }}"
|
||||
query_params:
|
||||
"\"foo\"": "\"bar\""
|
||||
request_headers:
|
||||
add_headers:
|
||||
foo: bar
|
||||
foo: "\"bar\""
|
||||
remove_headers:
|
||||
- User-Agent
|
||||
|
@ -19,5 +19,5 @@
|
||||
- first_name
|
||||
- last_name
|
||||
replace: false
|
||||
transform:
|
||||
request_transform:
|
||||
content_type: application/json
|
||||
|
@ -19,6 +19,6 @@
|
||||
- first_name
|
||||
- last_name
|
||||
replace: false
|
||||
transform:
|
||||
request_transform:
|
||||
template_engine: Kriti
|
||||
content_type: application/x-www-form-urlencoded
|
||||
|
@ -0,0 +1,26 @@
|
||||
- description: Test Webhook Transform Bad Eval
|
||||
url: /v1/metadata
|
||||
headers:
|
||||
X-Hasura-Role: admin
|
||||
status: 200
|
||||
response:
|
||||
body:
|
||||
- error_code: InvalidPathCode
|
||||
source_position:
|
||||
end_column: 14
|
||||
start_line: 1
|
||||
end_line: 1
|
||||
start_column: 9
|
||||
message: 'Path Lookup Error: "$body.world"'
|
||||
headers: []
|
||||
method: GET
|
||||
webhook_url: http://localhost:1234/
|
||||
query:
|
||||
type: test_webhook_transform
|
||||
args:
|
||||
webhook_url: http://localhost:1234
|
||||
body:
|
||||
hello: world
|
||||
request_transform:
|
||||
body: "{{ $body.world }}"
|
||||
template_engine: Kriti
|
@ -0,0 +1,18 @@
|
||||
- description: Test Webhook Transform Bad Parse
|
||||
url: /v1/metadata
|
||||
headers:
|
||||
X-Hasura-Role: admin
|
||||
status: 400
|
||||
response:
|
||||
path: "$.args.request_transform.body"
|
||||
error: "sourceName:1:2:\n |\n1 | \n | \nunexpected \"$\"\n"
|
||||
code: "parse-failed"
|
||||
query:
|
||||
type: test_webhook_transform
|
||||
args:
|
||||
webhook_url: http://localhost:1234
|
||||
body:
|
||||
hello: world
|
||||
request_transform:
|
||||
body: "$body.hello }}"
|
||||
template_engine: Kriti
|
@ -0,0 +1,30 @@
|
||||
- description: Test Webhook Transform
|
||||
url: /v1/metadata
|
||||
headers:
|
||||
X-Hasura-Role: admin
|
||||
status: 200
|
||||
response:
|
||||
body: hello_world
|
||||
headers:
|
||||
- - content-type
|
||||
- application/json
|
||||
- - foo
|
||||
- bar
|
||||
method: POST
|
||||
webhook_url: http://www.google.com?foo=bar
|
||||
query:
|
||||
type: test_webhook_transform
|
||||
args:
|
||||
webhook_url: http://localhost:1234
|
||||
body:
|
||||
hello: world
|
||||
request_transform:
|
||||
url: "\"http://www.google.com\""
|
||||
template_engine: Kriti
|
||||
body: "\"hello_{{ $body.hello }}\""
|
||||
method: POST
|
||||
query_params:
|
||||
"\"foo\"": "\"bar\""
|
||||
request_headers:
|
||||
add_headers:
|
||||
foo: "\"bar\""
|
@ -1,28 +0,0 @@
|
||||
- description: Clear metadata
|
||||
url: /v1/metadata
|
||||
headers:
|
||||
X-Hasura-Role: admin
|
||||
status: 200
|
||||
response:
|
||||
payload:
|
||||
error_code: InvalidPathCode
|
||||
source_position:
|
||||
end_column: 10
|
||||
start_line: 1
|
||||
end_line: 1
|
||||
start_column: 5
|
||||
message: 'Path Lookup Error: "$.world"'
|
||||
headers:
|
||||
- - content-type
|
||||
- application/json
|
||||
method: GET
|
||||
webhook_url: http://localhost:1234/
|
||||
query:
|
||||
type: validate_webhook_transform
|
||||
args:
|
||||
webhook_url: http://localhost:1234
|
||||
payload:
|
||||
hello: world
|
||||
transformer:
|
||||
body: "{{ $.world }}"
|
||||
template_engine: Kriti
|
@ -1,26 +0,0 @@
|
||||
- description: Clear metadata
|
||||
url: /v1/metadata
|
||||
headers:
|
||||
X-Hasura-Role: admin
|
||||
status: 200
|
||||
response:
|
||||
payload:
|
||||
error_code: ParseErrorCode
|
||||
source_position:
|
||||
start_line: 1
|
||||
start_column: 1
|
||||
message: "sourceName:1:2:\n |\n1 | \n | \nunexpected \"$\"\n"
|
||||
headers:
|
||||
- - content-type
|
||||
- application/json
|
||||
method: GET
|
||||
webhook_url: http://localhost:1234/
|
||||
query:
|
||||
type: validate_webhook_transform
|
||||
args:
|
||||
webhook_url: http://localhost:1234
|
||||
payload:
|
||||
hello: world
|
||||
transformer:
|
||||
body: "$.hello }}"
|
||||
template_engine: Kriti
|
@ -1,21 +0,0 @@
|
||||
- description: Clear metadata
|
||||
url: /v1/metadata
|
||||
headers:
|
||||
X-Hasura-Role: admin
|
||||
status: 200
|
||||
response:
|
||||
payload: world
|
||||
headers:
|
||||
- - content-type
|
||||
- application/json
|
||||
method: GET
|
||||
webhook_url: http://localhost:1234/
|
||||
query:
|
||||
type: validate_webhook_transform
|
||||
args:
|
||||
webhook_url: http://localhost:1234
|
||||
payload:
|
||||
hello: world
|
||||
transformer:
|
||||
body: "{{ $.hello }}"
|
||||
template_engine: Kriti
|
@ -807,7 +807,7 @@ class TestManualEvents(object):
|
||||
st_code, resp = hge_ctx.v1metadataq(reload_metadata_q)
|
||||
assert st_code == 200, resp
|
||||
|
||||
self.test_basic(hge_ctx, evts_webhook)
|
||||
self.test_basic(hge_ctx, evts_webhook)
|
||||
|
||||
@usefixtures('per_method_tests_db_state')
|
||||
class TestEventsAsynchronousExecution(object):
|
||||
@ -871,31 +871,6 @@ class TestEventTransform(object):
|
||||
webhook_path=expectedPath)
|
||||
assert st_code == 200, resp
|
||||
|
||||
def test_bad_template_parse_err(self, hge_ctx, evts_webhook):
|
||||
# GIVEN
|
||||
check_query_f(hge_ctx, self.dir() + '/bad_template_transform.yaml')
|
||||
|
||||
# WHEN
|
||||
table = {"schema": "hge_tests", "name": "test_t1"}
|
||||
insert_row = {"id": 0, "first_name": "Simon", "last_name": "Marlow"}
|
||||
st_code, resp = insert(hge_ctx, table, insert_row)
|
||||
|
||||
# THEN
|
||||
expected_body = {
|
||||
'error_code': 'ParseErrorCode',
|
||||
'source_position': {'start_line': 1, 'start_column': 1},
|
||||
'message': 'sourceName:1:2:\n |\n1 | \n | \nunexpected \"$\"\n'
|
||||
}
|
||||
|
||||
#{
|
||||
# "old": None,
|
||||
# "new": insert_row
|
||||
#}
|
||||
|
||||
check_event_transformed(hge_ctx, evts_webhook, expected_body)
|
||||
#check_event(hge_ctx, evts_webhook, "sample_trigger", table, "INSERT", expected_event_data)
|
||||
assert st_code == 200, resp
|
||||
|
||||
def test_content_type_allows_json(self, hge_ctx, evts_webhook):
|
||||
# GIVEN
|
||||
check_query_f(hge_ctx, self.dir() + '/content_type_transform.yaml')
|
||||
|
@ -194,14 +194,14 @@ class TestMetadata:
|
||||
|
||||
check_query_f(hge_ctx, self.dir() + '/pg_track_function_with_comment_teardown.yaml')
|
||||
|
||||
def test_validate_webhook_transform_success(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/validate_webhook_transform_success.yaml')
|
||||
def test_test_webhook_transform_success(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/test_webhook_transform_success.yaml')
|
||||
|
||||
def test_validate_webhook_transform_bad_parse(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/validate_webhook_transform_bad_parse.yaml')
|
||||
def test_test_webhook_transform_bad_parse(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/test_webhook_transform_bad_parse.yaml')
|
||||
|
||||
def test_validate_webhook_transform_bad_eval(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/validate_webhook_transform_bad_eval.yaml')
|
||||
def test_test_webhook_transform_bad_eval(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/test_webhook_transform_bad_eval.yaml')
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.getenv('HASURA_GRAPHQL_PG_SOURCE_URL_1') == os.getenv('HASURA_GRAPHQL_PG_SOURCE_URL_2') or
|
||||
|
@ -57,7 +57,12 @@ def validate_event_webhook(ev_webhook_path, webhook_path):
|
||||
|
||||
# Make some assertions on a single event recorded by webhook. Waits up to 3
|
||||
# seconds by default for an event to appear
|
||||
def check_event(hge_ctx, evts_webhook, trig_name, table, operation, exp_ev_data,
|
||||
def check_event(hge_ctx,
|
||||
evts_webhook,
|
||||
trig_name,
|
||||
table,
|
||||
operation,
|
||||
exp_ev_data,
|
||||
headers = {},
|
||||
webhook_path = '/',
|
||||
session_variables = {'x-hasura-role': 'admin'},
|
||||
@ -75,7 +80,9 @@ def check_event(hge_ctx, evts_webhook, trig_name, table, operation, exp_ev_data,
|
||||
assert ev_full['body']['delivery_info']['current_retry'] == retry
|
||||
|
||||
|
||||
def check_event_transformed(hge_ctx, evts_webhook, exp_payload,
|
||||
def check_event_transformed(hge_ctx,
|
||||
evts_webhook,
|
||||
exp_payload,
|
||||
headers = {},
|
||||
webhook_path = '/',
|
||||
session_variables = {'x-hasura-role': 'admin'},
|
||||
|
Loading…
Reference in New Issue
Block a user