mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-23 23:43:44 +03:00
4c8ea8e865
Result of executing the following commands: ```shell # replace "as Q" imports with "as PG" (in retrospect this didn't need a regex) git grep -lE 'as Q($|[^a-zA-Z])' -- '*.hs' | xargs sed -i -E 's/as Q($|[^a-zA-Z])/as PG\1/' # replace " Q." with " PG." git grep -lE ' Q\.' -- '*.hs' | xargs sed -i 's/ Q\./ PG./g' # replace "(Q." with "(PG." git grep -lE '\(Q\.' -- '*.hs' | xargs sed -i 's/(Q\./(PG./g' # ditto, but for [, |, { and ! git grep -lE '\[Q\.' -- '*.hs' | xargs sed -i 's/\[Q\./\[PG./g' git grep -l '|Q\.' -- '*.hs' | xargs sed -i 's/|Q\./|PG./g' git grep -l '{Q\.' -- '*.hs' | xargs sed -i 's/{Q\./{PG./g' git grep -l '!Q\.' -- '*.hs' | xargs sed -i 's/!Q\./!PG./g' ``` (Doing the `grep -l` before the `sed`, instead of `sed` on the entire codebase, reduces the number of `mtime` updates, and so reduces how many times a file gets recompiled while checking intermediate results.) Finally, I manually removed a broken and unused `Arbitrary` instance in `Hasura.RQL.Network`. (It used an `import Test.QuickCheck.Arbitrary as Q` statement, which was erroneously caught by the first find-replace command.) After this PR, `Q` is no longer used as an import qualifier. That was not the goal of this PR, but perhaps it's a useful fact for future efforts. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5933 GitOrigin-RevId: 8c84c59d57789111d40f5d3322c5a885dcfbf40e
424 lines
12 KiB
Haskell
424 lines
12 KiB
Haskell
{-# LANGUAGE Arrows #-}
|
|
|
|
module Hasura.Base.Error
|
|
( Code (..),
|
|
QErr (..),
|
|
QErrExtra (..),
|
|
overrideQErrStatus,
|
|
prefixQErr,
|
|
showQErr,
|
|
encodeQErr,
|
|
encodeGQLErr,
|
|
noInternalQErrEnc,
|
|
err400,
|
|
err404,
|
|
err405,
|
|
err401,
|
|
err409,
|
|
err429,
|
|
err500,
|
|
internalError,
|
|
QErrM,
|
|
throw400,
|
|
throw404,
|
|
throw405,
|
|
throw409,
|
|
throw429,
|
|
throw500,
|
|
throw500WithDetail,
|
|
throw401,
|
|
iResultToMaybe,
|
|
-- Aeson helpers
|
|
runAesonParser,
|
|
decodeValue,
|
|
-- Modify error messages
|
|
modifyErr,
|
|
modifyErrAndSet500,
|
|
modifyQErr,
|
|
modifyErrA,
|
|
-- Attach context
|
|
withPathK,
|
|
withPathKA,
|
|
withPathI,
|
|
withPathIA,
|
|
indexedFoldlA',
|
|
indexedForM,
|
|
indexedMapM,
|
|
indexedForM_,
|
|
indexedMapM_,
|
|
indexedTraverseA_,
|
|
)
|
|
where
|
|
|
|
import Control.Arrow.Extended
|
|
import Data.Aeson
|
|
import Data.Aeson.Internal
|
|
import Data.Aeson.Key qualified as K
|
|
import Data.Aeson.Types
|
|
import Data.Parser.JSONPath (encodeJSONPath)
|
|
import Data.Text qualified as T
|
|
import Data.Text.Lazy qualified as TL
|
|
import Data.Text.Lazy.Encoding qualified as TL
|
|
import Database.PG.Query qualified as PG
|
|
import Hasura.Prelude
|
|
import Network.HTTP.Types qualified as HTTP
|
|
|
|
data Code
|
|
= AccessDenied
|
|
| ActionWebhookCode !Text
|
|
| AlreadyExists
|
|
| AlreadyTracked
|
|
| AlreadyUntracked
|
|
| BadRequest
|
|
| BigQueryError
|
|
| Busy
|
|
| ConcurrentUpdate
|
|
| CoercionError
|
|
| Conflict
|
|
| ConstraintError
|
|
| ConstraintViolation
|
|
| -- | Custom code for extending this sum-type easily
|
|
CustomCode !Text
|
|
| CyclicDependency
|
|
| DataException
|
|
| DataConnectorError
|
|
| DependencyError
|
|
| InvalidConfiguration
|
|
| InvalidHeaders
|
|
| InvalidJSON
|
|
| InvalidParams
|
|
| JWTInvalid
|
|
| JWTInvalidClaims
|
|
| JWTRoleClaimMissing
|
|
| MSSQLError
|
|
| MethodNotAllowed
|
|
| NotExists
|
|
| NotFound
|
|
| NotSupported
|
|
| ParseFailed
|
|
| PermissionDenied
|
|
| PermissionError
|
|
| PostgresError
|
|
| PostgresMaxConnectionsError
|
|
| RemoteSchemaConflicts
|
|
| RemoteSchemaError
|
|
| -- | Websockets
|
|
StartFailed
|
|
| Unexpected
|
|
| UnexpectedPayload
|
|
| ValidationFailed
|
|
deriving (Show, Eq)
|
|
|
|
instance ToJSON Code where
|
|
toJSON code = String $ case code of
|
|
AccessDenied -> "access-denied"
|
|
ActionWebhookCode t -> t
|
|
AlreadyExists -> "already-exists"
|
|
AlreadyTracked -> "already-tracked"
|
|
AlreadyUntracked -> "already-untracked"
|
|
BadRequest -> "bad-request"
|
|
BigQueryError -> "bigquery-error"
|
|
Busy -> "busy"
|
|
ConcurrentUpdate -> "concurrent-update"
|
|
CoercionError -> "coercion-error"
|
|
Conflict -> "conflict"
|
|
ConstraintError -> "constraint-error"
|
|
ConstraintViolation -> "constraint-violation"
|
|
CustomCode t -> t
|
|
CyclicDependency -> "cyclic-dependency"
|
|
DataException -> "data-exception"
|
|
DataConnectorError -> "data-connector-error"
|
|
DependencyError -> "dependency-error"
|
|
InvalidConfiguration -> "invalid-configuration"
|
|
InvalidHeaders -> "invalid-headers"
|
|
InvalidJSON -> "invalid-json"
|
|
InvalidParams -> "invalid-params"
|
|
JWTInvalid -> "invalid-jwt"
|
|
JWTInvalidClaims -> "jwt-invalid-claims"
|
|
JWTRoleClaimMissing -> "jwt-missing-role-claims"
|
|
MSSQLError -> "mssql-error"
|
|
MethodNotAllowed -> "method-not-allowed"
|
|
NotExists -> "not-exists"
|
|
NotFound -> "not-found"
|
|
NotSupported -> "not-supported"
|
|
ParseFailed -> "parse-failed"
|
|
PermissionDenied -> "permission-denied"
|
|
PermissionError -> "permission-error"
|
|
PostgresError -> "postgres-error"
|
|
PostgresMaxConnectionsError -> "postgres-max-connections-error"
|
|
RemoteSchemaConflicts -> "remote-schema-conflicts"
|
|
RemoteSchemaError -> "remote-schema-error"
|
|
StartFailed -> "start-failed"
|
|
Unexpected -> "unexpected"
|
|
UnexpectedPayload -> "unexpected-payload"
|
|
ValidationFailed -> "validation-failed"
|
|
|
|
data QErr = QErr
|
|
{ qePath :: JSONPath,
|
|
qeStatus :: HTTP.Status,
|
|
qeError :: Text,
|
|
qeCode :: Code,
|
|
qeInternal :: Maybe QErrExtra
|
|
}
|
|
deriving (Eq)
|
|
|
|
-- | Extra context for a QErr, which can either be information from an internal
|
|
-- error (e.g. from Postgres, or from a network operation timing out), or
|
|
-- context provided when an external service or operation fails, for instance, a
|
|
-- webhook error response may provide additional context in the `extensions`
|
|
-- key.
|
|
data QErrExtra
|
|
= ExtraExtensions Value
|
|
| ExtraInternal Value
|
|
deriving (Eq)
|
|
|
|
instance ToJSON QErrExtra where
|
|
toJSON = \case
|
|
ExtraExtensions v -> v
|
|
ExtraInternal v -> v
|
|
|
|
instance ToJSON QErr where
|
|
toJSON (QErr jPath _ msg code Nothing) =
|
|
object
|
|
[ "path" .= encodeJSONPath jPath,
|
|
"error" .= msg,
|
|
"code" .= code
|
|
]
|
|
toJSON (QErr jPath _ msg code (Just extra)) = object $
|
|
case extra of
|
|
ExtraInternal e -> err ++ ["internal" .= e]
|
|
ExtraExtensions {} -> err
|
|
where
|
|
err =
|
|
[ "path" .= encodeJSONPath jPath,
|
|
"error" .= msg,
|
|
"code" .= code
|
|
]
|
|
|
|
-- | Overrides the status and code of a QErr while retaining all other fields.
|
|
overrideQErrStatus :: HTTP.Status -> Code -> QErr -> QErr
|
|
overrideQErrStatus newStatus newCode err = err {qeStatus = newStatus, qeCode = newCode}
|
|
|
|
-- | Prefixes the message of a QErr while retaining all other fields.
|
|
prefixQErr :: Text -> QErr -> QErr
|
|
prefixQErr prefix err = err {qeError = prefix <> qeError err}
|
|
|
|
-- Temporary function until we have a better one in place.
|
|
showQErr :: QErr -> Text
|
|
showQErr = TL.toStrict . TL.decodeUtf8 . encode
|
|
|
|
noInternalQErrEnc :: QErr -> Value
|
|
noInternalQErrEnc (QErr jPath _ msg code _) =
|
|
object
|
|
[ "path" .= encodeJSONPath jPath,
|
|
"error" .= msg,
|
|
"code" .= code
|
|
]
|
|
|
|
encodeGQLErr :: Bool -> QErr -> Value
|
|
encodeGQLErr includeInternal (QErr jPath _ msg code maybeExtra) =
|
|
object
|
|
[ "message" .= msg,
|
|
"extensions" .= extnsObj
|
|
]
|
|
where
|
|
appendIf cond a b = if cond then a ++ b else a
|
|
|
|
extnsObj = case maybeExtra of
|
|
Nothing -> object codeAndPath
|
|
-- if an `extensions` key is given in the error response from the webhook,
|
|
-- we ignore the `code` key regardless of whether the `extensions` object
|
|
-- contains a `code` field:
|
|
Just (ExtraExtensions v) -> v
|
|
Just (ExtraInternal v) ->
|
|
object $ appendIf includeInternal codeAndPath ["internal" .= v]
|
|
codeAndPath =
|
|
[ "path" .= encodeJSONPath jPath,
|
|
"code" .= code
|
|
]
|
|
|
|
-- whether internal should be included or not
|
|
encodeQErr :: Bool -> QErr -> Value
|
|
encodeQErr True = toJSON
|
|
encodeQErr _ = noInternalQErrEnc
|
|
|
|
-- Postgres Connection Errors
|
|
instance PG.FromPGConnErr QErr where
|
|
fromPGConnErr c
|
|
| "too many clients" `T.isInfixOf` (PG.getConnErr c) =
|
|
let e = err500 PostgresMaxConnectionsError "max connections reached on postgres"
|
|
in e {qeInternal = Just $ ExtraInternal $ toJSON c}
|
|
| "root certificate file" `T.isInfixOf` (PG.getConnErr c) =
|
|
err500 PostgresError "root certificate error"
|
|
| "certificate file" `T.isInfixOf` (PG.getConnErr c) =
|
|
err500 PostgresError "certificate error"
|
|
| "private key file" `T.isInfixOf` (PG.getConnErr c) =
|
|
err500 PostgresError "private-key error"
|
|
fromPGConnErr c =
|
|
(err500 PostgresError "connection error")
|
|
{ qeInternal = Just $ ExtraInternal $ toJSON c
|
|
}
|
|
|
|
-- Postgres Transaction error
|
|
instance PG.FromPGTxErr QErr where
|
|
fromPGTxErr txe =
|
|
(err500 PostgresError "postgres tx error")
|
|
{ qeInternal = Just $ ExtraInternal $ toJSON txe
|
|
}
|
|
|
|
err400 :: Code -> Text -> QErr
|
|
err400 c t = QErr [] HTTP.status400 t c Nothing
|
|
|
|
err404 :: Code -> Text -> QErr
|
|
err404 c t = QErr [] HTTP.status404 t c Nothing
|
|
|
|
err405 :: Code -> Text -> QErr
|
|
err405 c t = QErr [] HTTP.status405 t c Nothing
|
|
|
|
err401 :: Code -> Text -> QErr
|
|
err401 c t = QErr [] HTTP.status401 t c Nothing
|
|
|
|
err409 :: Code -> Text -> QErr
|
|
err409 c t = QErr [] HTTP.status409 t c Nothing
|
|
|
|
err429 :: Code -> Text -> QErr
|
|
err429 c t = QErr [] HTTP.status429 t c Nothing
|
|
|
|
err500 :: Code -> Text -> QErr
|
|
err500 c t = QErr [] HTTP.status500 t c Nothing
|
|
|
|
type QErrM m = (MonadError QErr m)
|
|
|
|
throw400 :: (QErrM m) => Code -> Text -> m a
|
|
throw400 c t = throwError $ err400 c t
|
|
|
|
throw404 :: (QErrM m) => Text -> m a
|
|
throw404 t = throwError $ err404 NotFound t
|
|
|
|
-- | MethodNotAllowed
|
|
throw405 :: (QErrM m) => Text -> m a
|
|
throw405 t = throwError $ err405 MethodNotAllowed t
|
|
|
|
-- | AccessDenied
|
|
throw401 :: (QErrM m) => Text -> m a
|
|
throw401 t = throwError $ err401 AccessDenied t
|
|
|
|
-- | Conflict
|
|
throw409 :: (QErrM m) => Text -> m a
|
|
throw409 t = throwError $ err409 Conflict t
|
|
|
|
throw429 :: (QErrM m) => Code -> Text -> m a
|
|
throw429 c t = throwError $ err429 c t
|
|
|
|
throw500 :: (QErrM m) => Text -> m a
|
|
throw500 t = throwError $ internalError t
|
|
|
|
internalError :: Text -> QErr
|
|
internalError = err500 Unexpected
|
|
|
|
throw500WithDetail :: (QErrM m) => Text -> Value -> m a
|
|
throw500WithDetail t detail =
|
|
throwError $ (err500 Unexpected t) {qeInternal = Just $ ExtraInternal detail}
|
|
|
|
modifyQErr ::
|
|
(QErrM m) =>
|
|
(QErr -> QErr) ->
|
|
m a ->
|
|
m a
|
|
modifyQErr f a = catchError a (throwError . f)
|
|
|
|
modifyErr ::
|
|
(QErrM m) =>
|
|
(Text -> Text) ->
|
|
m a ->
|
|
m a
|
|
modifyErr f = modifyQErr (liftTxtMod f)
|
|
|
|
modifyErrA :: (ArrowError QErr arr) => arr (e, s) a -> arr (e, (Text -> Text, s)) a
|
|
modifyErrA f = proc (e, (g, s)) -> (| mapErrorA (f -< (e, s)) |) (liftTxtMod g)
|
|
|
|
liftTxtMod :: (Text -> Text) -> QErr -> QErr
|
|
liftTxtMod f (QErr path st s c i) = QErr path st (f s) c i
|
|
|
|
modifyErrAndSet500 ::
|
|
(QErrM m) =>
|
|
(Text -> Text) ->
|
|
m a ->
|
|
m a
|
|
modifyErrAndSet500 f = modifyQErr (liftTxtMod500 f)
|
|
|
|
liftTxtMod500 :: (Text -> Text) -> QErr -> QErr
|
|
liftTxtMod500 f (QErr path _ s c i) = QErr path HTTP.status500 (f s) c i
|
|
|
|
withPathE :: (ArrowError QErr arr) => arr (e, s) a -> arr (e, (JSONPathElement, s)) a
|
|
withPathE f = proc (e, (pe, s)) -> (| mapErrorA ((e, s) >- f) |) (injectPrefix pe)
|
|
where
|
|
injectPrefix pe (QErr path st msg code i) = QErr (pe : path) st msg code i
|
|
|
|
withPathKA :: (ArrowError QErr arr) => arr (e, s) a -> arr (e, (Text, s)) a
|
|
withPathKA f = second (first $ arr (Key . K.fromText)) >>> withPathE f
|
|
|
|
withPathK :: (QErrM m) => Text -> m a -> m a
|
|
withPathK a = runKleisli proc m -> (| withPathKA (m >- bindA) |) a
|
|
|
|
withPathIA :: (ArrowError QErr arr) => arr (e, s) a -> arr (e, (Int, s)) a
|
|
withPathIA f = second (first $ arr Index) >>> withPathE f
|
|
|
|
withPathI :: (QErrM m) => Int -> m a -> m a
|
|
withPathI a = runKleisli proc m -> (| withPathIA (m >- bindA) |) a
|
|
|
|
indexedFoldlA' ::
|
|
(ArrowChoice arr, ArrowError QErr arr, Foldable t) =>
|
|
arr (e, (b, (a, s))) b ->
|
|
arr (e, (b, (t a, s))) b
|
|
indexedFoldlA' f = proc (e, (acc0, (xs, s))) ->
|
|
(|
|
|
foldlA'
|
|
(\acc (i, v) -> (| withPathIA ((e, (acc, (v, s))) >- f) |) i)
|
|
|) acc0 (zip [0 ..] (toList xs))
|
|
|
|
indexedTraverseA_ ::
|
|
(ArrowChoice arr, ArrowError QErr arr, Foldable t) =>
|
|
arr (e, (a, s)) b ->
|
|
arr (e, (t a, s)) ()
|
|
indexedTraverseA_ f = proc (e, (xs, s)) ->
|
|
(| indexedFoldlA' (\() x -> do (e, (x, s)) >- f; () >- returnA) |) () xs
|
|
|
|
indexedMapM_ :: (QErrM m, Foldable t) => (a -> m b) -> t a -> m ()
|
|
indexedMapM_ f = runKleisli proc xs -> (| indexedTraverseA_ (\x -> f x >- bindA) |) xs
|
|
|
|
indexedForM_ :: (QErrM m, Foldable t) => t a -> (a -> m b) -> m ()
|
|
indexedForM_ = flip indexedMapM_
|
|
|
|
indexedMapM :: (QErrM m) => (a -> m b) -> [a] -> m [b]
|
|
indexedMapM f = traverse (\(i, x) -> withPathI i (f x)) . zip [0 ..]
|
|
|
|
indexedForM :: (QErrM m) => [a] -> (a -> m b) -> m [b]
|
|
indexedForM = flip indexedMapM
|
|
|
|
liftIResult :: (QErrM m) => IResult a -> m a
|
|
liftIResult (IError path msg) =
|
|
throwError $ QErr path HTTP.status400 (T.pack $ formatMsg msg) ParseFailed Nothing
|
|
liftIResult (ISuccess a) =
|
|
return a
|
|
|
|
iResultToMaybe :: IResult a -> Maybe a
|
|
iResultToMaybe (IError _ _) = Nothing
|
|
iResultToMaybe (ISuccess a) = Just a
|
|
|
|
formatMsg :: String -> String
|
|
formatMsg str = case T.splitOn "the key " txt of
|
|
[_, txt2] -> case T.splitOn " was not present" txt2 of
|
|
[key, _] -> "the key '" ++ T.unpack key ++ "' was not present"
|
|
_ -> str
|
|
_ -> str
|
|
where
|
|
txt = T.pack str
|
|
|
|
runAesonParser :: (QErrM m) => (v -> Parser a) -> v -> m a
|
|
runAesonParser p =
|
|
liftIResult . iparse p
|
|
|
|
decodeValue :: (FromJSON a, QErrM m) => Value -> m a
|
|
decodeValue = liftIResult . ifromJSON
|