From c0d41a56ce81bb8ce385a56a9e36ac8f0f05df23 Mon Sep 17 00:00:00 2001 From: Eugen Wissner Date: Tue, 31 Aug 2021 17:18:48 +0200 Subject: [PATCH] Show the value and expected type in value completion errors --- src/Language/GraphQL/Execute.hs | 896 ++++++++++++++++++++++++++++++-- 1 file changed, 843 insertions(+), 53 deletions(-) diff --git a/src/Language/GraphQL/Execute.hs b/src/Language/GraphQL/Execute.hs index 62754a3..1ccdee0 100644 --- a/src/Language/GraphQL/Execute.hs +++ b/src/Language/GraphQL/Execute.hs @@ -1,71 +1,861 @@ -{-# LANGUAGE ExplicitForAll #-} +{- This Source Code Form is subject to the terms of the Mozilla Public License, + v. 2.0. If a copy of the MPL was not distributed with this file, You can + obtain one at https://mozilla.org/MPL/2.0/. -} + +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} --- | This module provides functions to execute a @GraphQL@ request. module Language.GraphQL.Execute - ( execute - , module Language.GraphQL.Execute.Coerce - ) where + ( Error(..) + , Operation(..) + , Path(..) + , Response(..) + , execute + ) where -import Control.Monad.Catch (MonadCatch) -import Data.HashMap.Strict (HashMap) -import Data.Sequence (Seq(..)) -import Data.Text (Text) +import Conduit (mapMC, (.|)) +import Control.Arrow (left) +import Control.Monad.Catch + ( Exception(..) + , MonadCatch(..) + , MonadThrow(..) + , SomeException(..) + ) +import Control.Monad.Trans.Class (MonadTrans(..)) +import Control.Monad.Trans.Reader (ReaderT(..), ask, local, runReaderT) +import Control.Monad.Trans.Writer (WriterT(..), runWriterT, tell) +import qualified Control.Monad.Trans.Reader as Reader +import Control.Monad (foldM) import qualified Language.GraphQL.AST.Document as Full -import Language.GraphQL.Execute.Coerce -import Language.GraphQL.Execute.Execution -import Language.GraphQL.Execute.Internal -import qualified Language.GraphQL.Execute.Transform as Transform -import qualified Language.GraphQL.Execute.Subscribe as Subscribe -import Language.GraphQL.Error - ( Error - , ResponseEventStream - , Response(..) - , runCollectErrs - ) -import qualified Language.GraphQL.Type.Definition as Definition +import Data.Bifunctor (first) +import Data.Foldable (find) +import Data.Functor ((<&>)) +import Data.HashMap.Strict (HashMap) +import qualified Data.HashMap.Strict as HashMap +import Data.HashSet (HashSet) +import qualified Data.HashSet as HashSet +import Data.Int (Int32) +import Data.List.NonEmpty (NonEmpty(..)) +import qualified Data.List.NonEmpty as NonEmpty +import Data.Maybe (fromMaybe, isJust) +import Data.Sequence (Seq, (><)) +import qualified Data.Sequence as Seq +import Data.Text (Text) +import qualified Data.Text as Text +import Data.Typeable (cast) +import qualified Language.GraphQL.Execute.Coerce as Coerce +import Language.GraphQL.Execute.OrderedMap (OrderedMap) +import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap +import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.Out as Out -import Language.GraphQL.Type.Schema -import Prelude hiding (null) +import qualified Language.GraphQL.Type as Type +import qualified Language.GraphQL.Type.Internal as Type.Internal +import Language.GraphQL.Type.Schema (Schema, Type) +import qualified Language.GraphQL.Type.Schema as Schema +import Language.GraphQL.Error + ( Error(..) + , Response(..) + , Path(..) + , ResponseEventStream + ) --- | The substitution is applied to the document, and the resolvers are applied --- to the resulting fields. The operation name can be used if the document --- defines multiple root operations. --- --- Returns the result of the query against the schema wrapped in a /data/ --- field, or errors wrapped in an /errors/ field. -execute :: (MonadCatch m, VariableValue a, Serialize b) +data Replacement m = Replacement + { variableValues :: Type.Subs + , fragmentDefinitions :: HashMap Full.Name Full.FragmentDefinition + , visitedFragments :: HashSet Full.Name + , types :: HashMap Full.Name (Type m) + } + +newtype TransformT m a = TransformT + { runTransformT :: ReaderT (Replacement m) m a + } + +instance Functor m => Functor (TransformT m) where + fmap f = TransformT . fmap f . runTransformT + +instance Applicative m => Applicative (TransformT m) where + pure = TransformT . pure + TransformT f <*> TransformT x = TransformT $ f <*> x + +instance Monad m => Monad (TransformT m) where + TransformT x >>= f = TransformT $ x >>= runTransformT . f + +instance MonadTrans TransformT where + lift = TransformT . lift + +instance MonadThrow m => MonadThrow (TransformT m) where + throwM = lift . throwM + +instance MonadCatch m => MonadCatch (TransformT m) where + catch (TransformT stack) handler = + TransformT $ catch stack $ runTransformT . handler + +newtype ExecutorT m a = ExecutorT + { runExecutorT :: ReaderT (HashMap Full.Name (Type m)) (WriterT (Seq Error) m) a + } + +instance Functor m => Functor (ExecutorT m) where + fmap f = ExecutorT . fmap f . runExecutorT + +instance Applicative m => Applicative (ExecutorT m) where + pure = ExecutorT . pure + ExecutorT f <*> ExecutorT x = ExecutorT $ f <*> x + +instance Monad m => Monad (ExecutorT m) where + ExecutorT x >>= f = ExecutorT $ x >>= runExecutorT . f + +instance MonadTrans ExecutorT where + lift = ExecutorT . lift . lift + +instance MonadThrow m => MonadThrow (ExecutorT m) where + throwM = lift . throwM + +instance MonadCatch m => MonadCatch (ExecutorT m) where + catch (ExecutorT stack) handler = + ExecutorT $ catch stack $ runExecutorT . handler + +data GraphQLException = forall e. Exception e => GraphQLException e + +instance Show GraphQLException where + show (GraphQLException e) = show e + +instance Exception GraphQLException + +graphQLExceptionToException :: Exception e => e -> SomeException +graphQLExceptionToException = toException . GraphQLException + +graphQLExceptionFromException :: Exception e => SomeException -> Maybe e +graphQLExceptionFromException e = do + GraphQLException graphqlException <- fromException e + cast graphqlException + +data ResolverException = forall e. Exception e => ResolverException e + +instance Show ResolverException where + show (ResolverException e) = show e + +instance Exception ResolverException where + toException = graphQLExceptionToException + fromException = graphQLExceptionFromException + +data FieldError + = ArgumentTypeError + | MissingArgumentError + | ResultCoercionError + | NullResultError + +instance Show FieldError where + show ArgumentTypeError = "Invalid argument type." + show MissingArgumentError = "Required argument not specified." + show ResultCoercionError = "Result coercion failed." + show NullResultError = "Non-Nullable field resolver returned Null." + +newtype FieldException = FieldException FieldError + deriving Show + +instance Exception FieldException where + toException = graphQLExceptionToException + fromException = graphQLExceptionFromException + +data ValueCompletionException = ValueCompletionException String Type.Value + +instance Show ValueCompletionException where + show (ValueCompletionException typeRepresentation found) = concat + [ "Value completion error. Expected type " + , typeRepresentation + , ", found: " + , show found + , "." + ] + +instance Exception ValueCompletionException where + toException = graphQLExceptionToException + fromException = graphQLExceptionFromException + +data QueryError + = OperationNameRequired + | OperationNotFound String + | CoercionError Full.VariableDefinition + | UnknownInputType Full.VariableDefinition + +asks :: Monad m => forall a. (Replacement m -> a) -> TransformT m a +asks = TransformT . Reader.asks + +queryError :: QueryError -> Error +queryError OperationNameRequired = + Error{ message = "Operation name is required.", locations = [], path = [] } +queryError (OperationNotFound operationName) = + let queryErrorMessage = Text.concat + [ "Operation \"" + , Text.pack operationName + , "\" not found." + ] + in Error{ message = queryErrorMessage, locations = [], path = [] } +queryError (CoercionError variableDefinition) = + let Full.VariableDefinition variableName _ _ location = variableDefinition + queryErrorMessage = Text.concat + [ "Failed to coerce the variable \"" + , variableName + , "\"." + ] + in Error{ message = queryErrorMessage, locations = [location], path = [] } +queryError (UnknownInputType variableDefinition) = + let Full.VariableDefinition variableName variableTypeName _ location = variableDefinition + queryErrorMessage = Text.concat + [ "Variable \"" + , variableName + , "\" has unknown type \"" + , Text.pack $ show variableTypeName + , "\"." + ] + in Error{ message = queryErrorMessage, locations = [location], path = [] } + +data Operation m + = Operation Full.OperationType (Seq (Selection m)) Full.Location + +data Selection m + = FieldSelection (Field m) + | FragmentSelection (Fragment m) + +data Field m = Field + (Maybe Full.Name) + Full.Name + (HashMap Full.Name (Full.Node Input)) + (Seq (Selection m)) + Full.Location + +data Fragment m = Fragment + (Type.Internal.CompositeType m) (Seq (Selection m)) Full.Location + +data Input + = Variable Type.Value + | Int Int32 + | Float Double + | String Text + | Boolean Bool + | Null + | Enum Full.Name + | List [Input] + | Object (HashMap Full.Name Input) + +document :: Full.Document + -> ([Full.OperationDefinition], HashMap Full.Name Full.FragmentDefinition) +document = foldr filterOperation ([], HashMap.empty) + where + filterOperation (Full.ExecutableDefinition executableDefinition) accumulator + | Full.DefinitionOperation operationDefinition' <- executableDefinition = + first (operationDefinition' :) accumulator + | Full.DefinitionFragment fragmentDefinition <- executableDefinition + , Full.FragmentDefinition fragmentName _ _ _ _ <- fragmentDefinition = + HashMap.insert fragmentName fragmentDefinition <$> accumulator + filterOperation _ accumulator = accumulator -- Type system definitions. + +transform :: Monad m => Full.OperationDefinition -> TransformT m (Operation m) +transform (Full.OperationDefinition operationType _ _ _ selectionSet' operationLocation) = do + transformedSelections <- selectionSet selectionSet' + pure $ Operation operationType transformedSelections operationLocation +transform (Full.SelectionSet selectionSet' operationLocation) = do + transformedSelections <- selectionSet selectionSet' + pure $ Operation Full.Query transformedSelections operationLocation + +selectionSet :: Monad m => Full.SelectionSet -> TransformT m (Seq (Selection m)) +selectionSet = selectionSetOpt . NonEmpty.toList + +selectionSetOpt :: Monad m => Full.SelectionSetOpt -> TransformT m (Seq (Selection m)) +selectionSetOpt = foldM go Seq.empty + where + go accumulatedSelections currentSelection = + selection currentSelection <&> (accumulatedSelections ><) + +selection :: Monad m => Full.Selection -> TransformT m (Seq (Selection m)) +selection (Full.FieldSelection field') = + maybeToSelectionSet FieldSelection $ field field' +selection (Full.FragmentSpreadSelection fragmentSpread') = + maybeToSelectionSet FragmentSelection $ fragmentSpread fragmentSpread' +selection (Full.InlineFragmentSelection inlineFragment') = + either id (pure . FragmentSelection) <$> inlineFragment inlineFragment' + +maybeToSelectionSet :: Monad m + => forall a + . (a -> Selection m) + -> TransformT m (Maybe a) + -> TransformT m (Seq (Selection m)) +maybeToSelectionSet selectionType = fmap (maybe Seq.empty $ pure . selectionType) + +directives :: Monad m => [Full.Directive] -> TransformT m (Maybe [Type.Directive]) +directives = fmap Type.selection . traverse directive + +inlineFragment :: Monad m + => Full.InlineFragment + -> TransformT m (Either (Seq (Selection m)) (Fragment m)) +inlineFragment (Full.InlineFragment maybeCondition directives' selectionSet' location) + | Just typeCondition <- maybeCondition = do + transformedSelections <- selectionSet selectionSet' + transformedDirectives <- directives directives' + maybeFragmentType <- asks + $ Type.Internal.lookupTypeCondition typeCondition + . types + pure $ case transformedDirectives >> maybeFragmentType of + Just fragmentType -> Right + $ Fragment fragmentType transformedSelections location + Nothing -> Left Seq.empty + | otherwise = do + transformedSelections <- selectionSet selectionSet' + transformedDirectives <- directives directives' + pure $ if isJust transformedDirectives + then Left transformedSelections + else Left Seq.empty + +fragmentSpread :: Monad m => Full.FragmentSpread -> TransformT m (Maybe (Fragment m)) +fragmentSpread (Full.FragmentSpread spreadName directives' location) = do + transformedDirectives <- directives directives' + visitedFragment <- asks $ HashSet.member spreadName . visitedFragments + possibleFragmentDefinition <- asks + $ HashMap.lookup spreadName + . fragmentDefinitions + case transformedDirectives >> possibleFragmentDefinition of + Just (Full.FragmentDefinition _ typeCondition _ selections _) + | visitedFragment -> pure Nothing + | otherwise -> do + fragmentType <- asks + $ Type.Internal.lookupTypeCondition typeCondition + . types + traverse (traverseSelections selections) fragmentType + Nothing -> pure Nothing + where + traverseSelections selections typeCondition = do + transformedSelections <- TransformT + $ local fragmentInserter + $ runTransformT + $ selectionSet selections + pure $ Fragment typeCondition transformedSelections location + fragmentInserter replacement@Replacement{ visitedFragments } = replacement + { visitedFragments = HashSet.insert spreadName visitedFragments } + +field :: Monad m => Full.Field -> TransformT m (Maybe (Field m)) +field (Full.Field alias' name' arguments' directives' selectionSet' location') = do + transformedSelections <- selectionSetOpt selectionSet' + transformedDirectives <- directives directives' + transformedArguments <- arguments arguments' + let transformedField = Field + alias' + name' + transformedArguments + transformedSelections + location' + pure $ transformedDirectives >> pure transformedField + +arguments :: Monad m => [Full.Argument] -> TransformT m (HashMap Full.Name (Full.Node Input)) +arguments = foldM go HashMap.empty + where + go accumulator (Full.Argument name' valueNode _) = do + argumentValue <- node valueNode + pure $ insertIfGiven name' argumentValue accumulator + +directive :: Monad m => Full.Directive -> TransformT m Type.Directive +directive (Full.Directive name' arguments' _) + = Type.Directive name' + . Type.Arguments + <$> foldM go HashMap.empty arguments' + where + go accumulator (Full.Argument argumentName Full.Node{ node = node' } _) = do + transformedValue <- directiveValue node' + pure $ HashMap.insert argumentName transformedValue accumulator + +directiveValue :: Monad m => Full.Value -> TransformT m Type.Value +directiveValue = \case + (Full.Variable name') -> asks + $ HashMap.lookupDefault Type.Null name' + . variableValues + (Full.Int integer) -> pure $ Type.Int integer + (Full.Float double) -> pure $ Type.Float double + (Full.String string) -> pure $ Type.String string + (Full.Boolean boolean) -> pure $ Type.Boolean boolean + Full.Null -> pure Type.Null + (Full.Enum enum) -> pure $ Type.Enum enum + (Full.List list) -> Type.List <$> traverse directiveNode list + (Full.Object objectFields) -> + Type.Object <$> foldM objectField HashMap.empty objectFields + where + directiveNode Full.Node{ node = node'} = directiveValue node' + objectField accumulator Full.ObjectField{ name, value } = do + transformedValue <- directiveNode value + pure $ HashMap.insert name transformedValue accumulator + +input :: Monad m => Full.Value -> TransformT m (Maybe Input) +input (Full.Variable name') = + asks (HashMap.lookup name' . variableValues) <&> fmap Variable +input (Full.Int integer) = pure $ Just $ Int integer +input (Full.Float double) = pure $ Just $ Float double +input (Full.String string) = pure $ Just $ String string +input (Full.Boolean boolean) = pure $ Just $ Boolean boolean +input Full.Null = pure $ Just Null +input (Full.Enum enum) = pure $ Just $ Enum enum +input (Full.List list) = Just . List + <$> traverse (fmap (fromMaybe Null) . input . Full.node) list +input (Full.Object objectFields) = Just . Object + <$> foldM objectField HashMap.empty objectFields + where + objectField accumulator Full.ObjectField{..} = do + objectFieldValue <- fmap Full.node <$> node value + pure $ insertIfGiven name objectFieldValue accumulator + +insertIfGiven :: forall a + . Full.Name + -> Maybe a + -> HashMap Full.Name a + -> HashMap Full.Name a +insertIfGiven name (Just v) = HashMap.insert name v +insertIfGiven _ _ = id + +node :: Monad m => Full.Node Full.Value -> TransformT m (Maybe (Full.Node Input)) +node Full.Node{node = node', ..} = + traverse Full.Node <$> input node' <*> pure location + +execute :: (MonadCatch m, Coerce.VariableValue a, Coerce.Serialize b) => Schema m -- ^ Resolvers. -> Maybe Text -- ^ Operation name. -> HashMap Full.Name a -- ^ Variable substitution function. -> Full.Document -- @GraphQL@ document. -> m (Either (ResponseEventStream m b) (Response b)) -execute schema' operationName subs document - = either (pure . rightErrorResponse . singleError [] . show) executeRequest - $ Transform.document schema' operationName subs document +execute schema' operationName subs document' = + executeRequest schema' document' (Text.unpack <$> operationName) subs -executeRequest :: (MonadCatch m, Serialize a) - => Transform.Document m +executeRequest :: (MonadCatch m, Coerce.Serialize a, Coerce.VariableValue b) + => Schema m + -> Full.Document + -> Maybe String + -> HashMap Full.Name b -> m (Either (ResponseEventStream m a) (Response a)) -executeRequest (Transform.Document types' rootObjectType operation) - | (Transform.Query _ fields objectLocation) <- operation = - Right <$> executeOperation types' rootObjectType objectLocation fields - | (Transform.Mutation _ fields objectLocation) <- operation = - Right <$> executeOperation types' rootObjectType objectLocation fields - | (Transform.Subscription _ fields objectLocation) <- operation - = either rightErrorResponse Left - <$> Subscribe.subscribe types' rootObjectType objectLocation fields +executeRequest schema sourceDocument operationName variableValues = do + operationAndVariables <- sequence buildOperation + case operationAndVariables of + Left queryError' -> pure + $ Right + $ Response Coerce.null $ pure $ queryError queryError' + Right operation + | Operation Full.Query topSelections _operationLocation <- operation -> + Right <$> executeQuery topSelections schema + | Operation Full.Mutation topSelections operationLocation <- operation -> + Right <$> executeMutation topSelections schema operationLocation + | Operation Full.Subscription topSelections operationLocation <- operation -> + either rightErrorResponse Left <$> subscribe topSelections schema operationLocation + where + schemaTypes = Schema.types schema + (operationDefinitions, fragmentDefinitions') = document sourceDocument + buildOperation = do + operationDefinition <- getOperation operationDefinitions operationName + coercedVariableValues <- coerceVariableValues + schemaTypes + operationDefinition + variableValues + let replacement = Replacement + { variableValues = coercedVariableValues + , fragmentDefinitions = fragmentDefinitions' + , visitedFragments = mempty + , types = schemaTypes + } + pure $ flip runReaderT replacement + $ runTransformT + $ transform operationDefinition --- This is actually executeMutation, but we don't distinguish between queries --- and mutations yet. -executeOperation :: (MonadCatch m, Serialize a) +rightErrorResponse :: Coerce.Serialize b => forall a. Error -> Either a (Response b) +rightErrorResponse = Right . Response Coerce.null . pure + +getOperation :: [Full.OperationDefinition] -> Maybe String -> Either QueryError Full.OperationDefinition +getOperation [operation] Nothing = Right operation +getOperation operations (Just givenOperationName) + = maybe (Left $ OperationNotFound givenOperationName) Right + $ find findOperationByName operations + where + findOperationByName (Full.OperationDefinition _ (Just operationName) _ _ _ _) = + givenOperationName == Text.unpack operationName + findOperationByName _ = False +getOperation _ _ = Left OperationNameRequired + +executeQuery :: (MonadCatch m, Coerce.Serialize a) + => Seq (Selection m) + -> Schema m + -> m (Response a) +executeQuery topSelections schema = do + let queryType = Schema.query schema + (data', errors) <- runWriterT + $ flip runReaderT (Schema.types schema) + $ runExecutorT + $ executeSelectionSet topSelections queryType Type.Null [] + pure $ Response data' errors + +executeMutation :: (MonadCatch m, Coerce.Serialize a) + => Seq (Selection m) + -> Schema m + -> Full.Location + -> m (Response a) +executeMutation topSelections schema operationLocation + | Just mutationType <- Schema.mutation schema = do + (data', errors) <- runWriterT + $ flip runReaderT (Schema.types schema) + $ runExecutorT + $ executeSelectionSet topSelections mutationType Type.Null [] + pure $ Response data' errors + | otherwise = pure + $ Response Coerce.null + $ Seq.singleton + $ Error "Schema doesn't support mutations." [operationLocation] [] + +executeSelectionSet :: (MonadCatch m, Coerce.Serialize a) + => Seq (Selection m) + -> Out.ObjectType m + -> Type.Value + -> [Path] + -> ExecutorT m a +executeSelectionSet selections objectType objectValue errorPath = do + let groupedFieldSet = collectFields objectType selections + resolvedValues <- OrderedMap.traverseMaybe go groupedFieldSet + coerceResult (Out.NonNullObjectType objectType) $ Coerce.Object resolvedValues + where + executeField' fields resolver = + executeField objectValue fields resolver errorPath + Out.ObjectType _ _ _ resolvers = objectType + go fields@(Field _ fieldName _ _ _ :| _) = + traverse (executeField' fields) $ HashMap.lookup fieldName resolvers + +fieldsSegment :: forall m. NonEmpty (Field m) -> Path +fieldsSegment (Field alias fieldName _ _ _ :| _) = + Segment (fromMaybe fieldName alias) + +executeField :: (MonadCatch m, Coerce.Serialize a) + => Type.Value + -> NonEmpty (Field m) + -> Out.Resolver m + -> [Path] + -> ExecutorT m a +executeField objectValue fields resolver errorPath = + let Field _ fieldName inputArguments _ fieldLocation :| _ = fields + in catch (go fieldName inputArguments) $ exceptionHandler fieldLocation + where + exceptionHandler :: (MonadCatch m, Coerce.Serialize a) + => Full.Location + -> GraphQLException + -> ExecutorT m a + exceptionHandler fieldLocation e = + let newError = Error (Text.pack $ displayException e) [fieldLocation] errorPath + in ExecutorT (lift $ tell $ Seq.singleton newError) >> pure Coerce.null + go fieldName inputArguments = do + let (Out.Field _ fieldType argumentTypes, resolveFunction) = + resolverField resolver + argumentValues <- coerceArgumentValues argumentTypes inputArguments + resolvedValue <- + resolveFieldValue resolveFunction objectValue fieldName argumentValues + completeValue fieldType fields errorPath resolvedValue + resolverField (Out.ValueResolver resolverField' resolveFunction) = + (resolverField', resolveFunction) + resolverField (Out.EventStreamResolver resolverField' resolveFunction _) = + (resolverField', resolveFunction) + +resolveFieldValue :: MonadCatch m + => Out.Resolve m + -> Type.Value + -> Full.Name + -> Type.Subs + -> ExecutorT m Type.Value +resolveFieldValue resolver objectValue _fieldName argumentValues = + lift $ runReaderT resolver context + where + context = Type.Context + { Type.arguments = Type.Arguments argumentValues + , Type.values = objectValue + } + +resolveAbstractType :: Monad m + => Type.Internal.AbstractType m + -> Type.Subs + -> ExecutorT m (Maybe (Out.ObjectType m)) +resolveAbstractType abstractType values' + | Just (Type.String typeName) <- HashMap.lookup "__typename" values' = do + types' <- ExecutorT ask + case HashMap.lookup typeName types' of + Just (Type.Internal.ObjectType objectType) -> + if Type.Internal.instanceOf objectType abstractType + then pure $ Just objectType + else pure Nothing + _ -> pure Nothing + | otherwise = pure Nothing + +completeValue :: (MonadCatch m, Coerce.Serialize a) + => Out.Type m + -> NonEmpty (Field m) + -> [Path] + -> Type.Value + -> ExecutorT m a +completeValue outputType _ _ Type.Null + | Out.isNonNullType outputType = throwFieldError NullResultError + | otherwise = pure Coerce.null +completeValue outputType@(Out.ListBaseType listType) fields errorPath (Type.List list) + = foldM go (0, []) list >>= coerceResult outputType . Coerce.List . snd + where + go (index, accumulator) listItem = do + let updatedPath = Index index : errorPath + completedValue <- completeValue listType fields updatedPath listItem + pure (index + 1, completedValue : accumulator) +completeValue outputType@(Out.ScalarBaseType _) _ _ (Type.Int int) = + coerceResult outputType $ Coerce.Int int +completeValue outputType@(Out.ScalarBaseType _) _ _ (Type.Boolean boolean) = + coerceResult outputType $ Coerce.Boolean boolean +completeValue outputType@(Out.ScalarBaseType _) _ _ (Type.Float float) = + coerceResult outputType $ Coerce.Float float +completeValue outputType@(Out.ScalarBaseType _) _ _ (Type.String string) = + coerceResult outputType $ Coerce.String string +completeValue outputType@(Out.EnumBaseType enumType) _ _ (Type.Enum enum) = + let Type.EnumType _ _ enumMembers = enumType + in if HashMap.member enum enumMembers + then coerceResult outputType $ Coerce.Enum enum + else throwM + $ ValueCompletionException (show outputType) + $ Type.Enum enum +completeValue (Out.ObjectBaseType objectType) fields errorPath result + = executeSelectionSet (mergeSelectionSets fields) objectType result + $ fieldsSegment fields : errorPath +completeValue outputType@(Out.InterfaceBaseType interfaceType) fields errorPath result + | Type.Object objectMap <- result = do + let abstractType = Type.Internal.AbstractInterfaceType interfaceType + concreteType <- resolveAbstractType abstractType objectMap + case concreteType of + Just objectType + -> executeSelectionSet (mergeSelectionSets fields) objectType result + $ fieldsSegment fields : errorPath + Nothing -> throwM + $ ValueCompletionException (show outputType) result +completeValue outputType@(Out.UnionBaseType unionType) fields errorPath result + | Type.Object objectMap <- result = do + let abstractType = Type.Internal.AbstractUnionType unionType + concreteType <- resolveAbstractType abstractType objectMap + case concreteType of + Just objectType + -> executeSelectionSet (mergeSelectionSets fields) objectType result + $ fieldsSegment fields : errorPath + Nothing -> throwM + $ ValueCompletionException (show outputType) result +completeValue outputType _ _ result = + throwM $ ValueCompletionException (show outputType) result + +coerceResult :: (MonadCatch m, Coerce.Serialize a) + => Out.Type m + -> Coerce.Output a + -> ExecutorT m a +coerceResult outputType result + | Just serialized <- Coerce.serialize outputType result = pure serialized + | otherwise = throwFieldError ResultCoercionError + +mergeSelectionSets :: MonadCatch m + => NonEmpty (Field m) + -> Seq (Selection m) +mergeSelectionSets = foldr forEach mempty + where + forEach (Field _ _ _ fieldSelectionSet _) selectionSet' = + selectionSet' <> fieldSelectionSet + +throwFieldError :: MonadCatch m => FieldError -> m a +throwFieldError = throwM . FieldException + +coerceArgumentValues :: MonadCatch m + => HashMap Full.Name In.Argument + -> HashMap Full.Name (Full.Node Input) + -> m Type.Subs +coerceArgumentValues argumentDefinitions argumentValues = + HashMap.foldrWithKey c pure argumentDefinitions mempty + where + c argumentName argumentType pure' resultMap = + forEach argumentName argumentType resultMap >>= pure' + forEach :: MonadCatch m + => Full.Name + -> In.Argument + -> Type.Subs + -> m Type.Subs + forEach argumentName (In.Argument _ variableType defaultValue) resultMap = do + let matchedMap + = matchFieldValues' argumentName variableType defaultValue + $ Just resultMap + in case matchedMap of + Just matchedValues -> pure matchedValues + Nothing + | Just _ <- HashMap.lookup argumentName argumentValues -> + throwFieldError ArgumentTypeError + | otherwise -> throwFieldError MissingArgumentError + matchFieldValues' = Coerce.matchFieldValues coerceArgumentValue + $ Full.node <$> argumentValues + coerceArgumentValue inputType (Int integer) = + Coerce.coerceInputLiteral inputType (Type.Int integer) + coerceArgumentValue inputType (Boolean boolean) = + Coerce.coerceInputLiteral inputType (Type.Boolean boolean) + coerceArgumentValue inputType (String string) = + Coerce.coerceInputLiteral inputType (Type.String string) + coerceArgumentValue inputType (Float float) = + Coerce.coerceInputLiteral inputType (Type.Float float) + coerceArgumentValue inputType (Enum enum) = + Coerce.coerceInputLiteral inputType (Type.Enum enum) + coerceArgumentValue inputType Null + | In.isNonNullType inputType = Nothing + | otherwise = Coerce.coerceInputLiteral inputType Type.Null + coerceArgumentValue (In.ListBaseType inputType) (List list) = + let coerceItem = coerceArgumentValue inputType + in Type.List <$> traverse coerceItem list + coerceArgumentValue (In.InputObjectBaseType inputType) (Object object) + | In.InputObjectType _ _ inputFields <- inputType = + let go = forEachField object + resultMap = HashMap.foldrWithKey go (pure mempty) inputFields + in Type.Object <$> resultMap + coerceArgumentValue _ (Variable variable) = pure variable + coerceArgumentValue _ _ = Nothing + forEachField object variableName (In.InputField _ variableType defaultValue) = + Coerce.matchFieldValues coerceArgumentValue object variableName variableType defaultValue + +collectFields :: Monad m + => Out.ObjectType m + -> Seq (Selection m) + -> OrderedMap (NonEmpty (Field m)) +collectFields objectType = foldl forEach OrderedMap.empty + where + forEach groupedFields (FieldSelection fieldSelection) = + let Field maybeAlias fieldName _ _ _ = fieldSelection + responseKey = fromMaybe fieldName maybeAlias + in OrderedMap.insert responseKey (fieldSelection :| []) groupedFields + forEach groupedFields (FragmentSelection selectionFragment) + | Fragment fragmentType fragmentSelectionSet _ <- selectionFragment + , Type.Internal.doesFragmentTypeApply fragmentType objectType = + let fragmentGroupedFieldSet = + collectFields objectType fragmentSelectionSet + in groupedFields <> fragmentGroupedFieldSet + | otherwise = groupedFields + +coerceVariableValues :: (Monad m, Coerce.VariableValue b) + => HashMap Full.Name (Schema.Type m) + -> Full.OperationDefinition + -> HashMap Full.Name b + -> Either QueryError Type.Subs +coerceVariableValues types operationDefinition' variableValues + | Full.OperationDefinition _ _ variableDefinitions _ _ _ <- + operationDefinition' + = foldr forEach (Right HashMap.empty) variableDefinitions + | otherwise = pure mempty + where + forEach variableDefinition (Right coercedValues) = + let Full.VariableDefinition variableName variableTypeName defaultValue _ = + variableDefinition + defaultValue' = constValue . Full.node <$> defaultValue + in case Type.Internal.lookupInputType variableTypeName types of + Just variableType -> + maybe (Left $ CoercionError variableDefinition) Right + $ Coerce.matchFieldValues + coerceVariableValue' + variableValues + variableName + variableType + defaultValue' + $ Just coercedValues + Nothing -> Left $ UnknownInputType variableDefinition + forEach _ coercedValuesOrError = coercedValuesOrError + coerceVariableValue' variableType value' + = Coerce.coerceVariableValue variableType value' + >>= Coerce.coerceInputLiteral variableType + +constValue :: Full.ConstValue -> Type.Value +constValue (Full.ConstInt i) = Type.Int i +constValue (Full.ConstFloat f) = Type.Float f +constValue (Full.ConstString x) = Type.String x +constValue (Full.ConstBoolean b) = Type.Boolean b +constValue Full.ConstNull = Type.Null +constValue (Full.ConstEnum e) = Type.Enum e +constValue (Full.ConstList list) = Type.List $ constValue . Full.node <$> list +constValue (Full.ConstObject o) = + Type.Object $ HashMap.fromList $ constObjectField <$> o + where + constObjectField Full.ObjectField{value = value', ..} = + (name, constValue $ Full.node value') + +subscribe :: (MonadCatch m, Coerce.Serialize a) + => Seq (Selection m) + -> Schema m + -> Full.Location + -> m (Either Error (ResponseEventStream m a)) +subscribe fields schema objectLocation + | Just objectType <- Schema.subscription schema = do + let types' = Schema.types schema + sourceStream <- + createSourceEventStream types' objectType objectLocation fields + let traverser = + mapSourceToResponseEvent types' objectType fields + traverse traverser sourceStream + | otherwise = pure $ Left + $ Error "Schema doesn't support subscriptions." [] [] + +mapSourceToResponseEvent :: (MonadCatch m, Coerce.Serialize a) + => HashMap Full.Name (Type m) + -> Out.ObjectType m + -> Seq (Selection m) + -> Out.SourceEventStream m + -> m (ResponseEventStream m a) +mapSourceToResponseEvent types' subscriptionType fields sourceStream + = pure + $ sourceStream + .| mapMC (executeSubscriptionEvent types' subscriptionType fields) + +createSourceEventStream :: MonadCatch m => HashMap Full.Name (Type m) -> Out.ObjectType m -> Full.Location - -> Seq (Transform.Selection m) - -> m (Response a) -executeOperation types' objectType objectLocation fields - = runCollectErrs types' - $ executeSelectionSet Definition.Null objectType objectLocation fields + -> Seq (Selection m) + -> m (Either Error (Out.SourceEventStream m)) +createSourceEventStream _types subscriptionType objectLocation fields + | [fieldGroup] <- OrderedMap.elems groupedFieldSet + , Field _ fieldName arguments' _ errorLocation <- NonEmpty.head fieldGroup + , Out.ObjectType _ _ _ fieldTypes <- subscriptionType + , resolverT <- fieldTypes HashMap.! fieldName + , Out.EventStreamResolver fieldDefinition _ resolver <- resolverT + , Out.Field _ _fieldType argumentDefinitions <- fieldDefinition = + case coerceArgumentValues argumentDefinitions arguments' of + Left _ -> pure + $ Left + $ Error "Argument coercion failed." [errorLocation] [] + Right argumentValues -> left (singleError' [errorLocation]) + <$> resolveFieldEventStream Type.Null argumentValues resolver + | otherwise = pure + $ Left + $ Error "Subscription contains more than one field." [objectLocation] [] + where + groupedFieldSet = collectFields subscriptionType fields -rightErrorResponse :: Serialize b => forall a. Error -> Either a (Response b) -rightErrorResponse = Right . Response null . pure +singleError' :: [Full.Location] -> String -> Error +singleError' errorLocations message = Error (Text.pack message) errorLocations [] + +resolveFieldEventStream :: MonadCatch m + => Type.Value + -> Type.Subs + -> Out.Subscribe m + -> m (Either String (Out.SourceEventStream m)) +resolveFieldEventStream result args resolver = + catch (Right <$> runReaderT resolver context) handleEventStreamError + where + handleEventStreamError :: MonadCatch m + => ResolverException + -> m (Either String (Out.SourceEventStream m)) + handleEventStreamError = pure . Left . displayException + context = Type.Context + { Type.arguments = Type.Arguments args + , Type.values = result + } + +executeSubscriptionEvent :: (MonadCatch m, Coerce.Serialize a) + => HashMap Full.Name (Type m) + -> Out.ObjectType m + -> Seq (Selection m) + -> Type.Value + -> m (Response a) +executeSubscriptionEvent types' objectType fields initialValue = do + (data', errors) <- runWriterT + $ flip runReaderT types' + $ runExecutorT + $ executeSelectionSet fields objectType initialValue [] + pure $ Response data' errors