mirror of
https://github.com/Airsequel/AirGQL.git
synced 2025-09-23 04:24:31 +02:00
- I realised by PK querying doesn't need arguments like filter, limit or offset, so I removed the argument name collision logic I added in the last commit - I implemented the actual logic for querying by PK. The code is a lot shorter due to the new introspection system - I moved the normal queries to the new introspection system for consistency. - I moved the logic of "coerce columns without a datatype" away from the resolver and into the "sql -> gql" converter as to be able to reuse the default introspection resolver which simply looks up fields into the parent resolver's result. - I added some comments documenting the Introspection.Resolver stuff and it's behaviour
202 lines
6.9 KiB
Haskell
202 lines
6.9 KiB
Haskell
module AirGQL.Introspection.Resolver (
|
|
makeType,
|
|
makeConstField,
|
|
makeField,
|
|
) where
|
|
|
|
import Protolude (
|
|
Either (Left),
|
|
IO,
|
|
Int,
|
|
MonadReader (ask),
|
|
Text,
|
|
fromMaybe,
|
|
pure,
|
|
show,
|
|
($),
|
|
(+),
|
|
(<$>),
|
|
(<&>),
|
|
(<>),
|
|
(>=),
|
|
)
|
|
import Protolude qualified as P
|
|
|
|
import AirGQL.Introspection.Types qualified as IType
|
|
import Data.HashMap.Strict qualified as HashMap
|
|
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
|
|
|
|
|
|
{-| 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.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 ty.kind
|
|
|
|
-- Creates a field which looks up it's value in the object returned by the
|
|
-- parent resolver.
|
|
makeChildField :: Int -> IType.Field -> Result (Out.Resolver IO)
|
|
makeChildField depth field = do
|
|
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
|
|
Type.String $
|
|
"Error: field '"
|
|
<> field.name
|
|
<> "' not found "
|
|
else Type.Null
|
|
|
|
case context.values of
|
|
Type.Object obj ->
|
|
pure $
|
|
fromMaybe defaultValue $
|
|
HashMap.lookup field.name obj
|
|
_ -> pure defaultValue
|
|
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 by calling 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 ty.kind
|