mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
Adds x-www-form-urlencoded body transformation
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3764 GitOrigin-RevId: 60ce63496d94406476dd446a498d7a7d625465be
This commit is contained in:
parent
eed47e973a
commit
876300c049
@ -10,6 +10,7 @@
|
|||||||
(Add entries below in the order of server, console, cli, docs, others)
|
(Add entries below in the order of server, console, cli, docs, others)
|
||||||
|
|
||||||
- server: add custom function for case insensitive lookup in session variable in request transformation
|
- server: add custom function for case insensitive lookup in session variable in request transformation
|
||||||
|
- server: Webhook Tranforms can now produce `x-www-url-formencoded` bodies.
|
||||||
- server: Webhook Transforms can now delete request/response bodies explicitly.
|
- server: Webhook Transforms can now delete request/response bodies explicitly.
|
||||||
- server: Fix truncation of session variables with variable length column types in MSSQL (#8158)
|
- server: Fix truncation of session variables with variable length column types in MSSQL (#8158)
|
||||||
- server: improve performance of `replace_metadata` for large schemas
|
- server: improve performance of `replace_metadata` for large schemas
|
||||||
|
@ -2189,6 +2189,10 @@ BodyTransform
|
|||||||
- String
|
- String
|
||||||
- The transformation template to be applied to the body. This is
|
- The transformation template to be applied to the body. This is
|
||||||
required if the action is `transform`.
|
required if the action is `transform`.
|
||||||
|
* - form_template
|
||||||
|
- false
|
||||||
|
- Object (:ref:`String` : :ref:`String`)
|
||||||
|
- The key/value pairs to be used in a `x-www-url-formencoded` body. The values can be transfomation templates.
|
||||||
|
|
||||||
.. _TemplateEngine:
|
.. _TemplateEngine:
|
||||||
|
|
||||||
|
@ -513,7 +513,7 @@ runTestWebhookTransform (TestWebhookTransform env headers urlE payload mt _ sv)
|
|||||||
|
|
||||||
case result of
|
case result of
|
||||||
Right transformed ->
|
Right transformed ->
|
||||||
let body = J.toJSON $ J.decode @J.Value =<< (transformed ^. HTTP.body)
|
let body = decodeBody (transformed ^. HTTP.body)
|
||||||
in pure $ packTransformResult transformed body
|
in pure $ packTransformResult transformed body
|
||||||
Left (RequestTransformationError req err) -> pure $ packTransformResult req (J.toJSON err)
|
Left (RequestTransformationError req err) -> pure $ packTransformResult req (J.toJSON err)
|
||||||
-- NOTE: In the following two cases we have failed before producing a valid request.
|
-- NOTE: In the following two cases we have failed before producing a valid request.
|
||||||
@ -530,6 +530,21 @@ interpolateFromEnv env url =
|
|||||||
err e = throwError $ err400 NotFound $ "Missing Env Var: " <> e
|
err e = throwError $ err400 NotFound $ "Missing Env Var: " <> e
|
||||||
in either err (pure . fold) result
|
in either err (pure . fold) result
|
||||||
|
|
||||||
|
decodeBody :: Maybe BL.ByteString -> J.Value
|
||||||
|
decodeBody Nothing = J.Null
|
||||||
|
decodeBody (Just bs) = fromMaybe J.Null $ jsonToValue bs <|> formUrlEncodedToValue bs
|
||||||
|
|
||||||
|
-- | Attempt to encode a 'ByteString' as an Aeson 'Value'
|
||||||
|
jsonToValue :: BL.ByteString -> Maybe J.Value
|
||||||
|
jsonToValue bs = J.decode bs
|
||||||
|
|
||||||
|
-- | Quote a 'ByteString' then attempt to encode it as a JSON
|
||||||
|
-- String. This is necessary for 'x-www-url-formencoded' bodies. They
|
||||||
|
-- are a list of key/value pairs encoded as a raw 'ByteString' with no
|
||||||
|
-- quoting whereas JSON Strings must be quoted.
|
||||||
|
formUrlEncodedToValue :: BL.ByteString -> Maybe J.Value
|
||||||
|
formUrlEncodedToValue bs = J.decode ("\"" <> bs <> "\"")
|
||||||
|
|
||||||
parseEnvTemplate :: AT.Parser [Either T.Text T.Text]
|
parseEnvTemplate :: AT.Parser [Either T.Text T.Text]
|
||||||
parseEnvTemplate = AT.many1 $ pEnv <|> pLit <|> fmap Right "{"
|
parseEnvTemplate = AT.many1 $ pEnv <|> pLit <|> fmap Right "{"
|
||||||
where
|
where
|
||||||
|
@ -1,5 +1,21 @@
|
|||||||
{-# LANGUAGE DeriveAnyClass #-}
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
|
||||||
|
-- | Webhook Transformations are data transformations used to modify HTTP
|
||||||
|
-- Requests before those requests are executed or Responses after
|
||||||
|
-- requests are executed.
|
||||||
|
--
|
||||||
|
-- 'MetadataRequestTransform'/'MetadataResponseTransform' values are
|
||||||
|
-- stored in Metadata within the 'CreateAction' and
|
||||||
|
-- 'CreateEventTriggerQuery' Types and then converted into
|
||||||
|
-- 'RequestTransform'/'ResponseTransform' values using
|
||||||
|
-- 'mkRequestTransform'/'mkResponseTransform'.
|
||||||
|
--
|
||||||
|
-- 'RequestTransforms' and 'ResponseTransforms' are applied to an HTTP
|
||||||
|
-- Request/Response using 'applyRequestTransform' and
|
||||||
|
-- 'applyResponseTransform'.
|
||||||
|
--
|
||||||
|
-- In the case of body transformations, a user specified templating
|
||||||
|
-- script is applied.
|
||||||
module Hasura.RQL.DDL.WebhookTransforms
|
module Hasura.RQL.DDL.WebhookTransforms
|
||||||
( applyRequestTransform,
|
( applyRequestTransform,
|
||||||
applyResponseTransform,
|
applyResponseTransform,
|
||||||
@ -8,7 +24,7 @@ module Hasura.RQL.DDL.WebhookTransforms
|
|||||||
mkRequestTransform,
|
mkRequestTransform,
|
||||||
mkResponseTransform,
|
mkResponseTransform,
|
||||||
RequestMethod (..),
|
RequestMethod (..),
|
||||||
RemoveOrTransform (..),
|
BodyTransform (..),
|
||||||
StringTemplateText (..),
|
StringTemplateText (..),
|
||||||
TemplatingEngine (..),
|
TemplatingEngine (..),
|
||||||
TemplateText (..),
|
TemplateText (..),
|
||||||
@ -25,13 +41,14 @@ where
|
|||||||
|
|
||||||
import Control.Lens (traverseOf, view)
|
import Control.Lens (traverseOf, view)
|
||||||
import Data.Aeson qualified as J
|
import Data.Aeson qualified as J
|
||||||
import Data.Bifunctor (bimap, first)
|
import Data.Bifunctor
|
||||||
import Data.Bitraversable
|
import Data.Bitraversable
|
||||||
import Data.ByteString qualified as B
|
import Data.ByteString qualified as B
|
||||||
import Data.ByteString.Lazy qualified as BL
|
import Data.ByteString.Lazy qualified as BL
|
||||||
import Data.CaseInsensitive qualified as CI
|
import Data.CaseInsensitive qualified as CI
|
||||||
import Data.Either.Validation
|
import Data.Either.Validation
|
||||||
import Data.HashMap.Strict qualified as M
|
import Data.HashMap.Strict qualified as M
|
||||||
|
import Data.List qualified as L
|
||||||
import Data.Text qualified as T
|
import Data.Text qualified as T
|
||||||
import Data.Text.Encoding qualified as TE
|
import Data.Text.Encoding qualified as TE
|
||||||
import Data.Validation qualified as V
|
import Data.Validation qualified as V
|
||||||
@ -44,27 +61,6 @@ import Kriti.Parser (parser)
|
|||||||
import Network.HTTP.Client.Transformable qualified as HTTP
|
import Network.HTTP.Client.Transformable qualified as HTTP
|
||||||
import Network.URI qualified as URI
|
import Network.URI qualified as URI
|
||||||
|
|
||||||
{-
|
|
||||||
|
|
||||||
Webhook Transformations are data transformations used to modify HTTP
|
|
||||||
Requests before those requests are executed or Responses after
|
|
||||||
requests are executed.
|
|
||||||
|
|
||||||
'MetadataRequestTransform'/'MetadataResponseTransform' values are
|
|
||||||
stored in Metadata within the 'CreateAction' and
|
|
||||||
'CreateEventTriggerQuery' Types and then converted into
|
|
||||||
'RequestTransform'/'ResponseTransform' values using
|
|
||||||
'mkRequestTransform'/'mkResponseTransform'.
|
|
||||||
|
|
||||||
'RequestTransforms' and 'ResponseTransforms' are applied to an HTTP
|
|
||||||
Request/Response using 'applyRequestTransform' and
|
|
||||||
'applyResponseTransform'.
|
|
||||||
|
|
||||||
In the case of body transformations, a user specified templating
|
|
||||||
script is applied.
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
-------------
|
-------------
|
||||||
--- Types ---
|
--- Types ---
|
||||||
-------------
|
-------------
|
||||||
@ -95,7 +91,12 @@ data RequestTransform = RequestTransform
|
|||||||
-- | A function which contructs a new URL given the request context.
|
-- | A function which contructs a new URL given the request context.
|
||||||
reqTransformRequestURL :: Maybe (ReqTransformCtx -> Either TransformErrorBundle Text),
|
reqTransformRequestURL :: Maybe (ReqTransformCtx -> Either TransformErrorBundle Text),
|
||||||
-- | A function for transforming the request body.
|
-- | A function for transforming the request body.
|
||||||
reqTransformBody :: Maybe (RemoveOrTransform (ReqTransformCtx -> Either TransformErrorBundle J.Value)),
|
reqTransformBody ::
|
||||||
|
Maybe
|
||||||
|
( BodyTransform
|
||||||
|
(ReqTransformCtx -> Either TransformErrorBundle J.Value)
|
||||||
|
(ReqTransformCtx -> Either TransformErrorBundle B.ByteString)
|
||||||
|
),
|
||||||
-- | A function which contructs new query parameters given the request context.
|
-- | A function which contructs new query parameters given the request context.
|
||||||
reqTransformQueryParams :: Maybe (ReqTransformCtx -> Either TransformErrorBundle HTTP.Query),
|
reqTransformQueryParams :: Maybe (ReqTransformCtx -> Either TransformErrorBundle HTTP.Query),
|
||||||
-- | A function which contructs new Headers given the request context.
|
-- | A function which contructs new Headers given the request context.
|
||||||
@ -105,7 +106,14 @@ data RequestTransform = RequestTransform
|
|||||||
-- | A set of data transformation functions generated from a
|
-- | A set of data transformation functions generated from a
|
||||||
-- 'MetadataResponseTransform'. 'Nothing' means use the original
|
-- 'MetadataResponseTransform'. 'Nothing' means use the original
|
||||||
-- response value.
|
-- response value.
|
||||||
newtype ResponseTransform = ResponseTransform {respTransformBody :: Maybe (RemoveOrTransform (RespTransformCtx -> Either TransformErrorBundle J.Value))}
|
newtype ResponseTransform = ResponseTransform
|
||||||
|
{ respTransformBody ::
|
||||||
|
Maybe
|
||||||
|
( BodyTransform
|
||||||
|
(RespTransformCtx -> Either TransformErrorBundle J.Value)
|
||||||
|
(ReqTransformCtx -> Either TransformErrorBundle B.ByteString)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
-- | A de/serializable request transformation template which can be stored in
|
-- | A de/serializable request transformation template which can be stored in
|
||||||
-- the metadata associated with an action/event trigger/etc. and used to produce
|
-- the metadata associated with an action/event trigger/etc. and used to produce
|
||||||
@ -129,7 +137,7 @@ data MetadataRequestTransform = MetadataRequestTransform
|
|||||||
-- | Template script for transforming the URL.
|
-- | Template script for transforming the URL.
|
||||||
mtRequestURL :: Maybe StringTemplateText,
|
mtRequestURL :: Maybe StringTemplateText,
|
||||||
-- | Template script for transforming the request body.
|
-- | Template script for transforming the request body.
|
||||||
mtBodyTransform :: Maybe (RemoveOrTransform TemplateText),
|
mtBodyTransform :: Maybe (BodyTransform TemplateText StringTemplateText),
|
||||||
-- | A list of template scripts for constructing new Query Params.
|
-- | A list of template scripts for constructing new Query Params.
|
||||||
mtQueryParams :: Maybe [(StringTemplateText, Maybe StringTemplateText)],
|
mtQueryParams :: Maybe [(StringTemplateText, Maybe StringTemplateText)],
|
||||||
-- | A list of template scripts for constructing headers.
|
-- | A list of template scripts for constructing headers.
|
||||||
@ -179,7 +187,7 @@ data MetadataResponseTransform = MetadataResponseTransform
|
|||||||
{ -- | The Schema Version
|
{ -- | The Schema Version
|
||||||
mrtVersion :: Version,
|
mrtVersion :: Version,
|
||||||
-- | Template script for transforming the response body.
|
-- | Template script for transforming the response body.
|
||||||
mrtBodyTransform :: Maybe (RemoveOrTransform TemplateText),
|
mrtBodyTransform :: Maybe (BodyTransform TemplateText StringTemplateText),
|
||||||
-- | The template engine to use for transformations. Default: Kriti
|
-- | The template engine to use for transformations. Default: Kriti
|
||||||
mrtTemplatingEngine :: TemplatingEngine
|
mrtTemplatingEngine :: TemplatingEngine
|
||||||
}
|
}
|
||||||
@ -211,11 +219,17 @@ instance J.FromJSON MetadataResponseTransform where
|
|||||||
let templateEngine' = fromMaybe Kriti templateEngine
|
let templateEngine' = fromMaybe Kriti templateEngine
|
||||||
pure $ MetadataResponseTransform version body templateEngine'
|
pure $ MetadataResponseTransform version body templateEngine'
|
||||||
|
|
||||||
data RemoveOrTransform a = Remove | Transform a
|
data BodyTransform a b = Remove | Transform a | FormUrlEncoded (M.HashMap T.Text b)
|
||||||
deriving stock (Show, Eq, Functor, Generic)
|
deriving stock (Show, Eq, Functor, Generic)
|
||||||
deriving anyclass (NFData, Cacheable)
|
deriving anyclass (NFData, Cacheable)
|
||||||
|
|
||||||
instance J.ToJSON a => J.ToJSON (RemoveOrTransform a) where
|
instance Bifunctor BodyTransform where
|
||||||
|
bimap f g = \case
|
||||||
|
Remove -> Remove
|
||||||
|
Transform a -> Transform $ f a
|
||||||
|
FormUrlEncoded form -> FormUrlEncoded $ fmap g form
|
||||||
|
|
||||||
|
instance (J.ToJSON a, J.ToJSON b) => J.ToJSON (BodyTransform a b) where
|
||||||
toJSON = \case
|
toJSON = \case
|
||||||
Remove -> J.object ["action" J..= ("remove" :: T.Text)]
|
Remove -> J.object ["action" J..= ("remove" :: T.Text)]
|
||||||
Transform a ->
|
Transform a ->
|
||||||
@ -223,16 +237,24 @@ instance J.ToJSON a => J.ToJSON (RemoveOrTransform a) where
|
|||||||
[ "action" J..= ("transform" :: T.Text),
|
[ "action" J..= ("transform" :: T.Text),
|
||||||
"template" J..= J.toJSON a
|
"template" J..= J.toJSON a
|
||||||
]
|
]
|
||||||
|
FormUrlEncoded form ->
|
||||||
|
J.object
|
||||||
|
[ "action" J..= ("x_www_form_urlencoded" :: T.Text),
|
||||||
|
"form_template" J..= J.toJSON form
|
||||||
|
]
|
||||||
|
|
||||||
instance J.FromJSON a => J.FromJSON (RemoveOrTransform a) where
|
instance (J.FromJSON a, J.FromJSON b) => J.FromJSON (BodyTransform a b) where
|
||||||
parseJSON = J.withObject "RemoveOrTransform" $ \o -> do
|
parseJSON = J.withObject "body" \o -> do
|
||||||
action :: T.Text <- o J..: "action"
|
action <- o J..: "action"
|
||||||
if action == "remove"
|
case action of
|
||||||
then pure Remove
|
"remove" -> pure Remove
|
||||||
else do
|
"transform" -> do
|
||||||
"transform" :: T.Text <- o J..: "action"
|
|
||||||
template <- o J..: "template"
|
template <- o J..: "template"
|
||||||
pure $ Transform template
|
pure $ Transform template
|
||||||
|
"x_www_form_urlencoded" -> do
|
||||||
|
template <- o J..: "form_template"
|
||||||
|
pure $ FormUrlEncoded template
|
||||||
|
x -> fail $ "'" <> x <> "'" <> "is not a valid body action."
|
||||||
|
|
||||||
newtype RequestMethod = RequestMethod (CI.CI T.Text)
|
newtype RequestMethod = RequestMethod (CI.CI T.Text)
|
||||||
deriving stock (Generic)
|
deriving stock (Generic)
|
||||||
@ -377,12 +399,12 @@ mkRequestTransform MetadataRequestTransform {..} =
|
|||||||
urlTransform = mkUrlTransform mtTemplatingEngine <$> mtRequestURL
|
urlTransform = mkUrlTransform mtTemplatingEngine <$> mtRequestURL
|
||||||
queryTransform = mkQueryParamsTransform mtTemplatingEngine <$> mtQueryParams
|
queryTransform = mkQueryParamsTransform mtTemplatingEngine <$> mtQueryParams
|
||||||
headerTransform = mkHeaderTransform mtTemplatingEngine <$> mtRequestHeaders
|
headerTransform = mkHeaderTransform mtTemplatingEngine <$> mtRequestHeaders
|
||||||
bodyTransform = fmap (mkReqTemplateTransform mtTemplatingEngine) <$> mtBodyTransform
|
bodyTransform = bimap (mkReqTemplateTransform mtTemplatingEngine) (flip (transformST mtTemplatingEngine)) <$> mtBodyTransform
|
||||||
in RequestTransform methodTransform urlTransform bodyTransform queryTransform headerTransform
|
in RequestTransform methodTransform urlTransform bodyTransform queryTransform headerTransform
|
||||||
|
|
||||||
mkResponseTransform :: MetadataResponseTransform -> ResponseTransform
|
mkResponseTransform :: MetadataResponseTransform -> ResponseTransform
|
||||||
mkResponseTransform MetadataResponseTransform {..} =
|
mkResponseTransform MetadataResponseTransform {..} =
|
||||||
let bodyTransform = fmap (mkRespTemplateTransform mrtTemplatingEngine) <$> mrtBodyTransform
|
let bodyTransform = bimap (mkRespTemplateTransform mrtTemplatingEngine) (flip (transformST mrtTemplatingEngine)) <$> mrtBodyTransform
|
||||||
in ResponseTransform bodyTransform
|
in ResponseTransform bodyTransform
|
||||||
|
|
||||||
-- | Transform a String Template
|
-- | Transform a String Template
|
||||||
@ -509,6 +531,7 @@ applyRequestTransform transformCtx' RequestTransform {..} reqData =
|
|||||||
case reqTransformBody of
|
case reqTransformBody of
|
||||||
Nothing -> pure body
|
Nothing -> pure body
|
||||||
Just Remove -> pure Nothing
|
Just Remove -> pure Nothing
|
||||||
|
Just (FormUrlEncoded kv) -> fmap (Just . foldForm) $ traverse (\f -> f transformCtx) kv
|
||||||
Just (Transform f) -> pure . J.encode <$> f transformCtx
|
Just (Transform f) -> pure . J.encode <$> f transformCtx
|
||||||
|
|
||||||
urlFunc :: Text -> Either TransformErrorBundle Text
|
urlFunc :: Text -> Either TransformErrorBundle Text
|
||||||
@ -545,6 +568,7 @@ applyResponseTransform ResponseTransform {..} ctx@RespTransformCtx {..} =
|
|||||||
case respTransformBody of
|
case respTransformBody of
|
||||||
Nothing -> pure body
|
Nothing -> pure body
|
||||||
Just Remove -> pure mempty
|
Just Remove -> pure mempty
|
||||||
|
Just (FormUrlEncoded _) -> pure mempty
|
||||||
Just (Transform f) -> J.encode <$> f ctx
|
Just (Transform f) -> J.encode <$> f ctx
|
||||||
in bodyFunc (J.encode rtcBody)
|
in bodyFunc (J.encode rtcBody)
|
||||||
|
|
||||||
@ -580,3 +604,14 @@ infixr 8 .=?
|
|||||||
-- | Wrap a template with escaped double quotes.
|
-- | Wrap a template with escaped double quotes.
|
||||||
wrapTemplate :: StringTemplateText -> StringTemplateText
|
wrapTemplate :: StringTemplateText -> StringTemplateText
|
||||||
wrapTemplate (StringTemplateText t) = StringTemplateText $ "\"" <> t <> "\""
|
wrapTemplate (StringTemplateText t) = StringTemplateText $ "\"" <> t <> "\""
|
||||||
|
|
||||||
|
escapeURIText :: T.Text -> T.Text
|
||||||
|
escapeURIText =
|
||||||
|
T.pack . URI.escapeURIString URI.isUnescapedInURIComponent . T.unpack
|
||||||
|
|
||||||
|
escapeURIBS :: B.ByteString -> B.ByteString
|
||||||
|
escapeURIBS =
|
||||||
|
TE.encodeUtf8 . T.pack . URI.escapeURIString URI.isUnescapedInURIComponent . T.unpack . TE.decodeUtf8
|
||||||
|
|
||||||
|
foldForm :: HashMap Text B.ByteString -> BL.ByteString
|
||||||
|
foldForm = fold . L.intersperse "&" . M.foldMapWithKey @[BL.ByteString] \k v -> [BL.fromStrict $ TE.encodeUtf8 (escapeURIText k) <> "=" <> escapeURIBS v]
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
- description: Test Webhook Transform
|
||||||
|
url: /v1/metadata
|
||||||
|
headers:
|
||||||
|
X-Hasura-Role: admin
|
||||||
|
status: 200
|
||||||
|
response:
|
||||||
|
body: "foo=bar&baz=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:
|
||||||
|
version: 2
|
||||||
|
url: "http://www.google.com"
|
||||||
|
template_engine: Kriti
|
||||||
|
body:
|
||||||
|
action: x_www_form_urlencoded
|
||||||
|
form_template:
|
||||||
|
foo: bar
|
||||||
|
baz: "{{$body.hello}}"
|
||||||
|
method: POST
|
||||||
|
query_params:
|
||||||
|
"foo": "bar"
|
||||||
|
request_headers:
|
||||||
|
add_headers:
|
||||||
|
foo: "bar"
|
||||||
|
content-type: "application/json"
|
@ -229,6 +229,9 @@ class TestMetadata:
|
|||||||
def test_webhook_transform_success_old_body_schema(self, hge_ctx):
|
def test_webhook_transform_success_old_body_schema(self, hge_ctx):
|
||||||
check_query_f(hge_ctx, self.dir() + '/test_webhook_transform_success_old_body_schema.yaml')
|
check_query_f(hge_ctx, self.dir() + '/test_webhook_transform_success_old_body_schema.yaml')
|
||||||
|
|
||||||
|
def test_webhook_transform_success_form_urlencoded(self, hge_ctx):
|
||||||
|
check_query_f(hge_ctx, self.dir() + '/test_webhook_transform_success_form_urlencoded.yaml')
|
||||||
|
|
||||||
def test_webhook_transform_with_url_env_reference_success(self, hge_ctx):
|
def test_webhook_transform_with_url_env_reference_success(self, hge_ctx):
|
||||||
check_query_f(hge_ctx, self.dir() + '/test_webhook_transform_env_reference_success.yaml')
|
check_query_f(hge_ctx, self.dir() + '/test_webhook_transform_env_reference_success.yaml')
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user