Feature/removable request transform body and modified request transform API

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3475
GitOrigin-RevId: bc847b18d491fe4957a190f5d0fe2ae6e6719791
This commit is contained in:
Solomon 2022-02-16 20:36:24 -08:00 committed by hasura-bot
parent 972caf65f3
commit d1ba271c3d
18 changed files with 367 additions and 391 deletions

View File

@ -3,7 +3,7 @@
## Next release
### Bug fixes and improvements
(Add entries below in the order of server, console, cli, docs, others)
- server: Webhook Transforms can now delete request/response bodies explicitly.
## v2.3.0-beta.1

View File

@ -2042,6 +2042,10 @@ RequestTransformation
- required
- Schema
- Description
* - version
- false
- "1" | "2"
- Sets the `RequestTransformation` schema version. Version `1` uses a `String` for the `body` field and Version `2` takes a :ref:`BodyTransform`. `Defaults to version `1`.
* - method
- false
- String
@ -2052,7 +2056,7 @@ RequestTransformation
- Change the request URL to this value.
* - body
- false
- String
- :ref:`BodyTransform` | String
- A template script for transforming the request body.
* - content_type
- false
@ -2092,7 +2096,6 @@ TransformHeaders
- Array of (:ref:`HeaderKey`)
- Headers to be removed from the request. Content-Type cannot be removed.
.. _HeaderKey:
HeaderKey
@ -2113,6 +2116,29 @@ HeaderValue
String
.. _BodyTransform:
BodyTransform
^^^^^^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- required
- Schema
- Description
* - action
- true
- remove | transform
- The action to perform on the request body.
* - template
- false
- String
- The transformation template to be applied to the body. This is
required if the action is `transform`.
.. _TemplateEngine:
TemplateEngine
@ -2137,9 +2163,13 @@ ResponseTransformation
- required
- Schema
- Description
* - version
- false
- "1" | "2"
- Sets the `RequestTransformation` schema version. Version `1` uses a `String` for the `body` field and Version `2` takes a :ref:`BodyTransform`. `Defaults to version `1`.
* - body
- false
- String
- :ref:`BodyTransform` | String
- A template script for transforming the response body.
* - template_engine
- false

View File

@ -1,3 +1,5 @@
{-# LANGUAGE DeriveAnyClass #-}
module Hasura.RQL.DDL.WebhookTransforms
( applyRequestTransform,
applyResponseTransform,
@ -6,17 +8,18 @@ module Hasura.RQL.DDL.WebhookTransforms
mkRequestTransform,
mkResponseTransform,
RequestMethod (..),
RemoveOrTransform (..),
StringTemplateText (..),
TemplatingEngine (..),
TemplateText (..),
ContentType (..),
ReqTransformCtx (..),
TransformHeaders (..),
TransformErrorBundle (..),
TransformHeaders (..),
MetadataRequestTransform (..),
MetadataResponseTransform (..),
RequestTransform (..),
ResponseTransform (..),
Version (..),
)
where
@ -24,13 +27,14 @@ import Control.Lens (traverseOf, view)
import Data.Aeson qualified as J
import Data.Bifunctor (bimap, first)
import Data.Bitraversable
import Data.ByteString qualified as B
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 Data.Validation qualified as V
import Hasura.Incremental (Cacheable)
import Hasura.Prelude hiding (first)
import Hasura.Session (SessionVariables)
@ -86,13 +90,11 @@ data RespTransformCtx = RespTransformCtx
-- use the original request value, unless otherwise indicated.
data RequestTransform = RequestTransform
{ -- | Change the request method to one provided here. Nothing means POST.
reqTransformRequestMethod :: Maybe RequestMethod,
reqTransformRequestMethod :: Maybe (ReqTransformCtx -> Either TransformErrorBundle RequestMethod),
-- | A function which contructs a new URL given the request context.
reqTransformRequestURL :: Maybe (ReqTransformCtx -> Either TransformErrorBundle Text),
-- | A function for transforming the request body.
reqTransformBody :: Maybe (ReqTransformCtx -> Either TransformErrorBundle J.Value),
-- | Change content type to one provided here.
reqTransformContentType :: Maybe ContentType,
reqTransformBody :: Maybe (RemoveOrTransform (ReqTransformCtx -> Either TransformErrorBundle J.Value)),
-- | A function which contructs new query parameters given the request context.
reqTransformQueryParams :: Maybe (ReqTransformCtx -> Either TransformErrorBundle HTTP.Query),
-- | A function which contructs new Headers given the request context.
@ -102,7 +104,7 @@ data RequestTransform = RequestTransform
-- | A set of data transformation functions generated from a
-- 'MetadataResponseTransform'. 'Nothing' means use the original
-- response value.
newtype ResponseTransform = ResponseTransform {respTransformBody :: Maybe (RespTransformCtx -> Either TransformErrorBundle J.Value)}
newtype ResponseTransform = ResponseTransform {respTransformBody :: Maybe (RemoveOrTransform (RespTransformCtx -> Either TransformErrorBundle J.Value))}
-- | A de/serializable request transformation template which can be stored in
-- the metadata associated with an action/event trigger/etc. and used to produce
@ -119,114 +121,133 @@ newtype ResponseTransform = ResponseTransform {respTransformBody :: Maybe (RespT
-- Nothing values mean use the original request value, unless
-- otherwise indicated.
data MetadataRequestTransform = MetadataRequestTransform
{ -- | Change the request method to one provided here. Nothing means POST.
mtRequestMethod :: Maybe RequestMethod,
{ -- | The Schema Version
mtVersion :: Version,
-- | Change the request method to one provided here. Nothing means POST.
mtRequestMethod :: Maybe StringTemplateText,
-- | Template script for transforming the URL.
mtRequestURL :: Maybe StringTemplateText,
-- | 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,
mtBodyTransform :: Maybe (RemoveOrTransform TemplateText),
-- | A list of template scripts for constructing new Query Params.
mtQueryParams :: Maybe [(StringTemplateText, Maybe StringTemplateText)],
-- | Transform headers as defined here.
-- | A list of template scripts for constructing headers.
mtRequestHeaders :: Maybe TransformHeaders,
-- | The template engine to use for transformations. Default: Kriti
mtTemplatingEngine :: TemplatingEngine
}
deriving (Show, Eq, Generic)
instance NFData MetadataRequestTransform
instance Cacheable MetadataRequestTransform
infixr 8 .=?
(.=?) :: J.ToJSON v => k -> Maybe v -> Maybe (k, J.Value)
(.=?) _ Nothing = Nothing
(.=?) k (Just v) = Just (k, J.toJSON v)
deriving stock (Show, Eq, Generic)
deriving anyclass (NFData, Cacheable)
instance J.ToJSON MetadataRequestTransform where
toJSON MetadataRequestTransform {..} =
J.object $
["template_engine" J..= mtTemplatingEngine]
<> catMaybes
[ "method" .=? mtRequestMethod,
"url" .=? mtRequestURL,
"body" .=? mtBodyTransform,
"content_type" .=? mtContentType,
"query_params" .=? fmap M.fromList mtQueryParams,
"request_headers" .=? mtRequestHeaders
let body = case mtVersion of
V1 -> case mtBodyTransform of
Just (Transform template) -> Just ("body", J.toJSON template)
_ -> Nothing
V2 -> "body" .=? mtBodyTransform
in J.object $
[ "template_engine" J..= mtTemplatingEngine,
"version" J..= mtVersion
]
<> catMaybes
[ "method" .=? mtRequestMethod,
"url" .=? mtRequestURL,
"query_params" .=? fmap M.fromList mtQueryParams,
"request_headers" .=? mtRequestHeaders,
body
]
instance J.FromJSON MetadataRequestTransform where
parseJSON = J.withObject "Object" $ \o -> do
version <- o J..:? "version" J..!= V1
method <- o J..:? "method"
url <- o J..:? "url"
body <- o J..:? "body"
contentType <- o J..:? "content_type"
body <- case version of
V1 -> do
template :: (Maybe TemplateText) <- o J..:? "body"
pure $ fmap Transform template
V2 -> o J..:? "body"
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 $ MetadataRequestTransform method url body contentType queryParams headers templateEngine'
templateEngine <- o J..:? "template_engine" J..!= Kriti
pure $ MetadataRequestTransform version method url body queryParams headers templateEngine
data MetadataResponseTransform = MetadataResponseTransform
{ mrtBodyTransform :: Maybe TemplateText,
{ -- | The Schema Version
mrtVersion :: Version,
-- | Template script for transforming the response body.
mrtBodyTransform :: Maybe (RemoveOrTransform TemplateText),
-- | The template engine to use for transformations. Default: Kriti
mrtTemplatingEngine :: TemplatingEngine
}
deriving (Show, Eq, Generic)
instance NFData MetadataResponseTransform
instance Cacheable MetadataResponseTransform
deriving stock (Show, Eq, Generic)
deriving anyclass (NFData, Cacheable)
instance J.ToJSON MetadataResponseTransform where
toJSON MetadataResponseTransform {..} =
J.object $
["template_engine" J..= mrtTemplatingEngine]
<> catMaybes ["body" .=? mrtBodyTransform]
let body = case mrtVersion of
V1 -> case mrtBodyTransform of
Just (Transform template) -> Just ("body", J.toJSON template)
_ -> Nothing
V2 -> "body" .=? mrtBodyTransform
in J.object $
[ "template_engine" J..= mrtTemplatingEngine,
"version" J..= mrtVersion
]
<> catMaybes [body]
instance J.FromJSON MetadataResponseTransform where
parseJSON = J.withObject "Object" $ \o -> do
body <- o J..:? "body"
version <- o J..:? "version" J..!= V1
body <- case version of
V1 -> do
template :: (Maybe TemplateText) <- o J..:? "body"
pure $ fmap Transform template
V2 -> o J..:? "body"
templateEngine <- o J..:? "template_engine"
let templateEngine' = fromMaybe Kriti templateEngine
pure $ MetadataResponseTransform body templateEngine'
pure $ MetadataResponseTransform version body templateEngine'
data RequestMethod = GET | POST | PUT | PATCH | DELETE
deriving (Show, Eq, Enum, Bounded, Generic)
data RemoveOrTransform a = Remove | Transform a
deriving stock (Show, Eq, Functor, Generic)
deriving anyclass (NFData, Cacheable)
renderRequestMethod :: RequestMethod -> Text
renderRequestMethod = \case
GET -> "GET"
POST -> "POST"
PUT -> "PUT"
PATCH -> "PATCH"
DELETE -> "DELETE"
instance J.ToJSON a => J.ToJSON (RemoveOrTransform a) where
toJSON = \case
Remove -> J.object ["action" J..= ("remove" :: T.Text)]
Transform a ->
J.object
[ "action" J..= ("transform" :: T.Text),
"template" J..= J.toJSON a
]
instance J.FromJSON a => J.FromJSON (RemoveOrTransform a) where
parseJSON = J.withObject "RemoveOrTransform" $ \o -> do
action :: T.Text <- o J..: "action"
if action == "remove"
then pure Remove
else do
"transform" :: T.Text <- o J..: "action"
template <- o J..: "template"
pure $ Transform template
newtype RequestMethod = RequestMethod (CI.CI T.Text)
deriving stock (Generic)
deriving newtype (Show, Eq)
deriving anyclass (NFData, Cacheable)
instance J.ToJSON RequestMethod where
toJSON = J.String . renderRequestMethod
toJSON = J.String . CI.original . coerce
instance J.FromJSON RequestMethod where
parseJSON = J.withText "RequestMethod" \case
"GET" -> pure GET
"POST" -> pure POST
"PUT" -> pure PUT
"PATCH" -> pure PATCH
"DELETE" -> pure DELETE
_ -> fail "Invalid Request Method"
instance NFData RequestMethod
instance Cacheable RequestMethod
parseJSON = J.withText "RequestMethod" (pure . coerce . CI.mk)
-- | Available Template Languages
data TemplatingEngine = Kriti
deriving (Show, Eq, Enum, Bounded, Generic)
deriving stock (Show, Eq, Enum, Bounded, Generic)
deriving anyclass (NFData, Cacheable)
renderTemplatingEngine :: TemplatingEngine -> Text
renderTemplatingEngine _ = "Kriti"
@ -239,18 +260,28 @@ instance J.FromJSON TemplatingEngine where
instance J.ToJSON TemplatingEngine where
toJSON = J.String . renderTemplatingEngine
instance NFData TemplatingEngine
data Version = V1 | V2
deriving stock (Show, Eq, Generic)
deriving anyclass (NFData, Cacheable, Hashable)
instance Cacheable TemplatingEngine
instance J.FromJSON Version where
parseJSON v = do
version :: Int <- J.parseJSON v
case version of
1 -> pure V1
2 -> pure V2
i -> fail $ "expected 1 or 3, encountered " ++ show i
instance J.ToJSON Version where
toJSON = \case
V1 -> J.toJSON @Int 1
V2 -> J.toJSON @Int 2
-- | Unparsed Kriti templating code
newtype TemplateText = TemplateText {unTemplateText :: T.Text}
deriving stock (Show, Eq, Ord, Generic)
deriving newtype (Hashable, J.ToJSONKey, J.FromJSONKey)
instance NFData TemplateText
instance Cacheable TemplateText
deriving anyclass (NFData, Cacheable)
instance J.FromJSON TemplateText where
parseJSON = J.withText "TemplateText" \t ->
@ -297,54 +328,22 @@ instance J.FromJSON StringTemplateText where
instance J.ToJSON StringTemplateText where
toJSON = J.String . coerce
-- | Wrap a template with escaped double quotes.
wrapTemplate :: StringTemplateText -> StringTemplateText
wrapTemplate (StringTemplateText t) = StringTemplateText $ "\"" <> t <> "\""
data ContentType = JSON | XWWWFORM
deriving (Show, Eq, Enum, Bounded, Generic)
renderContentType :: ContentType -> Text
renderContentType = \case
JSON -> "application/json"
XWWWFORM -> "application/x-www-form-urlencoded"
instance J.ToJSON ContentType where
toJSON = J.String . renderContentType
instance J.FromJSON ContentType where
parseJSON = J.withText "ContentType" \case
"application/json" -> pure JSON
"application/x-www-form-urlencoded" -> pure XWWWFORM
_ -> fail "Invalid ContentType"
instance NFData ContentType
instance Cacheable ContentType
-- | This newtype exists solely to anchor a `FromJSON` instance and is
-- eliminated in the `TransformHeaders` `FromJSON` instance.
newtype HeaderKey = HeaderKey {unHeaderKey :: CI.CI Text}
deriving (Show, Eq, Ord, Generic)
instance NFData HeaderKey
instance Cacheable HeaderKey
deriving stock (Show, Eq, Ord, Generic)
deriving anyclass (NFData, Cacheable)
instance J.FromJSON HeaderKey where
parseJSON = J.withText "HeaderKey" \txt -> case CI.mk txt of
"Content-Type" -> fail "Restricted Header: Content-Type"
key -> pure $ HeaderKey key
data TransformHeaders = TransformHeaders
{ addHeaders :: [(CI.CI Text, StringTemplateText)],
removeHeaders :: [CI.CI Text]
}
deriving (Show, Eq, Ord, Generic)
instance NFData TransformHeaders
instance Cacheable TransformHeaders
deriving stock (Show, Eq, Ord, Generic)
deriving anyclass (NFData, Cacheable)
instance J.ToJSON TransformHeaders where
toJSON TransformHeaders {..} =
@ -364,10 +363,7 @@ instance J.FromJSON TransformHeaders where
newtype TransformErrorBundle = TransformErrorBundle {teMessages :: [J.Value]}
deriving stock (Show, Eq, Generic)
deriving newtype (Semigroup, Monoid, J.ToJSON)
instance NFData TransformErrorBundle
instance Cacheable TransformErrorBundle
deriving anyclass (NFData, Cacheable)
-------------------------------
--- Constructing Transforms ---
@ -376,84 +372,42 @@ instance Cacheable TransformErrorBundle
-- | Construct a `RequestTransform` from its metadata representation.
mkRequestTransform :: MetadataRequestTransform -> RequestTransform
mkRequestTransform MetadataRequestTransform {..} =
let urlTransform = mkUrlTransform mtTemplatingEngine <$> mtRequestURL
let methodTransform = mkMethodTransform mtTemplatingEngine <$> mtRequestMethod
urlTransform = mkUrlTransform mtTemplatingEngine <$> mtRequestURL
queryTransform = mkQueryParamsTransform mtTemplatingEngine <$> mtQueryParams
headerTransform = mkHeaderTransform mtTemplatingEngine <$> mtRequestHeaders
bodyTransform = mkReqTemplateTransform mtTemplatingEngine <$> mtBodyTransform
in RequestTransform mtRequestMethod urlTransform bodyTransform mtContentType queryTransform headerTransform
bodyTransform = fmap (mkReqTemplateTransform mtTemplatingEngine) <$> mtBodyTransform
in RequestTransform methodTransform urlTransform bodyTransform queryTransform headerTransform
mkResponseTransform :: MetadataResponseTransform -> ResponseTransform
mkResponseTransform MetadataResponseTransform {..} =
let bodyTransform = mkRespTemplateTransform mrtTemplatingEngine <$> mrtBodyTransform
let bodyTransform = fmap (mkRespTemplateTransform mrtTemplatingEngine) <$> mrtBodyTransform
in ResponseTransform bodyTransform
-- | Transform a String Template
transformST :: TemplatingEngine -> ReqTransformCtx -> StringTemplateText -> Either TransformErrorBundle B.ByteString
transformST engine transformCtx t =
valueToString =<< mkReqTemplateTransform engine (coerce wrapTemplate t) transformCtx
mkMethodTransform :: TemplatingEngine -> StringTemplateText -> ReqTransformCtx -> Either TransformErrorBundle RequestMethod
mkMethodTransform engine template transformCtx =
fmap (coerce . CI.mk . TE.decodeUtf8) $ transformST engine transformCtx template
mkQueryParamsTransform :: TemplatingEngine -> [(StringTemplateText, Maybe StringTemplateText)] -> ReqTransformCtx -> Either TransformErrorBundle HTTP.Query
mkQueryParamsTransform engine templates transformCtx =
let transform t =
case mkReqTemplateTransform engine (coerce wrapTemplate 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))
let transform = eitherToValidation . transformST engine transformCtx
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)
results = fmap (bimap transform (fmap transform)) templates
collectedResults = validationToEither $ collectErrors results
in (fmap . fmap) (bimap TE.encodeUtf8 (fmap TE.encodeUtf8)) collectedResults
transformationResults :: [(Validation TransformErrorBundle B.ByteString, Maybe (Validation TransformErrorBundle B.ByteString))]
transformationResults = fmap (bimap transform (fmap transform)) templates
in validationToEither $ traverse (bitraverse id sequenceA) transformationResults
-- | Given a `TransformHeaders` and the `ReqTransformCtx`, Construct a
-- function to transform the existing headers.
mkHeaderTransform :: TemplatingEngine -> TransformHeaders -> ReqTransformCtx -> Either TransformErrorBundle ([HTTP.Header] -> [HTTP.Header])
mkHeaderTransform engine TransformHeaders {..} transformCtx =
let transform t =
case mkReqTemplateTransform engine (coerce $ wrapTemplate 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' <>))
mkHeaderTransform engine TransformHeaders {..} ctx = do
add <- V.toEither $ fmap mappend $ traverse (bitraverse (pure . CI.map TE.encodeUtf8) (V.fromEither . transformST engine ctx)) addHeaders
let filter' xs = filter (flip elem (fmap (CI.map TE.encodeUtf8) removeHeaders) . fst) xs
pure $ add . filter'
mkUrlTransform :: TemplatingEngine -> StringTemplateText -> ReqTransformCtx -> Either TransformErrorBundle Text
mkUrlTransform engine template transformCtx =
@ -532,13 +486,18 @@ buildRespTransformCtx reqCtx respBody =
applyRequestTransform :: (HTTP.Request -> ReqTransformCtx) -> RequestTransform -> HTTP.Request -> Either TransformErrorBundle HTTP.Request
applyRequestTransform transformCtx' RequestTransform {..} reqData =
let transformCtx = transformCtx' reqData
method = fmap (TE.encodeUtf8 . renderRequestMethod) reqTransformRequestMethod
methodFunc :: B.ByteString -> Either TransformErrorBundle B.ByteString
methodFunc method =
case reqTransformRequestMethod of
Nothing -> pure method
Just f -> TE.encodeUtf8 . CI.original . coerce <$> f transformCtx
bodyFunc :: Maybe BL.ByteString -> Either TransformErrorBundle (Maybe BL.ByteString)
bodyFunc body =
case reqTransformBody of
Nothing -> pure body
Just f -> pure . J.encode <$> f transformCtx
Just Remove -> pure Nothing
Just (Transform f) -> pure . J.encode <$> f transformCtx
urlFunc :: Text -> Either TransformErrorBundle Text
urlFunc url =
@ -556,18 +515,12 @@ applyRequestTransform transformCtx' RequestTransform {..} reqData =
headerFunc headers =
case reqTransformRequestHeaders 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) reqTransformContentType
Just f -> f transformCtx <*> pure headers
in reqData & traverseOf HTTP.url urlFunc
>>= traverseOf HTTP.body bodyFunc
>>= traverseOf HTTP.queryParams queryFunc
>>= traverseOf HTTP.method (pure . (`fromMaybe` method))
>>= traverseOf HTTP.method methodFunc
>>= traverseOf HTTP.headers headerFunc
>>= traverseOf HTTP.headers contentTypeFunc
-- | At the moment we only transform the body of
-- Responses. 'http-client' does not export the constructors for
@ -579,5 +532,39 @@ applyResponseTransform ResponseTransform {..} ctx@RespTransformCtx {..} =
bodyFunc body =
case respTransformBody of
Nothing -> pure body
Just f -> J.encode <$> f ctx
Just Remove -> pure mempty
Just (Transform f) -> J.encode <$> f ctx
in bodyFunc (J.encode rtcBody)
-----------------
--- Utilities ---
-----------------
valueToString :: MonadError TransformErrorBundle m => J.Value -> m B.ByteString
valueToString = \case
J.String str -> pure $ TE.encodeUtf8 str
J.Number num -> pure $ TE.encodeUtf8 (tshow num)
J.Bool True -> pure "true"
J.Bool False -> pure "false"
val -> throwErrorBundle "Template must produce a String, Number, or Boolean value" (Just val)
throwErrorBundle :: MonadError TransformErrorBundle m => T.Text -> Maybe J.Value -> m a
throwErrorBundle msg val =
throwError $
TransformErrorBundle $
pure $
J.object $
[ "error_code" J..= J.String "TransformationError",
"message" J..= J.String msg
]
<> catMaybes [("value" J..=) <$> val]
infixr 8 .=?
(.=?) :: J.ToJSON v => k -> Maybe v -> Maybe (k, J.Value)
(.=?) _ Nothing = Nothing
(.=?) k (Just v) = Just (k, J.toJSON v)
-- | Wrap a template with escaped double quotes.
wrapTemplate :: StringTemplateText -> StringTemplateText
wrapTemplate (StringTemplateText t) = StringTemplateText $ "\"" <> t <> "\""

View File

@ -13,8 +13,8 @@ import Test.Hspec.Hedgehog
spec :: Spec
spec = do
it "RequstMethod RoundTrip" $
hedgehog $ forAll genRequestMethod >>= trippingJSON
it "StringTemplateText RoundTrip" $
hedgehog $ forAll genStringTemplateText >>= trippingJSON
it "TemplateEngine RoundTrip" $
hedgehog $ forAll genTemplatingEngine >>= trippingJSON
@ -22,9 +22,6 @@ spec = do
it "TemplateText RoundTrip" $
hedgehog $ forAll genTemplateText >>= trippingJSON
it "ContentType RoundTrip" $
hedgehog $ forAll genContentType >>= trippingJSON
it "TransformHeaders" $
hedgehog $ do
headers <- forAll genTransformHeaders
@ -36,16 +33,13 @@ spec = do
hedgehog $ do
transform <- forAll genMetadataRequestTransform
let sortH TransformHeaders {..} = TransformHeaders (sort addHeaders) (sort removeHeaders)
sortMT mt@MetadataRequestTransform {mtRequestHeaders, mtQueryParams} = mt {mtRequestHeaders = sortH <$> mtRequestHeaders, mtQueryParams = sort <$> mtQueryParams}
let sortMT mt@MetadataRequestTransform {mtRequestHeaders, mtQueryParams} = mt {mtRequestHeaders = sortH <$> mtRequestHeaders, mtQueryParams = sort <$> mtQueryParams}
transformMaybe = eitherDecode $ encode transform
Right (sortMT transform) === fmap sortMT transformMaybe
trippingJSON :: (Show a, Eq a, ToJSON a, FromJSON a, MonadTest m) => a -> m ()
trippingJSON val = tripping val (toJSON) (fromJSON)
genRequestMethod :: Gen RequestMethod
genRequestMethod = Gen.enumBounded @_ @RequestMethod
genTemplatingEngine :: Gen TemplatingEngine
genTemplatingEngine = Gen.enumBounded @_ @TemplatingEngine
@ -59,9 +53,6 @@ genTemplateText = TemplateText . wrap <$> Gen.text (Range.constant 3 20) Gen.alp
genStringTemplateText :: Gen StringTemplateText
genStringTemplateText = StringTemplateText <$> Gen.text (Range.constant 3 20) Gen.alphaNum
genContentType :: Gen ContentType
genContentType = Gen.enumBounded @_ @ContentType
genTransformHeaders :: Gen TransformHeaders
genTransformHeaders = do
numHeaders <- Gen.integral $ Range.constant 1 20
@ -93,19 +84,18 @@ genUrl = do
genMetadataRequestTransform :: Gen MetadataRequestTransform
genMetadataRequestTransform = do
method <- Gen.maybe genRequestMethod
method <- Gen.maybe genStringTemplateText
-- NOTE: At the moment no need to generate valid urls or templates
-- but such instances maybe useful in the future.
url <- Gen.maybe $ genUrl
bodyTransform <- Gen.maybe $ genTemplateText
contentType <- Gen.maybe $ genContentType
bodyTransform <- Gen.maybe $ fmap Transform $ genTemplateText
queryParams <- Gen.maybe $ genQueryParams
reqHeaders <- Gen.maybe $ genTransformHeaders
MetadataRequestTransform
V2
method
url
bodyTransform
contentType
queryParams
reqHeaders
<$> genTemplatingEngine

View File

@ -72,14 +72,17 @@ args:
output_type: UserId
handler: http://127.0.0.1:5593/create-user
request_transform:
version: 2
template_engine: Kriti
body: |
{
"input": {
"email": "foo@bar.com",
"name": "notClarke"
body:
action: transform
template: |
{
"input": {
"email": "foo@bar.com",
"name": "notClarke"
}
}
}
- type: create_action
args:

View File

@ -252,16 +252,19 @@ args:
output_type: OutObject
handler: http://127.0.0.1:5593/mirror-action
request_transform:
version: 2
template_engine: Kriti
body: |
{
"input": {
"arg": {
"id": {{ $body.input.arg.name }},
"name": {{ $body.input.arg.id }}
body:
action: transform
template: |
{
"input": {
"arg": {
"id": {{ $body.input.arg.name }},
"name": {{ $body.input.arg.id }}
}
}
}
}
- type: create_action
args:
@ -285,12 +288,15 @@ args:
output_type: OutObjectTransformed
handler: http://127.0.0.1:5593/mirror-action
response_transform:
version: 2
template_engine: Kriti
body: |
{
"foo": {{ $body.id }},
"bar": {{ $body.name }}
}
body:
action: transform
template: |
{
"foo": {{ $body.id }},
"bar": {{ $body.name }}
}
- type: create_action
args:
@ -333,24 +339,27 @@ args:
output_type: NestedOutObjectTransformed!
handler: http://127.0.0.1:5593/get-user-by-email-nested
response_transform:
version: 2
template_engine: Kriti
body: |
{
"uid": {{ $body.user_id.id }},
"city0": {{ $body.addresses[0].city }},
"country0": {{ $body.addresses[0].country }},
"other_addresses":
{{ range i, x := $body.addresses }}
{{ if i > 0 }}
{
"city": {{ x.city }},
"country": {{ x.country }}
}
{{ else }}
null
body:
action: transform
template: |
{
"uid": {{ $body.user_id.id }},
"city0": {{ $body.addresses[0].city }},
"country0": {{ $body.addresses[0].country }},
"other_addresses":
{{ range i, x := $body.addresses }}
{{ if i > 0 }}
{
"city": {{ x.city }},
"country": {{ x.country }}
}
{{ else }}
null
{{ end }}
{{ end }}
{{ end }}
}
}
- type: create_action
args:
@ -445,10 +454,13 @@ args:
output_type: '[Result]'
handler: http://127.0.0.1:5593/get-results
response_transform:
version: 2
template_engine: Kriti
body: |
{{ range i, x := $body.result_ids }}
{
"id": {{ x }}
}
{{ end }}
body:
action: transform
template: |
{{ range i, x := $body.result_ids }}
{
"id": {{ x }}
}
{{ end }}

View File

@ -1,26 +0,0 @@
- description: PG create event trigger
url: /v1/metadata
status: 400
response:
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:
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
request_transform:
template_engine: Kriti
content_type: multipart/form-data

View File

@ -20,13 +20,13 @@
- last_name
replace: false
request_transform:
content_type: application/json
version: 2
template_engine: Kriti
body: "{{ $body.event.data.new }}"
body:
action: transform
template: "{{ $body.event.data.new }}"
query_params:
"foo": "bar"
request_headers:
add_headers:
foo: "bar"
remove_headers:
- User-Agent

View File

@ -1,23 +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
request_transform:
content_type: application/json

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
request_transform:
template_engine: Kriti
content_type: application/x-www-form-urlencoded

View File

@ -22,5 +22,8 @@
body:
hello: world
request_transform:
body: "{{ $body.world }}"
version: 2
body:
action: transform
template: "{{ $body.world }}"
template_engine: Kriti

View File

@ -4,7 +4,7 @@
X-Hasura-Role: admin
status: 400
response:
path: "$.args.request_transform.body"
path: "$.args.request_transform.body.template"
error: "Unexpected token '$body'."
code: "parse-failed"
query:
@ -14,5 +14,8 @@
body:
hello: world
request_transform:
body: "$body.hello }}"
version: 2
body:
action: transform
template: "$body.hello }}"
template_engine: Kriti

View File

@ -22,12 +22,16 @@
body:
hello: world
request_transform:
version: 2
url: "http://www.google.com"
template_engine: Kriti
body: "\"hello_{{ $body.hello }}\""
body:
action: transform
template: "\"hello_{{ $body.hello }}\""
method: POST
query_params:
"foo": "bar"
request_headers:
add_headers:
foo: "bar"
content-type: "application/json"

View File

@ -19,12 +19,16 @@
body:
hello: world
request_transform:
version: 2
url: "http://www.google.com"
template_engine: Kriti
body: "\"hello_{{ $body.hello }}\""
body:
action: transform
template: "\"hello_{{ $body.hello }}\""
method: POST
query_params:
"foo": "bar"
request_headers:
add_headers:
foo: "bar"
content-type: "application/json"

View File

@ -0,0 +1,31 @@
- description: Test Webhook Transform Old Body Schema
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"
content-type: "application/json"

View File

@ -0,0 +1,30 @@
- description: Test Webhook Transform
url: /v1/metadata
headers:
X-Hasura-Role: admin
status: 200
response:
headers:
- - foo
- bar
body:
method: GET
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: remove
method: GET
query_params:
"foo": "bar"
request_headers:
add_headers:
foo: "bar"

View File

@ -870,57 +870,3 @@ class TestEventTransform(object):
removedHeaders=["user-agent"],
webhook_path=expectedPath)
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')
# 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_event_data = {
"old": None,
"new": insert_row
}
check_event(hge_ctx,
evts_webhook,
"sample_trigger",
table,
"INSERT",
expected_event_data,
{"Content-Type": "application/json"})
assert st_code == 200, resp
def test_content_type_allows_urlencoded(self, hge_ctx, evts_webhook):
# GIVEN
check_query_f(hge_ctx, self.dir() + '/url_encoded_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_event_data = {
"old": None,
"new": insert_row
}
check_event(hge_ctx,
evts_webhook,
"sample_trigger",
table,
"INSERT",
expected_event_data,
{"Content-Type": "application/x-www-form-urlencoded"})
assert st_code == 200, resp
def test_content_type_disallows_bad_content_types(self, hge_ctx, evts_webhook):
check_query_f(hge_ctx, self.dir() + '/bad_content_type_transform.yaml')
def test_transform_headers_disallows_bad_content_types(self, hge_ctx, evts_webhook):
check_query_f(hge_ctx, self.dir() + '/bad_header_transform.yaml')

View File

@ -223,6 +223,12 @@ class TestMetadata:
def test_webhook_transform_success(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/test_webhook_transform_success.yaml')
def test_webhook_transform_success_remove_body(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/test_webhook_transform_success_remove_body.yaml')
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')
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')