Webhook Transforms: Move RequestCtx into a type family

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5975
GitOrigin-RevId: 08ad528b2600379deb4cef9d39968126c7c745d8
This commit is contained in:
Solomon 2022-10-15 20:53:13 -07:00 committed by hasura-bot
parent 60411b95e5
commit 4700ac44fb
14 changed files with 498 additions and 383 deletions

View File

@ -784,8 +784,11 @@ library
, Hasura.RQL.DDL.Webhook.Transform.Headers , Hasura.RQL.DDL.Webhook.Transform.Headers
, Hasura.RQL.DDL.Webhook.Transform.Method , Hasura.RQL.DDL.Webhook.Transform.Method
, Hasura.RQL.DDL.Webhook.Transform.QueryParams , Hasura.RQL.DDL.Webhook.Transform.QueryParams
, Hasura.RQL.DDL.Webhook.Transform.Response
, Hasura.RQL.DDL.Webhook.Transform.Request
, Hasura.RQL.DDL.Webhook.Transform.Validation , Hasura.RQL.DDL.Webhook.Transform.Validation
, Hasura.RQL.DDL.Webhook.Transform.Url , Hasura.RQL.DDL.Webhook.Transform.Url
, Hasura.RQL.DDL.Webhook.Transform.WithOptional
, Hasura.RQL.DDL.SourceKinds , Hasura.RQL.DDL.SourceKinds
, Hasura.RQL.DDL.Schema , Hasura.RQL.DDL.Schema
, Hasura.RQL.DDL.Schema.Cache , Hasura.RQL.DDL.Schema.Cache

View File

@ -63,8 +63,7 @@ import Hasura.HTTP (HttpException (..), addDefaultHeaders)
import Hasura.Logging import Hasura.Logging
import Hasura.Prelude import Hasura.Prelude
import Hasura.RQL.DDL.Headers import Hasura.RQL.DDL.Headers
import Hasura.RQL.DDL.Webhook.Transform import Hasura.RQL.DDL.Webhook.Transform qualified as Transform
import Hasura.RQL.DDL.Webhook.Transform.Class (mkReqTransformCtx)
import Hasura.RQL.Types.Common (ResolvedWebhook (..)) import Hasura.RQL.Types.Common (ResolvedWebhook (..))
import Hasura.RQL.Types.EventTrigger import Hasura.RQL.Types.EventTrigger
import Hasura.RQL.Types.Eventing import Hasura.RQL.Types.Eventing
@ -143,7 +142,7 @@ data RequestDetails = RequestDetails
_rdOriginalSize :: Int64, _rdOriginalSize :: Int64,
_rdTransformedRequest :: Maybe HTTP.Request, _rdTransformedRequest :: Maybe HTTP.Request,
_rdTransformedSize :: Maybe Int64, _rdTransformedSize :: Maybe Int64,
_rdReqTransformCtx :: Maybe RequestTransformCtx, _rdReqTransformCtx :: Maybe Transform.RequestContext,
_rdSessionVars :: Maybe SessionVariables _rdSessionVars :: Maybe SessionVariables
} }
@ -276,7 +275,7 @@ runHTTP manager req = do
data TransformableRequestError a data TransformableRequestError a
= HTTPError J.Value (HTTPErr a) = HTTPError J.Value (HTTPErr a)
| TransformationError J.Value TransformErrorBundle | TransformationError J.Value Transform.TransformErrorBundle
deriving (Show) deriving (Show)
mkRequest :: mkRequest ::
@ -287,7 +286,7 @@ mkRequest ::
-- log the request size. As the logging happens outside the function, we pass -- log the request size. As the logging happens outside the function, we pass
-- it the final request body, instead of 'Value' -- it the final request body, instead of 'Value'
LBS.ByteString -> LBS.ByteString ->
Maybe RequestTransform -> Maybe Transform.RequestTransform ->
ResolvedWebhook -> ResolvedWebhook ->
m RequestDetails m RequestDetails
mkRequest headers timeout payload mRequestTransform (ResolvedWebhook webhook) = mkRequest headers timeout payload mRequestTransform (ResolvedWebhook webhook) =
@ -309,13 +308,14 @@ mkRequest headers timeout payload mRequestTransform (ResolvedWebhook webhook) =
in case mRequestTransform of in case mRequestTransform of
Nothing -> Nothing ->
pure $ RequestDetails req (LBS.length payload) Nothing Nothing Nothing sessionVars pure $ RequestDetails req (LBS.length payload) Nothing Nothing Nothing sessionVars
Just RequestTransform {..} -> Just Transform.RequestTransform {..} ->
let reqTransformCtx = mkReqTransformCtx webhook sessionVars templateEngine let reqTransformCtx = Transform.mkReqTransformCtx webhook sessionVars templateEngine
in case applyRequestTransform reqTransformCtx requestFields req of requestContext = fmap Transform.mkRequestContext reqTransformCtx
in case Transform.applyRequestTransform requestContext requestFields req of
Left err -> throwError $ TransformationError body err Left err -> throwError $ TransformationError body err
Right transformedReq -> Right transformedReq ->
let transformedReqSize = HTTP.getReqSize transformedReq let transformedReqSize = HTTP.getReqSize transformedReq
in pure $ RequestDetails req (LBS.length payload) (Just transformedReq) (Just transformedReqSize) (Just $ reqTransformCtx req) sessionVars in pure $ RequestDetails req (LBS.length payload) (Just transformedReq) (Just transformedReqSize) (Just $ requestContext req) sessionVars
invokeRequest :: invokeRequest ::
( MonadReader r m, ( MonadReader r m,
@ -326,7 +326,7 @@ invokeRequest ::
MonadTrace m MonadTrace m
) => ) =>
RequestDetails -> RequestDetails ->
Maybe ResponseTransform -> Maybe Transform.ResponseTransform ->
Maybe SessionVariables -> Maybe SessionVariables ->
((Either (HTTPErr a) (HTTPResp a)) -> RequestDetails -> m ()) -> ((Either (HTTPErr a) (HTTPResp a)) -> RequestDetails -> m ()) ->
m (HTTPResp a) m (HTTPResp a)
@ -343,9 +343,9 @@ invokeRequest reqDetails@RequestDetails {..} respTransform' sessionVars logger =
Nothing -> pure resp Nothing -> pure resp
Just respTransform -> do Just respTransform -> do
let respBody = SB.toLBS $ hrsBody resp let respBody = SB.toLBS $ hrsBody resp
engine = respTransformTemplateEngine respTransform engine = Transform.respTransformTemplateEngine respTransform
respTransformCtx = buildRespTransformCtx _rdReqTransformCtx sessionVars engine respBody respTransformCtx = Transform.buildRespTransformCtx _rdReqTransformCtx sessionVars engine respBody
in case applyResponseTransform respTransform respTransformCtx of in case Transform.applyResponseTransform respTransform respTransformCtx of
Left err -> do Left err -> do
-- Log The Response Transformation Error -- Log The Response Transformation Error
logger' :: Logger Hasura <- asks getter logger' :: Logger Hasura <- asks getter

View File

@ -60,7 +60,6 @@ import Hasura.Name qualified as Name
import Hasura.Prelude import Hasura.Prelude
import Hasura.RQL.DDL.Headers import Hasura.RQL.DDL.Headers
import Hasura.RQL.DDL.Webhook.Transform import Hasura.RQL.DDL.Webhook.Transform
import Hasura.RQL.DDL.Webhook.Transform.Class (mkReqTransformCtx)
import Hasura.RQL.IR.Action qualified as IR import Hasura.RQL.IR.Action qualified as IR
import Hasura.RQL.IR.BoolExp import Hasura.RQL.IR.BoolExp
import Hasura.RQL.IR.Select qualified as RS import Hasura.RQL.IR.Select qualified as RS
@ -557,7 +556,7 @@ callWebhook
(transformedReq, transformedReqSize, reqTransformCtx) <- case metadataRequestTransform of (transformedReq, transformedReqSize, reqTransformCtx) <- case metadataRequestTransform of
Nothing -> pure (Nothing, Nothing, Nothing) Nothing -> pure (Nothing, Nothing, Nothing)
Just RequestTransform {..} -> Just RequestTransform {..} ->
let reqTransformCtx = mkReqTransformCtx webhookUrl sessionVars templateEngine let reqTransformCtx = fmap mkRequestContext $ mkReqTransformCtx webhookUrl sessionVars templateEngine
in case applyRequestTransform reqTransformCtx requestFields req of in case applyRequestTransform reqTransformCtx requestFields req of
Left err -> do Left err -> do
-- Log The Transformation Error -- Log The Transformation Error

View File

@ -60,7 +60,6 @@ import Hasura.RQL.DDL.ScheduledTrigger
import Hasura.RQL.DDL.Schema import Hasura.RQL.DDL.Schema
import Hasura.RQL.DDL.Schema.Source import Hasura.RQL.DDL.Schema.Source
import Hasura.RQL.DDL.Webhook.Transform import Hasura.RQL.DDL.Webhook.Transform
import Hasura.RQL.DDL.Webhook.Transform.Class (mkReqTransformCtx)
import Hasura.RQL.Types.Allowlist import Hasura.RQL.Types.Allowlist
import Hasura.RQL.Types.ApiLimit import Hasura.RQL.Types.ApiLimit
import Hasura.RQL.Types.Backend import Hasura.RQL.Types.Backend
@ -682,7 +681,7 @@ runTestWebhookTransform (TestWebhookTransform env headers urlE payload rt _ sv)
let req = initReq & HTTP.body .~ pure (J.encode payload) & HTTP.headers .~ headers' let req = initReq & HTTP.body .~ pure (J.encode payload) & HTTP.headers .~ headers'
reqTransform = requestFields rt reqTransform = requestFields rt
engine = templateEngine rt engine = templateEngine rt
reqTransformCtx = mkReqTransformCtx url sv engine reqTransformCtx = fmap mkRequestContext $ mkReqTransformCtx url sv engine
hoistEither $ first (RequestTransformationError req) $ applyRequestTransform reqTransformCtx reqTransform req hoistEither $ first (RequestTransformationError req) $ applyRequestTransform reqTransformCtx reqTransform req
case result of case result of

View File

@ -34,6 +34,9 @@ module Hasura.RQL.DDL.Webhook.Transform
-- * Request Transformation Context -- * Request Transformation Context
RequestTransformCtx (..), RequestTransformCtx (..),
RequestContext,
mkRequestContext,
mkReqTransformCtx,
TransformErrorBundle (..), TransformErrorBundle (..),
-- * Optional Functor -- * Optional Functor
@ -54,12 +57,10 @@ where
import Control.Lens (Lens', lens, set, traverseOf, view) import Control.Lens (Lens', lens, set, traverseOf, view)
import Data.Aeson (FromJSON, ToJSON) import Data.Aeson (FromJSON, ToJSON)
import Data.Aeson.Extended qualified as J import Data.Aeson.Extended ((.!=), (.:?), (.=), (.=?))
import Data.Aeson.Kriti.Functions qualified as KFunc import Data.Aeson.Extended qualified as Aeson
import Data.Bifunctor (first)
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.Coerce (Coercible)
import Data.Functor.Barbie (AllBF, ApplicativeB, ConstraintsB, FunctorB, TraversableB) import Data.Functor.Barbie (AllBF, ApplicativeB, ConstraintsB, FunctorB, TraversableB)
import Data.Functor.Barbie qualified as B import Data.Functor.Barbie qualified as B
import Data.Text.Encoding qualified as TE import Data.Text.Encoding qualified as TE
@ -69,10 +70,13 @@ import Hasura.Prelude hiding (first)
import Hasura.RQL.DDL.Webhook.Transform.Body (Body (..), BodyTransformFn, TransformFn (BodyTransformFn_)) import Hasura.RQL.DDL.Webhook.Transform.Body (Body (..), BodyTransformFn, TransformFn (BodyTransformFn_))
import Hasura.RQL.DDL.Webhook.Transform.Body qualified as Body import Hasura.RQL.DDL.Webhook.Transform.Body qualified as Body
import Hasura.RQL.DDL.Webhook.Transform.Class import Hasura.RQL.DDL.Webhook.Transform.Class
import Hasura.RQL.DDL.Webhook.Transform.Headers (Headers (..), HeadersTransformFn (..), TransformFn (HeadersTransformFn_)) import Hasura.RQL.DDL.Webhook.Transform.Headers
import Hasura.RQL.DDL.Webhook.Transform.Method import Hasura.RQL.DDL.Webhook.Transform.Method
import Hasura.RQL.DDL.Webhook.Transform.QueryParams import Hasura.RQL.DDL.Webhook.Transform.QueryParams
import Hasura.RQL.DDL.Webhook.Transform.Request
import Hasura.RQL.DDL.Webhook.Transform.Response
import Hasura.RQL.DDL.Webhook.Transform.Url import Hasura.RQL.DDL.Webhook.Transform.Url
import Hasura.RQL.DDL.Webhook.Transform.WithOptional (WithOptional (..), withOptional)
import Hasura.Session (SessionVariables) import Hasura.Session (SessionVariables)
import Network.HTTP.Client.Transformable qualified as HTTP import Network.HTTP.Client.Transformable qualified as HTTP
@ -93,17 +97,17 @@ data RequestTransform = RequestTransform
deriving anyclass (NFData, Cacheable) deriving anyclass (NFData, Cacheable)
instance FromJSON RequestTransform where instance FromJSON RequestTransform where
parseJSON = J.withObject "RequestTransform" \o -> do parseJSON = Aeson.withObject "RequestTransform" \o -> do
version <- o J..:? "version" J..!= V1 version <- o .:? "version" .!= V1
method <- o J..:? "method" method <- o .:? "method"
url <- o J..:? "url" url <- o .:? "url"
body <- case version of body <- case version of
V1 -> do V1 -> do
template :: Maybe Template <- o J..:? "body" template :: Maybe Template <- o .:? "body"
pure $ fmap Body.ModifyAsJSON template pure $ fmap Body.ModifyAsJSON template
V2 -> o J..:? "body" V2 -> o .:? "body"
queryParams <- o J..:? "query_params" queryParams <- o .:? "query_params"
headers <- o J..:? "request_headers" headers <- o .:? "request_headers"
let requestFields = let requestFields =
RequestFields RequestFields
{ method = withOptional @MethodTransformFn method, { method = withOptional @MethodTransformFn method,
@ -112,7 +116,7 @@ instance FromJSON RequestTransform where
queryParams = withOptional @QueryParamsTransformFn queryParams, queryParams = withOptional @QueryParamsTransformFn queryParams,
requestHeaders = withOptional @HeadersTransformFn headers requestHeaders = withOptional @HeadersTransformFn headers
} }
templateEngine <- o J..:? "template_engine" J..!= Kriti templateEngine <- o .:? "template_engine" .!= Kriti
pure $ RequestTransform {..} pure $ RequestTransform {..}
instance ToJSON RequestTransform where instance ToJSON RequestTransform where
@ -121,24 +125,24 @@ instance ToJSON RequestTransform where
body' = case version of body' = case version of
V1 -> case (getOptional body) of V1 -> case (getOptional body) of
Just (BodyTransformFn_ (Body.ModifyAsJSON template)) -> Just (BodyTransformFn_ (Body.ModifyAsJSON template)) ->
Just ("body", J.toJSON template) Just ("body", Aeson.toJSON template)
_ -> Nothing _ -> Nothing
V2 -> "body" J..=? getOptional body V2 -> "body" .=? getOptional body
in J.object $ in Aeson.object $
[ "version" J..= version, [ "version" .= version,
"template_engine" J..= templateEngine "template_engine" .= templateEngine
] ]
<> catMaybes <> catMaybes
[ "method" J..=? getOptional method, [ "method" .=? getOptional method,
"url" J..=? getOptional url, "url" .=? getOptional url,
"query_params" J..=? getOptional queryParams, "query_params" .=? getOptional queryParams,
"request_headers" J..=? getOptional requestHeaders, "request_headers" .=? getOptional requestHeaders,
body' body'
] ]
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- | Defunctionalized Webhook Transformation -- | Defunctionalized Webhook Request Transformation
-- --
-- We represent a defunctionalized request transformation by parameterizing -- We represent a defunctionalized request transformation by parameterizing
-- our HKD with 'WithOptional'@ @'TransformFn', which marks each of the fields -- our HKD with 'WithOptional'@ @'TransformFn', which marks each of the fields
@ -184,12 +188,12 @@ deriving anyclass instance
-- NOTE: It is likely that we can derive these instances. Possibly if -- NOTE: It is likely that we can derive these instances. Possibly if
-- we move the aeson instances onto the *Transform types. -- we move the aeson instances onto the *Transform types.
instance FromJSON RequestTransformFns where instance FromJSON RequestTransformFns where
parseJSON = J.withObject "RequestTransformFns" $ \o -> do parseJSON = Aeson.withObject "RequestTransformFns" $ \o -> do
method <- o J..:? "method" method <- o .:? "method"
url <- o J..:? "url" url <- o .:? "url"
body <- o J..:? "body" body <- o .:? "body"
queryParams <- o J..:? "query_params" queryParams <- o .:? "query_params"
headers <- o J..:? "request_headers" headers <- o .:? "request_headers"
pure $ pure $
RequestFields RequestFields
{ method = withOptional @MethodTransformFn method, { method = withOptional @MethodTransformFn method,
@ -201,14 +205,37 @@ instance FromJSON RequestTransformFns where
instance ToJSON RequestTransformFns where instance ToJSON RequestTransformFns where
toJSON RequestFields {..} = toJSON RequestFields {..} =
J.object . catMaybes $ Aeson.object . catMaybes $
[ "method" J..=? getOptional method, [ "method" .=? getOptional method,
"url" J..=? getOptional url, "url" .=? getOptional url,
"body" J..=? getOptional body, "body" .=? getOptional body,
"query_params" J..=? getOptional queryParams, "query_params" .=? getOptional queryParams,
"request_headers" J..=? getOptional requestHeaders "request_headers" .=? getOptional requestHeaders
] ]
type RequestContext = RequestFields TransformCtx
instance ToJSON RequestContext where
toJSON RequestFields {..} =
Aeson.object
[ "method" .= coerce @_ @RequestTransformCtx method,
"url" .= coerce @_ @RequestTransformCtx url,
"body" .= coerce @_ @RequestTransformCtx body,
"query_params" .= coerce @_ @RequestTransformCtx queryParams,
"request_headers" .= coerce @_ @RequestTransformCtx requestHeaders
]
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
}
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- TODO(SOLOMON): Add lens law unit tests -- TODO(SOLOMON): Add lens law unit tests
@ -228,14 +255,14 @@ requestL = lens getter setter
RequestFields RequestFields
{ method = coerce $ CI.mk $ TE.decodeUtf8 $ view HTTP.method req, { method = coerce $ CI.mk $ TE.decodeUtf8 $ view HTTP.method req,
url = coerce $ view HTTP.url req, url = coerce $ view HTTP.url req,
body = coerce $ JSONBody $ J.decode =<< view HTTP.body req, body = coerce $ JSONBody $ Aeson.decode =<< view HTTP.body req,
queryParams = coerce $ view HTTP.queryParams req, queryParams = coerce $ view HTTP.queryParams req,
requestHeaders = coerce $ view HTTP.headers req requestHeaders = coerce $ view HTTP.headers req
} }
serializeBody :: Body -> Maybe BL.ByteString serializeBody :: Body -> Maybe BL.ByteString
serializeBody = \case serializeBody = \case
JSONBody body -> fmap J.encode body JSONBody body -> fmap Aeson.encode body
RawBody "" -> Nothing RawBody "" -> Nothing
RawBody bs -> Just bs RawBody bs -> Just bs
@ -260,7 +287,7 @@ requestL = lens getter setter
applyRequestTransform :: applyRequestTransform ::
forall m. forall m.
MonadError TransformErrorBundle m => MonadError TransformErrorBundle m =>
(HTTP.Request -> RequestTransformCtx) -> (HTTP.Request -> RequestContext) ->
RequestTransformFns -> RequestTransformFns ->
HTTP.Request -> HTTP.Request ->
m HTTP.Request m HTTP.Request
@ -272,10 +299,11 @@ applyRequestTransform mkCtx transformations request =
where where
-- Apply all of the provided request transformation functions to the -- Apply all of the provided request transformation functions to the
-- request data extracted from the given 'HTTP.Request'. -- request data extracted from the given 'HTTP.Request'.
transformReqData ctx reqData = transformReqData transformCtx reqData =
B.bsequence' $ B.bsequence' $
B.bzipWithC @Transform B.bzipWith3C @Transform
(transformField ctx) transformField
transformCtx
transformations transformations
reqData reqData
-- Apply a transformation to some request data, if it exists; otherwise -- Apply a transformation to some request data, if it exists; otherwise
@ -285,44 +313,6 @@ applyRequestTransform mkCtx transformations request =
Nothing -> pure a Nothing -> pure a
Just fn -> transform fn ctx a Just fn -> transform fn ctx a
-------------------------------------------------------------------------------
-- | Enrich a 'Functor' @f@ with optionality; this is primarily useful when
-- one wants to annotate fields as optional when using the Higher-Kinded Data
-- pattern.
--
-- 'WithOptional'@ f@ is equivalent to @Compose Maybe f@.
newtype WithOptional f result = WithOptional
{ getOptional :: Maybe (f result)
}
deriving stock (Eq, Functor, Foldable, Generic, Show)
deriving newtype (FromJSON, ToJSON)
deriving newtype instance
(Cacheable (f result)) =>
Cacheable (WithOptional f result)
deriving newtype instance
(NFData (f result)) =>
NFData (WithOptional f result)
-- | 'WithOptional' smart constructor for the special case of optional values
-- that are representationally equivalent to some "wrapper" type.
--
-- For example:
-- @
-- withOptional \@HeaderTransformsAction headers == WithOptional $ fmap HeadersTransform headers
-- @
--
-- In other words: this function observes the isomorphism between @'Maybe' a@
-- and @'WithOptional' f b@ if an isomorphism exists between @a@ and @f b@.
withOptional ::
forall a b f.
Coercible a (f b) =>
Maybe a ->
WithOptional f b
withOptional = coerce
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- TODO(SOLOMON): Rewrite with HKD -- TODO(SOLOMON): Rewrite with HKD
@ -330,7 +320,7 @@ withOptional = coerce
-- 'MetadataResponseTransform'. 'Nothing' means use the original -- 'MetadataResponseTransform'. 'Nothing' means use the original
-- response value. -- response value.
data ResponseTransform = ResponseTransform data ResponseTransform = ResponseTransform
{ respTransformBody :: Maybe (ResponseTransformCtx -> Either TransformErrorBundle J.Value), { respTransformBody :: Maybe (ResponseTransformCtx -> Either TransformErrorBundle Aeson.Value),
respTransformTemplateEngine :: TemplatingEngine respTransformTemplateEngine :: TemplatingEngine
} }
@ -342,39 +332,39 @@ data MetadataResponseTransform = MetadataResponseTransform
deriving stock (Show, Eq, Generic) deriving stock (Show, Eq, Generic)
deriving anyclass (NFData, Cacheable) deriving anyclass (NFData, Cacheable)
instance J.FromJSON MetadataResponseTransform where instance FromJSON MetadataResponseTransform where
parseJSON = J.withObject "MetadataResponseTransform" $ \o -> do parseJSON = Aeson.withObject "MetadataResponseTransform" $ \o -> do
mrtVersion <- o J..:? "version" J..!= V1 mrtVersion <- o .:? "version" .!= V1
mrtBodyTransform <- case mrtVersion of mrtBodyTransform <- case mrtVersion of
V1 -> do V1 -> do
template :: (Maybe Template) <- o J..:? "body" template :: (Maybe Template) <- o .:? "body"
pure $ fmap Body.ModifyAsJSON template pure $ fmap Body.ModifyAsJSON template
V2 -> o J..:? "body" V2 -> o .:? "body"
templateEngine <- o J..:? "template_engine" templateEngine <- o .:? "template_engine"
let mrtTemplatingEngine = fromMaybe Kriti templateEngine let mrtTemplatingEngine = fromMaybe Kriti templateEngine
pure $ MetadataResponseTransform {..} pure $ MetadataResponseTransform {..}
instance J.ToJSON MetadataResponseTransform where instance ToJSON MetadataResponseTransform where
toJSON MetadataResponseTransform {..} = toJSON MetadataResponseTransform {..} =
let body = case mrtVersion of let body = case mrtVersion of
V1 -> case mrtBodyTransform of V1 -> case mrtBodyTransform of
Just (Body.ModifyAsJSON template) -> Just ("body", J.toJSON template) Just (Body.ModifyAsJSON template) -> Just ("body", Aeson.toJSON template)
_ -> Nothing _ -> Nothing
V2 -> "body" J..=? mrtBodyTransform V2 -> "body" .=? mrtBodyTransform
in J.object $ in Aeson.object $
[ "template_engine" J..= mrtTemplatingEngine, [ "template_engine" .= mrtTemplatingEngine,
"version" J..= mrtVersion "version" .= mrtVersion
] ]
<> catMaybes [body] <> maybeToList body
-- | A helper function for constructing the 'RespTransformCtx' -- | A helper function for constructing the 'ResponseTransformCtx'
buildRespTransformCtx :: Maybe RequestTransformCtx -> Maybe SessionVariables -> TemplatingEngine -> BL.ByteString -> ResponseTransformCtx buildRespTransformCtx :: Maybe RequestContext -> Maybe SessionVariables -> TemplatingEngine -> BL.ByteString -> ResponseTransformCtx
buildRespTransformCtx reqCtx sessionVars engine respBody = buildRespTransformCtx requestContext sessionVars engine respBody =
ResponseTransformCtx ResponseTransformCtx
{ responseTransformBody = fromMaybe J.Null $ J.decode @J.Value respBody, { responseTransformBody = fromMaybe Aeson.Null $ Aeson.decode @Aeson.Value respBody,
responseTransformReqCtx = J.toJSON reqCtx, responseTransformReqCtx = Aeson.toJSON requestContext,
responseTransformEngine = engine, responseSessionVariables = sessionVars,
responseTransformFunctions = KFunc.sessionFunctions sessionVars responseTransformEngine = engine
} }
-- | Construct a Template Transformation function for Responses -- | Construct a Template Transformation function for Responses
@ -383,32 +373,27 @@ buildRespTransformCtx reqCtx sessionVars engine respBody =
-- impure exception when the supplied 'ByteString' cannot be decoded into valid -- impure exception when the supplied 'ByteString' cannot be decoded into valid
-- UTF8 text! -- UTF8 text!
mkRespTemplateTransform :: mkRespTemplateTransform ::
TemplatingEngine ->
BodyTransformFn -> BodyTransformFn ->
ResponseTransformCtx -> ResponseTransformCtx ->
Either TransformErrorBundle J.Value Either TransformErrorBundle Aeson.Value
mkRespTemplateTransform _ Body.Remove _ = pure J.Null mkRespTemplateTransform Body.Remove _ = pure Aeson.Null
mkRespTemplateTransform engine (Body.ModifyAsJSON (Template template)) ResponseTransformCtx {..} = mkRespTemplateTransform (Body.ModifyAsJSON template) context =
let context = [("$body", responseTransformBody), ("$request", responseTransformReqCtx)] runResponseTemplateTransform template context
in case engine of mkRespTemplateTransform (Body.ModifyAsFormURLEncoded formTemplates) context = do
Kriti -> first (TransformErrorBundle . pure . J.toJSON) $ KFunc.runKriti template context result <-
mkRespTemplateTransform engine (Body.ModifyAsFormURLEncoded formTemplates) context = liftEither . V.toEither . for formTemplates $
case engine of runUnescapedResponseTemplateTransform' context
Kriti -> do pure . Aeson.String . TE.decodeUtf8 . BL.toStrict $ Body.foldFormEncoded result
result <-
liftEither . V.toEither . for formTemplates $
runUnescapedResponseTemplateTransform' context
pure . J.String . TE.decodeUtf8 . BL.toStrict $ Body.foldFormEncoded result
mkResponseTransform :: MetadataResponseTransform -> ResponseTransform mkResponseTransform :: MetadataResponseTransform -> ResponseTransform
mkResponseTransform MetadataResponseTransform {..} = mkResponseTransform MetadataResponseTransform {..} =
let bodyTransform = mkRespTemplateTransform mrtTemplatingEngine <$> mrtBodyTransform let bodyTransform = mkRespTemplateTransform <$> mrtBodyTransform
in ResponseTransform bodyTransform mrtTemplatingEngine in ResponseTransform bodyTransform mrtTemplatingEngine
-- | At the moment we only transform the body of -- | At the moment we only transform the body of
-- Responses. 'http-client' does not export the constructors for -- Responses. 'http-client' does not export the constructors for
-- 'Response'. If we want to transform then we will need additional -- 'Response'. If we want to transform other fields then we will need
-- 'apply' functions. -- additional 'apply' functions.
applyResponseTransform :: applyResponseTransform ::
ResponseTransform -> ResponseTransform ->
ResponseTransformCtx -> ResponseTransformCtx ->
@ -418,5 +403,5 @@ applyResponseTransform ResponseTransform {..} ctx@ResponseTransformCtx {..} =
bodyFunc body = bodyFunc body =
case respTransformBody of case respTransformBody of
Nothing -> pure body Nothing -> pure body
Just f -> J.encode <$> f ctx Just f -> Aeson.encode <$> f ctx
in bodyFunc (J.encode responseTransformBody) in bodyFunc (Aeson.encode responseTransformBody)

View File

@ -5,6 +5,7 @@ module Hasura.RQL.DDL.Webhook.Transform.Body
( -- * Body Transformations ( -- * Body Transformations
Body (..), Body (..),
TransformFn (..), TransformFn (..),
TransformCtx (..),
BodyTransformFn (..), BodyTransformFn (..),
foldFormEncoded, foldFormEncoded,
) )
@ -25,12 +26,14 @@ import Data.Validation qualified as V
import Hasura.Incremental (Cacheable) import Hasura.Incremental (Cacheable)
import Hasura.Prelude import Hasura.Prelude
import Hasura.RQL.DDL.Webhook.Transform.Class import Hasura.RQL.DDL.Webhook.Transform.Class
( RequestTransformCtx (..), ( Template (..),
Template (..),
TemplatingEngine, TemplatingEngine,
Transform (..), Transform (..),
TransformErrorBundle (..), TransformErrorBundle (..),
UnescapedTemplate, UnescapedTemplate,
)
import Hasura.RQL.DDL.Webhook.Transform.Request
( RequestTransformCtx,
runRequestTemplateTransform, runRequestTemplateTransform,
runUnescapedRequestTemplateTransform', runUnescapedRequestTemplateTransform',
validateRequestTemplateTransform', validateRequestTemplateTransform',
@ -53,9 +56,11 @@ instance Transform Body where
deriving stock (Eq, Generic, Show) deriving stock (Eq, Generic, Show)
deriving newtype (Cacheable, NFData, FromJSON, ToJSON) deriving newtype (Cacheable, NFData, FromJSON, ToJSON)
newtype TransformCtx Body = TransformCtx RequestTransformCtx
-- NOTE: GHC does not let us attach Haddock documentation to typeclass -- NOTE: GHC does not let us attach Haddock documentation to typeclass
-- method implementations, so 'applyBodyTransformFn' is defined separately. -- method implementations, so 'applyBodyTransformFn' is defined separately.
transform (BodyTransformFn_ fn) = applyBodyTransformFn fn transform (BodyTransformFn_ fn) (TransformCtx reqCtx) = applyBodyTransformFn fn reqCtx
-- NOTE: GHC does not let us attach Haddock documentation to typeclass -- NOTE: GHC does not let us attach Haddock documentation to typeclass
-- method implementations, so 'validateBodyTransformFn' is defined -- method implementations, so 'validateBodyTransformFn' is defined

View File

@ -12,53 +12,30 @@ module Hasura.RQL.DDL.Webhook.Transform.Class
TransformErrorBundle (..), TransformErrorBundle (..),
throwErrorBundle, throwErrorBundle,
-- ** Request Transformation Context
RequestTransformCtx (..),
ResponseTransformCtx (..),
mkReqTransformCtx,
-- * Templating -- * Templating
TemplatingEngine (..), TemplatingEngine (..),
Template (..), Template (..),
Version (..),
runRequestTemplateTransform,
validateRequestTemplateTransform,
validateRequestTemplateTransform',
-- * Unescaped -- * Unescaped
UnescapedTemplate (..), UnescapedTemplate (..),
wrapUnescapedTemplate, wrapUnescapedTemplate,
runUnescapedRequestTemplateTransform, encodeScalar,
runUnescapedRequestTemplateTransform',
runUnescapedResponseTemplateTransform,
runUnescapedResponseTemplateTransform',
validateRequestUnescapedTemplateTransform,
validateRequestUnescapedTemplateTransform',
) )
where where
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
import Control.Arrow (left)
import Control.Lens (bimap, view)
import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey) import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey)
import Data.Aeson qualified as J import Data.Aeson qualified as J
import Data.Aeson.Kriti.Functions as KFunc
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.ByteString.Builder (toLazyByteString) import Data.ByteString.Builder (toLazyByteString)
import Data.ByteString.Builder.Scientific (scientificBuilder) import Data.ByteString.Builder.Scientific (scientificBuilder)
import Data.ByteString.Lazy qualified as LBS import Data.ByteString.Lazy qualified as LBS
import Data.HashMap.Strict qualified as M
import Data.Kind (Constraint, Type) import Data.Kind (Constraint, Type)
import Data.Text.Encoding (encodeUtf8) import Data.Text.Encoding (encodeUtf8)
import Data.Text.Encoding qualified as TE import Data.Validation (Validation)
import Data.Validation (Validation, fromEither)
import Hasura.Incremental (Cacheable) import Hasura.Incremental (Cacheable)
import Hasura.Prelude import Hasura.Prelude
import Hasura.Session (SessionVariables)
import Kriti.Error qualified as Kriti (CustomFunctionError (..), serialize)
import Kriti.Parser qualified as Kriti (parser)
import Network.HTTP.Client.Transformable qualified as HTTP
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
@ -73,6 +50,8 @@ class Transform a where
-- the transformation. -- the transformation.
data TransformFn a :: Type data TransformFn a :: Type
data TransformCtx a :: Type
-- | 'transform' is a function which takes 'TransformFn' of @a@ and reifies -- | 'transform' is a function which takes 'TransformFn' of @a@ and reifies
-- it into a function of the form: -- it into a function of the form:
-- --
@ -82,7 +61,7 @@ class Transform a where
transform :: transform ::
MonadError TransformErrorBundle m => MonadError TransformErrorBundle m =>
TransformFn a -> TransformFn a ->
RequestTransformCtx -> TransformCtx a ->
a -> a ->
m a m a
@ -121,69 +100,6 @@ throwErrorBundle msg val = do
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- | Common context that is made available to all request transformations.
data RequestTransformCtx = RequestTransformCtx
{ rtcBaseUrl :: Maybe J.Value,
rtcBody :: J.Value,
rtcSessionVariables :: J.Value,
rtcQueryParams :: Maybe J.Value,
rtcEngine :: TemplatingEngine,
rtcFunctions :: M.HashMap Text (J.Value -> Either Kriti.CustomFunctionError J.Value)
}
instance ToJSON RequestTransformCtx where
toJSON RequestTransformCtx {..} =
let required =
[ "body" J..= rtcBody,
"session_variables" J..= rtcSessionVariables
]
optional =
[ ("base_url" J..=) <$> rtcBaseUrl,
("query_params" J..=) <$> rtcQueryParams
]
in J.object (required <> catMaybes optional)
-- | A smart constructor for constructing the 'RequestTransformCtx'
--
-- 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!
mkReqTransformCtx ::
Text ->
Maybe SessionVariables ->
TemplatingEngine ->
HTTP.Request ->
RequestTransformCtx
mkReqTransformCtx url sessionVars rtcEngine reqData =
let rtcBaseUrl = Just $ J.toJSON url
rtcBody =
let mBody = view HTTP.body reqData >>= J.decode @J.Value
in fromMaybe J.Null mBody
rtcSessionVariables = J.toJSON sessionVars
rtcQueryParams =
let queryParams =
view HTTP.queryParams reqData & fmap \(key, val) ->
(TE.decodeUtf8 key, fmap TE.decodeUtf8 val)
in Just $ J.toJSON queryParams
in RequestTransformCtx
{ rtcBaseUrl,
rtcBody,
rtcSessionVariables,
rtcQueryParams,
rtcEngine,
rtcFunctions = KFunc.sessionFunctions sessionVars
}
-- | Common context that is made available to all response transformations.
data ResponseTransformCtx = ResponseTransformCtx
{ responseTransformBody :: J.Value,
responseTransformReqCtx :: J.Value,
responseTransformFunctions :: M.HashMap Text (J.Value -> Either Kriti.CustomFunctionError J.Value),
responseTransformEngine :: TemplatingEngine
}
-------------------------------------------------------------------------------
-- | Available templating engines. -- | Available templating engines.
data TemplatingEngine data TemplatingEngine
= Kriti = Kriti
@ -226,86 +142,6 @@ instance J.FromJSON Template where
instance J.ToJSON Template where instance J.ToJSON Template where
toJSON = J.String . coerce toJSON = J.String . coerce
-- | A helper function for executing transformations from a 'Template'
-- and a 'RequestTransformCtx'.
--
-- NOTE: This and all related funtions are hard-coded to Kriti at the
-- moment. When we add additional template engines this function will
-- need to take a 'TemplatingEngine' parameter.
runRequestTemplateTransform ::
Template ->
RequestTransformCtx ->
Either TransformErrorBundle J.Value
runRequestTemplateTransform template RequestTransformCtx {rtcEngine = Kriti, ..} =
let context =
[ ("$body", rtcBody),
("$session_variables", rtcSessionVariables)
]
<> catMaybes
[ ("$query_params",) <$> rtcQueryParams,
("$base_url",) <$> rtcBaseUrl
]
eResult = KFunc.runKritiWith (unTemplate $ template) context rtcFunctions
in eResult & left \kritiErr ->
let renderedErr = J.toJSON kritiErr
in TransformErrorBundle [renderedErr]
-- TODO: Should this live in 'Hasura.RQL.DDL.Webhook.Transform.Validation'?
validateRequestTemplateTransform ::
TemplatingEngine ->
Template ->
Either TransformErrorBundle ()
validateRequestTemplateTransform Kriti (Template template) =
bimap packBundle (const ()) $ Kriti.parser $ TE.encodeUtf8 template
where
packBundle = TransformErrorBundle . pure . J.toJSON . Kriti.serialize
validateRequestTemplateTransform' ::
TemplatingEngine ->
Template ->
Validation TransformErrorBundle ()
validateRequestTemplateTransform' engine =
fromEither . validateRequestTemplateTransform engine
-- | A helper function for executing transformations from a 'Template'
-- and a 'ResponseTransformCtx'.
--
-- NOTE: This and all related funtions are hard-coded to Kriti at the
-- moment. When we add additional template engines this function will
-- need to take a 'TemplatingEngine' parameter.
runResponseTemplateTransform ::
Template ->
ResponseTransformCtx ->
Either TransformErrorBundle J.Value
runResponseTemplateTransform template ResponseTransformCtx {responseTransformEngine = Kriti, ..} =
let context = [("$body", responseTransformBody), ("$request", responseTransformReqCtx)]
eResult = KFunc.runKritiWith (unTemplate $ template) context responseTransformFunctions
in eResult & left \kritiErr ->
let renderedErr = J.toJSON kritiErr
in TransformErrorBundle [renderedErr]
-------------------------------------------------------------------------------
-- | 'RequestTransform' Versioning
data Version
= V1
| V2
deriving stock (Eq, Generic, Show)
deriving anyclass (Cacheable, Hashable, NFData)
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 2, encountered " ++ show i
instance J.ToJSON Version where
toJSON = \case
V1 -> J.toJSON @Int 1
V2 -> J.toJSON @Int 2
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- | Validated textual transformation template /for string -- | Validated textual transformation template /for string
@ -330,66 +166,6 @@ instance J.ToJSON UnescapedTemplate where
wrapUnescapedTemplate :: UnescapedTemplate -> Template wrapUnescapedTemplate :: UnescapedTemplate -> Template
wrapUnescapedTemplate (UnescapedTemplate txt) = Template $ "\"" <> txt <> "\"" wrapUnescapedTemplate (UnescapedTemplate txt) = Template $ "\"" <> txt <> "\""
-- | A helper function for executing Kriti transformations from a
-- 'UnescapedTemplate' and a 'RequestTrasformCtx'.
--
-- The difference from 'runRequestTemplateTransform' is that this
-- function will wrap the template text in double quotes before
-- running Kriti.
runUnescapedRequestTemplateTransform ::
RequestTransformCtx ->
UnescapedTemplate ->
Either TransformErrorBundle ByteString
runUnescapedRequestTemplateTransform context unescapedTemplate = do
result <-
runRequestTemplateTransform
(wrapUnescapedTemplate unescapedTemplate)
context
encodeScalar result
-- | Run a Kriti transformation with an unescaped template in
-- 'Validation' instead of 'Either'.
runUnescapedRequestTemplateTransform' ::
RequestTransformCtx ->
UnescapedTemplate ->
Validation TransformErrorBundle ByteString
runUnescapedRequestTemplateTransform' context unescapedTemplate =
fromEither $
runUnescapedRequestTemplateTransform context unescapedTemplate
-- TODO: Should this live in 'Hasura.RQL.DDL.Webhook.Transform.Validation'?
validateRequestUnescapedTemplateTransform ::
TemplatingEngine ->
UnescapedTemplate ->
Either TransformErrorBundle ()
validateRequestUnescapedTemplateTransform engine =
validateRequestTemplateTransform engine . wrapUnescapedTemplate
validateRequestUnescapedTemplateTransform' ::
TemplatingEngine ->
UnescapedTemplate ->
Validation TransformErrorBundle ()
validateRequestUnescapedTemplateTransform' engine =
fromEither . validateRequestUnescapedTemplateTransform engine
-- | Run an 'UnescapedTemplate' with a 'ResponseTransformCtx'.
runUnescapedResponseTemplateTransform ::
ResponseTransformCtx ->
UnescapedTemplate ->
Either TransformErrorBundle ByteString
runUnescapedResponseTemplateTransform context unescapedTemplate = do
result <- runResponseTemplateTransform (wrapUnescapedTemplate unescapedTemplate) context
encodeScalar result
-- | Run an 'UnescapedTemplate' with a 'ResponseTransformCtx' in 'Validation'.
runUnescapedResponseTemplateTransform' ::
ResponseTransformCtx ->
UnescapedTemplate ->
Validation TransformErrorBundle ByteString
runUnescapedResponseTemplateTransform' context unescapedTemplate =
fromEither $
runUnescapedResponseTemplateTransform context unescapedTemplate
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- Utility functions. -- Utility functions.

View File

@ -6,6 +6,7 @@ module Hasura.RQL.DDL.Webhook.Transform.Headers
( -- * Header Transformations ( -- * Header Transformations
Headers (..), Headers (..),
TransformFn (..), TransformFn (..),
TransformCtx (..),
HeadersTransformFn (..), HeadersTransformFn (..),
AddReplaceOrRemoveFields (..), AddReplaceOrRemoveFields (..),
) )
@ -23,11 +24,13 @@ import Data.Validation qualified as V
import Hasura.Incremental (Cacheable) import Hasura.Incremental (Cacheable)
import Hasura.Prelude import Hasura.Prelude
import Hasura.RQL.DDL.Webhook.Transform.Class import Hasura.RQL.DDL.Webhook.Transform.Class
( RequestTransformCtx (..), ( TemplatingEngine,
TemplatingEngine,
Transform (..), Transform (..),
TransformErrorBundle (..), TransformErrorBundle (..),
UnescapedTemplate (..), UnescapedTemplate (..),
)
import Hasura.RQL.DDL.Webhook.Transform.Request
( RequestTransformCtx,
runUnescapedRequestTemplateTransform', runUnescapedRequestTemplateTransform',
validateRequestUnescapedTemplateTransform', validateRequestUnescapedTemplateTransform',
) )
@ -49,10 +52,12 @@ instance Transform Headers where
deriving stock (Eq, Generic, Show) deriving stock (Eq, Generic, Show)
deriving newtype (Cacheable, NFData, FromJSON, ToJSON) deriving newtype (Cacheable, NFData, FromJSON, ToJSON)
newtype TransformCtx Headers = TransformCtx RequestTransformCtx
-- NOTE: GHC does not let us attach Haddock documentation to typeclass -- NOTE: GHC does not let us attach Haddock documentation to typeclass
-- method implementations, so 'applyHeadersTransformFn' is defined -- method implementations, so 'applyHeadersTransformFn' is defined
-- separately. -- separately.
transform (HeadersTransformFn_ fn) = applyHeadersTransformFn fn transform (HeadersTransformFn_ fn) (TransformCtx reqCtx) = applyHeadersTransformFn fn reqCtx
-- NOTE: GHC does not let us attach Haddock documentation to typeclass -- NOTE: GHC does not let us attach Haddock documentation to typeclass
-- method implementations, so 'validateHeadersTransformFn' is defined -- method implementations, so 'validateHeadersTransformFn' is defined

View File

@ -4,6 +4,7 @@ module Hasura.RQL.DDL.Webhook.Transform.Method
( -- * Method transformations ( -- * Method transformations
Method (..), Method (..),
TransformFn (..), TransformFn (..),
TransformCtx (..),
MethodTransformFn (..), MethodTransformFn (..),
) )
where where
@ -18,11 +19,11 @@ import Data.Validation
import Hasura.Incremental (Cacheable) import Hasura.Incremental (Cacheable)
import Hasura.Prelude import Hasura.Prelude
import Hasura.RQL.DDL.Webhook.Transform.Class import Hasura.RQL.DDL.Webhook.Transform.Class
( RequestTransformCtx (..), ( TemplatingEngine,
TemplatingEngine,
Transform (..), Transform (..),
TransformErrorBundle (..), TransformErrorBundle (..),
) )
import Hasura.RQL.DDL.Webhook.Transform.Request (RequestTransformCtx)
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
@ -49,10 +50,12 @@ instance Transform Method where
deriving stock (Eq, Generic, Show) deriving stock (Eq, Generic, Show)
deriving newtype (Cacheable, NFData, FromJSON, ToJSON) deriving newtype (Cacheable, NFData, FromJSON, ToJSON)
newtype TransformCtx Method = TransformCtx RequestTransformCtx
-- NOTE: GHC does not let us attach Haddock documentation to typeclass -- NOTE: GHC does not let us attach Haddock documentation to typeclass
-- method implementations, so 'applyMethodTransformFn' is defined -- method implementations, so 'applyMethodTransformFn' is defined
-- separately. -- separately.
transform (MethodTransformFn_ fn) = applyMethodTransformFn fn transform (MethodTransformFn_ fn) (TransformCtx reqCtx) = applyMethodTransformFn fn reqCtx
-- NOTE: GHC does not let us attach Haddock documentation to typeclass -- NOTE: GHC does not let us attach Haddock documentation to typeclass
-- method implementations, so 'validateMethodTransformFn' is defined -- method implementations, so 'validateMethodTransformFn' is defined

View File

@ -4,6 +4,7 @@ module Hasura.RQL.DDL.Webhook.Transform.QueryParams
( -- * Query transformations ( -- * Query transformations
QueryParams (..), QueryParams (..),
TransformFn (..), TransformFn (..),
TransformCtx (..),
QueryParamsTransformFn (..), QueryParamsTransformFn (..),
) )
where where
@ -18,11 +19,13 @@ import Data.Validation qualified as V
import Hasura.Incremental (Cacheable) import Hasura.Incremental (Cacheable)
import Hasura.Prelude import Hasura.Prelude
import Hasura.RQL.DDL.Webhook.Transform.Class import Hasura.RQL.DDL.Webhook.Transform.Class
( RequestTransformCtx (..), ( TemplatingEngine,
TemplatingEngine,
Transform (..), Transform (..),
TransformErrorBundle (..), TransformErrorBundle (..),
UnescapedTemplate (..), UnescapedTemplate (..),
)
import Hasura.RQL.DDL.Webhook.Transform.Request
( RequestTransformCtx,
runUnescapedRequestTemplateTransform', runUnescapedRequestTemplateTransform',
validateRequestUnescapedTemplateTransform', validateRequestUnescapedTemplateTransform',
) )
@ -45,10 +48,12 @@ instance Transform QueryParams where
deriving stock (Show, Eq, Generic) deriving stock (Show, Eq, Generic)
deriving newtype (NFData, Cacheable, FromJSON, ToJSON) deriving newtype (NFData, Cacheable, FromJSON, ToJSON)
newtype TransformCtx QueryParams = TransformCtx RequestTransformCtx
-- NOTE: GHC does not let us attach Haddock documentation to typeclass -- NOTE: GHC does not let us attach Haddock documentation to typeclass
-- method implementations, so 'applyQueryParamsTransformFn' is defined -- method implementations, so 'applyQueryParamsTransformFn' is defined
-- separately. -- separately.
transform (QueryParamsTransformFn_ fn) = applyQueryParamsTransformFn fn transform (QueryParamsTransformFn_ fn) (TransformCtx reqCtx) = applyQueryParamsTransformFn fn reqCtx
-- NOTE: GHC does not let us attach Haddock documentation to typeclass -- NOTE: GHC does not let us attach Haddock documentation to typeclass
-- method implementations, so 'validateQueryParamsTransformFn' is defined -- method implementations, so 'validateQueryParamsTransformFn' is defined

View File

@ -0,0 +1,200 @@
{-# LANGUAGE DeriveAnyClass #-}
-- | Types and subroutines related to constructing transformations on
-- HTTP requests.
module Hasura.RQL.DDL.Webhook.Transform.Request
( -- ** Request Transformation Context
RequestTransformCtx (..),
mkReqTransformCtx,
-- * Templating
TemplatingEngine (..),
Template (..),
Version (..),
runRequestTemplateTransform,
validateRequestTemplateTransform,
validateRequestTemplateTransform',
-- * Unescaped
runUnescapedRequestTemplateTransform,
runUnescapedRequestTemplateTransform',
validateRequestUnescapedTemplateTransform,
validateRequestUnescapedTemplateTransform',
)
where
-------------------------------------------------------------------------------
import Control.Arrow (left)
import Control.Lens qualified as Lens
import Data.Aeson (FromJSON, ToJSON, (.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.Kriti.Functions qualified as KFunc
import Data.Bifunctor
import Data.ByteString (ByteString)
import Data.Text.Encoding qualified as TE
import Data.Validation (Validation, fromEither)
import Hasura.Incremental (Cacheable)
import Hasura.Prelude
import Hasura.RQL.DDL.Webhook.Transform.Class (Template (..), TemplatingEngine (..), TransformErrorBundle (..), UnescapedTemplate, encodeScalar, wrapUnescapedTemplate)
import Hasura.Session (SessionVariables)
import Kriti.Error qualified as Kriti
import Kriti.Parser qualified as Kriti
import Network.HTTP.Client.Transformable qualified as HTTP
-------------------------------------------------------------------------------
-- | Common context that is made available to all request transformations.
data RequestTransformCtx = RequestTransformCtx
{ rtcBaseUrl :: Maybe Aeson.Value,
rtcBody :: Aeson.Value,
rtcSessionVariables :: Maybe SessionVariables,
rtcQueryParams :: Maybe Aeson.Value,
rtcEngine :: TemplatingEngine
}
instance ToJSON RequestTransformCtx where
toJSON RequestTransformCtx {..} =
let required =
[ "body" .= rtcBody,
"session_variables" .= rtcSessionVariables
]
optional =
[ ("base_url" .=) <$> rtcBaseUrl,
("query_params" .=) <$> rtcQueryParams
]
in Aeson.object (required <> catMaybes optional)
-- | A smart constructor for constructing the 'RequestTransformCtx'
--
-- 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!
mkReqTransformCtx ::
Text ->
Maybe SessionVariables ->
TemplatingEngine ->
HTTP.Request ->
RequestTransformCtx
mkReqTransformCtx url sessionVars rtcEngine reqData =
let rtcBaseUrl = Just $ Aeson.toJSON url
rtcBody =
let mBody = Lens.view HTTP.body reqData >>= Aeson.decode @Aeson.Value
in fromMaybe Aeson.Null mBody
rtcSessionVariables = sessionVars
rtcQueryParams =
let queryParams =
Lens.view HTTP.queryParams reqData & fmap \(key, val) ->
(TE.decodeUtf8 key, fmap TE.decodeUtf8 val)
in Just $ Aeson.toJSON queryParams
in RequestTransformCtx {..}
-------------------------------------------------------------------------------
-- | A helper function for executing transformations from a 'Template'
-- and a 'RequestTransformCtx'.
--
-- NOTE: This and all related funtions are hard-coded to Kriti at the
-- moment. When we add additional template engines this function will
-- need to take a 'TemplatingEngine' parameter.
runRequestTemplateTransform ::
Template ->
RequestTransformCtx ->
Either TransformErrorBundle Aeson.Value
runRequestTemplateTransform template RequestTransformCtx {rtcEngine = Kriti, ..} =
let context =
[ ("$body", rtcBody),
("$session_variables", Aeson.toJSON rtcSessionVariables)
]
<> catMaybes
[ ("$query_params",) <$> rtcQueryParams,
("$base_url",) <$> rtcBaseUrl
]
kritiFuncs = KFunc.sessionFunctions rtcSessionVariables
eResult = KFunc.runKritiWith (unTemplate $ template) context kritiFuncs
in eResult & left \kritiErr ->
let renderedErr = Aeson.toJSON kritiErr
in TransformErrorBundle [renderedErr]
-- TODO: Should this live in 'Hasura.RQL.DDL.Webhook.Transform.Validation'?
validateRequestTemplateTransform ::
TemplatingEngine ->
Template ->
Either TransformErrorBundle ()
validateRequestTemplateTransform Kriti (Template template) =
bimap packBundle (const ()) $ Kriti.parser $ TE.encodeUtf8 template
where
packBundle = TransformErrorBundle . pure . Aeson.toJSON . Kriti.serialize
validateRequestTemplateTransform' ::
TemplatingEngine ->
Template ->
Validation TransformErrorBundle ()
validateRequestTemplateTransform' engine =
fromEither . validateRequestTemplateTransform engine
-------------------------------------------------------------------------------
-- | 'RequestTransform' Versioning
data Version
= V1
| V2
deriving stock (Eq, Generic, Show)
deriving anyclass (Cacheable, Hashable, NFData)
instance FromJSON Version where
parseJSON v = do
version :: Int <- Aeson.parseJSON v
case version of
1 -> pure V1
2 -> pure V2
i -> fail $ "expected 1 or 2, encountered " ++ show i
instance ToJSON Version where
toJSON = \case
V1 -> Aeson.toJSON @Int 1
V2 -> Aeson.toJSON @Int 2
-------------------------------------------------------------------------------
-- | A helper function for executing Kriti transformations from a
-- 'UnescapedTemplate' and a 'RequestTrasformCtx'.
--
-- The difference from 'runRequestTemplateTransform' is that this
-- function will wrap the template text in double quotes before
-- running Kriti.
runUnescapedRequestTemplateTransform ::
RequestTransformCtx ->
UnescapedTemplate ->
Either TransformErrorBundle ByteString
runUnescapedRequestTemplateTransform context unescapedTemplate = do
result <-
runRequestTemplateTransform
(wrapUnescapedTemplate unescapedTemplate)
context
encodeScalar result
-- | Run a Kriti transformation with an unescaped template in
-- 'Validation' instead of 'Either'.
runUnescapedRequestTemplateTransform' ::
RequestTransformCtx ->
UnescapedTemplate ->
Validation TransformErrorBundle ByteString
runUnescapedRequestTemplateTransform' context unescapedTemplate =
fromEither $
runUnescapedRequestTemplateTransform context unescapedTemplate
-- TODO: Should this live in 'Hasura.RQL.DDL.Webhook.Transform.Validation'?
validateRequestUnescapedTemplateTransform ::
TemplatingEngine ->
UnescapedTemplate ->
Either TransformErrorBundle ()
validateRequestUnescapedTemplateTransform engine =
validateRequestTemplateTransform engine . wrapUnescapedTemplate
validateRequestUnescapedTemplateTransform' ::
TemplatingEngine ->
UnescapedTemplate ->
Validation TransformErrorBundle ()
validateRequestUnescapedTemplateTransform' engine =
fromEither . validateRequestUnescapedTemplateTransform engine

View File

@ -0,0 +1,79 @@
-- | Types and subroutines related to constructing transformations on
-- HTTP responses.
module Hasura.RQL.DDL.Webhook.Transform.Response
( -- ** Request Transformation Context
ResponseTransformCtx (..),
runResponseTemplateTransform,
-- * Unescaped
runUnescapedResponseTemplateTransform,
runUnescapedResponseTemplateTransform',
)
where
-------------------------------------------------------------------------------
import Control.Arrow (left)
import Data.Aeson qualified as Aeson
import Data.Aeson.Kriti.Functions as KFunc
import Data.ByteString (ByteString)
import Data.Validation (Validation, fromEither)
import Hasura.Prelude
import Hasura.RQL.DDL.Webhook.Transform.Class
( Template (..),
TemplatingEngine (Kriti),
TransformErrorBundle (..),
UnescapedTemplate,
encodeScalar,
wrapUnescapedTemplate,
)
import Hasura.Session (SessionVariables)
-------------------------------------------------------------------------------
-- | Common context that is made available to all response transformations.
data ResponseTransformCtx = ResponseTransformCtx
{ responseTransformBody :: Aeson.Value,
-- NOTE: This is a @Nothing@ if you have a Response Transform but no Request Transform:
responseTransformReqCtx :: Aeson.Value,
responseSessionVariables :: Maybe SessionVariables,
responseTransformEngine :: TemplatingEngine
}
-------------------------------------------------------------------------------
-- | A helper function for executing transformations from a 'Template'
-- and a 'ResponseTransformCtx'.
--
-- NOTE: This and all related funtions are hard-coded to Kriti at the
-- moment. When we add additional template engines this function will
-- need to take a 'TemplatingEngine' parameter.
runResponseTemplateTransform ::
Template ->
ResponseTransformCtx ->
Either TransformErrorBundle Aeson.Value
runResponseTemplateTransform template ResponseTransformCtx {responseTransformEngine = Kriti, ..} =
let context = [("$body", responseTransformBody), ("$request", Aeson.toJSON responseTransformReqCtx)]
customFunctions = KFunc.sessionFunctions responseSessionVariables
eResult = KFunc.runKritiWith (unTemplate $ template) context customFunctions
in eResult & left \kritiErr ->
let renderedErr = Aeson.toJSON kritiErr
in TransformErrorBundle [renderedErr]
-- | Run an 'UnescapedTemplate' with a 'ResponseTransformCtx'.
runUnescapedResponseTemplateTransform ::
ResponseTransformCtx ->
UnescapedTemplate ->
Either TransformErrorBundle ByteString
runUnescapedResponseTemplateTransform context unescapedTemplate = do
result <- runResponseTemplateTransform (wrapUnescapedTemplate unescapedTemplate) context
encodeScalar result
-- | Run an 'UnescapedTemplate' with a 'ResponseTransformCtx' in 'Validation'.
runUnescapedResponseTemplateTransform' ::
ResponseTransformCtx ->
UnescapedTemplate ->
Validation TransformErrorBundle ByteString
runUnescapedResponseTemplateTransform' context unescapedTemplate =
fromEither $
runUnescapedResponseTemplateTransform context unescapedTemplate

View File

@ -2,6 +2,7 @@ module Hasura.RQL.DDL.Webhook.Transform.Url
( -- * Url Transformations ( -- * Url Transformations
Url (..), Url (..),
TransformFn (..), TransformFn (..),
TransformCtx (..),
UrlTransformFn (..), UrlTransformFn (..),
) )
where where
@ -15,16 +16,18 @@ import Data.Validation
import Hasura.Incremental (Cacheable) import Hasura.Incremental (Cacheable)
import Hasura.Prelude import Hasura.Prelude
import Hasura.RQL.DDL.Webhook.Transform.Class import Hasura.RQL.DDL.Webhook.Transform.Class
( RequestTransformCtx (..), ( TemplatingEngine,
TemplatingEngine,
Transform (..), Transform (..),
TransformErrorBundle (..), TransformErrorBundle (..),
UnescapedTemplate (..), UnescapedTemplate (..),
runRequestTemplateTransform,
throwErrorBundle, throwErrorBundle,
validateRequestUnescapedTemplateTransform',
wrapUnescapedTemplate, wrapUnescapedTemplate,
) )
import Hasura.RQL.DDL.Webhook.Transform.Request
( RequestTransformCtx,
runRequestTemplateTransform,
validateRequestUnescapedTemplateTransform',
)
import Network.URI (parseURI) import Network.URI (parseURI)
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
@ -44,9 +47,11 @@ instance Transform Url where
deriving stock (Eq, Generic, Show) deriving stock (Eq, Generic, Show)
deriving newtype (Cacheable, NFData, FromJSON, ToJSON) deriving newtype (Cacheable, NFData, FromJSON, ToJSON)
newtype TransformCtx Url = TransformCtx RequestTransformCtx
-- NOTE: GHC does not let us attach Haddock documentation to typeclass -- NOTE: GHC does not let us attach Haddock documentation to typeclass
-- method implementations, so 'applyUrlTransformFn' is defined separately. -- method implementations, so 'applyUrlTransformFn' is defined separately.
transform (UrlTransformFn_ fn) = applyUrlTransformFn fn transform (UrlTransformFn_ fn) (TransformCtx reqCtx) = applyUrlTransformFn fn reqCtx
-- NOTE: GHC does not let us attach Haddock documentation to typeclass -- NOTE: GHC does not let us attach Haddock documentation to typeclass
-- method implementations, so 'validateUrlTransformFn' is defined separately. -- method implementations, so 'validateUrlTransformFn' is defined separately.

View File

@ -0,0 +1,51 @@
-- | The 'WithOptional' Functor and associated operations.
module Hasura.RQL.DDL.Webhook.Transform.WithOptional
( WithOptional (..),
withOptional,
)
where
-------------------------------------------------------------------------------
import Data.Aeson (FromJSON, ToJSON)
import Data.Coerce (Coercible)
import Hasura.Incremental (Cacheable)
import Hasura.Prelude
-------------------------------------------------------------------------------
-- | Enrich a 'Functor' @f@ with optionality; this is primarily useful when
-- one wants to annotate fields as optional when using the Higher-Kinded Data
-- pattern.
--
-- 'WithOptional'@ f@ is equivalent to @Compose Maybe f@.
newtype WithOptional f result = WithOptional
{ getOptional :: Maybe (f result)
}
deriving stock (Eq, Functor, Foldable, Generic, Show)
deriving newtype (FromJSON, ToJSON)
deriving newtype instance
(Cacheable (f result)) =>
Cacheable (WithOptional f result)
deriving newtype instance
(NFData (f result)) =>
NFData (WithOptional f result)
-- | 'WithOptional' smart constructor for the special case of optional values
-- that are representationally equivalent to some "wrapper" type.
--
-- For example:
-- @
-- withOptional \@HeaderTransformsAction headers == WithOptional $ fmap HeadersTransform headers
-- @
--
-- In other words: this function observes the isomorphism between @'Maybe' a@
-- and @'WithOptional' f b@ if an isomorphism exists between @a@ and @f b@.
withOptional ::
forall a b f.
Coercible a (f b) =>
Maybe a ->
WithOptional f b
withOptional = coerce