mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 13:02:11 +03:00
11a454c2d6
This commit applies ormolu to the whole Haskell code base by running `make format`. For in-flight branches, simply merging changes from `main` will result in merge conflicts. To avoid this, update your branch using the following instructions. Replace `<format-commit>` by the hash of *this* commit. $ git checkout my-feature-branch $ git merge <format-commit>^ # and resolve conflicts normally $ make format $ git commit -a -m "reformat with ormolu" $ git merge -s ours post-ormolu https://github.com/hasura/graphql-engine-mono/pull/2404 GitOrigin-RevId: 75049f5c12f430c615eafb4c6b8e83e371e01c8e
285 lines
10 KiB
Haskell
285 lines
10 KiB
Haskell
module Hasura.RQL.DDL.RequestTransform
|
|
( applyRequestTransform,
|
|
mkRequestTransform,
|
|
mkRequestTransformDebug,
|
|
RequestMethod (..),
|
|
TemplatingEngine (..),
|
|
TemplateText (..),
|
|
ContentType (..),
|
|
TransformHeaders (..),
|
|
MetadataTransform (..),
|
|
RequestTransform (..),
|
|
)
|
|
where
|
|
|
|
import Control.Lens (over)
|
|
import Data.Aeson qualified as J
|
|
import Data.Bifunctor (bimap)
|
|
import Data.CaseInsensitive qualified as CI
|
|
import Data.HashMap.Strict qualified as M
|
|
import Data.List (nubBy)
|
|
import Data.Text qualified as T
|
|
import Data.Text.Encoding qualified as TE
|
|
import Hasura.Incremental (Cacheable)
|
|
import Hasura.Prelude
|
|
import Hasura.Session (SessionVariables)
|
|
import Kriti (runKriti)
|
|
import Network.HTTP.Client.Transformable qualified as HTTP
|
|
|
|
{-
|
|
|
|
Request Transformations are data transformations used to modify HTTP
|
|
Requests before those requests are executed.
|
|
|
|
`MetadataTransform` values are stored in Metadata within the
|
|
`CreateAction` and `CreateEventTriggerQuery` Types and then converted
|
|
into `RequestTransform` values using `mkRequestTransform`.
|
|
|
|
`RequestTransforms` are applied to an HTTP Request using
|
|
`applyRequestTransform`.
|
|
|
|
In the case of body transformations, a user specified templating
|
|
script is applied. Currently a runtime failure of the template script
|
|
will return the original request body.
|
|
|
|
-}
|
|
|
|
-- | A set of data transformation functions and substitutions generated from a
|
|
-- MetadataTransform
|
|
data RequestTransform = RequestTransform
|
|
{ -- | Change the request method to one provided here. Nothing means POST.
|
|
rtRequestMethod :: Maybe RequestMethod,
|
|
-- | Change the request URL to one provided here. Nothing means original URL.
|
|
rtRequestURL :: Maybe Text,
|
|
-- | A function for transforming the request body using a Kriti template.
|
|
rtBodyTransform :: J.Value -> Maybe SessionVariables -> J.Value,
|
|
-- | Change content type to one provided here. Nothing means use original.
|
|
rtContentType :: Maybe ContentType,
|
|
-- | Change Request query params to those provided here. Nothing means use original.
|
|
rtQueryParams :: Maybe HTTP.Query,
|
|
-- | A transformation function for modifying the Request Headers.
|
|
rtRequestHeaders :: [HTTP.Header] -> [HTTP.Header]
|
|
}
|
|
|
|
data RequestMethod = GET | POST | PUT | PATCH | DELETE
|
|
deriving (Show, Eq, Enum, Bounded, Generic)
|
|
|
|
renderRequestMethod :: RequestMethod -> Text
|
|
renderRequestMethod = \case
|
|
GET -> "GET"
|
|
POST -> "POST"
|
|
PUT -> "PUT"
|
|
PATCH -> "PATCH"
|
|
DELETE -> "DELETE"
|
|
|
|
instance J.ToJSON RequestMethod where
|
|
toJSON = J.String . renderRequestMethod
|
|
|
|
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
|
|
|
|
-- | A de/serializable request transformation template which can be stored in
|
|
-- the metadata associated with an action/event trigger/etc. and used to produce
|
|
-- a 'RequestTransform'.
|
|
--
|
|
-- NOTE: This data type is _only_ intended to be parsed from and stored as user
|
|
-- metadata; no direct logical transformations should be done upon it.
|
|
--
|
|
-- NOTE: Users should convert this to 'RequestTransform' as close as possible to
|
|
-- the call site that performs a transformed HTTP request, as 'RequestTransform'
|
|
-- has a representation that makes it more difficult to deserialize for
|
|
-- debugging.
|
|
data MetadataTransform = MetadataTransform
|
|
{ -- | Change the request method to one provided here. Nothing means POST.
|
|
mtRequestMethod :: Maybe RequestMethod,
|
|
-- | Change the request URL to one provided here. Nothing means original URL.
|
|
mtRequestURL :: Maybe Text,
|
|
-- | Go-Basic template script for transforming the request body
|
|
mtBodyTransform :: Maybe TemplateText,
|
|
-- | Only the following Content-Types are allowed (default: application/json):
|
|
mtContentType :: Maybe ContentType,
|
|
-- | Replace any existing query params with those provided here
|
|
mtQueryParams :: Maybe (M.HashMap Text (Maybe Text)),
|
|
-- | Transform headers as defined here.
|
|
mtRequestHeaders :: Maybe TransformHeaders,
|
|
-- | The template engine to use for transformations. Default: Kriti
|
|
mtTemplatingEngine :: TemplatingEngine
|
|
}
|
|
deriving (Show, Eq, Generic)
|
|
|
|
instance NFData MetadataTransform
|
|
|
|
instance Cacheable MetadataTransform
|
|
|
|
instance J.ToJSON MetadataTransform where
|
|
toJSON MetadataTransform {..} =
|
|
J.object
|
|
[ "method" J..= mtRequestMethod,
|
|
"url" J..= mtRequestURL,
|
|
"body" J..= mtBodyTransform,
|
|
"content_type" J..= mtContentType,
|
|
"query_params" J..= mtQueryParams,
|
|
"request_headers" J..= mtRequestHeaders,
|
|
"template_engine" J..= mtTemplatingEngine
|
|
]
|
|
|
|
instance J.FromJSON MetadataTransform where
|
|
parseJSON = J.withObject "Object" $ \o -> do
|
|
method <- o J..:? "method"
|
|
url <- o J..:? "url"
|
|
body <- o J..:? "body"
|
|
contentType <- o J..:? "content_type"
|
|
queryParams <- o J..:? "query_params"
|
|
headers <- o J..:? "request_headers"
|
|
templateEngine <- o J..:? "template_engine"
|
|
let templateEngine' = maybe Kriti id templateEngine
|
|
pure $ MetadataTransform method url body contentType queryParams headers templateEngine'
|
|
|
|
-- | Available Template Languages
|
|
data TemplatingEngine = Kriti
|
|
deriving (Show, Eq, Enum, Bounded, Generic)
|
|
|
|
renderTemplatingEngine :: TemplatingEngine -> Text
|
|
renderTemplatingEngine _ = "Kriti"
|
|
|
|
instance J.FromJSON TemplatingEngine where
|
|
parseJSON = J.withText "TemplatingEngine" \case
|
|
"Kriti" -> pure Kriti
|
|
_ -> fail "Invalid TemplatingEngine"
|
|
|
|
instance J.ToJSON TemplatingEngine where
|
|
toJSON = J.String . renderTemplatingEngine
|
|
|
|
instance NFData TemplatingEngine
|
|
|
|
instance Cacheable TemplatingEngine
|
|
|
|
newtype TemplateText = TemplateText T.Text
|
|
deriving (Show, Eq, Generic)
|
|
|
|
instance NFData TemplateText
|
|
|
|
instance Cacheable TemplateText
|
|
|
|
instance J.FromJSON TemplateText where
|
|
parseJSON = J.withText "TemplateText" (pure . TemplateText)
|
|
|
|
instance J.ToJSON TemplateText where
|
|
toJSON = J.String . coerce
|
|
|
|
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
|
|
|
|
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, Text)],
|
|
removeHeaders :: [CI.CI Text]
|
|
}
|
|
deriving (Show, Eq, Ord, Generic)
|
|
|
|
instance NFData TransformHeaders
|
|
|
|
instance Cacheable TransformHeaders
|
|
|
|
instance J.ToJSON TransformHeaders where
|
|
toJSON TransformHeaders {..} =
|
|
J.object
|
|
[ "add_headers" J..= M.fromList (fmap (first CI.original) addHeaders),
|
|
"remove_headers" J..= fmap CI.original removeHeaders
|
|
]
|
|
|
|
instance J.FromJSON TransformHeaders where
|
|
parseJSON = J.withObject "TransformHeaders" $ \o -> do
|
|
addHeaders :: M.HashMap Text Text <- fromMaybe mempty <$> o J..:? "add_headers"
|
|
let headers = M.toList $ mapKeys CI.mk addHeaders
|
|
removeHeaders <- o J..:? "remove_headers"
|
|
let removeHeaders' = unHeaderKey <$> fromMaybe mempty removeHeaders
|
|
pure $ TransformHeaders headers removeHeaders'
|
|
|
|
-- | Construct a `RequestTransform` from its metadata representation.
|
|
mkRequestTransform :: MetadataTransform -> RequestTransform
|
|
mkRequestTransform MetadataTransform {..} =
|
|
let transformParams = fmap (bimap TE.encodeUtf8 (fmap TE.encodeUtf8)) . M.toList
|
|
queryParams = transformParams <$> mtQueryParams
|
|
headerTransform = maybe id mkHeaderTransform mtRequestHeaders
|
|
in RequestTransform mtRequestMethod mtRequestURL (mkBodyTransform mtBodyTransform mtTemplatingEngine) mtContentType queryParams headerTransform
|
|
|
|
mkRequestTransformDebug :: MetadataTransform -> RequestTransform
|
|
mkRequestTransformDebug mt@MetadataTransform {mtBodyTransform, mtTemplatingEngine} =
|
|
(mkRequestTransform mt) {rtBodyTransform = mkBodyTransform mtBodyTransform mtTemplatingEngine}
|
|
|
|
-- | Construct a Header Transformation function from a `TransformHeaders` value.
|
|
mkHeaderTransform :: TransformHeaders -> [HTTP.Header] -> [HTTP.Header]
|
|
mkHeaderTransform TransformHeaders {..} headers =
|
|
let toBeRemoved = (fmap . CI.map) (TE.encodeUtf8) removeHeaders
|
|
filteredHeaders = filter ((`notElem` toBeRemoved) . fst) headers
|
|
newHeaders = fmap (bimap (CI.map TE.encodeUtf8) TE.encodeUtf8) addHeaders
|
|
in newHeaders <> filteredHeaders
|
|
|
|
-- | Construct a Body Transformation function
|
|
mkBodyTransform :: Maybe TemplateText -> TemplatingEngine -> J.Value -> Maybe SessionVariables -> J.Value
|
|
mkBodyTransform Nothing _ source _ = source
|
|
mkBodyTransform (Just (TemplateText template)) engine source sessionVars =
|
|
let context = [("$", source)] <> catMaybes [("$session",) . J.toJSON <$> sessionVars]
|
|
in case engine of
|
|
Kriti -> case runKriti template context of
|
|
Left err -> J.toJSON err
|
|
Right res -> res
|
|
|
|
-- | Transform an `HTTP.Request` with a `RequestTransform`.
|
|
applyRequestTransform :: RequestTransform -> HTTP.Request -> Maybe SessionVariables -> HTTP.Request
|
|
applyRequestTransform RequestTransform {..} reqData sessionVars =
|
|
let method = fmap (TE.encodeUtf8 . renderRequestMethod) rtRequestMethod
|
|
bodyFunc (Just b) = case J.decode @J.Value b of
|
|
Just val -> pure $ J.encode $ rtBodyTransform val sessionVars
|
|
Nothing -> pure b
|
|
bodyFunc Nothing = Nothing
|
|
contentType = maybe "application/json" (TE.encodeUtf8 . renderContentType) rtContentType
|
|
headerFunc = nubBy (\a b -> fst a == fst b) . (:) ("Content-Type", contentType) . rtRequestHeaders
|
|
in reqData & over HTTP.url (`fromMaybe` rtRequestURL)
|
|
& over HTTP.method (`fromMaybe` method)
|
|
& over HTTP.queryParams (`fromMaybe` rtQueryParams)
|
|
& over HTTP.headers headerFunc
|
|
& over HTTP.body bodyFunc
|