mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-20 06:58:39 +03:00
Feature/improved webhook debug endpoint errors
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3782 Co-authored-by: Abhijeet Khangarot <26903230+abhi40308@users.noreply.github.com> GitOrigin-RevId: 404197e766efa94a1814e8a0287cd55d9175f2a7
This commit is contained in:
parent
2e04bd5ac2
commit
ca85acbfe3
@ -121,6 +121,7 @@ function:
|
||||
|
||||
- server: add metadata inconsistency information in `reload_metadata` API call
|
||||
- server: add custom function for case insensitive lookup in session variable in request transformation
|
||||
- server: Improved error messaging for `test_webhook_transform` metadata API endpoint
|
||||
- server: Webhook Tranforms can now produce `x-www-url-formencoded` bodies.
|
||||
- server: Webhook Transforms can now delete request/response bodies explicitly.
|
||||
- server: Fix truncation of session variables with variable length column types in MSSQL (#8158)
|
||||
|
@ -156,8 +156,7 @@ const getErrorFromCode = (data: Record<string, any>) => {
|
||||
return `${errorCode}: ${errorMsg}`;
|
||||
};
|
||||
|
||||
const getErrorFromBody = (data: Record<string, any>) => {
|
||||
const errorObj = data.body[0];
|
||||
const getErrorFromBody = (errorObj: Record<string, any>) => {
|
||||
const errorCode = errorObj?.error_code;
|
||||
const errorMsg = errorObj?.message;
|
||||
const stPos = errorObj?.source_position?.start_line
|
||||
@ -170,16 +169,16 @@ const getErrorFromBody = (data: Record<string, any>) => {
|
||||
};
|
||||
|
||||
export const parseValidateApiData = (
|
||||
requestData: Record<string, any>,
|
||||
requestData: Record<string, any> | Record<string, any>[],
|
||||
setError: (error: string) => void,
|
||||
setUrl?: (data: string) => void,
|
||||
setBody?: (data: string) => void
|
||||
) => {
|
||||
if (requestData?.code) {
|
||||
const errorMessage = getErrorFromCode(requestData);
|
||||
if (Array.isArray(requestData)) {
|
||||
const errorMessage = getErrorFromBody(requestData[0]);
|
||||
setError(errorMessage);
|
||||
} else if (requestData?.body?.[0]?.error_code) {
|
||||
const errorMessage = getErrorFromBody(requestData);
|
||||
} else if (requestData?.code) {
|
||||
const errorMessage = getErrorFromCode(requestData);
|
||||
setError(errorMessage);
|
||||
} else if (requestData?.webhook_url || requestData?.body) {
|
||||
setError('');
|
||||
|
@ -617,6 +617,7 @@ library
|
||||
, Hasura.RQL.DDL.Webhook.Transform.Headers
|
||||
, Hasura.RQL.DDL.Webhook.Transform.Method
|
||||
, Hasura.RQL.DDL.Webhook.Transform.QueryParams
|
||||
, Hasura.RQL.DDL.Webhook.Transform.Validation
|
||||
, Hasura.RQL.DDL.Webhook.Transform.Url
|
||||
, Hasura.RQL.DDL.Schema
|
||||
, Hasura.RQL.DDL.Schema.Cache
|
||||
|
@ -12,10 +12,16 @@ module Hasura.RQL.DDL.Action
|
||||
DropActionPermission,
|
||||
runDropActionPermission,
|
||||
dropActionPermissionInMetadata,
|
||||
caName,
|
||||
caDefinition,
|
||||
caComment,
|
||||
uaName,
|
||||
uaDefinition,
|
||||
uaComment,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Lens ((.~), (^.))
|
||||
import Control.Lens (makeLenses, (.~), (^.))
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Aeson.TH qualified as J
|
||||
import Data.Dependent.Map qualified as DMap
|
||||
@ -49,6 +55,8 @@ data CreateAction = CreateAction
|
||||
_caComment :: !(Maybe Text)
|
||||
}
|
||||
|
||||
$(makeLenses ''CreateAction)
|
||||
|
||||
$(J.deriveJSON hasuraJSON ''CreateAction)
|
||||
|
||||
runCreateAction ::
|
||||
@ -197,6 +205,7 @@ data UpdateAction = UpdateAction
|
||||
_uaComment :: !(Maybe Text)
|
||||
}
|
||||
|
||||
$(makeLenses ''UpdateAction)
|
||||
$(J.deriveFromJSON hasuraJSON ''UpdateAction)
|
||||
|
||||
runUpdateAction ::
|
||||
|
@ -12,10 +12,24 @@ module Hasura.RQL.DDL.EventTrigger
|
||||
getHeaderInfosFromConf,
|
||||
getWebhookInfoFromConf,
|
||||
buildEventTriggerInfo,
|
||||
cetqSource,
|
||||
cetqName,
|
||||
cetqTable,
|
||||
cetqInsert,
|
||||
cetqUpdate,
|
||||
cetqDelete,
|
||||
cetqEnableManual,
|
||||
cetqRetryConf,
|
||||
cetqWebhook,
|
||||
cetqWebhookFromEnv,
|
||||
cetqHeaders,
|
||||
cetqReplace,
|
||||
cetqRequestTransform,
|
||||
cetqResponseTrasnform,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Lens ((.~))
|
||||
import Control.Lens (makeLenses, (.~))
|
||||
import Data.Aeson
|
||||
import Data.ByteString.Lazy qualified as LBS
|
||||
import Data.Environment qualified as Env
|
||||
@ -52,6 +66,8 @@ data CreateEventTriggerQuery (b :: BackendType) = CreateEventTriggerQuery
|
||||
_cetqResponseTrasnform :: !(Maybe MetadataResponseTransform)
|
||||
}
|
||||
|
||||
$(makeLenses ''CreateEventTriggerQuery)
|
||||
|
||||
instance Backend b => FromJSON (CreateEventTriggerQuery b) where
|
||||
parseJSON = withObject "CreateEventTriggerQuery" \o -> do
|
||||
sourceName <- o .:? "source" .!= defaultSource
|
||||
|
@ -21,7 +21,7 @@ import Control.Lens ((.~), (^.), (^?))
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Aeson.Ordered qualified as AO
|
||||
import Data.Attoparsec.Text qualified as AT
|
||||
import Data.Bifunctor (bimap, first)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Bitraversable
|
||||
import Data.ByteString.Lazy qualified as BL
|
||||
import Data.CaseInsensitive qualified as CI
|
||||
@ -59,8 +59,6 @@ import Hasura.RQL.DDL.Webhook.Transform.Class (mkReqTransformCtx)
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.RQL.Types.Eventing.Backend (BackendEventTrigger (..))
|
||||
import Hasura.SQL.AnyBackend qualified as AB
|
||||
import Kriti qualified as K
|
||||
import Kriti.Error qualified as K
|
||||
import Network.HTTP.Client.Transformable qualified as HTTP
|
||||
|
||||
runClearMetadata ::
|
||||
@ -523,8 +521,7 @@ runRemoveMetricsConfig = do
|
||||
pure successMsg
|
||||
|
||||
data TestTransformError
|
||||
= UrlInterpError K.SerializedError
|
||||
| RequestInitializationError HTTP.HttpException
|
||||
= RequestInitializationError HTTP.HttpException
|
||||
| RequestTransformationError HTTP.Request TransformErrorBundle
|
||||
|
||||
runTestWebhookTransform ::
|
||||
@ -541,28 +538,25 @@ runTestWebhookTransform (TestWebhookTransform env headers urlE payload rt _ sv)
|
||||
headers' <- traverse (traverse (fmap TE.encodeUtf8 . interpolateFromEnv env . TE.decodeUtf8)) headers
|
||||
|
||||
result <- runExceptT $ do
|
||||
let env' = bimap T.pack (J.String . T.pack) <$> Env.toList env
|
||||
decodeKritiResult = TE.decodeUtf8 . BL.toStrict . J.encode
|
||||
|
||||
kritiUrlResult <- hoistEither $ first (UrlInterpError . K.serialize) $ decodeKritiResult <$> K.runKriti ("\"" <> url <> "\"") env'
|
||||
|
||||
let unwrappedUrl = T.drop 1 $ T.dropEnd 1 kritiUrlResult
|
||||
initReq <- hoistEither $ first RequestInitializationError $ HTTP.mkRequestEither unwrappedUrl
|
||||
initReq <- hoistEither $ first RequestInitializationError $ HTTP.mkRequestEither url
|
||||
|
||||
let req = initReq & HTTP.body .~ pure (J.encode payload) & HTTP.headers .~ headers'
|
||||
reqTransform = requestFields rt
|
||||
engine = templateEngine rt
|
||||
reqTransformCtx = mkReqTransformCtx unwrappedUrl sv engine
|
||||
reqTransformCtx = mkReqTransformCtx url sv engine
|
||||
hoistEither $ first (RequestTransformationError req) $ applyRequestTransform reqTransformCtx reqTransform req
|
||||
|
||||
case result of
|
||||
Right transformed ->
|
||||
let body = decodeBody (transformed ^. HTTP.body)
|
||||
in pure $ packTransformResult transformed body
|
||||
Left (RequestTransformationError req err) -> pure $ packTransformResult req (J.toJSON err)
|
||||
-- NOTE: In the following two cases we have failed before producing a valid request.
|
||||
Left (UrlInterpError err) -> pure $ encJFromJValue $ J.toJSON err
|
||||
Left (RequestInitializationError err) -> pure $ encJFromJValue $ J.String $ "Error: " <> tshow err
|
||||
pure $ packTransformResult $ Right transformed
|
||||
Left (RequestTransformationError _ err) -> pure $ packTransformResult (Left err)
|
||||
-- NOTE: In the following case we have failed before producing a valid request.
|
||||
Left (RequestInitializationError err) ->
|
||||
let errorBundle =
|
||||
TransformErrorBundle $
|
||||
pure $
|
||||
J.object ["error_code" J..= J.String "Request Initialization Error", "message" J..= J.String (tshow err)]
|
||||
in pure $ encJFromJValue $ J.toJSON errorBundle
|
||||
|
||||
interpolateFromEnv :: MonadError QErr m => Env.Environment -> Text -> m Text
|
||||
interpolateFromEnv env url =
|
||||
@ -574,6 +568,8 @@ interpolateFromEnv env url =
|
||||
err e = throwError $ err400 NotFound $ "Missing Env Var: " <> e
|
||||
in either err (pure . fold) result
|
||||
|
||||
-- | Deserialize a JSON or X-WWW-URL-FORMENCODED body from an
|
||||
-- 'HTTP.Request' as 'J.Value'.
|
||||
decodeBody :: Maybe BL.ByteString -> J.Value
|
||||
decodeBody Nothing = J.Null
|
||||
decodeBody (Just bs) = fromMaybe J.Null $ jsonToValue bs <|> formUrlEncodedToValue bs
|
||||
@ -598,12 +594,14 @@ parseEnvTemplate = AT.many1 $ pEnv <|> pLit <|> fmap Right "{"
|
||||
indistinct :: Either a a -> a
|
||||
indistinct = either id id
|
||||
|
||||
packTransformResult :: HTTP.Request -> J.Value -> EncJSON
|
||||
packTransformResult req body =
|
||||
encJFromJValue $
|
||||
J.object
|
||||
[ "webhook_url" J..= (req ^. HTTP.url),
|
||||
"method" J..= (req ^. HTTP.method),
|
||||
"headers" J..= (first CI.foldedCase <$> (req ^. HTTP.headers)),
|
||||
"body" J..= body
|
||||
]
|
||||
packTransformResult :: Either TransformErrorBundle HTTP.Request -> EncJSON
|
||||
packTransformResult = \case
|
||||
Right req ->
|
||||
encJFromJValue $
|
||||
J.object
|
||||
[ "webhook_url" J..= (req ^. HTTP.url),
|
||||
"method" J..= (req ^. HTTP.method),
|
||||
"headers" J..= (first CI.foldedCase <$> (req ^. HTTP.headers)),
|
||||
"body" J..= decodeBody (req ^. HTTP.body)
|
||||
]
|
||||
Left err -> encJFromJValue $ J.toJSON err
|
||||
|
@ -15,9 +15,17 @@ module Hasura.RQL.DDL.Metadata.Types
|
||||
AllowInconsistentMetadata (..),
|
||||
WebHookUrl (..),
|
||||
TestWebhookTransform (..),
|
||||
twtEnv,
|
||||
twtHeaders,
|
||||
twtWebhookUrl,
|
||||
twtPayload,
|
||||
twtTransformer,
|
||||
twtResponseTransformer,
|
||||
twtSessionVariables,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Lens (makeLenses)
|
||||
import Data.Aeson
|
||||
import Data.Aeson.TH
|
||||
import Data.CaseInsensitive qualified as CI
|
||||
@ -214,6 +222,8 @@ data TestWebhookTransform = TestWebhookTransform
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
$(makeLenses ''TestWebhookTransform)
|
||||
|
||||
instance FromJSON TestWebhookTransform where
|
||||
parseJSON = withObject "TestWebhookTransform" $ \o -> do
|
||||
env <- fmap (fromMaybe mempty) $ o .:? "env"
|
||||
|
@ -53,17 +53,17 @@ runCreateCronTrigger ::
|
||||
runCreateCronTrigger CreateCronTrigger {..} = do
|
||||
let q =
|
||||
CronTriggerMetadata
|
||||
cctName
|
||||
cctWebhook
|
||||
cctCronSchedule
|
||||
cctPayload
|
||||
cctRetryConf
|
||||
cctHeaders
|
||||
cctIncludeInMetadata
|
||||
cctComment
|
||||
cctRequestTransform
|
||||
cctResponseTransform
|
||||
case cctReplace of
|
||||
_cctName
|
||||
_cctWebhook
|
||||
_cctCronSchedule
|
||||
_cctPayload
|
||||
_cctRetryConf
|
||||
_cctHeaders
|
||||
_cctIncludeInMetadata
|
||||
_cctComment
|
||||
_cctRequestTransform
|
||||
_cctResponseTransform
|
||||
case _cctReplace of
|
||||
True -> updateCronTrigger q
|
||||
False -> do
|
||||
cronTriggersMap <- scCronTriggers <$> askSchemaCache
|
||||
@ -75,23 +75,23 @@ runCreateCronTrigger CreateCronTrigger {..} = do
|
||||
<> triggerNameToTxt (ctName q)
|
||||
<> " already exists"
|
||||
|
||||
let metadataObj = MOCronTrigger cctName
|
||||
let metadataObj = MOCronTrigger _cctName
|
||||
metadata =
|
||||
CronTriggerMetadata
|
||||
cctName
|
||||
cctWebhook
|
||||
cctCronSchedule
|
||||
cctPayload
|
||||
cctRetryConf
|
||||
cctHeaders
|
||||
cctIncludeInMetadata
|
||||
cctComment
|
||||
cctRequestTransform
|
||||
cctResponseTransform
|
||||
_cctName
|
||||
_cctWebhook
|
||||
_cctCronSchedule
|
||||
_cctPayload
|
||||
_cctRetryConf
|
||||
_cctHeaders
|
||||
_cctIncludeInMetadata
|
||||
_cctComment
|
||||
_cctRequestTransform
|
||||
_cctResponseTransform
|
||||
buildSchemaCacheFor metadataObj $
|
||||
MetadataModifier $
|
||||
metaCronTriggers %~ OMap.insert cctName metadata
|
||||
populateInitialCronTriggerEvents cctCronSchedule cctName
|
||||
metaCronTriggers %~ OMap.insert _cctName metadata
|
||||
populateInitialCronTriggerEvents _cctCronSchedule _cctName
|
||||
return successMsg
|
||||
|
||||
resolveCronTrigger ::
|
||||
|
@ -272,7 +272,7 @@ infixr 8 .=?
|
||||
newtype WithOptional fn result = WithOptional
|
||||
{ getOptional :: Maybe (fn result)
|
||||
}
|
||||
deriving stock (Eq, Functor, Generic, Show)
|
||||
deriving stock (Eq, Functor, Foldable, Generic, Show)
|
||||
deriving newtype (FromJSON, ToJSON)
|
||||
|
||||
deriving newtype instance
|
||||
@ -355,7 +355,7 @@ mkRespTemplateTransform engine (ModifyBody (Template template)) ResponseTransfor
|
||||
mkRespTemplateTransform engine (FormUrlEncoded formTemplates) context =
|
||||
case engine of
|
||||
Kriti -> do
|
||||
result <- liftEither . V.toEither $ traverse (validateUnescapedResponseTemplateTransform context) formTemplates
|
||||
result <- liftEither . V.toEither $ traverse (runUnescapedResponseTemplateTransform' context) formTemplates
|
||||
pure $ J.String $ TE.decodeUtf8 $ BL.toStrict $ foldFormEncoded result
|
||||
|
||||
mkResponseTransform :: MetadataResponseTransform -> ResponseTransform
|
||||
|
@ -22,17 +22,21 @@ import Data.HashMap.Internal.Strict qualified as M
|
||||
import Data.List qualified as L
|
||||
import Data.Text qualified as T
|
||||
import Data.Text.Encoding qualified as TE
|
||||
import Data.Validation (Validation)
|
||||
import Data.Validation qualified as V
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.Webhook.Transform.Class
|
||||
( RequestTransformCtx (..),
|
||||
Template (..),
|
||||
TemplatingEngine,
|
||||
Transform (..),
|
||||
TransformErrorBundle (..),
|
||||
UnescapedTemplate,
|
||||
mkRequestTemplateTransform,
|
||||
validateUnescapedRequestTemplateTransform,
|
||||
runRequestTemplateTransform,
|
||||
runUnescapedRequestTemplateTransform',
|
||||
validateRequestTemplateTransform',
|
||||
validateRequestUnescapedTemplateTransform',
|
||||
)
|
||||
import Network.URI.Extended qualified as URI
|
||||
|
||||
@ -60,12 +64,21 @@ instance Transform Body where
|
||||
case transformation of
|
||||
RemoveBody -> pure $ JSONBody Nothing
|
||||
ModifyBody template -> do
|
||||
result <- liftEither $ mkRequestTemplateTransform template context
|
||||
result <- liftEither $ runRequestTemplateTransform template context
|
||||
pure . JSONBody . Just $ result
|
||||
FormUrlEncoded formTemplates -> do
|
||||
result <- liftEither . V.toEither $ traverse (validateUnescapedRequestTemplateTransform context) formTemplates
|
||||
result <- liftEither . V.toEither $ traverse (runUnescapedRequestTemplateTransform' context) formTemplates
|
||||
pure $ RawBody $ foldFormEncoded result
|
||||
|
||||
validate ::
|
||||
TemplatingEngine ->
|
||||
TransformFn Body ->
|
||||
Validation TransformErrorBundle ()
|
||||
validate engine = \case
|
||||
BodyTransform (RemoveBody) -> pure ()
|
||||
BodyTransform (ModifyBody template) -> validateRequestTemplateTransform' engine template
|
||||
BodyTransform (FormUrlEncoded kv) -> traverse_ (validateRequestUnescapedTemplateTransform' engine) kv
|
||||
|
||||
-- | Helper function for URI escaping 'T.Text' values.
|
||||
escapeURIText :: T.Text -> T.Text
|
||||
escapeURIText =
|
||||
|
@ -2,7 +2,8 @@
|
||||
{-# LANGUAGE StandaloneKindSignatures #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
-- TODO(SOLOMON): Module Description.
|
||||
-- | The 'Transform' typeclass with various types and helper functions
|
||||
-- for evaluating transformations.
|
||||
module Hasura.RQL.DDL.Webhook.Transform.Class
|
||||
( -- * Transformation Interface and Utilities
|
||||
Transform (..),
|
||||
@ -20,15 +21,19 @@ module Hasura.RQL.DDL.Webhook.Transform.Class
|
||||
TemplatingEngine (..),
|
||||
Template (..),
|
||||
Version (..),
|
||||
mkRequestTemplateTransform,
|
||||
runRequestTemplateTransform,
|
||||
validateRequestTemplateTransform,
|
||||
validateRequestTemplateTransform',
|
||||
|
||||
-- * Unescaped
|
||||
UnescapedTemplate (..),
|
||||
wrapUnescapedTemplate,
|
||||
mkUnescapedRequestTemplateTransform,
|
||||
validateUnescapedRequestTemplateTransform,
|
||||
mkUnescapedResponseTemplateTransform,
|
||||
validateUnescapedResponseTemplateTransform,
|
||||
runUnescapedRequestTemplateTransform,
|
||||
runUnescapedRequestTemplateTransform',
|
||||
runUnescapedResponseTemplateTransform,
|
||||
runUnescapedResponseTemplateTransform',
|
||||
validateRequestUnescapedTemplateTransform,
|
||||
validateRequestUnescapedTemplateTransform',
|
||||
)
|
||||
where
|
||||
|
||||
@ -51,7 +56,7 @@ import Data.Validation (Validation, fromEither)
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
import Hasura.Session (SessionVariables, getSessionVariableValue, mkSessionVariable)
|
||||
import Kriti (SerializedError (..), runKritiWith)
|
||||
import Kriti (runKritiWith)
|
||||
import Kriti.Error qualified as Kriti (CustomFunctionError (..), serialize)
|
||||
import Kriti.Parser qualified as Kriti (parser)
|
||||
import Network.HTTP.Client.Transformable qualified as HTTP
|
||||
@ -82,6 +87,12 @@ class Transform a where
|
||||
a ->
|
||||
m a
|
||||
|
||||
-- | Validate a 'TransformFn' of @a@.
|
||||
validate ::
|
||||
TemplatingEngine ->
|
||||
TransformFn a ->
|
||||
Validation TransformErrorBundle ()
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-- | We use collect all transformation failures as a '[J.Value]'.
|
||||
@ -193,7 +204,7 @@ instance ToJSON TemplatingEngine where
|
||||
{ J.tagSingleConstructors = True
|
||||
}
|
||||
|
||||
-- | Validated textual transformation template.
|
||||
-- | Textual transformation template.
|
||||
newtype Template = Template
|
||||
{ unTemplate :: Text
|
||||
}
|
||||
@ -201,28 +212,23 @@ newtype Template = Template
|
||||
deriving newtype (Hashable, FromJSONKey, ToJSONKey)
|
||||
deriving anyclass (Cacheable, NFData)
|
||||
|
||||
-- XXX(jkachmar): We need roundtrip tests for these instances.
|
||||
instance J.FromJSON Template where
|
||||
parseJSON = J.withText "Template" \t -> do
|
||||
case Kriti.parser (encodeUtf8 t) of
|
||||
-- TODO(SOLOMON): Use the parsed ValueExt in 'RequestFields' so that we
|
||||
-- don't have to parse at every request.
|
||||
Right _ -> pure $ Template t
|
||||
Left err ->
|
||||
let SerializedError {_message} = Kriti.serialize err
|
||||
in fail $ T.unpack _message
|
||||
parseJSON = J.withText "Template" (pure . Template)
|
||||
|
||||
-- XXX(jkachmar): We need roundtrip tests for these instances.
|
||||
instance J.ToJSON Template where
|
||||
toJSON = J.String . coerce
|
||||
|
||||
-- | A helper function for executing transformations from a 'Template'
|
||||
-- and a 'RequestTrasformCtx'.
|
||||
mkRequestTemplateTransform ::
|
||||
-- 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
|
||||
mkRequestTemplateTransform template RequestTransformCtx {rtcEngine = Kriti, ..} =
|
||||
runRequestTemplateTransform template RequestTransformCtx {rtcEngine = Kriti, ..} =
|
||||
let context =
|
||||
[ ("$body", rtcBody),
|
||||
("$session_variables", rtcSessionVariables)
|
||||
@ -236,11 +242,34 @@ mkRequestTemplateTransform template RequestTransformCtx {rtcEngine = Kriti, ..}
|
||||
let renderedErr = J.toJSON $ Kriti.serialize kritiErr
|
||||
in TransformErrorBundle [renderedErr]
|
||||
|
||||
mkResponseTemplateTransform ::
|
||||
-- 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
|
||||
mkResponseTemplateTransform template ResponseTransformCtx {responseTransformEngine = Kriti, ..} =
|
||||
runResponseTemplateTransform template ResponseTransformCtx {responseTransformEngine = Kriti, ..} =
|
||||
let context = [("$body", responseTransformBody), ("$request", responseTransformReqCtx)]
|
||||
eResult = runKritiWith (unTemplate $ template) context responseTransformFunctions
|
||||
in eResult & left \kritiErr ->
|
||||
@ -283,26 +312,9 @@ newtype UnescapedTemplate = UnescapedTemplate
|
||||
deriving newtype (Hashable, FromJSONKey, ToJSONKey)
|
||||
deriving anyclass (Cacheable, NFData)
|
||||
|
||||
-- XXX(jkachmar): We need roundtrip tests for these instances.
|
||||
instance J.FromJSON UnescapedTemplate where
|
||||
parseJSON = J.withText "UnescapedTemplate" \t -> do
|
||||
let wrapped = "\"" <> t <> "\""
|
||||
-- TODO(SOLOMON): Use the parsed ValueExt in 'RequestFields' so that we
|
||||
-- don't have to parse at every request.
|
||||
case Kriti.parser (encodeUtf8 wrapped) of
|
||||
-- NOTE: We can't simplfy use the wrapped value because the
|
||||
-- 'FromJSONKey' instance uses coercion rather then this
|
||||
-- instance to wrap the newtype. This means we would need to
|
||||
-- handle QueryParam keys explicitly in 'mkQueryParamsTransform'
|
||||
-- while values would be handled implicitliy via this aeson
|
||||
-- instance. That is confusing, so we just handle everything
|
||||
-- explicitly down the call stack.
|
||||
Right _ -> pure $ UnescapedTemplate t
|
||||
Left err ->
|
||||
let SerializedError {_message} = Kriti.serialize err
|
||||
in fail $ T.unpack _message
|
||||
parseJSON = J.withText "Template" (pure . UnescapedTemplate)
|
||||
|
||||
-- XXX(jkachmar): We need roundtrip tests for these instances.
|
||||
instance J.ToJSON UnescapedTemplate where
|
||||
toJSON = J.String . coerce
|
||||
|
||||
@ -311,45 +323,64 @@ wrapUnescapedTemplate :: UnescapedTemplate -> Template
|
||||
wrapUnescapedTemplate (UnescapedTemplate txt) = Template $ "\"" <> txt <> "\""
|
||||
|
||||
-- | A helper function for executing Kriti transformations from a
|
||||
-- 'UnescapedTemplate' and a 'RequestTrasformCtx'. The differennce
|
||||
-- from 'mkRequestTemplateTransform' is that this function will wrap
|
||||
-- the template text in double quotes before running Kriti.
|
||||
mkUnescapedRequestTemplateTransform ::
|
||||
-- '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
|
||||
mkUnescapedRequestTemplateTransform context unescapedTemplate = do
|
||||
runUnescapedRequestTemplateTransform context unescapedTemplate = do
|
||||
result <-
|
||||
mkRequestTemplateTransform
|
||||
runRequestTemplateTransform
|
||||
(wrapUnescapedTemplate unescapedTemplate)
|
||||
context
|
||||
encodeScalar result
|
||||
|
||||
-- | Run a Kriti transformation with an unescaped template in
|
||||
-- 'Validation' instead of 'Either'.
|
||||
validateUnescapedRequestTemplateTransform ::
|
||||
runUnescapedRequestTemplateTransform' ::
|
||||
RequestTransformCtx ->
|
||||
UnescapedTemplate ->
|
||||
Validation TransformErrorBundle ByteString
|
||||
validateUnescapedRequestTemplateTransform context unescapedTemplate =
|
||||
runUnescapedRequestTemplateTransform' context unescapedTemplate =
|
||||
fromEither $
|
||||
mkUnescapedRequestTemplateTransform context unescapedTemplate
|
||||
runUnescapedRequestTemplateTransform context unescapedTemplate
|
||||
|
||||
mkUnescapedResponseTemplateTransform ::
|
||||
-- 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
|
||||
mkUnescapedResponseTemplateTransform context unescapedTemplate = do
|
||||
result <- mkResponseTemplateTransform (wrapUnescapedTemplate unescapedTemplate) context
|
||||
runUnescapedResponseTemplateTransform context unescapedTemplate = do
|
||||
result <- runResponseTemplateTransform (wrapUnescapedTemplate unescapedTemplate) context
|
||||
encodeScalar result
|
||||
|
||||
validateUnescapedResponseTemplateTransform ::
|
||||
-- | Run an 'UnescapedTemplate' with a 'ResponseTransformCtx' in 'Validation'.
|
||||
runUnescapedResponseTemplateTransform' ::
|
||||
ResponseTransformCtx ->
|
||||
UnescapedTemplate ->
|
||||
Validation TransformErrorBundle ByteString
|
||||
validateUnescapedResponseTemplateTransform context unescapedTemplate =
|
||||
runUnescapedResponseTemplateTransform' context unescapedTemplate =
|
||||
fromEither $
|
||||
mkUnescapedResponseTemplateTransform context unescapedTemplate
|
||||
runUnescapedResponseTemplateTransform context unescapedTemplate
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Utility functions.
|
||||
|
@ -1,6 +1,7 @@
|
||||
{-# LANGUAGE ApplicativeDo #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
|
||||
module Hasura.RQL.DDL.Webhook.Transform.Headers
|
||||
( -- * Header Transformations
|
||||
@ -20,15 +21,18 @@ import Data.Aeson qualified as J
|
||||
import Data.CaseInsensitive qualified as CI
|
||||
import Data.HashMap.Strict qualified as M
|
||||
import Data.Text.Encoding qualified as TE
|
||||
import Data.Validation (Validation)
|
||||
import Data.Validation qualified as V
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.Webhook.Transform.Class
|
||||
( RequestTransformCtx (..),
|
||||
TemplatingEngine,
|
||||
Transform (..),
|
||||
TransformErrorBundle (..),
|
||||
UnescapedTemplate (..),
|
||||
validateUnescapedRequestTemplateTransform,
|
||||
runUnescapedRequestTemplateTransform',
|
||||
validateRequestUnescapedTemplateTransform',
|
||||
)
|
||||
import Network.HTTP.Types qualified as HTTP.Types
|
||||
|
||||
@ -67,11 +71,18 @@ instance Transform Headers where
|
||||
-- Validation's applicative sequencing.
|
||||
newHeaders <- liftEither . V.toEither $ for rhf_addHeaders \(rawKey, rawValue) -> do
|
||||
let key = CI.map TE.encodeUtf8 rawKey
|
||||
value <- validateUnescapedRequestTemplateTransform context rawValue
|
||||
value <- runUnescapedRequestTemplateTransform' context rawValue
|
||||
pure (key, value)
|
||||
|
||||
pure . Headers $ filteredHeaders <> newHeaders
|
||||
|
||||
validate ::
|
||||
TemplatingEngine ->
|
||||
TransformFn Headers ->
|
||||
Validation TransformErrorBundle ()
|
||||
validate engine (HeadersTransform (ReplaceHeaders (fmap snd . rhf_addHeaders -> templates))) =
|
||||
traverse_ (validateRequestUnescapedTemplateTransform' engine) templates
|
||||
|
||||
-- | The defunctionalized transformation on 'Headers'
|
||||
newtype HeadersTransformAction
|
||||
= ReplaceHeaders ReplaceHeaderFields
|
||||
|
@ -16,10 +16,12 @@ import Data.Aeson (FromJSON, ToJSON)
|
||||
import Data.Aeson qualified as J
|
||||
import Data.CaseInsensitive qualified as CI
|
||||
import Data.Text qualified as T
|
||||
import Data.Validation
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.Webhook.Transform.Class
|
||||
( RequestTransformCtx (..),
|
||||
TemplatingEngine,
|
||||
Transform (..),
|
||||
TransformErrorBundle (..),
|
||||
)
|
||||
@ -48,6 +50,13 @@ instance Transform Method where
|
||||
transform :: MonadError TransformErrorBundle m => TransformFn Method -> RequestTransformCtx -> Method -> m Method
|
||||
transform (MethodTransform (ReplaceMethod method)) _ _ = pure method
|
||||
|
||||
-- NOTE: Do we want to validate the method verb?
|
||||
validate ::
|
||||
TemplatingEngine ->
|
||||
TransformFn Method ->
|
||||
Validation TransformErrorBundle ()
|
||||
validate _ _ = pure ()
|
||||
|
||||
-- | The defunctionalized transformation on 'Method'.
|
||||
--
|
||||
-- In this case our transformation simply replaces the 'Method' with a
|
||||
|
@ -15,15 +15,18 @@ where
|
||||
|
||||
import Data.Aeson qualified as J
|
||||
import Data.HashMap.Strict qualified as M
|
||||
import Data.Validation (Validation)
|
||||
import Data.Validation qualified as V
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.Webhook.Transform.Class
|
||||
( RequestTransformCtx (..),
|
||||
TemplatingEngine,
|
||||
Transform (..),
|
||||
TransformErrorBundle (..),
|
||||
UnescapedTemplate (..),
|
||||
validateUnescapedRequestTemplateTransform,
|
||||
runUnescapedRequestTemplateTransform',
|
||||
validateRequestUnescapedTemplateTransform',
|
||||
)
|
||||
import Network.HTTP.Client.Transformable qualified as HTTP
|
||||
|
||||
@ -47,11 +50,22 @@ instance Transform QueryParams where
|
||||
-- NOTE: We use `ApplicativeDo` here to take advantage of
|
||||
-- Validation's applicative sequencing.
|
||||
queryParams <- liftEither . V.toEither $ for replacements \(rawKey, rawValue) -> do
|
||||
key <- validateUnescapedRequestTemplateTransform context rawKey
|
||||
value <- traverse (validateUnescapedRequestTemplateTransform context) rawValue
|
||||
key <- runUnescapedRequestTemplateTransform' context rawKey
|
||||
value <- traverse (runUnescapedRequestTemplateTransform' context) rawValue
|
||||
pure (key, value)
|
||||
pure $ QueryParams queryParams
|
||||
|
||||
validate ::
|
||||
TemplatingEngine ->
|
||||
TransformFn QueryParams ->
|
||||
Validation TransformErrorBundle ()
|
||||
validate engine (QueryParamsTransform (QueryParamsTransformAction params)) =
|
||||
for_ params \(key, val) -> do
|
||||
-- Note: We are using ApplicativeDo here:
|
||||
validateRequestUnescapedTemplateTransform' engine key
|
||||
traverse_ (validateRequestUnescapedTemplateTransform' engine) val
|
||||
pure ()
|
||||
|
||||
-- | The defunctionalized transformation 'QueryParams'
|
||||
newtype QueryParamsTransformAction = QueryParamsTransformAction [(UnescapedTemplate, Maybe UnescapedTemplate)]
|
||||
deriving stock (Generic)
|
||||
|
@ -16,15 +16,18 @@ where
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Text qualified as T
|
||||
import Data.Validation
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.Webhook.Transform.Class
|
||||
( RequestTransformCtx (..),
|
||||
TemplatingEngine,
|
||||
Transform (..),
|
||||
TransformErrorBundle (..),
|
||||
UnescapedTemplate (..),
|
||||
mkRequestTemplateTransform,
|
||||
runRequestTemplateTransform,
|
||||
throwErrorBundle,
|
||||
validateRequestUnescapedTemplateTransform',
|
||||
wrapUnescapedTemplate,
|
||||
)
|
||||
import Network.URI (parseURI)
|
||||
@ -55,7 +58,7 @@ instance Transform Url where
|
||||
case transformation of
|
||||
ModifyUrl unescapedTemplate -> do
|
||||
let template = wrapUnescapedTemplate unescapedTemplate
|
||||
resultJson <- liftEither $ mkRequestTemplateTransform template context
|
||||
resultJson <- liftEither $ runRequestTemplateTransform template context
|
||||
templatedUrlTxt <- case resultJson of
|
||||
J.String templatedUrlTxt -> pure templatedUrlTxt
|
||||
val -> do
|
||||
@ -65,6 +68,13 @@ instance Transform Url where
|
||||
Nothing -> throwErrorBundle ("Invalid URL: " <> templatedUrlTxt) Nothing
|
||||
Just _validatedUrl -> pure $ Url templatedUrlTxt
|
||||
|
||||
validate ::
|
||||
TemplatingEngine ->
|
||||
TransformFn Url ->
|
||||
Validation TransformErrorBundle ()
|
||||
validate engine (UrlTransform (ModifyUrl template)) =
|
||||
validateRequestUnescapedTemplateTransform' engine template
|
||||
|
||||
-- | The defunctionalized transformation function on 'Url'
|
||||
newtype UrlTransformAction
|
||||
= ModifyUrl UnescapedTemplate
|
||||
|
112
server/src-lib/Hasura/RQL/DDL/Webhook/Transform/Validation.hs
Normal file
112
server/src-lib/Hasura/RQL/DDL/Webhook/Transform/Validation.hs
Normal file
@ -0,0 +1,112 @@
|
||||
{-# LANGUAGE PolyKinds #-}
|
||||
|
||||
-- | We validate 'TransformFn' terms inside 'RequestTransform' before
|
||||
-- dispatching Metadata actions in 'runMetadataQueryV1M'. Validation
|
||||
-- follows the same HKD pattern from 'applyRequestTransform' but using
|
||||
-- 'btraverseC' to call 'validate' from the 'Transform' class on all
|
||||
-- the HKD fields.
|
||||
module Hasura.RQL.DDL.Webhook.Transform.Validation
|
||||
( Unvalidated (..),
|
||||
Unvalidated1 (..),
|
||||
unUnvalidate,
|
||||
unUnvalidate1,
|
||||
validateRequestTransform,
|
||||
validateTransforms,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Lens (Lens', LensLike, lens, traverseOf)
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Functor.Barbie (FunctorB (bmap), btraverseC)
|
||||
import Data.Functor.Compose (Compose (..))
|
||||
import Data.Kind
|
||||
import Data.Validation (Validation, toEither)
|
||||
import Hasura.Base.Error (QErr)
|
||||
import Hasura.EncJSON (EncJSON, encJFromJValue)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.Webhook.Transform
|
||||
import Hasura.RQL.DDL.Webhook.Transform.Class
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
type Tuple1 a b = Compose ((,) a) b
|
||||
|
||||
type OptionalTuple1 a b = WithOptional (Tuple1 a b)
|
||||
|
||||
-- | A variation on 'RequestTransformFn' where 'TransformFn' is tupled
|
||||
-- with 'TemplatingEngine'. This is necessary to validate the 'TransformFn'.
|
||||
--
|
||||
-- TODO: In the future we most likely want to embed the
|
||||
-- 'TemplatingEngine' in the 'TransformFn' or the
|
||||
-- 'Template'/'UnwrappedTemplate', in which case we would not need
|
||||
-- this alias for validation.
|
||||
type ValidationFields = RequestFields (OptionalTuple1 TemplatingEngine TransformFn)
|
||||
|
||||
-- TODO(SOLOMON): Add lens law unit tests
|
||||
|
||||
-- | A lens for zipping our defunctionalized transform with the
|
||||
-- 'TemplatingEngine' for validation.
|
||||
transformFns :: Lens' RequestTransform ValidationFields
|
||||
transformFns = lens getter setter
|
||||
where
|
||||
getter :: RequestTransform -> ValidationFields
|
||||
getter RequestTransform {..} =
|
||||
bmap (WithOptional . fmap (Compose . (templateEngine,)) . getOptional) requestFields
|
||||
|
||||
setter :: RequestTransform -> ValidationFields -> RequestTransform
|
||||
setter rt requestFields' =
|
||||
rt {requestFields = bmap (WithOptional . fmap (snd . getCompose) . getOptional) requestFields'}
|
||||
|
||||
-- | Validate all 'TransformFn a' fields in the 'RequestTransform'.
|
||||
validateRequestTransform ::
|
||||
MonadError TransformErrorBundle m =>
|
||||
RequestTransform ->
|
||||
m RequestTransform
|
||||
validateRequestTransform reqTransform =
|
||||
liftEither $ toEither $ transformFns (btraverseC @Transform validate') reqTransform
|
||||
where
|
||||
validate' ::
|
||||
(Transform a) =>
|
||||
OptionalTuple1 TemplatingEngine TransformFn a ->
|
||||
Validation TransformErrorBundle (OptionalTuple1 TemplatingEngine TransformFn a)
|
||||
validate' = \case
|
||||
fn@(WithOptional (Just (Compose (engine, transformFn)))) ->
|
||||
fn <$ validate engine transformFn
|
||||
fn -> pure fn
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-- | Used to annotate that a 'RequestTransform', or some record
|
||||
-- containing a 'RequestTransform' has not yet been validated.
|
||||
newtype Unvalidated a = Unvalidated {_unUnvalidate :: a}
|
||||
deriving newtype (FromJSON, ToJSON)
|
||||
|
||||
-- | A lens for focusing through 'Unvalidated' in 'validateTransforms'.
|
||||
unUnvalidate :: Lens' (Unvalidated a) a
|
||||
unUnvalidate = lens _unUnvalidate (\_ a -> Unvalidated a)
|
||||
|
||||
-- | Used to annotate that a higher kinded type containing a
|
||||
-- 'RequestTransform' has not yet been validated.
|
||||
--
|
||||
-- This is needed specifically for 'CreateEventTriggerQuery' and any
|
||||
-- other type that is paramterized by a 'BackendType'.
|
||||
newtype Unvalidated1 (f :: k -> Type) (a :: k) = Unvalidated1 {_unUnvalidate1 :: f a}
|
||||
deriving newtype (FromJSON, ToJSON)
|
||||
|
||||
-- | A lens for focusing through 'Unvalidated1' in 'validateTransforms'.
|
||||
unUnvalidate1 :: Lens' (Unvalidated1 f a) (f a)
|
||||
unUnvalidate1 = lens _unUnvalidate1 (\_ a -> Unvalidated1 a)
|
||||
|
||||
-- | Used to focus into a records in 'RQLMetadataV1' and validate any
|
||||
-- 'RequestTransform' terms present.
|
||||
validateTransforms ::
|
||||
(MonadError QErr m) =>
|
||||
LensLike (Either TransformErrorBundle) api api RequestTransform RequestTransform ->
|
||||
(api -> m EncJSON) ->
|
||||
api ->
|
||||
m EncJSON
|
||||
validateTransforms focus f q =
|
||||
case traverseOf focus validateRequestTransform q of
|
||||
Left error' -> pure $ encJFromJValue $ J.toJSON error'
|
||||
Right q' -> f q'
|
@ -27,9 +27,21 @@ module Hasura.RQL.Types.ScheduledTrigger
|
||||
GetInvocationsBy (..),
|
||||
GetEventInvocations (..),
|
||||
ClearCronEvents (..),
|
||||
cctName,
|
||||
cctWebhook,
|
||||
cctCronSchedule,
|
||||
cctPayload,
|
||||
cctRetryConf,
|
||||
cctHeaders,
|
||||
cctIncludeInMetadata,
|
||||
cctComment,
|
||||
cctReplace,
|
||||
cctRequestTransform,
|
||||
cctResponseTransform,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Lens (makeLenses)
|
||||
import Data.Aeson
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Aeson.Casing
|
||||
@ -135,20 +147,22 @@ instance FromJSON CronTriggerMetadata where
|
||||
$(deriveToJSON hasuraJSON {omitNothingFields = True} ''CronTriggerMetadata)
|
||||
|
||||
data CreateCronTrigger = CreateCronTrigger
|
||||
{ cctName :: !TriggerName,
|
||||
cctWebhook :: !InputWebhook,
|
||||
cctCronSchedule :: !CronSchedule,
|
||||
cctPayload :: !(Maybe J.Value),
|
||||
cctRetryConf :: !STRetryConf,
|
||||
cctHeaders :: ![HeaderConf],
|
||||
cctIncludeInMetadata :: !Bool,
|
||||
cctComment :: !(Maybe Text),
|
||||
cctReplace :: !Bool,
|
||||
cctRequestTransform :: !(Maybe RequestTransform),
|
||||
cctResponseTransform :: !(Maybe MetadataResponseTransform)
|
||||
{ _cctName :: !TriggerName,
|
||||
_cctWebhook :: !InputWebhook,
|
||||
_cctCronSchedule :: !CronSchedule,
|
||||
_cctPayload :: !(Maybe J.Value),
|
||||
_cctRetryConf :: !STRetryConf,
|
||||
_cctHeaders :: ![HeaderConf],
|
||||
_cctIncludeInMetadata :: !Bool,
|
||||
_cctComment :: !(Maybe Text),
|
||||
_cctReplace :: !Bool,
|
||||
_cctRequestTransform :: !(Maybe RequestTransform),
|
||||
_cctResponseTransform :: !(Maybe MetadataResponseTransform)
|
||||
}
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
$(makeLenses ''CreateCronTrigger)
|
||||
|
||||
instance NFData CreateCronTrigger
|
||||
|
||||
instance Cacheable CreateCronTrigger
|
||||
@ -156,17 +170,17 @@ instance Cacheable CreateCronTrigger
|
||||
instance FromJSON CreateCronTrigger where
|
||||
parseJSON =
|
||||
withObject "CreateCronTrigger" $ \o -> do
|
||||
cctName <- o .: "name"
|
||||
cctWebhook <- o .: "webhook"
|
||||
cctPayload <- o .:? "payload"
|
||||
cctCronSchedule <- o .: "schedule"
|
||||
cctRetryConf <- o .:? "retry_conf" .!= defaultSTRetryConf
|
||||
cctHeaders <- o .:? "headers" .!= []
|
||||
cctIncludeInMetadata <- o .: "include_in_metadata"
|
||||
cctComment <- o .:? "comment"
|
||||
cctReplace <- o .:? "replace" .!= False
|
||||
cctRequestTransform <- o .:? "request_transform"
|
||||
cctResponseTransform <- o .:? "response_transform"
|
||||
_cctName <- o .: "name"
|
||||
_cctWebhook <- o .: "webhook"
|
||||
_cctPayload <- o .:? "payload"
|
||||
_cctCronSchedule <- o .: "schedule"
|
||||
_cctRetryConf <- o .:? "retry_conf" .!= defaultSTRetryConf
|
||||
_cctHeaders <- o .:? "headers" .!= []
|
||||
_cctIncludeInMetadata <- o .: "include_in_metadata"
|
||||
_cctComment <- o .:? "comment"
|
||||
_cctReplace <- o .:? "replace" .!= False
|
||||
_cctRequestTransform <- o .:? "request_transform"
|
||||
_cctResponseTransform <- o .:? "response_transform"
|
||||
pure CreateCronTrigger {..}
|
||||
|
||||
$(deriveToJSON hasuraJSON {omitNothingFields = True} ''CreateCronTrigger)
|
||||
|
@ -45,9 +45,12 @@ import Language.Haskell.TH hiding (Type)
|
||||
-- backend. This declaration generates the following type:
|
||||
--
|
||||
-- data AnyBackend (i :: BackendType -> Type)
|
||||
-- = PostgresValue (i 'Postgres)
|
||||
-- | MSSQLValue (i 'MSSQL)
|
||||
-- | ...
|
||||
-- = PostgresVanillaValue (i '(Postgres Vanilla))
|
||||
-- | PostgresCitusValue (i '(Postgres Citus))
|
||||
-- | BigQueryValue (i 'BigQuery)
|
||||
-- | MySQLValue (i 'MySQL)
|
||||
-- | MSSQLValue (i 'MSSQL)
|
||||
-- | ExperimentalValue (i 'Experimental)
|
||||
$( do
|
||||
-- the kind of the type variable, expressed with a quote
|
||||
varKind <- [t|BackendType -> Type|]
|
||||
|
@ -8,6 +8,7 @@ module Hasura.Server.API.Metadata
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Lens (_Just)
|
||||
import Control.Monad.Trans.Control (MonadBaseControl)
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Casing
|
||||
@ -20,7 +21,7 @@ import Hasura.Base.Error
|
||||
import Hasura.EncJSON
|
||||
import Hasura.Logging qualified as L
|
||||
import Hasura.Metadata.Class
|
||||
import Hasura.Prelude
|
||||
import Hasura.Prelude hiding (first)
|
||||
import Hasura.RQL.DDL.Action
|
||||
import Hasura.RQL.DDL.ApiLimit
|
||||
import Hasura.RQL.DDL.ComputedField
|
||||
@ -41,6 +42,7 @@ import Hasura.RQL.DDL.RemoteSchema
|
||||
import Hasura.RQL.DDL.ScheduledTrigger
|
||||
import Hasura.RQL.DDL.Schema
|
||||
import Hasura.RQL.DDL.Schema.Source
|
||||
import Hasura.RQL.DDL.Webhook.Transform.Validation
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.RQL.Types.Eventing.Backend
|
||||
import Hasura.RQL.Types.Run
|
||||
@ -96,7 +98,7 @@ data RQLMetadataV1
|
||||
RMAddComputedField !(AddComputedField ('Postgres 'Vanilla))
|
||||
| RMDropComputedField !(DropComputedField ('Postgres 'Vanilla))
|
||||
| -- Tables event triggers
|
||||
RMCreateEventTrigger !(AnyBackend CreateEventTriggerQuery)
|
||||
RMCreateEventTrigger !(AnyBackend (Unvalidated1 CreateEventTriggerQuery))
|
||||
| RMDeleteEventTrigger !(AnyBackend DeleteEventTriggerQuery)
|
||||
| RMRedeliverEvent !(AnyBackend RedeliverEventQuery)
|
||||
| RMInvokeEventTrigger !(AnyBackend InvokeEventTriggerQuery)
|
||||
@ -110,7 +112,7 @@ data RQLMetadataV1
|
||||
RMAddRemoteSchemaPermissions !AddRemoteSchemaPermission
|
||||
| RMDropRemoteSchemaPermissions !DropRemoteSchemaPermissions
|
||||
| -- Scheduled triggers
|
||||
RMCreateCronTrigger !CreateCronTrigger
|
||||
RMCreateCronTrigger !(Unvalidated CreateCronTrigger)
|
||||
| RMDeleteCronTrigger !ScheduledTriggerName
|
||||
| RMCreateScheduledEvent !CreateScheduledEvent
|
||||
| RMDeleteScheduledEvent !DeleteScheduledEvent
|
||||
@ -118,9 +120,9 @@ data RQLMetadataV1
|
||||
| RMGetEventInvocations !GetEventInvocations
|
||||
| RMGetCronTriggers
|
||||
| -- Actions
|
||||
RMCreateAction !CreateAction
|
||||
RMCreateAction !(Unvalidated CreateAction)
|
||||
| RMDropAction !DropAction
|
||||
| RMUpdateAction !UpdateAction
|
||||
| RMUpdateAction !(Unvalidated UpdateAction)
|
||||
| RMCreateActionPermission !CreateActionPermission
|
||||
| RMDropActionPermission !DropActionPermission
|
||||
| -- Query collections, allow list related
|
||||
@ -163,7 +165,7 @@ data RQLMetadataV1
|
||||
RMDumpInternalState !DumpInternalState
|
||||
| RMGetCatalogState !GetCatalogState
|
||||
| RMSetCatalogState !SetCatalogState
|
||||
| RMTestWebhookTransform !TestWebhookTransform
|
||||
| RMTestWebhookTransform !(Unvalidated TestWebhookTransform)
|
||||
| -- Bulk metadata queries
|
||||
RMBulk [RQLMetadataRequest]
|
||||
|
||||
@ -429,7 +431,13 @@ runMetadataQueryV1M env currentResourceVersion = \case
|
||||
RMDropFunctionPermission q -> dispatchMetadata runDropFunctionPermission q
|
||||
RMAddComputedField q -> runAddComputedField q
|
||||
RMDropComputedField q -> runDropComputedField q
|
||||
RMCreateEventTrigger q -> dispatchMetadata runCreateEventTriggerQuery q
|
||||
RMCreateEventTrigger q ->
|
||||
dispatchMetadata
|
||||
( validateTransforms
|
||||
(unUnvalidate1 . cetqRequestTransform . _Just)
|
||||
(runCreateEventTriggerQuery . _unUnvalidate1)
|
||||
)
|
||||
q
|
||||
RMDeleteEventTrigger q -> dispatchMetadataAndEventTrigger runDeleteEventTriggerQuery q
|
||||
RMRedeliverEvent q -> dispatchEventTrigger runRedeliverEvent q
|
||||
RMInvokeEventTrigger q -> dispatchEventTrigger runInvokeEventTrigger q
|
||||
@ -440,16 +448,28 @@ runMetadataQueryV1M env currentResourceVersion = \case
|
||||
RMIntrospectRemoteSchema q -> runIntrospectRemoteSchema q
|
||||
RMAddRemoteSchemaPermissions q -> runAddRemoteSchemaPermissions q
|
||||
RMDropRemoteSchemaPermissions q -> runDropRemoteSchemaPermissions q
|
||||
RMCreateCronTrigger q -> runCreateCronTrigger q
|
||||
RMCreateCronTrigger q ->
|
||||
validateTransforms
|
||||
(unUnvalidate . cctRequestTransform . _Just)
|
||||
(runCreateCronTrigger . _unUnvalidate)
|
||||
q
|
||||
RMDeleteCronTrigger q -> runDeleteCronTrigger q
|
||||
RMCreateScheduledEvent q -> runCreateScheduledEvent q
|
||||
RMDeleteScheduledEvent q -> runDeleteScheduledEvent q
|
||||
RMGetScheduledEvents q -> runGetScheduledEvents q
|
||||
RMGetEventInvocations q -> runGetEventInvocations q
|
||||
RMGetCronTriggers -> runGetCronTriggers
|
||||
RMCreateAction q -> runCreateAction q
|
||||
RMCreateAction q ->
|
||||
validateTransforms
|
||||
(unUnvalidate . caDefinition . adRequestTransform . _Just)
|
||||
(runCreateAction . _unUnvalidate)
|
||||
q
|
||||
RMDropAction q -> runDropAction q
|
||||
RMUpdateAction q -> runUpdateAction q
|
||||
RMUpdateAction q ->
|
||||
validateTransforms
|
||||
(unUnvalidate . uaDefinition . adRequestTransform . _Just)
|
||||
(runUpdateAction . _unUnvalidate)
|
||||
q
|
||||
RMCreateActionPermission q -> runCreateActionPermission q
|
||||
RMDropActionPermission q -> runDropActionPermission q
|
||||
RMCreateQueryCollection q -> runCreateCollection q
|
||||
@ -480,7 +500,11 @@ runMetadataQueryV1M env currentResourceVersion = \case
|
||||
RMDumpInternalState q -> runDumpInternalState q
|
||||
RMGetCatalogState q -> runGetCatalogState q
|
||||
RMSetCatalogState q -> runSetCatalogState q
|
||||
RMTestWebhookTransform q -> runTestWebhookTransform q
|
||||
RMTestWebhookTransform q ->
|
||||
validateTransforms
|
||||
(unUnvalidate . twtTransformer)
|
||||
(runTestWebhookTransform . _unUnvalidate)
|
||||
q
|
||||
RMSetQueryTagsConfig q -> runSetQueryTagsConfig q
|
||||
RMBulk q -> encJFromList <$> indexedMapM (runMetadataQueryM env currentResourceVersion) q
|
||||
where
|
||||
|
@ -14,6 +14,7 @@ import Hasura.RQL.DDL.Relationship.Rename
|
||||
import Hasura.RQL.DDL.RemoteRelationship
|
||||
import Hasura.RQL.DDL.Schema
|
||||
import Hasura.RQL.DDL.Schema.Source
|
||||
import Hasura.RQL.DDL.Webhook.Transform.Validation
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.SQL.AnyBackend
|
||||
|
||||
@ -59,7 +60,7 @@ data RQLMetadataV1
|
||||
RMAddComputedField !(AddComputedField ('Postgres 'Vanilla))
|
||||
| RMDropComputedField !(DropComputedField ('Postgres 'Vanilla))
|
||||
| -- Tables event triggers
|
||||
RMCreateEventTrigger !(AnyBackend CreateEventTriggerQuery)
|
||||
RMCreateEventTrigger !(AnyBackend (Unvalidated1 CreateEventTriggerQuery))
|
||||
| RMDeleteEventTrigger !(AnyBackend DeleteEventTriggerQuery)
|
||||
| RMRedeliverEvent !(AnyBackend RedeliverEventQuery)
|
||||
| RMInvokeEventTrigger !(AnyBackend InvokeEventTriggerQuery)
|
||||
@ -73,7 +74,7 @@ data RQLMetadataV1
|
||||
RMAddRemoteSchemaPermissions !AddRemoteSchemaPermission
|
||||
| RMDropRemoteSchemaPermissions !DropRemoteSchemaPermissions
|
||||
| -- Scheduled triggers
|
||||
RMCreateCronTrigger !CreateCronTrigger
|
||||
RMCreateCronTrigger !(Unvalidated CreateCronTrigger)
|
||||
| RMDeleteCronTrigger !ScheduledTriggerName
|
||||
| RMCreateScheduledEvent !CreateScheduledEvent
|
||||
| RMDeleteScheduledEvent !DeleteScheduledEvent
|
||||
@ -81,9 +82,9 @@ data RQLMetadataV1
|
||||
| RMGetEventInvocations !GetEventInvocations
|
||||
| RMGetCronTriggers
|
||||
| -- Actions
|
||||
RMCreateAction !CreateAction
|
||||
RMCreateAction !(Unvalidated CreateAction)
|
||||
| RMDropAction !DropAction
|
||||
| RMUpdateAction !UpdateAction
|
||||
| RMUpdateAction !(Unvalidated UpdateAction)
|
||||
| RMCreateActionPermission !CreateActionPermission
|
||||
| RMDropActionPermission !DropActionPermission
|
||||
| -- Query collections, allow list related
|
||||
@ -126,7 +127,7 @@ data RQLMetadataV1
|
||||
RMDumpInternalState !DumpInternalState
|
||||
| RMGetCatalogState !GetCatalogState
|
||||
| RMSetCatalogState !SetCatalogState
|
||||
| RMTestWebhookTransform !TestWebhookTransform
|
||||
| RMTestWebhookTransform !(Unvalidated TestWebhookTransform)
|
||||
| -- Bulk metadata queries
|
||||
RMBulk [RQLMetadataRequest]
|
||||
|
||||
|
@ -4,17 +4,13 @@
|
||||
X-Hasura-Role: admin
|
||||
status: 200
|
||||
response:
|
||||
body:
|
||||
- error_code: Invalid Path
|
||||
source_position:
|
||||
end_column: 15
|
||||
start_line: 0
|
||||
end_line: 0
|
||||
start_column: 4
|
||||
message: '"$body.world"'
|
||||
headers: []
|
||||
method: GET
|
||||
webhook_url: http://localhost:1234/
|
||||
- error_code: Invalid Path
|
||||
source_position:
|
||||
end_column: 15
|
||||
start_line: 0
|
||||
end_line: 0
|
||||
start_column: 4
|
||||
message: '"$body.world"'
|
||||
query:
|
||||
type: test_webhook_transform
|
||||
args:
|
||||
|
@ -2,11 +2,15 @@
|
||||
url: /v1/metadata
|
||||
headers:
|
||||
X-Hasura-Role: admin
|
||||
status: 400
|
||||
status: 200
|
||||
response:
|
||||
path: "$.args.request_transform.body.template"
|
||||
error: "Unexpected token '$body'."
|
||||
code: "parse-failed"
|
||||
- error_code: Parse Error
|
||||
source_position:
|
||||
end_column: 15
|
||||
start_line: 0
|
||||
end_line: 0
|
||||
start_column: 14
|
||||
message: Unexpected token '$body'.
|
||||
query:
|
||||
type: test_webhook_transform
|
||||
args:
|
||||
|
@ -26,7 +26,6 @@
|
||||
X-Hasura-Role: admin
|
||||
status: 200
|
||||
response:
|
||||
body:
|
||||
- error_code: Function Error
|
||||
source_position:
|
||||
end_column: 51
|
||||
@ -34,9 +33,6 @@
|
||||
end_line: 0
|
||||
start_column: 23
|
||||
message: Session variable "this_variable_doesnt_exist" not found
|
||||
headers: []
|
||||
method: GET
|
||||
webhook_url: http://localhost:1234/
|
||||
query:
|
||||
type: test_webhook_transform
|
||||
args:
|
||||
@ -55,7 +51,6 @@
|
||||
X-Hasura-Role: admin
|
||||
status: 200
|
||||
response:
|
||||
body:
|
||||
- error_code: Function Error
|
||||
source_position:
|
||||
end_column: 15
|
||||
@ -63,9 +58,6 @@
|
||||
end_line: 0
|
||||
start_column: 4
|
||||
message: Function fooFunction is not defined.
|
||||
headers: []
|
||||
method: GET
|
||||
webhook_url: http://localhost:1234/
|
||||
query:
|
||||
type: test_webhook_transform
|
||||
args:
|
||||
|
@ -7,7 +7,7 @@
|
||||
body: "foo=bar&baz=world"
|
||||
headers:
|
||||
- - content-type
|
||||
- application/json
|
||||
- application/x-www-form-urlencoded
|
||||
- - foo
|
||||
- bar
|
||||
method: POST
|
||||
@ -33,4 +33,4 @@
|
||||
request_headers:
|
||||
add_headers:
|
||||
foo: "bar"
|
||||
content-type: "application/json"
|
||||
content-type: "application/x-www-form-urlencoded"
|
||||
|
Loading…
Reference in New Issue
Block a user