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:
Solomon Bothwell 2021-09-29 08:13:30 +00:00 committed by hasura-bot
parent 20f7c85382
commit 4e05bdcaec
41 changed files with 694 additions and 421 deletions

View File

@ -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

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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

View File

@ -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"
}

View File

@ -115,6 +115,7 @@ library
, deepseq
, dependent-map >=0.4 && <0.5
, dependent-sum
, either
, exceptions
, fast-logger
, ghc-heap-view

View File

@ -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

View File

@ -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 ()

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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
]

View File

@ -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
]

View File

@ -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

View File

@ -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

View File

@ -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 ----------------

View File

@ -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)

View File

@ -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"

View File

@ -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,

View File

@ -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

View File

@ -121,7 +121,7 @@ data RQLMetadataV1
RMDumpInternalState !DumpInternalState
| RMGetCatalogState !GetCatalogState
| RMSetCatalogState !SetCatalogState
| RMValidateWebhookTransform !ValidateWebhookTransform
| RMTestWebhookTransform !TestWebhookTransform
| -- Bulk metadata queries
RMBulk [RQLMetadataRequest]

View File

@ -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

View File

@ -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: |
{

View File

@ -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 }}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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 }}"

View File

@ -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

View File

@ -19,5 +19,5 @@
- first_name
- last_name
replace: false
transform:
request_transform:
content_type: application/json

View File

@ -19,6 +19,6 @@
- first_name
- last_name
replace: false
transform:
request_transform:
template_engine: Kriti
content_type: application/x-www-form-urlencoded

View File

@ -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

View File

@ -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

View File

@ -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\""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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'},