2022-03-08 03:42:06 +03:00
|
|
|
{-# LANGUAGE UndecidableInstances #-}
|
2023-03-22 02:59:42 +03:00
|
|
|
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
|
|
|
|
|
|
|
{-# HLINT ignore "Use maybe" #-}
|
2022-03-08 03:42:06 +03:00
|
|
|
|
|
|
|
-- | Webhook Transformations are data transformations used to modify
|
|
|
|
-- HTTP Requests/Responses before requests are executed and after
|
|
|
|
-- responses are received.
|
|
|
|
--
|
|
|
|
-- Transformations are supplied by users as part of the Metadata for a
|
|
|
|
-- particular Action or EventTrigger as a 'RequestTransform'
|
|
|
|
-- record. Per-field Transformations are stored as data
|
|
|
|
-- (defunctionalized), often in the form of a Kriti template, and then
|
|
|
|
-- converted into actual functions (reified) at runtime by the
|
|
|
|
-- 'Transform' typeclass.
|
|
|
|
--
|
|
|
|
-- We take a Higher Kinded Data (HKD) approach to representing the
|
|
|
|
-- transformations. 'RequestFields' is an HKD which can represent the
|
|
|
|
-- actual request data as 'RequestFields Identity' or the
|
|
|
|
-- defunctionalized transforms as 'RequestFields (WithOptional
|
|
|
|
-- TransformFn)'.
|
|
|
|
--
|
|
|
|
-- We can then traverse over the entire 'RequestFields' HKD to reify
|
|
|
|
-- all the fields at once and apply them to our actual request
|
|
|
|
-- data.
|
|
|
|
--
|
|
|
|
-- NOTE: We don't literally use 'traverse' or the HKD equivalent
|
|
|
|
-- 'btraverse', but you can think of this operation morally as a
|
|
|
|
-- traversal. See 'applyRequestTransform' for implementation details.
|
|
|
|
module Hasura.RQL.DDL.Webhook.Transform
|
|
|
|
( -- * Request Transformation
|
|
|
|
RequestFields (..),
|
|
|
|
RequestTransform (..),
|
2022-03-23 23:23:46 +03:00
|
|
|
RequestTransformFns,
|
2022-03-08 03:42:06 +03:00
|
|
|
applyRequestTransform,
|
|
|
|
|
|
|
|
-- * Request Transformation Context
|
|
|
|
RequestTransformCtx (..),
|
2022-10-16 06:53:13 +03:00
|
|
|
RequestContext,
|
|
|
|
mkRequestContext,
|
|
|
|
mkReqTransformCtx,
|
2022-03-08 03:42:06 +03:00
|
|
|
TransformErrorBundle (..),
|
|
|
|
|
|
|
|
-- * Optional Functor
|
|
|
|
WithOptional (..),
|
2022-03-23 23:23:46 +03:00
|
|
|
withOptional,
|
2022-03-08 03:42:06 +03:00
|
|
|
|
|
|
|
-- * Old Style Response Transforms
|
|
|
|
MetadataResponseTransform (..),
|
|
|
|
ResponseTransform (..),
|
|
|
|
ResponseTransformCtx (..),
|
|
|
|
applyResponseTransform,
|
|
|
|
buildRespTransformCtx,
|
|
|
|
mkResponseTransform,
|
|
|
|
)
|
|
|
|
where
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
2023-03-22 02:59:42 +03:00
|
|
|
import Control.Lens (Lens', lens, preview, set, traverseOf, view)
|
2023-04-26 20:28:48 +03:00
|
|
|
import Data.Aeson.Extended qualified as J
|
2022-03-08 03:42:06 +03:00
|
|
|
import Data.ByteString.Lazy qualified as BL
|
|
|
|
import Data.CaseInsensitive qualified as CI
|
2022-03-23 23:23:46 +03:00
|
|
|
import Data.Functor.Barbie qualified as B
|
2022-03-08 03:42:06 +03:00
|
|
|
import Data.Text.Encoding qualified as TE
|
|
|
|
import Data.Validation qualified as V
|
|
|
|
import Hasura.Prelude hiding (first)
|
2023-04-28 13:46:44 +03:00
|
|
|
import Hasura.RQL.DDL.Webhook.Transform.Body (Body (..), BodyTransformFn)
|
2022-03-23 23:23:46 +03:00
|
|
|
import Hasura.RQL.DDL.Webhook.Transform.Body qualified as Body
|
2022-03-08 03:42:06 +03:00
|
|
|
import Hasura.RQL.DDL.Webhook.Transform.Class
|
2022-10-16 06:53:13 +03:00
|
|
|
import Hasura.RQL.DDL.Webhook.Transform.Headers
|
2022-03-08 03:42:06 +03:00
|
|
|
import Hasura.RQL.DDL.Webhook.Transform.Method
|
|
|
|
import Hasura.RQL.DDL.Webhook.Transform.QueryParams
|
2022-10-16 06:53:13 +03:00
|
|
|
import Hasura.RQL.DDL.Webhook.Transform.Request
|
|
|
|
import Hasura.RQL.DDL.Webhook.Transform.Response
|
2022-03-08 03:42:06 +03:00
|
|
|
import Hasura.RQL.DDL.Webhook.Transform.Url
|
2023-04-28 13:46:44 +03:00
|
|
|
import Hasura.RQL.Types.Webhook.Transform (MetadataResponseTransform (..), RequestContext, RequestData, RequestFields (..), RequestTransform (..), RequestTransformFns)
|
|
|
|
import Hasura.RQL.Types.Webhook.Transform.WithOptional (WithOptional (..), withOptional)
|
2022-07-21 10:05:46 +03:00
|
|
|
import Hasura.Session (SessionVariables)
|
2022-03-08 03:42:06 +03:00
|
|
|
import Network.HTTP.Client.Transformable qualified as HTTP
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
-- TODO(SOLOMON): Add lens law unit tests
|
|
|
|
|
2022-03-23 23:23:46 +03:00
|
|
|
-- | A 'Lens\'' for viewing a 'HTTP.Request' as our 'RequestData' HKD; it does
|
|
|
|
-- so by wrapping each of the matching request fields in a corresponding
|
|
|
|
-- 'TransformFn'.
|
2022-03-16 03:39:21 +03:00
|
|
|
--
|
|
|
|
-- XXX: This function makes internal usage of 'TE.decodeUtf8', which throws an
|
|
|
|
-- impure exception when the supplied 'ByteString' cannot be decoded into valid
|
|
|
|
-- UTF8 text!
|
2022-03-23 23:23:46 +03:00
|
|
|
requestL :: Lens' HTTP.Request RequestData
|
|
|
|
requestL = lens getter setter
|
2022-03-08 03:42:06 +03:00
|
|
|
where
|
|
|
|
getter :: HTTP.Request -> RequestData
|
|
|
|
getter req =
|
|
|
|
RequestFields
|
|
|
|
{ method = coerce $ CI.mk $ TE.decodeUtf8 $ view HTTP.method req,
|
|
|
|
url = coerce $ view HTTP.url req,
|
2023-04-26 20:28:48 +03:00
|
|
|
body = coerce $ JSONBody $ J.decode =<< preview (HTTP.body . HTTP._RequestBodyLBS) req,
|
2022-03-08 03:42:06 +03:00
|
|
|
queryParams = coerce $ view HTTP.queryParams req,
|
|
|
|
requestHeaders = coerce $ view HTTP.headers req
|
|
|
|
}
|
|
|
|
|
2023-03-22 02:59:42 +03:00
|
|
|
serializeBody :: Body -> HTTP.RequestBody
|
2022-03-08 03:42:06 +03:00
|
|
|
serializeBody = \case
|
2023-04-26 20:28:48 +03:00
|
|
|
JSONBody body -> HTTP.RequestBodyLBS $ fromMaybe mempty $ fmap J.encode body
|
2023-03-22 02:59:42 +03:00
|
|
|
RawBody "" -> mempty
|
|
|
|
RawBody bs -> HTTP.RequestBodyLBS bs
|
2022-03-08 03:42:06 +03:00
|
|
|
|
|
|
|
setter :: HTTP.Request -> RequestData -> HTTP.Request
|
|
|
|
setter req RequestFields {..} =
|
|
|
|
req
|
|
|
|
& set HTTP.method (TE.encodeUtf8 $ CI.original $ coerce method)
|
|
|
|
& set HTTP.body (serializeBody $ coerce body)
|
|
|
|
& set HTTP.url (coerce url)
|
|
|
|
& set HTTP.queryParams (unQueryParams $ coerce queryParams)
|
|
|
|
& set HTTP.headers (coerce requestHeaders)
|
|
|
|
|
|
|
|
-- | Transform an 'HTTP.Request' with a 'RequestTransform'.
|
|
|
|
--
|
|
|
|
-- Note: we pass in the request url explicitly for use in the
|
|
|
|
-- 'ReqTransformCtx'. We do this so that we can ensure that the url
|
|
|
|
-- is syntactically identical to what the use submits. If we use the
|
|
|
|
-- parsed request from the 'HTTP.Request' term then it is possible
|
|
|
|
-- that the url is semantically equivalent but syntactically
|
|
|
|
-- different. An example of this is the presence or lack of a trailing
|
|
|
|
-- slash on the URL path. This important when performing string
|
|
|
|
-- interpolation on the request url.
|
|
|
|
applyRequestTransform ::
|
2022-03-23 23:23:46 +03:00
|
|
|
forall m.
|
2022-03-08 03:42:06 +03:00
|
|
|
MonadError TransformErrorBundle m =>
|
2022-10-16 06:53:13 +03:00
|
|
|
(HTTP.Request -> RequestContext) ->
|
2022-03-23 23:23:46 +03:00
|
|
|
RequestTransformFns ->
|
2022-03-08 03:42:06 +03:00
|
|
|
HTTP.Request ->
|
|
|
|
m HTTP.Request
|
2022-03-23 23:23:46 +03:00
|
|
|
applyRequestTransform mkCtx transformations request =
|
|
|
|
traverseOf
|
|
|
|
requestL
|
|
|
|
(transformReqData (mkCtx request))
|
|
|
|
request
|
|
|
|
where
|
|
|
|
-- Apply all of the provided request transformation functions to the
|
|
|
|
-- request data extracted from the given 'HTTP.Request'.
|
2022-10-16 06:53:13 +03:00
|
|
|
transformReqData transformCtx reqData =
|
2022-03-23 23:23:46 +03:00
|
|
|
B.bsequence' $
|
2022-10-16 06:53:13 +03:00
|
|
|
B.bzipWith3C @Transform
|
|
|
|
transformField
|
|
|
|
transformCtx
|
2022-03-23 23:23:46 +03:00
|
|
|
transformations
|
|
|
|
reqData
|
|
|
|
-- Apply a transformation to some request data, if it exists; otherwise
|
|
|
|
-- return the original request data.
|
|
|
|
transformField ctx (WithOptional maybeFn) (Identity a) =
|
|
|
|
case maybeFn of
|
|
|
|
Nothing -> pure a
|
|
|
|
Just fn -> transform fn ctx a
|
2022-03-08 03:42:06 +03:00
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- TODO(SOLOMON): Rewrite with HKD
|
|
|
|
|
|
|
|
-- | A set of data transformation functions generated from a
|
|
|
|
-- 'MetadataResponseTransform'. 'Nothing' means use the original
|
|
|
|
-- response value.
|
|
|
|
data ResponseTransform = ResponseTransform
|
2023-04-26 20:28:48 +03:00
|
|
|
{ respTransformBody :: Maybe (ResponseTransformCtx -> Either TransformErrorBundle J.Value),
|
2022-03-08 03:42:06 +03:00
|
|
|
respTransformTemplateEngine :: TemplatingEngine
|
|
|
|
}
|
|
|
|
|
2022-10-16 06:53:13 +03:00
|
|
|
-- | A helper function for constructing the 'ResponseTransformCtx'
|
2023-05-15 19:33:49 +03:00
|
|
|
buildRespTransformCtx :: Maybe RequestContext -> Maybe SessionVariables -> TemplatingEngine -> BL.ByteString -> Int -> ResponseTransformCtx
|
|
|
|
buildRespTransformCtx requestContext sessionVars engine respBody respStatusCode =
|
2022-03-08 03:42:06 +03:00
|
|
|
ResponseTransformCtx
|
2023-04-26 20:28:48 +03:00
|
|
|
{ responseTransformBody = fromMaybe J.Null $ J.decode @J.Value respBody,
|
|
|
|
responseTransformReqCtx = J.toJSON requestContext,
|
2022-10-16 06:53:13 +03:00
|
|
|
responseSessionVariables = sessionVars,
|
2023-05-15 19:33:49 +03:00
|
|
|
responseTransformEngine = engine,
|
|
|
|
responseStatusCode = respStatusCode
|
2022-03-08 03:42:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
-- | Construct a Template Transformation function for Responses
|
2022-03-16 03:39:21 +03:00
|
|
|
--
|
|
|
|
-- XXX: This function makes internal usage of 'TE.decodeUtf8', which throws an
|
|
|
|
-- impure exception when the supplied 'ByteString' cannot be decoded into valid
|
|
|
|
-- UTF8 text!
|
2022-03-23 23:23:46 +03:00
|
|
|
mkRespTemplateTransform ::
|
|
|
|
BodyTransformFn ->
|
|
|
|
ResponseTransformCtx ->
|
2023-04-26 20:28:48 +03:00
|
|
|
Either TransformErrorBundle J.Value
|
|
|
|
mkRespTemplateTransform Body.Remove _ = pure J.Null
|
2022-10-16 06:53:13 +03:00
|
|
|
mkRespTemplateTransform (Body.ModifyAsJSON template) context =
|
|
|
|
runResponseTemplateTransform template context
|
|
|
|
mkRespTemplateTransform (Body.ModifyAsFormURLEncoded formTemplates) context = do
|
|
|
|
result <-
|
|
|
|
liftEither . V.toEither . for formTemplates $
|
|
|
|
runUnescapedResponseTemplateTransform' context
|
2023-04-26 20:28:48 +03:00
|
|
|
pure . J.String . TE.decodeUtf8 . BL.toStrict $ Body.foldFormEncoded result
|
2022-03-08 03:42:06 +03:00
|
|
|
|
|
|
|
mkResponseTransform :: MetadataResponseTransform -> ResponseTransform
|
|
|
|
mkResponseTransform MetadataResponseTransform {..} =
|
2022-10-16 06:53:13 +03:00
|
|
|
let bodyTransform = mkRespTemplateTransform <$> mrtBodyTransform
|
2022-03-08 03:42:06 +03:00
|
|
|
in ResponseTransform bodyTransform mrtTemplatingEngine
|
|
|
|
|
|
|
|
-- | At the moment we only transform the body of
|
|
|
|
-- Responses. 'http-client' does not export the constructors for
|
2022-10-16 06:53:13 +03:00
|
|
|
-- 'Response'. If we want to transform other fields then we will need
|
|
|
|
-- additional 'apply' functions.
|
2022-03-23 23:23:46 +03:00
|
|
|
applyResponseTransform ::
|
|
|
|
ResponseTransform ->
|
|
|
|
ResponseTransformCtx ->
|
|
|
|
Either TransformErrorBundle BL.ByteString
|
2022-03-08 03:42:06 +03:00
|
|
|
applyResponseTransform ResponseTransform {..} ctx@ResponseTransformCtx {..} =
|
|
|
|
let bodyFunc :: BL.ByteString -> Either TransformErrorBundle BL.ByteString
|
|
|
|
bodyFunc body =
|
|
|
|
case respTransformBody of
|
|
|
|
Nothing -> pure body
|
2023-04-26 20:28:48 +03:00
|
|
|
Just f -> J.encode <$> f ctx
|
|
|
|
in bodyFunc (J.encode responseTransformBody)
|
2023-04-28 13:46:44 +03:00
|
|
|
|
|
|
|
mkRequestContext :: RequestTransformCtx -> RequestContext
|
|
|
|
mkRequestContext ctx =
|
|
|
|
-- NOTE: Type Applications are here for documentation purposes.
|
|
|
|
RequestFields
|
|
|
|
{ method = coerce @RequestTransformCtx @(TransformCtx Method) ctx,
|
|
|
|
url = coerce @RequestTransformCtx @(TransformCtx Url) ctx,
|
|
|
|
body = coerce @RequestTransformCtx @(TransformCtx Body) ctx,
|
|
|
|
queryParams = coerce @RequestTransformCtx @(TransformCtx QueryParams) ctx,
|
|
|
|
requestHeaders = coerce @RequestTransformCtx @(TransformCtx Headers) ctx
|
|
|
|
}
|