From ccf518c9eb33438a871295c616dd9352c59c9fab Mon Sep 17 00:00:00 2001 From: prescientmoon <git@moonythm.dev> Date: Tue, 8 Oct 2024 05:40:53 +0200 Subject: [PATCH] Make introspection work with new graphiql verison --- source/AirGQL/Introspection.hs | 8 +- source/AirGQL/Introspection/Resolver.hs | 238 ++++++++++++------------ source/AirGQL/Introspection/Types.hs | 183 +++++++++--------- 3 files changed, 216 insertions(+), 213 deletions(-) diff --git a/source/AirGQL/Introspection.hs b/source/AirGQL/Introspection.hs index 280a275..5def2ba 100644 --- a/source/AirGQL/Introspection.hs +++ b/source/AirGQL/Introspection.hs @@ -12,7 +12,6 @@ import Protolude ( Monoid (mempty), Semigroup ((<>)), Text, - fromMaybe, ($), (&), (<&>), @@ -58,7 +57,10 @@ typeNameResolver = columnTypeName :: ColumnEntry -> Text -columnTypeName entry = fromMaybe "String" (columnType entry).name +columnTypeName entry = + case entry.datatype_gql of + Nothing -> "String" + Just type_ -> type_.full columnType :: ColumnEntry -> IntrospectionType @@ -284,7 +286,7 @@ tableUpdateField accessMode table = do & Type.withArguments [ Type.inputValue "set" - (Type.nonNull $ Type.nonNull updateRow) + (Type.nonNull updateRow) & Type.inputValueWithDescription "Fields to be updated" , Type.inputValue "filter" diff --git a/source/AirGQL/Introspection/Resolver.hs b/source/AirGQL/Introspection/Resolver.hs index f030c65..ff90ac5 100644 --- a/source/AirGQL/Introspection/Resolver.hs +++ b/source/AirGQL/Introspection/Resolver.hs @@ -1,4 +1,4 @@ -module AirGQL.Introspection.Resolver (makeType, makeConstField, makeChildField) where +module AirGQL.Introspection.Resolver (makeType, makeConstField) where import Protolude ( Either (Left), @@ -8,8 +8,8 @@ import Protolude ( Text, fromMaybe, mempty, - note, pure, + show, ($), (+), (<$>), @@ -29,150 +29,144 @@ import Language.GraphQL.Type.Out qualified as Out type Result = Either Text -maxDepth :: Int -maxDepth = 9 - - makeType :: IType.IntrospectionType -> Result (Out.Type IO) -makeType = makeTypeWithDepth 0 +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 = do - case ty.kind of - IType.Scalar -> do - name <- note "No `name` found for scalar" ty.name - pure $ Out.NamedScalarType $ Type.ScalarType name ty.description - IType.List -> do - ofType <- note "No `ofType` found for list" ty.ofType - Out.ListType <$> makeTypeWithDepth depth ofType - IType.Enum -> do - name <- note "No `name` found for enum" ty.name - enumValues <- note "No `enumValues` found for enum" ty.enumValues - let variants = enumValues <&> \variant -> (variant.name, Type.EnumValue variant.description) - pure $ - Out.NamedEnumType $ - Type.EnumType name ty.description $ - HashMap.fromList variants - IType.NonNull -> do - ofType <- note "No `ofType` found for nonnull" ty.ofType - inner <- makeTypeWithDepth 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.InputObject -> do - Left "input object in out position" - IType.Object -> do - name <- note "No `name` found for object" ty.name - fields <- note "No `fields` found for object" ty.fields - resolvers <- - if depth >= maxDepth - then pure [] - else P.for fields $ \field -> do - resolver <- makeChildFieldWithDepth (depth + 1) field + 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 <- - makeConstFieldWithDepth - depth - (IType.field "__typename" $ IType.nonNull IType.typeString) - (Type.String name) + 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 + pure + $ Out.NamedObjectType + $ Type.ObjectType + name + -- ty.description + P.Nothing + [] + $ 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 makeConstField :: IType.Field -> Type.Value -> Result (Out.Resolver IO) -makeConstField = makeConstFieldWithDepth 0 - - -makeConstFieldWithDepth :: Int -> IType.Field -> Type.Value -> Result (Out.Resolver IO) -makeConstFieldWithDepth depth field value = do - ty <- makeTypeWithDepth depth field.type_ +makeConstField field value = do + ty <- makeType field.type_ let gqlField = Out.Field field.description ty mempty pure $ Out.ValueResolver gqlField $ pure value -makeChildField :: IType.Field -> Result (Out.Resolver IO) -makeChildField = makeChildFieldWithDepth 0 - - -makeChildFieldWithDepth :: Int -> IType.Field -> Result (Out.Resolver IO) -makeChildFieldWithDepth depth field = do - args <- P.for field.args $ \arg -> do - ty <- makeInTypeWithDepth depth arg.type_ - pure (arg.name, In.Argument arg.description ty arg.defaultValue) - ty <- makeTypeWithDepth 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 - - -makeInTypeWithDepth :: Int -> IType.IntrospectionType -> Result In.Type -makeInTypeWithDepth depth ty = do +makeInType :: IType.IntrospectionType -> Result In.Type +makeInType ty = do case ty.kind of - IType.Scalar -> do - name <- note "No `name` found for scalar" ty.name + IType.Scalar name -> do pure $ In.NamedScalarType $ Type.ScalarType name ty.description - IType.List -> do - ofType <- note "No `ofType` found for list" ty.ofType - In.ListType <$> makeInTypeWithDepth depth ofType - IType.Enum -> do - name <- note "No `name` found for enum" ty.name - enumValues <- note "No `enumValues` found for enum" ty.enumValues + 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 -> do - ofType <- note "No `ofType` found for nonnull" ty.ofType - inner <- makeInTypeWithDepth depth ofType + 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.Object -> do - Left "out object in input position" - IType.InputObject -> do - name <- note "No `name` found for object" ty.name - fields <- note "No `inputFields` found for object" ty.inputFields - gqlFields <- - if depth >= maxDepth - then pure [] - else P.for fields $ \field -> do - inner <- makeInTypeWithDepth (depth + 1) field.type_ - let inputField = - In.InputField - field.description - inner - field.defaultValue - pure (field.name, inputField) + 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 @@ -180,3 +174,5 @@ makeInTypeWithDepth depth ty = do name ty.description $ HashMap.fromList gqlFields + _ -> do + Left $ "invalid type in input position: " <> show ty.kind diff --git a/source/AirGQL/Introspection/Types.hs b/source/AirGQL/Introspection/Types.hs index ed8ecc4..d011505 100644 --- a/source/AirGQL/Introspection/Types.hs +++ b/source/AirGQL/Introspection/Types.hs @@ -1,5 +1,6 @@ module AirGQL.Introspection.Types ( Schema (..), + Name, IntrospectionType (..), TypeKind (..), Field (..), @@ -11,7 +12,6 @@ module AirGQL.Introspection.Types ( withArguments, inputValue, inputValueWithDescription, - withName, withDescription, fieldWithDescription, scalar, @@ -46,6 +46,7 @@ import Protolude ( execState, for_, not, + pure, show, when, ($), @@ -126,47 +127,83 @@ typeDirectiveLocation = ] -data TypeKind = Scalar | Object | Enum | InputObject | List | NonNull +-- | The name of a graphql type. +type Name = Text + + +data TypeKind + = Scalar Name + | Object Name [Field] + | Enum Name [EnumValue] + | InputObject Name [InputValue] + | List IntrospectionType + | NonNull IntrospectionType deriving (Show, Generic) --- $(deriveToGraphQL ''TypeKind) -instance ToGraphQL TypeKind where - toGraphQL Scalar = Value.Enum "SCALAR" - toGraphQL Object = Value.Enum "OBJECT" - toGraphQL Enum = Value.Enum "ENUM" - toGraphQL InputObject = Value.Enum "INPUT_OBJECT" - toGraphQL List = Value.Enum "LIST" - toGraphQL NonNull = Value.Enum "NON_NULL" - - data IntrospectionType = IType { kind :: TypeKind - , name :: Maybe Text , description :: Maybe Text - , interfaces :: Maybe [IntrospectionType] - , possibleTypes :: Maybe [IntrospectionType] - , fields :: Maybe [Field] - , inputFields :: Maybe [InputValue] - , enumValues :: Maybe [EnumValue] - , ofType :: Maybe IntrospectionType } deriving (Show, Generic) instance ToGraphQL IntrospectionType where - toGraphQL ty = + toGraphQL ty = do Value.Object $ HashMap.fromList - [ ("kind", toGraphQL ty.kind) - , ("name", toGraphQL ty.name) + [ + ( "kind" + , case ty.kind of + Scalar _ -> Value.Enum "SCALAR" + Object _ _ -> Value.Enum "OBJECT" + Enum _ _ -> Value.Enum "ENUM" + InputObject _ _ -> Value.Enum "INPUT_OBJECT" + List _ -> Value.Enum "LIST" + NonNull _ -> Value.Enum "NON_NULL" + ) + , + ( "name" + , case ty.kind of + Scalar name -> Value.String name + Object name _ -> Value.String name + Enum name _ -> Value.String name + InputObject name _ -> Value.String name + _ -> Value.Null + ) , ("description", toGraphQL ty.description) - , ("interfaces", toGraphQL ty.interfaces) - , ("possibleTypes", toGraphQL ty.possibleTypes) - , ("fields", toGraphQL ty.fields) - , ("enumValues", toGraphQL ty.enumValues) - , ("inputFields", toGraphQL ty.inputFields) - , ("ofType", toGraphQL ty.ofType) + , + ( "interfaces" + , case ty.kind of + Object _ _ -> Value.List [] + _ -> Value.Null + ) + , ("possibleTypes", Value.Null) + , + ( "fields" + , case ty.kind of + Object _ fields -> toGraphQL fields + _ -> Value.Null + ) + , + ( "enumValues" + , case ty.kind of + Enum _ variants -> toGraphQL variants + _ -> Value.Null + ) + , + ( "inputFields" + , case ty.kind of + InputObject _ fields -> toGraphQL fields + _ -> Value.Null + ) + , + ( "ofType" + , case ty.kind of + NonNull inner -> toGraphQL inner + List inner -> toGraphQL inner + _ -> Value.Null + ) ] @@ -199,67 +236,32 @@ typeIntrospectionType = ] -emptyType :: TypeKind -> IntrospectionType -emptyType kind = +mkType :: TypeKind -> IntrospectionType +mkType kind = IType { kind - , name = Nothing , description = Nothing - , interfaces = Nothing - , possibleTypes = Nothing - , fields = Nothing - , enumValues = Nothing - , inputFields = Nothing - , ofType = Nothing } nonNull :: IntrospectionType -> IntrospectionType -nonNull ty = - (emptyType NonNull) - { ofType = Just ty - } +nonNull ty = mkType $ NonNull ty list :: IntrospectionType -> IntrospectionType -list ty = - (emptyType List) - { ofType = Just ty - } +list ty = mkType $ List ty object :: Text -> [Field] -> IntrospectionType -object name fields = - (emptyType Object) - { fields = Just fields - , name = Just name - , interfaces = Just [] - } +object name fields = mkType $ Object name fields inputObject :: Text -> [InputValue] -> IntrospectionType -inputObject name fields = - (emptyType InputObject) - { inputFields = Just fields - , name = Just name - , interfaces = Just [] - } +inputObject name fields = mkType $ InputObject name fields enum :: Text -> [EnumValue] -> IntrospectionType -enum name variants = - (emptyType Enum) - { enumValues = Just variants - , name = Just name - } - - -withName :: Text -> IntrospectionType -> IntrospectionType -withName newName (IType{..}) = - IType - { name = Just newName - , .. - } +enum name variants = mkType $ Enum name variants withDescription :: Text -> IntrospectionType -> IntrospectionType @@ -271,9 +273,7 @@ withDescription newDesc (IType{..}) = scalar :: Text -> IntrospectionType -scalar tyName = - emptyType Scalar - & withName tyName +scalar tyName = mkType $ Scalar tyName data Field = Field @@ -463,19 +463,24 @@ collectSchemaTypes schema = do -- | Collect a map of all the named types occurring inside a type collectTypes :: IntrospectionType -> State (HashMap Text IntrospectionType) () collectTypes ty = do - for_ ty.ofType collectTypes - for_ ty.name $ \name -> do - current <- get - when (not $ HashMap.member name current) $ do - put $ HashMap.insert name ty current - for_ ty.interfaces $ \interfaces -> do - for_ interfaces collectTypes - for_ ty.possibleTypes $ \possibleTypes -> do - for_ possibleTypes collectTypes - for_ ty.inputFields $ \inputFields -> do - for_ inputFields $ \thisField -> collectTypes thisField.type_ - for_ ty.fields $ \fields -> do - for_ fields $ \thisField -> do - collectTypes thisField.type_ - for_ thisField.args $ \arg -> - collectTypes arg.type_ + -- Gives a name to the current type, and saves it. + -- + -- If the type hadn't been found already, runs a custom continuation. + let insertType name continue = do + current <- get + when (not $ HashMap.member name current) $ do + put $ HashMap.insert name ty current + continue + + case ty.kind of + NonNull inner -> collectTypes inner + List inner -> collectTypes inner + Enum name _ -> insertType name $ pure () + Scalar name -> insertType name $ pure () + Object name fields -> insertType name $ do + for_ fields $ \thisField -> do + collectTypes thisField.type_ + for_ thisField.args $ \arg -> + collectTypes arg.type_ + InputObject name fields -> insertType name $ do + for_ fields $ \thisField -> collectTypes thisField.type_