graphql-engine/server/src-lib/Hasura/Base/Error.hs
Auke Booij 4c8ea8e865 Import pg-client-hs as PG
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
2022-09-20 19:55:51 +00:00

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