mirror of
https://github.com/Airsequel/AirGQL.git
synced 2025-09-18 19:34:32 +02: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),
|
SchemaConf (accessMode, maxRowsPerTable, pragmaConf),
|
||||||
)
|
)
|
||||||
import AirGQL.Types.Utils (encodeToText)
|
import AirGQL.Types.Utils (encodeToText)
|
||||||
import AirGQL.Utils (colToFileUrl, collectErrorList, quoteKeyword, quoteText)
|
import AirGQL.Utils (colToFileUrl, quoteKeyword, quoteText)
|
||||||
import Data.Either.Extra qualified as Either
|
|
||||||
import Data.List qualified as List
|
import Data.List qualified as List
|
||||||
import Language.GraphQL.Class (FromGraphQL (fromGraphQL))
|
import Language.GraphQL.Class (FromGraphQL (fromGraphQL))
|
||||||
|
|
||||||
|
@ -385,7 +384,19 @@ gqlValueToSQLData = \case
|
||||||
Object obj -> SQLText $ show obj
|
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 =
|
rowToGraphQL dbId table row =
|
||||||
let
|
let
|
||||||
buildMetadataJson :: Text -> Text -> Text
|
buildMetadataJson :: Text -> Text -> Text
|
||||||
|
@ -393,80 +404,51 @@ rowToGraphQL dbId table row =
|
||||||
object ["url" .= colToFileUrl dbId table.name colName rowid]
|
object ["url" .= colToFileUrl dbId table.name colName rowid]
|
||||||
& encodeToText
|
& encodeToText
|
||||||
|
|
||||||
parseSqlData :: (ColumnEntry, SQLData) -> Either (Text, Text) (Text, Value)
|
parseSqlData :: (ColumnEntry, SQLData) -> (Text, Value)
|
||||||
parseSqlData (colEntry, colVal) =
|
parseSqlData (colEntry, colVal) =
|
||||||
if "BLOB" `T.isPrefixOf` colEntry.datatype
|
if "BLOB" `T.isPrefixOf` colEntry.datatype
|
||||||
then
|
then
|
||||||
pure
|
( colEntry.column_name_gql
|
||||||
( colEntry.column_name_gql
|
, case colVal of
|
||||||
, case colVal of
|
SQLNull -> Null
|
||||||
SQLNull -> Null
|
SQLInteger id ->
|
||||||
SQLInteger id ->
|
String $
|
||||||
String $
|
buildMetadataJson colEntry.column_name (show id)
|
||||||
buildMetadataJson colEntry.column_name (show id)
|
SQLText id ->
|
||||||
SQLText id ->
|
String $
|
||||||
String $
|
buildMetadataJson colEntry.column_name id
|
||||||
buildMetadataJson colEntry.column_name id
|
_ -> Null
|
||||||
_ -> Null
|
)
|
||||||
)
|
|
||||||
else case sqlDataToGQLValue colEntry.datatype colVal of
|
else case sqlDataToGQLValue colEntry.datatype colVal of
|
||||||
Left err ->
|
Left err ->
|
||||||
Left
|
( "__error_" <> colEntry.column_name_gql
|
||||||
(colEntry.column_name_gql, err)
|
, String err
|
||||||
|
)
|
||||||
Right gqlData ->
|
Right gqlData ->
|
||||||
Right
|
( colEntry.column_name_gql
|
||||||
( colEntry.column_name_gql
|
, case colEntry.datatype of
|
||||||
, case colEntry.datatype of
|
-- Coerce value to nullable String
|
||||||
-- Coerce value to nullable String
|
-- if no datatype is set.
|
||||||
-- if no datatype is set.
|
-- This happens for columns in views.
|
||||||
-- This happens for columns in views.
|
"" -> gqlValueToNullableString gqlData
|
||||||
"" -> gqlValueToNullableString gqlData
|
_ -> gqlData
|
||||||
_ -> gqlData
|
)
|
||||||
)
|
|
||||||
in
|
in
|
||||||
-- => [(ColumnEntry, SQLData)]
|
-- => [(ColumnEntry, SQLData)]
|
||||||
P.zip table.columns row
|
P.zip table.columns row
|
||||||
-- => [Either (Text, Text) (Text, Value)]
|
-- => [(Text, Value)]
|
||||||
<&> parseSqlData
|
<&> parseSqlData
|
||||||
-- => Either [(Text, Text)] (Text, Value)
|
-- => HashMap Text Value
|
||||||
& collectErrorList
|
& HashMap.fromList
|
||||||
-- => Either [(Text, Text)] (HashMap Text Value)
|
-- => Value
|
||||||
<&> HashMap.fromList
|
& Object
|
||||||
-- => Either [(Text, Text)] Value
|
|
||||||
<&> Object
|
|
||||||
|
|
||||||
|
|
||||||
rowsToGraphQL
|
rowsToGraphQL :: Text -> TableEntry -> [[SQLData]] -> Value
|
||||||
:: Text
|
|
||||||
-> TableEntry
|
|
||||||
-> [[SQLData]]
|
|
||||||
-> Either [(Text, Text)] Value
|
|
||||||
rowsToGraphQL dbId table updatedRows =
|
rowsToGraphQL dbId table updatedRows =
|
||||||
updatedRows
|
updatedRows
|
||||||
-- => [Either [(Text, Text)] Value]
|
|
||||||
<&> rowToGraphQL dbId table
|
<&> rowToGraphQL dbId table
|
||||||
-- => Either [[(Text, Text)]] [Value]
|
& List
|
||||||
& 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
|
|
||||||
|
|
||||||
|
|
||||||
tryGetArg
|
tryGetArg
|
||||||
|
@ -742,7 +724,7 @@ queryType connection accessMode dbId tables = do
|
||||||
orderElements
|
orderElements
|
||||||
paginationMb
|
paginationMb
|
||||||
|
|
||||||
colErrorsToUserError $ rowsToGraphQL dbId table rows
|
pure $ rowsToGraphQL dbId table rows
|
||||||
|
|
||||||
getDbEntriesByPK :: TableEntry -> Out.Resolve IO
|
getDbEntriesByPK :: TableEntry -> Out.Resolve IO
|
||||||
getDbEntriesByPK tableEntry = do
|
getDbEntriesByPK tableEntry = do
|
||||||
|
@ -764,7 +746,7 @@ queryType connection accessMode dbId tables = do
|
||||||
case P.head queryResult of
|
case P.head queryResult of
|
||||||
Nothing -> pure Null
|
Nothing -> pure Null
|
||||||
Just row ->
|
Just row ->
|
||||||
colErrorsToUserError $
|
pure $
|
||||||
rowToGraphQL
|
rowToGraphQL
|
||||||
dbId
|
dbId
|
||||||
tableEntry
|
tableEntry
|
||||||
|
@ -835,9 +817,7 @@ mutationType connection maxRowsPerTable accessMode dbId tables = do
|
||||||
|
|
||||||
mutationResponse :: TableEntry -> Int -> [[SQLData]] -> IO Value
|
mutationResponse :: TableEntry -> Int -> [[SQLData]] -> IO Value
|
||||||
mutationResponse table numChanges rows = do
|
mutationResponse table numChanges rows = do
|
||||||
returning <-
|
let returning = rowsToGraphQL dbId table rows
|
||||||
colErrorsToUserError $
|
|
||||||
rowsToGraphQL dbId table rows
|
|
||||||
|
|
||||||
pure $
|
pure $
|
||||||
Object $
|
Object $
|
||||||
|
@ -848,11 +828,9 @@ mutationType connection maxRowsPerTable accessMode dbId tables = do
|
||||||
|
|
||||||
mutationByPKResponse :: TableEntry -> Int -> Maybe [SQLData] -> IO Value
|
mutationByPKResponse :: TableEntry -> Int -> Maybe [SQLData] -> IO Value
|
||||||
mutationByPKResponse table numChanges mbRow = do
|
mutationByPKResponse table numChanges mbRow = do
|
||||||
returning <- case mbRow of
|
let returning = case mbRow of
|
||||||
Nothing -> pure Null
|
Nothing -> Null
|
||||||
Just row ->
|
Just row -> rowToGraphQL dbId table row
|
||||||
colErrorsToUserError $
|
|
||||||
rowToGraphQL dbId table row
|
|
||||||
|
|
||||||
pure $
|
pure $
|
||||||
Object $
|
Object $
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
||||||
|
|
||||||
|
{-# HLINT ignore "Replace case with maybe" #-}
|
||||||
module AirGQL.Introspection.Resolver (
|
module AirGQL.Introspection.Resolver (
|
||||||
makeType,
|
makeType,
|
||||||
makeConstField,
|
makeConstField,
|
||||||
|
@ -8,9 +11,9 @@ import Protolude (
|
||||||
Either (Left),
|
Either (Left),
|
||||||
IO,
|
IO,
|
||||||
Int,
|
Int,
|
||||||
|
Maybe (Just, Nothing),
|
||||||
MonadReader (ask),
|
MonadReader (ask),
|
||||||
Text,
|
Text,
|
||||||
fromMaybe,
|
|
||||||
pure,
|
pure,
|
||||||
show,
|
show,
|
||||||
($),
|
($),
|
||||||
|
@ -23,7 +26,11 @@ import Protolude (
|
||||||
import Protolude qualified as P
|
import Protolude qualified as P
|
||||||
|
|
||||||
import AirGQL.Introspection.Types qualified as IType
|
import AirGQL.Introspection.Types qualified as IType
|
||||||
|
import Control.Exception qualified as Exception
|
||||||
import Data.HashMap.Strict qualified as HashMap
|
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 qualified as Type
|
||||||
import Language.GraphQL.Type.In qualified as In
|
import Language.GraphQL.Type.In qualified as In
|
||||||
import Language.GraphQL.Type.Out qualified as Out
|
import Language.GraphQL.Type.Out qualified as Out
|
||||||
|
@ -32,6 +39,14 @@ import Language.GraphQL.Type.Out qualified as Out
|
||||||
type Result = Either Text
|
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
|
{-| 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
|
types. Child resolvers look up their respective fields in the value produced by
|
||||||
their parent.
|
their parent.
|
||||||
|
@ -123,18 +138,23 @@ makeType =
|
||||||
let defaultValue =
|
let defaultValue =
|
||||||
if Out.isNonNullType ty
|
if Out.isNonNullType ty
|
||||||
then
|
then
|
||||||
Type.String $
|
throwResolverError $
|
||||||
"Error: field '"
|
"Error: field '"
|
||||||
<> field.name
|
<> field.name
|
||||||
<> "' not found "
|
<> "' not found "
|
||||||
else Type.Null
|
else pure Type.Null
|
||||||
|
|
||||||
case context.values of
|
case context.values of
|
||||||
Type.Object obj ->
|
Type.Object obj -> do
|
||||||
pure $
|
let errorValue = HashMap.lookup ("__error_" <> field.name) obj
|
||||||
fromMaybe defaultValue $
|
P.for_ errorValue $ \case
|
||||||
HashMap.lookup field.name obj
|
Type.String err -> throwResolverError err
|
||||||
_ -> pure defaultValue
|
_ -> pure ()
|
||||||
|
|
||||||
|
case HashMap.lookup field.name obj of
|
||||||
|
Just value -> pure value
|
||||||
|
Nothing -> defaultValue
|
||||||
|
_ -> defaultValue
|
||||||
in
|
in
|
||||||
makeTypeWithDepth 0
|
makeTypeWithDepth 0
|
||||||
|
|
||||||
|
|
|
@ -820,15 +820,28 @@ main = void $ do
|
||||||
rmSpaces
|
rmSpaces
|
||||||
[raw|
|
[raw|
|
||||||
{
|
{
|
||||||
"data": null,
|
"data": {
|
||||||
|
"test": [{
|
||||||
|
"alsobig": null,
|
||||||
|
"big": null
|
||||||
|
}]
|
||||||
|
},
|
||||||
"errors": [{
|
"errors": [{
|
||||||
"locations": [{
|
"locations": [{
|
||||||
"column": 3,
|
"column": 5,
|
||||||
"line": 2
|
"line": 3
|
||||||
}],
|
}],
|
||||||
"message":
|
"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)",
|
"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"]
|
"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