{-# 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)