mirror of
https://github.com/Airsequel/AirGQL.git
synced 2025-07-26 18:08:43 +03:00
224 lines
7.8 KiB
Haskell
224 lines
7.8 KiB
Haskell
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
|
|
|
{-# HLINT ignore "Replace case with maybe" #-}
|
|
module AirGQL.Introspection.Resolver (
|
|
makeType,
|
|
makeConstField,
|
|
makeField,
|
|
) where
|
|
|
|
import Protolude (
|
|
Either (Left),
|
|
IO,
|
|
Int,
|
|
Maybe (Just, Nothing),
|
|
MonadReader (ask),
|
|
Text,
|
|
pure,
|
|
show,
|
|
($),
|
|
(+),
|
|
(<$>),
|
|
(<&>),
|
|
(<>),
|
|
(>=),
|
|
)
|
|
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.Class (ToGraphQL (toGraphQL))
|
|
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
|
|
|
|
|
|
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.
|
|
|
|
Lookups for `__Type` objects are memoized, and a maximum depth of 30 is enforced.
|
|
-}
|
|
makeType :: IType.IntrospectionType -> Result (Out.Type IO)
|
|
makeType =
|
|
let
|
|
-- This is the same as `makeTypeWithDepth`, except the outputs
|
|
-- for `__Type` are memoized.
|
|
--
|
|
-- This turns the memory usage from O(C^n) to O(n), where n is the depth.
|
|
-- This greatly speeds up introspection from the playground, which requires
|
|
-- a depth of around 10 (from what I can recall).
|
|
makeTypeWithDepthMemo :: Int -> IType.IntrospectionType -> Result (Out.Type IO)
|
|
makeTypeWithDepthMemo depth ty = case ty.kind of
|
|
IType.Object "__Type" _ ->
|
|
P.join $ P.note "(impossible)" $ P.atMay typeCache depth
|
|
_ -> makeTypeWithDepth depth ty
|
|
|
|
-- The memoization is done using a haskell lazy array.
|
|
typeCache = [makeTypeWithDepth i IType.typeIntrospectionType | i <- [0 ..]]
|
|
|
|
makeTypeWithDepth :: Int -> IType.IntrospectionType -> Result (Out.Type IO)
|
|
makeTypeWithDepth depth ty =
|
|
case ty.kind of
|
|
IType.Scalar name -> do
|
|
pure $ Out.NamedScalarType $ Type.ScalarType name ty.description
|
|
IType.List ofType -> do
|
|
Out.ListType <$> makeTypeWithDepthMemo depth ofType
|
|
IType.Enum name enumValues -> do
|
|
let variants =
|
|
enumValues
|
|
<&> \variant -> (variant.name, Type.EnumValue variant.description)
|
|
pure $
|
|
Out.NamedEnumType $
|
|
Type.EnumType name ty.description $
|
|
HashMap.fromList variants
|
|
IType.NonNull ofType -> do
|
|
inner <- makeTypeWithDepthMemo depth ofType
|
|
pure $ case inner of
|
|
Out.EnumBaseType enumType -> Out.NonNullEnumType enumType
|
|
Out.UnionBaseType unionType -> Out.NonNullUnionType unionType
|
|
Out.ScalarBaseType scalarType -> Out.NonNullScalarType scalarType
|
|
Out.ObjectBaseType objectType -> Out.NonNullObjectType objectType
|
|
Out.ListBaseType listType -> Out.NonNullListType listType
|
|
Out.InterfaceBaseType interfaceType -> Out.NonNullInterfaceType interfaceType
|
|
IType.Object name fields -> do
|
|
resolvers <- P.for fields $ \field -> do
|
|
resolver <-
|
|
if depth >= 30
|
|
then
|
|
makeConstField
|
|
(IType.field field.name $ IType.nonNull IType.typeString)
|
|
(Type.String "Maximum depth exceeded")
|
|
else makeChildField (depth + 1) field
|
|
pure (field.name, resolver)
|
|
|
|
typenameResolver <-
|
|
makeConstField
|
|
(IType.field "__typename" $ IType.nonNull IType.typeString)
|
|
(Type.String name)
|
|
|
|
pure $
|
|
Out.NamedObjectType $
|
|
Type.ObjectType name ty.description [] $
|
|
HashMap.fromList $
|
|
("__typename", typenameResolver) : resolvers
|
|
_ -> do
|
|
Left $ "invalid type in out position: " <> show (toGraphQL ty)
|
|
|
|
-- Creates a field which looks up its value in the object returned by the
|
|
-- parent resolver.
|
|
makeChildField :: Int -> IType.Field -> Result (Out.Resolver IO)
|
|
makeChildField depth field = do
|
|
-- These lines are the same as `makeField`, except calling
|
|
-- `makeTypeWithDepthMemo` instead of `makeType`
|
|
args <- P.for field.args $ \arg -> do
|
|
ty <- makeInType arg.type_
|
|
pure (arg.name, In.Argument arg.description ty arg.defaultValue)
|
|
ty <- makeTypeWithDepthMemo depth field.type_
|
|
let gqlField = Out.Field field.description ty $ HashMap.fromList args
|
|
|
|
pure $ Out.ValueResolver gqlField $ do
|
|
context <- ask
|
|
|
|
let defaultValue =
|
|
if Out.isNonNullType ty
|
|
then
|
|
throwResolverError $
|
|
"Error: field '"
|
|
<> field.name
|
|
<> "' not found "
|
|
else pure Type.Null
|
|
|
|
result <- case context.values of
|
|
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
|
|
|
|
field.customResolver result
|
|
in
|
|
makeTypeWithDepth 0
|
|
|
|
|
|
{-| Turns a field descriptor into a graphql field. See the documentation
|
|
for `makeType` for details about the behaviour of child resolvers.
|
|
-}
|
|
makeField :: IType.Field -> Result (Out.Field IO)
|
|
makeField field = do
|
|
args <- P.for field.args $ \arg -> do
|
|
ty <- makeInType arg.type_
|
|
pure (arg.name, In.Argument arg.description ty arg.defaultValue)
|
|
ty <- makeType field.type_
|
|
pure $ Out.Field field.description ty $ HashMap.fromList args
|
|
|
|
|
|
-- | Create a resolver which always returns a constant value.
|
|
makeConstField :: IType.Field -> Type.Value -> Result (Out.Resolver IO)
|
|
makeConstField field value = do
|
|
gqlField <- makeField field
|
|
pure $ Out.ValueResolver gqlField $ pure value
|
|
|
|
|
|
{-| The input-type version of `makeOutType`. No maximum depth is enforced, nor
|
|
is any memoization used. This is the case because input types are usually pretty
|
|
shallow.
|
|
-}
|
|
makeInType :: IType.IntrospectionType -> Result In.Type
|
|
makeInType ty = do
|
|
case ty.kind of
|
|
IType.Scalar name -> do
|
|
pure $ In.NamedScalarType $ Type.ScalarType name ty.description
|
|
IType.List ofType -> do
|
|
In.ListType <$> makeInType ofType
|
|
IType.Enum name enumValues -> do
|
|
let variants = enumValues <&> \variant -> (variant.name, Type.EnumValue variant.description)
|
|
pure $
|
|
In.NamedEnumType $
|
|
Type.EnumType name ty.description $
|
|
HashMap.fromList variants
|
|
IType.NonNull ofType -> do
|
|
inner <- makeInType ofType
|
|
pure $ case inner of
|
|
In.EnumBaseType enumType -> In.NonNullEnumType enumType
|
|
In.ScalarBaseType scalarType -> In.NonNullScalarType scalarType
|
|
In.InputObjectBaseType objectType -> In.NonNullInputObjectType objectType
|
|
In.ListBaseType listType -> In.NonNullListType listType
|
|
IType.InputObject name fields -> do
|
|
gqlFields <- P.for fields $ \field -> do
|
|
inner <- makeInType field.type_
|
|
let inputField =
|
|
In.InputField
|
|
field.description
|
|
inner
|
|
field.defaultValue
|
|
pure (field.name, inputField)
|
|
|
|
pure
|
|
$ In.NamedInputObjectType
|
|
$ Type.InputObjectType
|
|
name
|
|
ty.description
|
|
$ HashMap.fromList gqlFields
|
|
_ -> do
|
|
Left $ "invalid type in input position: " <> show (toGraphQL ty)
|