mirror of
https://github.com/Airsequel/AirGQL.git
synced 2025-07-29 14:23:18 +03:00
Implement partial errors in grqphql API
Make it so the parent resolver returns a failed field named `foo` as `__error_foo` containing the error message. This field can then be picked up by the child resolver in order to produce a local error.
This commit is contained in:
parent
d8385cc08b
commit
41f1752983
3 changed files with 96 additions and 85 deletions
|
@ -102,8 +102,7 @@ import AirGQL.Types.SchemaConf (
|
|||
SchemaConf (accessMode, maxRowsPerTable, pragmaConf),
|
||||
)
|
||||
import AirGQL.Types.Utils (encodeToText)
|
||||
import AirGQL.Utils (colToFileUrl, collectErrorList, quoteKeyword, quoteText)
|
||||
import Data.Either.Extra qualified as Either
|
||||
import AirGQL.Utils (colToFileUrl, quoteKeyword, quoteText)
|
||||
import Data.List qualified as List
|
||||
import Language.GraphQL.Class (FromGraphQL (fromGraphQL))
|
||||
|
||||
|
@ -385,7 +384,19 @@ gqlValueToSQLData = \case
|
|||
Object obj -> SQLText $ show obj
|
||||
|
||||
|
||||
rowToGraphQL :: Text -> TableEntry -> [SQLData] -> Either [(Text, Text)] Value
|
||||
-- The way Airsequel works at the moment is by generating one big GQL object at
|
||||
-- the root-most resolver, and then having child resolvers pick up the sections
|
||||
-- they need.
|
||||
--
|
||||
-- One issue with this way of doing things is that we don't have a way to
|
||||
-- generate location-specific errors, thus we used to simply error out at the
|
||||
-- first issue, without returning partial results.
|
||||
--
|
||||
-- The "hack" I came up to fix this, is to return a failed field named "foo" as
|
||||
-- a field named "__error_foo" containing the text of the error. The child
|
||||
-- resolvers can later pick up this error, and fail themselves only, thus
|
||||
-- returning partial results.
|
||||
rowToGraphQL :: Text -> TableEntry -> [SQLData] -> Value
|
||||
rowToGraphQL dbId table row =
|
||||
let
|
||||
buildMetadataJson :: Text -> Text -> Text
|
||||
|
@ -393,80 +404,51 @@ rowToGraphQL dbId table row =
|
|||
object ["url" .= colToFileUrl dbId table.name colName rowid]
|
||||
& encodeToText
|
||||
|
||||
parseSqlData :: (ColumnEntry, SQLData) -> Either (Text, Text) (Text, Value)
|
||||
parseSqlData :: (ColumnEntry, SQLData) -> (Text, Value)
|
||||
parseSqlData (colEntry, colVal) =
|
||||
if "BLOB" `T.isPrefixOf` colEntry.datatype
|
||||
then
|
||||
pure
|
||||
( colEntry.column_name_gql
|
||||
, case colVal of
|
||||
SQLNull -> Null
|
||||
SQLInteger id ->
|
||||
String $
|
||||
buildMetadataJson colEntry.column_name (show id)
|
||||
SQLText id ->
|
||||
String $
|
||||
buildMetadataJson colEntry.column_name id
|
||||
_ -> Null
|
||||
)
|
||||
( colEntry.column_name_gql
|
||||
, case colVal of
|
||||
SQLNull -> Null
|
||||
SQLInteger id ->
|
||||
String $
|
||||
buildMetadataJson colEntry.column_name (show id)
|
||||
SQLText id ->
|
||||
String $
|
||||
buildMetadataJson colEntry.column_name id
|
||||
_ -> Null
|
||||
)
|
||||
else case sqlDataToGQLValue colEntry.datatype colVal of
|
||||
Left err ->
|
||||
Left
|
||||
(colEntry.column_name_gql, err)
|
||||
( "__error_" <> colEntry.column_name_gql
|
||||
, String err
|
||||
)
|
||||
Right gqlData ->
|
||||
Right
|
||||
( colEntry.column_name_gql
|
||||
, case colEntry.datatype of
|
||||
-- Coerce value to nullable String
|
||||
-- if no datatype is set.
|
||||
-- This happens for columns in views.
|
||||
"" -> gqlValueToNullableString gqlData
|
||||
_ -> gqlData
|
||||
)
|
||||
( colEntry.column_name_gql
|
||||
, case colEntry.datatype of
|
||||
-- Coerce value to nullable String
|
||||
-- if no datatype is set.
|
||||
-- This happens for columns in views.
|
||||
"" -> gqlValueToNullableString gqlData
|
||||
_ -> gqlData
|
||||
)
|
||||
in
|
||||
-- => [(ColumnEntry, SQLData)]
|
||||
P.zip table.columns row
|
||||
-- => [Either (Text, Text) (Text, Value)]
|
||||
-- => [(Text, Value)]
|
||||
<&> parseSqlData
|
||||
-- => Either [(Text, Text)] (Text, Value)
|
||||
& collectErrorList
|
||||
-- => Either [(Text, Text)] (HashMap Text Value)
|
||||
<&> HashMap.fromList
|
||||
-- => Either [(Text, Text)] Value
|
||||
<&> Object
|
||||
-- => HashMap Text Value
|
||||
& HashMap.fromList
|
||||
-- => Value
|
||||
& Object
|
||||
|
||||
|
||||
rowsToGraphQL
|
||||
:: Text
|
||||
-> TableEntry
|
||||
-> [[SQLData]]
|
||||
-> Either [(Text, Text)] Value
|
||||
rowsToGraphQL :: Text -> TableEntry -> [[SQLData]] -> Value
|
||||
rowsToGraphQL dbId table updatedRows =
|
||||
updatedRows
|
||||
-- => [Either [(Text, Text)] Value]
|
||||
<&> rowToGraphQL dbId table
|
||||
-- => Either [[(Text, Text)]] [Value]
|
||||
& collectErrorList
|
||||
-- => Either [(Text, Text)] [Value]
|
||||
& Either.mapLeft P.join
|
||||
-- => Either [(Text, Text)] Value
|
||||
<&> List
|
||||
|
||||
|
||||
-- | Formats errors from `row(s)ToGraphQL` and throws them.
|
||||
colErrorsToUserError :: forall m a. (MonadIO m) => Either [(Text, Text)] a -> m a
|
||||
colErrorsToUserError = \case
|
||||
Right v -> pure v
|
||||
Left errors ->
|
||||
let
|
||||
errorLines =
|
||||
errors
|
||||
<&> \(column, err) -> "On column " <> show column <> ": " <> err
|
||||
in
|
||||
P.throwIO $
|
||||
userError $
|
||||
T.unpack $
|
||||
"Multiple errors occurred:\n" <> P.unlines errorLines
|
||||
& List
|
||||
|
||||
|
||||
tryGetArg
|
||||
|
@ -742,7 +724,7 @@ queryType connection accessMode dbId tables = do
|
|||
orderElements
|
||||
paginationMb
|
||||
|
||||
colErrorsToUserError $ rowsToGraphQL dbId table rows
|
||||
pure $ rowsToGraphQL dbId table rows
|
||||
|
||||
getDbEntriesByPK :: TableEntry -> Out.Resolve IO
|
||||
getDbEntriesByPK tableEntry = do
|
||||
|
@ -764,7 +746,7 @@ queryType connection accessMode dbId tables = do
|
|||
case P.head queryResult of
|
||||
Nothing -> pure Null
|
||||
Just row ->
|
||||
colErrorsToUserError $
|
||||
pure $
|
||||
rowToGraphQL
|
||||
dbId
|
||||
tableEntry
|
||||
|
@ -835,9 +817,7 @@ mutationType connection maxRowsPerTable accessMode dbId tables = do
|
|||
|
||||
mutationResponse :: TableEntry -> Int -> [[SQLData]] -> IO Value
|
||||
mutationResponse table numChanges rows = do
|
||||
returning <-
|
||||
colErrorsToUserError $
|
||||
rowsToGraphQL dbId table rows
|
||||
let returning = rowsToGraphQL dbId table rows
|
||||
|
||||
pure $
|
||||
Object $
|
||||
|
@ -848,11 +828,9 @@ mutationType connection maxRowsPerTable accessMode dbId tables = do
|
|||
|
||||
mutationByPKResponse :: TableEntry -> Int -> Maybe [SQLData] -> IO Value
|
||||
mutationByPKResponse table numChanges mbRow = do
|
||||
returning <- case mbRow of
|
||||
Nothing -> pure Null
|
||||
Just row ->
|
||||
colErrorsToUserError $
|
||||
rowToGraphQL dbId table row
|
||||
let returning = case mbRow of
|
||||
Nothing -> Null
|
||||
Just row -> rowToGraphQL dbId table row
|
||||
|
||||
pure $
|
||||
Object $
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
||||
|
||||
{-# HLINT ignore "Replace case with maybe" #-}
|
||||
module AirGQL.Introspection.Resolver (
|
||||
makeType,
|
||||
makeConstField,
|
||||
|
@ -8,9 +11,9 @@ import Protolude (
|
|||
Either (Left),
|
||||
IO,
|
||||
Int,
|
||||
Maybe (Just, Nothing),
|
||||
MonadReader (ask),
|
||||
Text,
|
||||
fromMaybe,
|
||||
pure,
|
||||
show,
|
||||
($),
|
||||
|
@ -23,7 +26,11 @@ import Protolude (
|
|||
import Protolude qualified as P
|
||||
|
||||
import AirGQL.Introspection.Types qualified as IType
|
||||
import Control.Exception qualified as Exception
|
||||
import Data.HashMap.Strict qualified as HashMap
|
||||
import Data.Text qualified as T
|
||||
import GHC.IO.Exception (userError)
|
||||
import Language.GraphQL.Error (ResolverException (ResolverException))
|
||||
import Language.GraphQL.Type qualified as Type
|
||||
import Language.GraphQL.Type.In qualified as In
|
||||
import Language.GraphQL.Type.Out qualified as Out
|
||||
|
@ -32,6 +39,14 @@ import Language.GraphQL.Type.Out qualified as Out
|
|||
type Result = Either Text
|
||||
|
||||
|
||||
throwResolverError :: Text -> m a
|
||||
throwResolverError err =
|
||||
Exception.throw $
|
||||
ResolverException $
|
||||
userError $
|
||||
T.unpack err
|
||||
|
||||
|
||||
{-| Turns a type descriptor into a graphql output type, erroring out on input
|
||||
types. Child resolvers look up their respective fields in the value produced by
|
||||
their parent.
|
||||
|
@ -123,18 +138,23 @@ makeType =
|
|||
let defaultValue =
|
||||
if Out.isNonNullType ty
|
||||
then
|
||||
Type.String $
|
||||
throwResolverError $
|
||||
"Error: field '"
|
||||
<> field.name
|
||||
<> "' not found "
|
||||
else Type.Null
|
||||
else pure Type.Null
|
||||
|
||||
case context.values of
|
||||
Type.Object obj ->
|
||||
pure $
|
||||
fromMaybe defaultValue $
|
||||
HashMap.lookup field.name obj
|
||||
_ -> pure defaultValue
|
||||
Type.Object obj -> do
|
||||
let errorValue = HashMap.lookup ("__error_" <> field.name) obj
|
||||
P.for_ errorValue $ \case
|
||||
Type.String err -> throwResolverError err
|
||||
_ -> pure ()
|
||||
|
||||
case HashMap.lookup field.name obj of
|
||||
Just value -> pure value
|
||||
Nothing -> defaultValue
|
||||
_ -> defaultValue
|
||||
in
|
||||
makeTypeWithDepth 0
|
||||
|
||||
|
|
|
@ -820,15 +820,28 @@ main = void $ do
|
|||
rmSpaces
|
||||
[raw|
|
||||
{
|
||||
"data": null,
|
||||
"data": {
|
||||
"test": [{
|
||||
"alsobig": null,
|
||||
"big": null
|
||||
}]
|
||||
},
|
||||
"errors": [{
|
||||
"locations": [{
|
||||
"column": 3,
|
||||
"line": 2
|
||||
"column": 5,
|
||||
"line": 3
|
||||
}],
|
||||
"message":
|
||||
"user error (Multiple errors occurred:\nOn column \"big\": Integer 8000000000 would overflow. This happens because SQLite uses 64-bit ints, but GraphQL uses 32-bit ints. Use a Number (64-bit float) or Text column instead.\nOn column \"alsobig\": Integer 9000000000 would overflow. This happens because SQLite uses 64-bit ints, but GraphQL uses 32-bit ints. Use a Number (64-bit float) or Text column instead.\n)",
|
||||
"path": ["test"]
|
||||
"user error (Integer 8000000000 would overflow. This happens because SQLite uses 64-bit ints, but GraphQL uses 32-bit ints. Use a Number (64-bit float) or Text column instead.)",
|
||||
"path": ["test", 0, "big"]
|
||||
}, {
|
||||
"locations": [{
|
||||
"column": 5,
|
||||
"line": 4
|
||||
}],
|
||||
"message":
|
||||
"user error (Integer 9000000000 would overflow. This happens because SQLite uses 64-bit ints, but GraphQL uses 32-bit ints. Use a Number (64-bit float) or Text column instead.)",
|
||||
"path": ["test", 0, "alsobig"]
|
||||
}]
|
||||
}
|
||||
|]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue