mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
a367525e68
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9666 Co-authored-by: Rakesh Emmadi <12475069+rakeshkky@users.noreply.github.com> GitOrigin-RevId: 5fa7702401065869c953b23c6734b9b367247634
542 lines
19 KiB
Haskell
542 lines
19 KiB
Haskell
{-# LANGUAGE TemplateHaskell #-}
|
|
|
|
-- | Postgres Execute Mutation
|
|
--
|
|
-- Generic combinators for translating and excecuting IR mutation statements.
|
|
-- Used by the specific mutation modules, e.g. 'Hasura.Backends.Postgres.Execute.Insert'.
|
|
--
|
|
-- See 'Hasura.Backends.Postgres.Instances.Execute'.
|
|
module Hasura.Backends.Postgres.Execute.Mutation
|
|
( MutateResp (..),
|
|
--
|
|
execDeleteQuery,
|
|
execInsertQuery,
|
|
execUpdateQuery,
|
|
--
|
|
executeMutationOutputQuery,
|
|
mutateAndFetchCols,
|
|
--
|
|
ValidateInputPayloadVersion,
|
|
validateInputPayloadVersion,
|
|
ValidateInputErrorResponse (..),
|
|
HttpHandlerLog (..),
|
|
ValidateInsertInputLog (..),
|
|
InsertValidationPayloadMap,
|
|
validateUpdateMutation,
|
|
validateDeleteMutation,
|
|
validateMutation,
|
|
)
|
|
where
|
|
|
|
import Control.Exception (try)
|
|
import Control.Lens qualified as Lens
|
|
import Control.Monad.Writer (runWriter)
|
|
import Data.Aeson
|
|
import Data.Aeson qualified as J
|
|
import Data.Aeson.Key qualified as J
|
|
import Data.Aeson.TH qualified as J
|
|
import Data.Environment qualified as Env
|
|
import Data.HashMap.Strict qualified as HashMap
|
|
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
|
|
import Data.Sequence qualified as DS
|
|
import Database.PG.Query qualified as PG
|
|
import Hasura.Backends.Postgres.Connection
|
|
import Hasura.Backends.Postgres.SQL.DML qualified as S
|
|
import Hasura.Backends.Postgres.SQL.Types hiding (TableName)
|
|
import Hasura.Backends.Postgres.SQL.Value
|
|
import Hasura.Backends.Postgres.Translate.Delete
|
|
import Hasura.Backends.Postgres.Translate.Insert
|
|
import Hasura.Backends.Postgres.Translate.Mutation
|
|
import Hasura.Backends.Postgres.Translate.Returning
|
|
import Hasura.Backends.Postgres.Translate.Select
|
|
import Hasura.Backends.Postgres.Translate.Select.Internal.Helpers (customSQLToTopLevelCTEs, toQuery)
|
|
import Hasura.Backends.Postgres.Translate.Update
|
|
import Hasura.Backends.Postgres.Types.Update qualified as Postgres
|
|
import Hasura.Base.Error
|
|
import Hasura.EncJSON
|
|
import Hasura.GraphQL.Parser.Internal.Convert
|
|
import Hasura.GraphQL.Parser.Variable qualified as G
|
|
import Hasura.HTTP
|
|
import Hasura.Logging qualified as L
|
|
import Hasura.Prelude
|
|
import Hasura.QueryTags
|
|
import Hasura.RQL.DDL.Headers
|
|
import Hasura.RQL.IR qualified as IR
|
|
import Hasura.RQL.IR.BoolExp
|
|
import Hasura.RQL.IR.Delete
|
|
import Hasura.RQL.IR.Insert
|
|
import Hasura.RQL.IR.Returning
|
|
import Hasura.RQL.IR.Select
|
|
import Hasura.RQL.IR.Update
|
|
import Hasura.RQL.Types.Backend
|
|
import Hasura.RQL.Types.BackendType
|
|
import Hasura.RQL.Types.Column
|
|
import Hasura.RQL.Types.Common
|
|
import Hasura.RQL.Types.Headers (HeaderConf)
|
|
import Hasura.RQL.Types.NamingCase (NamingCase)
|
|
import Hasura.RQL.Types.Permission
|
|
import Hasura.RQL.Types.Schema.Options qualified as Options
|
|
import Hasura.Server.Utils
|
|
import Hasura.Session
|
|
import Hasura.Tracing qualified as Tracing
|
|
import Language.GraphQL.Draft.Syntax qualified as G
|
|
import Network.HTTP.Client.Transformable qualified as HTTP
|
|
import Network.Wreq qualified as Wreq
|
|
|
|
data MutateResp (b :: BackendType) a = MutateResp
|
|
{ _mrAffectedRows :: Int,
|
|
_mrReturningColumns :: [ColumnValues b a]
|
|
}
|
|
deriving (Generic)
|
|
|
|
deriving instance (Backend b, Show a) => Show (MutateResp b a)
|
|
|
|
deriving instance (Backend b, Eq a) => Eq (MutateResp b a)
|
|
|
|
instance (Backend b, ToJSON a) => ToJSON (MutateResp b a) where
|
|
toJSON = genericToJSON hasuraJSON
|
|
|
|
instance (Backend b, FromJSON a) => FromJSON (MutateResp b a) where
|
|
parseJSON = genericParseJSON hasuraJSON
|
|
|
|
data Mutation (b :: BackendType) = Mutation
|
|
{ _mTable :: QualifiedTable,
|
|
_mQuery :: (MutationCTE, DS.Seq PG.PrepArg),
|
|
_mOutput :: MutationOutput b,
|
|
_mCols :: [ColumnInfo b],
|
|
_mStrfyNum :: Options.StringifyNumbers,
|
|
_mNamingConvention :: Maybe NamingCase
|
|
}
|
|
|
|
mkMutation ::
|
|
UserInfo ->
|
|
QualifiedTable ->
|
|
(MutationCTE, DS.Seq PG.PrepArg) ->
|
|
MutationOutput ('Postgres pgKind) ->
|
|
[ColumnInfo ('Postgres pgKind)] ->
|
|
Options.StringifyNumbers ->
|
|
Maybe NamingCase ->
|
|
Mutation ('Postgres pgKind)
|
|
mkMutation _userInfo table query output allCols strfyNum tCase =
|
|
Mutation table query output allCols strfyNum tCase
|
|
|
|
runMutation ::
|
|
( MonadTx m,
|
|
Backend ('Postgres pgKind),
|
|
PostgresAnnotatedFieldJSON pgKind,
|
|
MonadReader QueryTagsComment m
|
|
) =>
|
|
Mutation ('Postgres pgKind) ->
|
|
m EncJSON
|
|
runMutation mut =
|
|
bool (mutateAndReturn mut) (mutateAndSel mut)
|
|
$ hasNestedFld
|
|
$ _mOutput mut
|
|
|
|
mutateAndReturn ::
|
|
( MonadTx m,
|
|
Backend ('Postgres pgKind),
|
|
PostgresAnnotatedFieldJSON pgKind,
|
|
MonadReader QueryTagsComment m
|
|
) =>
|
|
Mutation ('Postgres pgKind) ->
|
|
m EncJSON
|
|
mutateAndReturn (Mutation qt (cte, p) mutationOutput allCols strfyNum tCase) =
|
|
executeMutationOutputQuery qt allCols Nothing cte mutationOutput strfyNum tCase (toList p)
|
|
|
|
execUpdateQuery ::
|
|
forall pgKind m.
|
|
( MonadTx m,
|
|
Backend ('Postgres pgKind),
|
|
PostgresAnnotatedFieldJSON pgKind,
|
|
MonadReader QueryTagsComment m
|
|
) =>
|
|
Options.StringifyNumbers ->
|
|
Maybe NamingCase ->
|
|
UserInfo ->
|
|
(AnnotatedUpdate ('Postgres pgKind), DS.Seq PG.PrepArg) ->
|
|
m EncJSON
|
|
execUpdateQuery strfyNum tCase userInfo (u, p) =
|
|
case updateCTE of
|
|
Update singleUpdate -> runCTE singleUpdate
|
|
MultiUpdate ctes -> encJFromList <$> traverse runCTE ctes
|
|
where
|
|
updateCTE :: UpdateCTE
|
|
updateCTE = mkUpdateCTE u
|
|
|
|
runCTE :: S.TopLevelCTE -> m EncJSON
|
|
runCTE cte =
|
|
runMutation
|
|
(mkMutation userInfo (_auTable u) (MCCheckConstraint cte, p) (_auOutput u) (_auAllCols u) strfyNum tCase)
|
|
|
|
execDeleteQuery ::
|
|
forall pgKind m.
|
|
( MonadTx m,
|
|
Backend ('Postgres pgKind),
|
|
PostgresAnnotatedFieldJSON pgKind,
|
|
MonadReader QueryTagsComment m
|
|
) =>
|
|
Options.StringifyNumbers ->
|
|
Maybe NamingCase ->
|
|
UserInfo ->
|
|
(AnnDel ('Postgres pgKind), DS.Seq PG.PrepArg) ->
|
|
m EncJSON
|
|
execDeleteQuery strfyNum tCase userInfo (u, p) =
|
|
runMutation
|
|
(mkMutation userInfo (_adTable u) (MCDelete delete, p) (_adOutput u) (_adAllCols u) strfyNum tCase)
|
|
where
|
|
delete = mkDelete u
|
|
|
|
execInsertQuery ::
|
|
( MonadTx m,
|
|
Backend ('Postgres pgKind),
|
|
PostgresAnnotatedFieldJSON pgKind,
|
|
MonadReader QueryTagsComment m
|
|
) =>
|
|
Options.StringifyNumbers ->
|
|
Maybe NamingCase ->
|
|
UserInfo ->
|
|
(InsertQueryP1 ('Postgres pgKind), DS.Seq PG.PrepArg) ->
|
|
m EncJSON
|
|
execInsertQuery strfyNum tCase userInfo (u, p) =
|
|
runMutation
|
|
(mkMutation userInfo (iqp1Table u) (MCCheckConstraint insertCTE, p) (iqp1Output u) (iqp1AllCols u) strfyNum tCase)
|
|
where
|
|
insertCTE = mkInsertCTE u
|
|
|
|
{- Note: [Prepared statements in Mutations]
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
The SQL statements we generate for mutations seem to include the actual values
|
|
in the statements in some cases which pretty much makes them unfit for reuse
|
|
(Handling relationships in the returning clause is the source of this
|
|
complexity). Further, `PGConn` has an internal cache which maps a statement to
|
|
a 'prepared statement id' on Postgres. As we prepare more and more single-use
|
|
SQL statements we end up leaking memory both on graphql-engine and Postgres
|
|
till the connection is closed. So a simpler but very crude fix is to not use
|
|
prepared statements for mutations. The performance of insert mutations
|
|
shouldn't be affected but updates and delete mutations with complex boolean
|
|
conditions **might** see some degradation.
|
|
-}
|
|
|
|
mutateAndSel ::
|
|
forall pgKind m.
|
|
( MonadTx m,
|
|
Backend ('Postgres pgKind),
|
|
PostgresAnnotatedFieldJSON pgKind,
|
|
MonadReader QueryTagsComment m
|
|
) =>
|
|
Mutation ('Postgres pgKind) ->
|
|
m EncJSON
|
|
mutateAndSel (Mutation qt q mutationOutput allCols strfyNum tCase) = do
|
|
-- Perform mutation and fetch unique columns
|
|
MutateResp _ columnVals <- liftTx $ mutateAndFetchCols qt allCols q strfyNum tCase
|
|
select <- mkSelectExpFromColumnValues qt allCols columnVals
|
|
-- Perform select query and fetch returning fields
|
|
executeMutationOutputQuery
|
|
qt
|
|
allCols
|
|
Nothing
|
|
(MCSelectValues select)
|
|
mutationOutput
|
|
strfyNum
|
|
tCase
|
|
[]
|
|
|
|
withCheckPermission :: (MonadError QErr m) => m (a, Bool) -> m a
|
|
withCheckPermission sqlTx = do
|
|
(rawResponse, checkConstraint) <- sqlTx
|
|
unless checkConstraint
|
|
$ throw400 PermissionError
|
|
$ "check constraint of an insert/update permission has failed"
|
|
pure rawResponse
|
|
|
|
executeMutationOutputQuery ::
|
|
forall pgKind m.
|
|
( MonadTx m,
|
|
Backend ('Postgres pgKind),
|
|
PostgresAnnotatedFieldJSON pgKind,
|
|
MonadReader QueryTagsComment m
|
|
) =>
|
|
QualifiedTable ->
|
|
[ColumnInfo ('Postgres pgKind)] ->
|
|
Maybe Int ->
|
|
MutationCTE ->
|
|
MutationOutput ('Postgres pgKind) ->
|
|
Options.StringifyNumbers ->
|
|
Maybe NamingCase ->
|
|
-- | Prepared params
|
|
[PG.PrepArg] ->
|
|
m EncJSON
|
|
executeMutationOutputQuery qt allCols preCalAffRows cte mutOutput strfyNum tCase prepArgs = do
|
|
queryTags <- ask
|
|
let queryTx :: (PG.FromRes a) => m a
|
|
queryTx = do
|
|
let selectWith = mkMutationOutputExp qt allCols preCalAffRows cte mutOutput strfyNum tCase
|
|
query = toQuery selectWith
|
|
queryWithQueryTags = query {PG.getQueryText = (PG.getQueryText query) <> (_unQueryTagsComment queryTags)}
|
|
-- See Note [Prepared statements in Mutations]
|
|
liftTx (PG.rawQE dmlTxErrorHandler queryWithQueryTags prepArgs False)
|
|
|
|
if checkPermissionRequired cte
|
|
then withCheckPermission $ PG.getRow <$> queryTx
|
|
else runIdentity . PG.getRow <$> queryTx
|
|
|
|
mutateAndFetchCols ::
|
|
forall pgKind.
|
|
(Backend ('Postgres pgKind), PostgresAnnotatedFieldJSON pgKind) =>
|
|
QualifiedTable ->
|
|
[ColumnInfo ('Postgres pgKind)] ->
|
|
(MutationCTE, DS.Seq PG.PrepArg) ->
|
|
Options.StringifyNumbers ->
|
|
Maybe NamingCase ->
|
|
PG.TxE QErr (MutateResp ('Postgres pgKind) TxtEncodedVal)
|
|
mutateAndFetchCols qt cols (cte, p) strfyNum tCase = do
|
|
let mutationTx :: (PG.FromRes a) => PG.TxE QErr a
|
|
mutationTx =
|
|
-- See Note [Prepared statements in Mutations]
|
|
PG.rawQE dmlTxErrorHandler sqlText (toList p) False
|
|
|
|
if checkPermissionRequired cte
|
|
then withCheckPermission $ (first PG.getViaJSON . PG.getRow) <$> mutationTx
|
|
else (PG.getViaJSON . runIdentity . PG.getRow) <$> mutationTx
|
|
where
|
|
rawAlias = S.mkTableAlias $ "mutres__" <> qualifiedObjectToText qt
|
|
rawIdentifier = S.tableAliasToIdentifier rawAlias
|
|
tabFrom = FromIdentifier $ FIIdentifier (unTableIdentifier rawIdentifier)
|
|
tabPerm = TablePerm annBoolExpTrue Nothing
|
|
selFlds = flip map cols
|
|
$ \ci -> (fromCol @('Postgres pgKind) $ ciColumn ci, mkAnnColumnFieldAsText ci)
|
|
|
|
sqlText = toQuery selectWith
|
|
|
|
select =
|
|
S.mkSelect
|
|
{ S.selExtr =
|
|
S.Extractor extrExp Nothing
|
|
: bool [] [S.Extractor checkErrExp Nothing] (checkPermissionRequired cte)
|
|
}
|
|
|
|
selectWith =
|
|
S.SelectWith
|
|
( [(rawAlias, getMutationCTE cte)]
|
|
<> customSQLToTopLevelCTEs customSQLCTEs
|
|
)
|
|
select
|
|
|
|
checkErrExp = mkCheckErrorExp rawIdentifier
|
|
extrExp =
|
|
S.applyJsonBuildObj
|
|
[ S.SELit "affected_rows",
|
|
affRowsSel,
|
|
S.SELit "returning_columns",
|
|
colSel
|
|
]
|
|
|
|
affRowsSel =
|
|
S.SESelect
|
|
$ S.mkSelect
|
|
{ S.selExtr = [S.Extractor S.countStar Nothing],
|
|
S.selFrom = Just $ S.FromExp [S.FIIdentifier rawIdentifier]
|
|
}
|
|
|
|
(colSel, customSQLCTEs) =
|
|
runWriter
|
|
$ S.SESelect
|
|
<$> mkSQLSelect
|
|
JASMultipleRows
|
|
( AnnSelectG selFlds tabFrom tabPerm noSelectArgs strfyNum tCase
|
|
)
|
|
|
|
-------------- Validating insert input using external HTTP webhook -----------------------
|
|
type ValidateInputPayloadVersion = Int
|
|
|
|
validateInputPayloadVersion :: ValidateInputPayloadVersion
|
|
validateInputPayloadVersion = 1
|
|
|
|
newtype ValidateInputErrorResponse = ValidateInputErrorResponse {_vierMessage :: Text}
|
|
deriving (Show, Eq)
|
|
|
|
$(J.deriveJSON hasuraJSON ''ValidateInputErrorResponse)
|
|
|
|
data HttpHandlerLog = HttpHandlerLog
|
|
{ _hhlUrl :: Text,
|
|
_hhlRequest :: J.Value,
|
|
_hhlRequestHeaders :: [HeaderConf],
|
|
_hhlResponse :: J.Value,
|
|
_hhlResponseStatus :: Int
|
|
}
|
|
deriving (Show)
|
|
|
|
$(J.deriveToJSON hasuraJSON ''HttpHandlerLog)
|
|
|
|
data ValidateInsertInputLog
|
|
= VIILHttpHandler HttpHandlerLog
|
|
|
|
instance J.ToJSON ValidateInsertInputLog where
|
|
toJSON (VIILHttpHandler httpHandlerLog) =
|
|
J.object $ ["type" J..= ("http" :: String), "details" J..= J.toJSON httpHandlerLog]
|
|
|
|
instance L.ToEngineLog ValidateInsertInputLog L.Hasura where
|
|
toEngineLog ahl = (L.LevelInfo, L.ELTValidateInputLog, J.toJSON ahl)
|
|
|
|
-- | Map of table name and the value that is being inserted for that table
|
|
-- This map is helpful for collecting all the insert mutation arguments for the
|
|
-- nested tables and then sending them all at onve to the input validation webhook.
|
|
type InsertValidationPayloadMap pgKind = InsOrdHashMap.InsOrdHashMap (TableName ('Postgres pgKind)) ([IR.AnnotatedInsertRow ('Postgres pgKind) (IR.UnpreparedValue ('Postgres pgKind))], (ValidateInput ResolvedWebhook))
|
|
|
|
validateUpdateMutation ::
|
|
forall pgKind m.
|
|
(MonadError QErr m, MonadIO m, Tracing.MonadTrace m) =>
|
|
Env.Environment ->
|
|
HTTP.Manager ->
|
|
L.Logger L.Hasura ->
|
|
UserInfo ->
|
|
ResolvedWebhook ->
|
|
[HeaderConf] ->
|
|
Timeout ->
|
|
Bool ->
|
|
[HTTP.Header] ->
|
|
IR.AnnotatedUpdateG ('Postgres pgKind) Void (IR.UnpreparedValue ('Postgres pgKind)) ->
|
|
Maybe (HashMap G.Name (G.Value G.Variable)) ->
|
|
m ()
|
|
validateUpdateMutation env manager logger userInfo resolvedWebHook confHeaders timeout forwardClientHeaders reqHeaders updateOperation maybeSelSetArgs = do
|
|
inputData <-
|
|
case maybeSelSetArgs of
|
|
Just arguments -> do
|
|
case (IR._auUpdateVariant updateOperation) of
|
|
-- Mutation arguments for single update (eg: update_customer) are
|
|
-- present as seperate root fields of the selection set.
|
|
-- eg:
|
|
Postgres.SingleBatch _ -> do
|
|
-- this constructs something like: {"_set":{"name": {"_eq": "abc"}}, "where":{"id":{"_eq":10}}}
|
|
let singleBatchinputVal =
|
|
J.object
|
|
$ map
|
|
(\(k, v) -> J.fromText (G.unName k) J..= graphQLToJSON v)
|
|
(HashMap.toList $ arguments)
|
|
return (J.object ["input" J..= [singleBatchinputVal]])
|
|
-- Mutation arguments for multiple updates (eg:
|
|
-- update_customer_many) are present in the "updates" field of the
|
|
-- selection set.
|
|
-- Look for "updates" field and get the mutation arguments from it.
|
|
-- eg: {"updates": [{"_set":{"id":{"_eq":10}}, "where":{"name":{"_eq":"abc"}}}]}
|
|
Postgres.MultipleBatches _ -> do
|
|
case (HashMap.lookup $$(G.litName "updates") arguments) of
|
|
Nothing -> return $ J.Null
|
|
Just val -> (return $ J.object ["input" J..= graphQLToJSON val])
|
|
Nothing -> return J.Null
|
|
validateMutation env manager logger userInfo resolvedWebHook confHeaders timeout forwardClientHeaders reqHeaders inputData
|
|
|
|
validateDeleteMutation ::
|
|
forall m pgKind.
|
|
(MonadError QErr m, MonadIO m, Tracing.MonadTrace m) =>
|
|
Env.Environment ->
|
|
HTTP.Manager ->
|
|
L.Logger L.Hasura ->
|
|
UserInfo ->
|
|
ResolvedWebhook ->
|
|
[HeaderConf] ->
|
|
Timeout ->
|
|
Bool ->
|
|
[HTTP.Header] ->
|
|
IR.AnnDelG ('Postgres pgKind) Void (IR.UnpreparedValue ('Postgres pgKind)) ->
|
|
Maybe (HashMap G.Name (G.Value G.Variable)) ->
|
|
m ()
|
|
validateDeleteMutation env manager logger userInfo resolvedWebHook confHeaders timeout forwardClientHeaders reqHeaders deleteOperation maybeSelSetArgs = do
|
|
inputData <-
|
|
case maybeSelSetArgs of
|
|
Just arguments -> do
|
|
-- this constructs something like: {"where":{"id":{"_eq":10}}}
|
|
let deleteInputVal =
|
|
J.object
|
|
$ map
|
|
(\(k, v) -> J.fromText (G.unName k) J..= graphQLToJSON v)
|
|
(HashMap.toList $ arguments)
|
|
if (_adIsDeleteByPk deleteOperation)
|
|
then -- If the delete operation is delete_<table>_by_pk, then we need to
|
|
-- include the pk_columns field manually in the input payload. This
|
|
-- is needed, because unlike the update mutation, the pk_columns for
|
|
-- `delete_<table>_by_pk` is not present in the mutation arguments.
|
|
-- for eg: the `delete_<table>_by_pk` looks like:
|
|
--
|
|
-- mutation DeleteCustomerByPk {
|
|
-- delete_customer_by_pk(id: 1) {
|
|
-- id
|
|
-- }
|
|
-- }
|
|
do
|
|
let deleteInputValByPk = J.object ["pk_columns" J..= deleteInputVal]
|
|
return (J.object ["input" J..= [deleteInputValByPk]])
|
|
else return (J.object ["input" J..= [deleteInputVal]])
|
|
Nothing -> return J.Null
|
|
validateMutation env manager logger userInfo resolvedWebHook confHeaders timeout forwardClientHeaders reqHeaders inputData
|
|
|
|
validateMutation ::
|
|
forall m.
|
|
( MonadError QErr m,
|
|
MonadIO m,
|
|
Tracing.MonadTrace m
|
|
) =>
|
|
Env.Environment ->
|
|
HTTP.Manager ->
|
|
L.Logger L.Hasura ->
|
|
UserInfo ->
|
|
ResolvedWebhook ->
|
|
[HeaderConf] ->
|
|
Timeout ->
|
|
Bool ->
|
|
[HTTP.Header] ->
|
|
J.Value ->
|
|
m ()
|
|
validateMutation env manager logger userInfo (ResolvedWebhook urlText) confHeaders timeout forwardClientHeaders reqHeaders inputData = do
|
|
let requestBody =
|
|
J.object
|
|
[ "version" J..= validateInputPayloadVersion,
|
|
"session_variables" J..= _uiSession userInfo,
|
|
"role" J..= _uiRole userInfo,
|
|
"data" J..= inputData
|
|
]
|
|
resolvedConfHeaders <- makeHeadersFromConf env confHeaders
|
|
let clientHeaders = if forwardClientHeaders then mkClientHeadersForward reqHeaders else mempty
|
|
-- Using HashMap to avoid duplicate headers between configuration headers
|
|
-- and client headers where configuration headers are preferred
|
|
hdrs = (HashMap.toList . HashMap.fromList) (resolvedConfHeaders <> defaultHeaders <> clientHeaders)
|
|
initRequest <- liftIO $ HTTP.mkRequestThrow urlText
|
|
let request =
|
|
initRequest
|
|
& Lens.set HTTP.method "POST"
|
|
& Lens.set HTTP.headers hdrs
|
|
& Lens.set HTTP.body (HTTP.RequestBodyLBS $ J.encode requestBody)
|
|
& Lens.set HTTP.timeout (HTTP.responseTimeoutMicro (unTimeout timeout * 1000000)) -- (default: 10 seconds)
|
|
httpResponse <-
|
|
Tracing.traceHTTPRequest request $ \request' ->
|
|
liftIO . try $ HTTP.httpLbs request' manager
|
|
|
|
case httpResponse of
|
|
Left e ->
|
|
throw500WithDetail "http exception when validating input data"
|
|
$ J.toJSON
|
|
$ HttpException e
|
|
Right response -> do
|
|
let responseStatus = response Lens.^. Wreq.responseStatus
|
|
responseBody = response Lens.^. Wreq.responseBody
|
|
responseBodyForLogging = fromMaybe (J.String $ lbsToTxt responseBody) $ J.decode' responseBody
|
|
-- Log the details of the HTTP webhook call
|
|
L.unLogger logger $ VIILHttpHandler $ HttpHandlerLog urlText requestBody confHeaders responseBodyForLogging (HTTP.statusCode responseStatus)
|
|
if
|
|
| HTTP.statusIsSuccessful responseStatus -> pure ()
|
|
| responseStatus == HTTP.status400 -> do
|
|
ValidateInputErrorResponse errorMessage <-
|
|
J.eitherDecode responseBody `onLeft` \e ->
|
|
throw500WithDetail "received invalid response from input validation webhook"
|
|
$ J.toJSON
|
|
$ "invalid response: "
|
|
<> e
|
|
throw400 ValidationFailed errorMessage
|
|
| otherwise -> do
|
|
let err =
|
|
J.toJSON
|
|
$ "expecting 200 or 400 status code, but found "
|
|
++ show (HTTP.statusCode responseStatus)
|
|
throw500WithDetail "internal error when validating input data" err
|