{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE PatternSynonyms #-} -- | Implement the \"Execution\" part of the GraphQL spec. -- -- Actually, most of the execution work takes place in 'GraphQL.Resolver', but -- there's still a fair bit required to glue together the results of -- 'GraphQL.Internal.Validation' and the processing in 'GraphQL.Resolver'. -- This module provides that glue. module GraphQL.Internal.Execution ( VariableValues , ExecutionError(..) , formatError , getOperation , substituteVariables ) where import Protolude import qualified Data.Map as Map import GraphQL.Value ( Name , Value , pattern ValueNull , Value'(..) , List'(..) , Object'(..) ) import GraphQL.Internal.Output (GraphQLError(..)) import GraphQL.Internal.Validation ( Operation , QueryDocument(..) , VariableDefinition(..) , VariableValue , Variable , Type(..) ) -- | Get an operation from a GraphQL document -- -- -- -- GetOperation(document, operationName): -- -- * If {operationName} is {null}: -- * If {document} contains exactly one operation. -- * Return the Operation contained in the {document}. -- * Otherwise produce a query error requiring {operationName}. -- * Otherwise: -- * Let {operation} be the Operation named {operationName} in {document}. -- * If {operation} was not found, produce a query error. -- * Return {operation}. getOperation :: QueryDocument value -> Maybe Name -> Either ExecutionError (Operation value) getOperation (LoneAnonymousOperation op) Nothing = pure op getOperation (MultipleOperations ops) (Just name) = note (NoSuchOperation name) (Map.lookup name ops) getOperation (MultipleOperations ops) Nothing = case toList ops of [op] -> pure op _ -> throwError NoAnonymousOperation getOperation _ (Just name) = throwError (NoSuchOperation name) -- | Substitute variables in a GraphQL document. -- -- Once this is done, there will be no variables in the document whatsoever. substituteVariables :: Operation VariableValue -> VariableValues -> Either ExecutionError (Operation Value) substituteVariables op vars = traverse (replaceVariable vars) op replaceVariable :: VariableValues -> VariableValue -> Either ExecutionError Value replaceVariable vars value = case value of ValueScalar' (Left defn) -> getValue defn ValueScalar' (Right v) -> pure (ValueScalar' v) ValueList' (List' xs) -> ValueList' . List' <$> traverse (replaceVariable vars) xs ValueObject' (Object' xs) -> ValueObject' . Object' <$> traverse (replaceVariable vars) xs where getValue :: VariableDefinition -> Either ExecutionError Value getValue (VariableDefinition variableName variableType defaultValue) = note (MissingValue variableName) $ Map.lookup variableName vars <|> defaultValue <|> allowNull variableType allowNull (TypeNonNull _) = empty allowNull _ = pure ValueNull -- | An error that occurs while executing a query. Technically, -- 'ResolverError' also falls into the same category, but is separate to help -- our code be a bit better organized. data ExecutionError = MissingValue Variable | NoSuchOperation Name | NoAnonymousOperation deriving (Eq, Show) instance GraphQLError ExecutionError where formatError (MissingValue name) = "Missing value for " <> show name <> " and must be non-null." formatError (NoSuchOperation name) = "Requested operation " <> show name <> " but couldn't find it." formatError NoAnonymousOperation = "No name supplied for opertaion, but no anonymous operation." -- | A map of variables to their values. -- -- In GraphQL the variable values are not part of the query itself, they are -- instead passed in through a separate channel. Create a 'VariableValues' -- from this other channel and pass it to 'substituteVariables'. -- -- GraphQL allows the values of variables to be specified, but doesn't provide -- a way for doing so in the language. type VariableValues = Map Variable Value