2019-11-11 15:46:52 +01:00
|
|
|
{-# LANGUAGE ExplicitForAll #-}
|
2020-05-21 10:20:59 +02:00
|
|
|
{-# LANGUAGE LambdaCase #-}
|
2020-05-22 10:11:48 +02:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2019-12-06 22:52:24 +01:00
|
|
|
{-# LANGUAGE TupleSections #-}
|
2019-11-03 10:42:10 +01:00
|
|
|
|
2019-09-25 05:35:36 +02:00
|
|
|
-- | After the document is parsed, before getting executed the AST is
|
|
|
|
-- transformed into a similar, simpler AST. This module is responsible for
|
|
|
|
-- this transformation.
|
2019-12-07 09:46:00 +01:00
|
|
|
module Language.GraphQL.Execute.Transform
|
2020-05-21 10:20:59 +02:00
|
|
|
( Document(..)
|
2020-05-22 10:11:48 +02:00
|
|
|
, QueryError(..)
|
2020-05-21 10:20:59 +02:00
|
|
|
, document
|
2020-05-22 10:11:48 +02:00
|
|
|
, queryError
|
2019-07-14 05:58:05 +02:00
|
|
|
) where
|
2017-01-29 22:44:03 +01:00
|
|
|
|
2019-11-11 15:46:52 +01:00
|
|
|
import Control.Monad (foldM, unless)
|
2019-11-06 06:34:36 +01:00
|
|
|
import Control.Monad.Trans.Class (lift)
|
2019-12-30 18:26:24 +01:00
|
|
|
import Control.Monad.Trans.Reader (ReaderT, asks, runReaderT)
|
2019-11-11 15:46:52 +01:00
|
|
|
import Control.Monad.Trans.State (StateT, evalStateT, gets, modify)
|
2020-05-22 10:11:48 +02:00
|
|
|
import Data.Foldable (find)
|
2019-10-31 07:32:51 +01:00
|
|
|
import Data.HashMap.Strict (HashMap)
|
|
|
|
import qualified Data.HashMap.Strict as HashMap
|
2020-05-22 10:11:48 +02:00
|
|
|
import Data.List.NonEmpty (NonEmpty(..))
|
2017-01-29 22:44:03 +01:00
|
|
|
import qualified Data.List.NonEmpty as NonEmpty
|
2019-11-16 11:41:40 +01:00
|
|
|
import Data.Sequence (Seq, (<|), (><))
|
2020-05-22 10:11:48 +02:00
|
|
|
import Data.Text (Text)
|
|
|
|
import qualified Data.Text as Text
|
2019-07-07 06:31:53 +02:00
|
|
|
import qualified Language.GraphQL.AST as Full
|
|
|
|
import qualified Language.GraphQL.AST.Core as Core
|
2020-05-22 10:11:48 +02:00
|
|
|
import Language.GraphQL.Execute.Coerce
|
2019-07-07 06:31:53 +02:00
|
|
|
import qualified Language.GraphQL.Schema as Schema
|
2020-05-22 10:11:48 +02:00
|
|
|
import qualified Language.GraphQL.Type.Definition as Definition
|
2019-12-17 09:03:18 +01:00
|
|
|
import qualified Language.GraphQL.Type.Directive as Directive
|
2020-05-22 10:11:48 +02:00
|
|
|
import Language.GraphQL.Type.Schema
|
2017-01-29 22:44:03 +01:00
|
|
|
|
2019-10-31 07:32:51 +01:00
|
|
|
-- | Associates a fragment name with a list of 'Core.Field's.
|
2019-11-06 06:34:36 +01:00
|
|
|
data Replacement = Replacement
|
2019-12-01 20:43:19 +01:00
|
|
|
{ fragments :: HashMap Core.Name Core.Fragment
|
2019-11-11 15:46:52 +01:00
|
|
|
, fragmentDefinitions :: HashMap Full.Name Full.FragmentDefinition
|
2019-11-06 06:34:36 +01:00
|
|
|
}
|
|
|
|
|
2019-11-11 15:46:52 +01:00
|
|
|
type TransformT a = StateT Replacement (ReaderT Schema.Subs Maybe) a
|
2019-11-06 06:34:36 +01:00
|
|
|
|
2019-12-07 09:46:00 +01:00
|
|
|
liftJust :: forall a. a -> TransformT a
|
|
|
|
liftJust = lift . lift . Just
|
|
|
|
|
2020-05-21 10:20:59 +02:00
|
|
|
-- | GraphQL document is a non-empty list of operations.
|
|
|
|
data Document = Document
|
2020-05-22 10:11:48 +02:00
|
|
|
Core.Operation
|
2020-05-21 10:20:59 +02:00
|
|
|
(HashMap Full.Name Full.FragmentDefinition)
|
|
|
|
|
|
|
|
data OperationDefinition = OperationDefinition
|
|
|
|
Full.OperationType
|
|
|
|
(Maybe Full.Name)
|
|
|
|
[Full.VariableDefinition]
|
|
|
|
[Full.Directive]
|
|
|
|
Full.SelectionSet
|
|
|
|
|
2020-05-22 10:11:48 +02:00
|
|
|
-- | Query error types.
|
|
|
|
data QueryError
|
|
|
|
= OperationNotFound Text
|
|
|
|
| OperationNameRequired
|
|
|
|
| CoercionError
|
|
|
|
| TransformationError
|
|
|
|
| EmptyDocument
|
|
|
|
|
|
|
|
queryError :: QueryError -> Text
|
|
|
|
queryError (OperationNotFound operationName) = Text.unwords
|
|
|
|
["Operation", operationName, "couldn't be found in the document."]
|
|
|
|
queryError OperationNameRequired = "Missing operation name."
|
|
|
|
queryError CoercionError = "Coercion error."
|
|
|
|
queryError TransformationError = "Schema transformation error."
|
|
|
|
queryError EmptyDocument =
|
|
|
|
"The document doesn't contain any executable operations."
|
|
|
|
|
|
|
|
getOperation
|
|
|
|
:: Maybe Full.Name
|
|
|
|
-> NonEmpty OperationDefinition
|
|
|
|
-> Either QueryError OperationDefinition
|
|
|
|
getOperation Nothing (operation' :| []) = pure operation'
|
|
|
|
getOperation Nothing _ = Left OperationNameRequired
|
|
|
|
getOperation (Just operationName) operations
|
|
|
|
| Just operation' <- find matchingName operations = pure operation'
|
|
|
|
| otherwise = Left $ OperationNotFound operationName
|
|
|
|
where
|
|
|
|
matchingName (OperationDefinition _ name _ _ _) =
|
|
|
|
name == Just operationName
|
|
|
|
|
|
|
|
lookupInputType
|
|
|
|
:: Full.Type
|
|
|
|
-> HashMap.HashMap Full.Name (Definition.TypeDefinition m)
|
|
|
|
-> Maybe Definition.InputType
|
|
|
|
lookupInputType (Full.TypeNamed name) types =
|
|
|
|
case HashMap.lookup name types of
|
|
|
|
Just (Definition.ScalarTypeDefinition scalarType) ->
|
|
|
|
Just $ Definition.ScalarInputType scalarType
|
|
|
|
Just (Definition.EnumTypeDefinition enumType) ->
|
|
|
|
Just $ Definition.EnumInputType enumType
|
|
|
|
Just (Definition.InputObjectTypeDefinition objectType) ->
|
|
|
|
Just $ Definition.ObjectInputType objectType
|
|
|
|
_ -> Nothing
|
|
|
|
lookupInputType (Full.TypeList list) types
|
|
|
|
= Definition.ListInputType
|
|
|
|
<$> lookupInputType list types
|
|
|
|
lookupInputType (Full.TypeNonNull (Full.NonNullTypeNamed nonNull)) types =
|
|
|
|
case HashMap.lookup nonNull types of
|
|
|
|
Just (Definition.ScalarTypeDefinition scalarType) ->
|
|
|
|
Just $ Definition.NonNullScalarInputType scalarType
|
|
|
|
Just (Definition.EnumTypeDefinition enumType) ->
|
|
|
|
Just $ Definition.NonNullEnumInputType enumType
|
|
|
|
Just (Definition.InputObjectTypeDefinition objectType) ->
|
|
|
|
Just $ Definition.NonNullObjectInputType objectType
|
|
|
|
_ -> Nothing
|
|
|
|
lookupInputType (Full.TypeNonNull (Full.NonNullTypeList nonNull)) types
|
|
|
|
= Definition.NonNullListInputType
|
|
|
|
<$> lookupInputType nonNull types
|
|
|
|
|
|
|
|
coerceVariableValues :: (Monad m, VariableValue a)
|
|
|
|
=> Schema m
|
|
|
|
-> OperationDefinition
|
|
|
|
-> HashMap.HashMap Full.Name a
|
|
|
|
-> Either QueryError Schema.Subs
|
|
|
|
coerceVariableValues schema (OperationDefinition _ _ variables _ _) values =
|
|
|
|
let referencedTypes = collectReferencedTypes schema
|
|
|
|
in maybe (Left CoercionError) Right
|
|
|
|
$ foldr (coerceValue referencedTypes) (Just HashMap.empty) variables
|
|
|
|
where
|
|
|
|
coerceValue referencedTypes variableDefinition coercedValues = do
|
|
|
|
let Full.VariableDefinition variableName variableTypeName _defaultValue =
|
|
|
|
variableDefinition
|
|
|
|
variableType <- lookupInputType variableTypeName referencedTypes
|
|
|
|
value' <- HashMap.lookup variableName values
|
|
|
|
coercedValue <- coerceVariableValue variableType value'
|
|
|
|
HashMap.insert variableName coercedValue <$> coercedValues
|
|
|
|
|
2019-08-29 07:40:50 +02:00
|
|
|
-- | Rewrites the original syntax tree into an intermediate representation used
|
|
|
|
-- for query execution.
|
2020-05-22 10:11:48 +02:00
|
|
|
document :: (Monad m, VariableValue a)
|
|
|
|
=> Schema m
|
|
|
|
-> Maybe Full.Name
|
|
|
|
-> HashMap Full.Name a
|
|
|
|
-> Full.Document
|
|
|
|
-> Either QueryError Document
|
|
|
|
document schema operationName subs ast = do
|
2020-05-21 10:20:59 +02:00
|
|
|
let (operations, fragmentTable) = foldr defragment ([], HashMap.empty) ast
|
2020-05-22 10:11:48 +02:00
|
|
|
nonEmptyOperations <- maybe (Left EmptyDocument) Right
|
|
|
|
$ NonEmpty.nonEmpty operations
|
|
|
|
chosenOperation <- getOperation operationName nonEmptyOperations
|
|
|
|
coercedValues <- coerceVariableValues schema chosenOperation subs
|
|
|
|
|
|
|
|
maybe (Left TransformationError) Right
|
|
|
|
$ Document
|
|
|
|
<$> operation fragmentTable coercedValues chosenOperation
|
|
|
|
<*> pure fragmentTable
|
2017-01-29 22:44:03 +01:00
|
|
|
where
|
2020-05-21 10:20:59 +02:00
|
|
|
defragment definition (operations, fragments')
|
|
|
|
| (Full.ExecutableDefinition executable) <- definition
|
|
|
|
, (Full.DefinitionOperation operation') <- executable =
|
|
|
|
(transform operation' : operations, fragments')
|
|
|
|
| (Full.ExecutableDefinition executable) <- definition
|
|
|
|
, (Full.DefinitionFragment fragment) <- executable
|
|
|
|
, (Full.FragmentDefinition name _ _ _) <- fragment =
|
|
|
|
(operations, HashMap.insert name fragment fragments')
|
2019-12-26 13:00:47 +01:00
|
|
|
defragment _ acc = acc
|
2020-05-21 10:20:59 +02:00
|
|
|
transform = \case
|
|
|
|
Full.OperationDefinition type' name variables directives' selections ->
|
|
|
|
OperationDefinition type' name variables directives' selections
|
|
|
|
Full.SelectionSet selectionSet ->
|
|
|
|
OperationDefinition Full.Query Nothing mempty mempty selectionSet
|
2017-01-29 22:44:03 +01:00
|
|
|
|
2017-02-04 01:48:26 +01:00
|
|
|
-- * Operation
|
|
|
|
|
2020-05-21 10:20:59 +02:00
|
|
|
operation
|
|
|
|
:: HashMap Full.Name Full.FragmentDefinition
|
|
|
|
-> Schema.Subs
|
|
|
|
-> OperationDefinition
|
|
|
|
-> Maybe Core.Operation
|
|
|
|
operation fragmentTable subs operationDefinition = flip runReaderT subs
|
|
|
|
$ evalStateT (collectFragments >> transform operationDefinition)
|
|
|
|
$ Replacement HashMap.empty fragmentTable
|
|
|
|
where
|
|
|
|
transform :: OperationDefinition -> TransformT Core.Operation
|
|
|
|
transform (OperationDefinition Full.Query name _ _ sels) =
|
|
|
|
Core.Query name <$> appendSelection sels
|
|
|
|
transform (OperationDefinition Full.Mutation name _ _ sels) =
|
|
|
|
Core.Mutation name <$> appendSelection sels
|
2019-10-31 07:32:51 +01:00
|
|
|
|
2019-12-06 22:52:24 +01:00
|
|
|
-- * Selection
|
|
|
|
|
2019-10-31 07:32:51 +01:00
|
|
|
selection ::
|
|
|
|
Full.Selection ->
|
2019-11-16 11:41:40 +01:00
|
|
|
TransformT (Either (Seq Core.Selection) Core.Selection)
|
2019-12-25 06:45:29 +01:00
|
|
|
selection (Full.Field alias name arguments' directives' selections) =
|
|
|
|
maybe (Left mempty) (Right . Core.SelectionField) <$> do
|
2020-01-01 10:58:11 +01:00
|
|
|
fieldArguments <- arguments arguments'
|
2019-12-25 06:45:29 +01:00
|
|
|
fieldSelections <- appendSelection selections
|
|
|
|
fieldDirectives <- Directive.selection <$> directives directives'
|
|
|
|
let field' = Core.Field alias name fieldArguments fieldSelections
|
|
|
|
pure $ field' <$ fieldDirectives
|
|
|
|
selection (Full.FragmentSpread name directives') =
|
|
|
|
maybe (Left mempty) (Right . Core.SelectionFragment) <$> do
|
|
|
|
spreadDirectives <- Directive.selection <$> directives directives'
|
|
|
|
fragments' <- gets fragments
|
|
|
|
fragment <- maybe lookupDefinition liftJust (HashMap.lookup name fragments')
|
|
|
|
pure $ fragment <$ spreadDirectives
|
|
|
|
where
|
|
|
|
lookupDefinition = do
|
|
|
|
fragmentDefinitions' <- gets fragmentDefinitions
|
|
|
|
found <- lift . lift $ HashMap.lookup name fragmentDefinitions'
|
|
|
|
fragmentDefinition found
|
|
|
|
selection (Full.InlineFragment type' directives' selections) = do
|
|
|
|
fragmentDirectives <- Directive.selection <$> directives directives'
|
|
|
|
case fragmentDirectives of
|
|
|
|
Nothing -> pure $ Left mempty
|
|
|
|
_ -> do
|
|
|
|
fragmentSelectionSet <- appendSelection selections
|
|
|
|
pure $ maybe Left selectionFragment type' fragmentSelectionSet
|
|
|
|
where
|
|
|
|
selectionFragment typeName = Right
|
|
|
|
. Core.SelectionFragment
|
|
|
|
. Core.Fragment typeName
|
2019-12-06 22:52:24 +01:00
|
|
|
|
|
|
|
appendSelection ::
|
|
|
|
Traversable t =>
|
|
|
|
t Full.Selection ->
|
|
|
|
TransformT (Seq Core.Selection)
|
|
|
|
appendSelection = foldM go mempty
|
|
|
|
where
|
|
|
|
go acc sel = append acc <$> selection sel
|
|
|
|
append acc (Left list) = list >< acc
|
|
|
|
append acc (Right one) = one <| acc
|
|
|
|
|
|
|
|
directives :: [Full.Directive] -> TransformT [Core.Directive]
|
|
|
|
directives = traverse directive
|
|
|
|
where
|
|
|
|
directive (Full.Directive directiveName directiveArguments) =
|
|
|
|
Core.Directive directiveName <$> arguments directiveArguments
|
2017-02-04 01:48:26 +01:00
|
|
|
|
2017-01-29 22:44:03 +01:00
|
|
|
-- * Fragment replacement
|
|
|
|
|
2019-11-11 15:46:52 +01:00
|
|
|
-- | Extract fragment definitions into a single 'HashMap'.
|
|
|
|
collectFragments :: TransformT ()
|
|
|
|
collectFragments = do
|
|
|
|
fragDefs <- gets fragmentDefinitions
|
|
|
|
let nextValue = head $ HashMap.elems fragDefs
|
|
|
|
unless (HashMap.null fragDefs) $ do
|
|
|
|
_ <- fragmentDefinition nextValue
|
|
|
|
collectFragments
|
|
|
|
|
2019-11-13 20:40:09 +01:00
|
|
|
fragmentDefinition ::
|
|
|
|
Full.FragmentDefinition ->
|
2019-12-01 20:43:19 +01:00
|
|
|
TransformT Core.Fragment
|
2019-12-06 22:52:24 +01:00
|
|
|
fragmentDefinition (Full.FragmentDefinition name type' _ selections) = do
|
2019-11-13 20:40:09 +01:00
|
|
|
modify deleteFragmentDefinition
|
2019-12-01 20:43:19 +01:00
|
|
|
fragmentSelection <- appendSelection selections
|
2019-12-06 22:52:24 +01:00
|
|
|
let newValue = Core.Fragment type' fragmentSelection
|
2019-11-13 20:40:09 +01:00
|
|
|
modify $ insertFragment newValue
|
2019-11-11 15:46:52 +01:00
|
|
|
liftJust newValue
|
2019-10-31 07:32:51 +01:00
|
|
|
where
|
2019-11-13 20:40:09 +01:00
|
|
|
deleteFragmentDefinition (Replacement fragments' fragmentDefinitions') =
|
|
|
|
Replacement fragments' $ HashMap.delete name fragmentDefinitions'
|
|
|
|
insertFragment newValue (Replacement fragments' fragmentDefinitions') =
|
|
|
|
let newFragments = HashMap.insert name newValue fragments'
|
|
|
|
in Replacement newFragments fragmentDefinitions'
|
2017-01-29 22:44:03 +01:00
|
|
|
|
2019-12-06 22:52:24 +01:00
|
|
|
arguments :: [Full.Argument] -> TransformT Core.Arguments
|
|
|
|
arguments = fmap Core.Arguments . foldM go HashMap.empty
|
|
|
|
where
|
2020-01-01 10:58:11 +01:00
|
|
|
go arguments' (Full.Argument name value') = do
|
|
|
|
substitutedValue <- value value'
|
|
|
|
return $ HashMap.insert name substitutedValue arguments'
|
2019-11-06 06:34:36 +01:00
|
|
|
|
|
|
|
value :: Full.Value -> TransformT Core.Value
|
2019-12-30 18:26:24 +01:00
|
|
|
value (Full.Variable name) = lift (asks $ HashMap.lookup name) >>= lift . lift
|
2019-11-06 06:34:36 +01:00
|
|
|
value (Full.Int i) = pure $ Core.Int i
|
|
|
|
value (Full.Float f) = pure $ Core.Float f
|
|
|
|
value (Full.String x) = pure $ Core.String x
|
|
|
|
value (Full.Boolean b) = pure $ Core.Boolean b
|
|
|
|
value Full.Null = pure Core.Null
|
|
|
|
value (Full.Enum e) = pure $ Core.Enum e
|
|
|
|
value (Full.List l) =
|
|
|
|
Core.List <$> traverse value l
|
|
|
|
value (Full.Object o) =
|
|
|
|
Core.Object . HashMap.fromList <$> traverse objectField o
|
|
|
|
|
2020-05-22 10:11:48 +02:00
|
|
|
objectField :: Full.ObjectField Full.Value -> TransformT (Core.Name, Core.Value)
|
2019-12-06 22:52:24 +01:00
|
|
|
objectField (Full.ObjectField name value') = (name,) <$> value value'
|