parent
b5157e141e
commit
a6f9cec413
@ -9,6 +9,9 @@ and this project adheres to
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
## Added
|
## Added
|
||||||
- `AST` reexports `AST.Parser`.
|
- `AST` reexports `AST.Parser`.
|
||||||
|
- `Execute` reexports `Execute.Coerce`.
|
||||||
|
- `Error.Error` is an error representation with a message and source location.
|
||||||
|
- `Error.Response` represents a result of running a GraphQL query.
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
- `Trans.ActionT` has become to `Type.Out.ResolverT`. Since `Type.Out.Resolver`
|
- `Trans.ActionT` has become to `Type.Out.ResolverT`. Since `Type.Out.Resolver`
|
||||||
@ -18,10 +21,15 @@ and this project adheres to
|
|||||||
- `AST.Core` contained only `Arguments` which was moved to `Type.Definition`.
|
- `AST.Core` contained only `Arguments` which was moved to `Type.Definition`.
|
||||||
`AST` provides now only functionality related to parsing and encoding, as it
|
`AST` provides now only functionality related to parsing and encoding, as it
|
||||||
should be.
|
should be.
|
||||||
|
- `Execute.execute` takes an additional argument, a possible operation name.
|
||||||
|
- `Error` module was changed to work with dedicated types for errors and the
|
||||||
|
response instead of JSON.
|
||||||
|
|
||||||
## Removed
|
## Removed
|
||||||
- `Type.Out.Resolver`: It is an unneeded layer of complexity. Resolvers are a
|
- `Type.Out.Resolver`: It is an unneeded layer of complexity. Resolvers are a
|
||||||
part of the fields and are called `Trans.ResolverT`.
|
part of the fields and are called `Trans.ResolverT`.
|
||||||
|
- `Execute.executeWithName`. `Execute.execute` takes the operation name and
|
||||||
|
completely replaces `executeWithName`.
|
||||||
|
|
||||||
## [0.8.0.0] - 2020-06-20
|
## [0.8.0.0] - 2020-06-20
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
-- | This module provides the functions to parse and execute @GraphQL@ queries.
|
-- | This module provides the functions to parse and execute @GraphQL@ queries.
|
||||||
module Language.GraphQL
|
module Language.GraphQL
|
||||||
( graphql
|
( graphql
|
||||||
@ -5,13 +8,11 @@ module Language.GraphQL
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as Aeson
|
import qualified Data.Aeson as Aeson
|
||||||
import Data.HashMap.Strict (HashMap)
|
import qualified Data.Sequence as Seq
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Language.GraphQL.AST.Document
|
import Language.GraphQL.AST
|
||||||
import Language.GraphQL.AST.Parser
|
|
||||||
import Language.GraphQL.Error
|
import Language.GraphQL.Error
|
||||||
import Language.GraphQL.Execute
|
import Language.GraphQL.Execute
|
||||||
import Language.GraphQL.Execute.Coerce
|
|
||||||
import Language.GraphQL.Type.Schema
|
import Language.GraphQL.Type.Schema
|
||||||
import Text.Megaparsec (parse)
|
import Text.Megaparsec (parse)
|
||||||
|
|
||||||
@ -21,16 +22,32 @@ graphql :: Monad m
|
|||||||
=> Schema m -- ^ Resolvers.
|
=> Schema m -- ^ Resolvers.
|
||||||
-> Text -- ^ Text representing a @GraphQL@ request document.
|
-> Text -- ^ Text representing a @GraphQL@ request document.
|
||||||
-> m Aeson.Value -- ^ Response.
|
-> m Aeson.Value -- ^ Response.
|
||||||
graphql = flip graphqlSubs (mempty :: Aeson.Object)
|
graphql schema = graphqlSubs schema mempty mempty
|
||||||
|
|
||||||
-- | If the text parses correctly as a @GraphQL@ query the substitution is
|
-- | If the text parses correctly as a @GraphQL@ query the substitution is
|
||||||
-- applied to the query and the query is then executed using to the given
|
-- applied to the query and the query is then executed using to the given
|
||||||
-- 'Schema'.
|
-- 'Schema'.
|
||||||
graphqlSubs :: (Monad m, VariableValue a)
|
graphqlSubs :: Monad m
|
||||||
=> Schema m -- ^ Resolvers.
|
=> Schema m -- ^ Resolvers.
|
||||||
-> HashMap Name a -- ^ Variable substitution function.
|
-> Maybe Text -- ^ Operation name.
|
||||||
|
-> Aeson.Object -- ^ Variable substitution function.
|
||||||
-> Text -- ^ Text representing a @GraphQL@ request document.
|
-> Text -- ^ Text representing a @GraphQL@ request document.
|
||||||
-> m Aeson.Value -- ^ Response.
|
-> m Aeson.Value -- ^ Response.
|
||||||
graphqlSubs schema f
|
graphqlSubs schema operationName variableValues document' =
|
||||||
= either parseError (execute schema f)
|
either parseError executeRequest parsed >>= formatResponse
|
||||||
. parse document ""
|
where
|
||||||
|
parsed = parse document "" document'
|
||||||
|
formatResponse (Response data'' Seq.Empty) =
|
||||||
|
pure $ Aeson.object [("data", data'')]
|
||||||
|
formatResponse (Response data'' errors') = pure $ Aeson.object
|
||||||
|
[ ("data", data'')
|
||||||
|
, ("errors", Aeson.toJSON $ toJSON <$> errors')
|
||||||
|
]
|
||||||
|
toJSON Error{ line = 0, column = 0, ..} =
|
||||||
|
Aeson.object [("message", Aeson.toJSON message)]
|
||||||
|
toJSON Error{..} = Aeson.object
|
||||||
|
[ ("message", Aeson.toJSON message)
|
||||||
|
, ("line", Aeson.toJSON line)
|
||||||
|
, ("column", Aeson.toJSON column)
|
||||||
|
]
|
||||||
|
executeRequest = execute schema operationName variableValues
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
@ -5,7 +6,9 @@
|
|||||||
module Language.GraphQL.Error
|
module Language.GraphQL.Error
|
||||||
( parseError
|
( parseError
|
||||||
, CollectErrsT
|
, CollectErrsT
|
||||||
|
, Error(..)
|
||||||
, Resolution(..)
|
, Resolution(..)
|
||||||
|
, Response(..)
|
||||||
, addErr
|
, addErr
|
||||||
, addErrMsg
|
, addErrMsg
|
||||||
, runCollectErrs
|
, runCollectErrs
|
||||||
@ -13,12 +16,16 @@ module Language.GraphQL.Error
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Monad.Trans.State (StateT, modify, runStateT)
|
import Control.Monad.Trans.State (StateT, modify, runStateT)
|
||||||
import qualified Data.Aeson as Aeson
|
|
||||||
import Data.HashMap.Strict (HashMap)
|
import Data.HashMap.Strict (HashMap)
|
||||||
|
import Data.Sequence (Seq(..), (|>))
|
||||||
|
import qualified Data.Sequence as Seq
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as Text
|
||||||
import Data.Void (Void)
|
import Data.Void (Void)
|
||||||
import Language.GraphQL.AST.Document (Name)
|
import Language.GraphQL.AST.Document (Name)
|
||||||
|
import Language.GraphQL.Execute.Coerce
|
||||||
import Language.GraphQL.Type.Schema
|
import Language.GraphQL.Type.Schema
|
||||||
|
import Prelude hiding (null)
|
||||||
import Text.Megaparsec
|
import Text.Megaparsec
|
||||||
( ParseErrorBundle(..)
|
( ParseErrorBundle(..)
|
||||||
, PosState(..)
|
, PosState(..)
|
||||||
@ -31,59 +38,72 @@ import Text.Megaparsec
|
|||||||
|
|
||||||
-- | Executor context.
|
-- | Executor context.
|
||||||
data Resolution m = Resolution
|
data Resolution m = Resolution
|
||||||
{ errors :: [Aeson.Value]
|
{ errors :: Seq Error
|
||||||
, types :: HashMap Name (Type m)
|
, types :: HashMap Name (Type m)
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Wraps a parse error into a list of errors.
|
-- | Wraps a parse error into a list of errors.
|
||||||
parseError :: Applicative f => ParseErrorBundle Text Void -> f Aeson.Value
|
parseError :: (Applicative f, Serialize a)
|
||||||
|
=> ParseErrorBundle Text Void
|
||||||
|
-> f (Response a)
|
||||||
parseError ParseErrorBundle{..} =
|
parseError ParseErrorBundle{..} =
|
||||||
pure $ Aeson.object [("errors", Aeson.toJSON $ fst $ foldl go ([], bundlePosState) bundleErrors)]
|
pure $ Response null $ fst
|
||||||
|
$ foldl go (Seq.empty, bundlePosState) bundleErrors
|
||||||
where
|
where
|
||||||
errorObject s SourcePos{..} = Aeson.object
|
errorObject s SourcePos{..} = Error
|
||||||
[ ("message", Aeson.toJSON $ init $ parseErrorTextPretty s)
|
(Text.pack $ init $ parseErrorTextPretty s)
|
||||||
, ("line", Aeson.toJSON $ unPos sourceLine)
|
(unPos' sourceLine)
|
||||||
, ("column", Aeson.toJSON $ unPos sourceColumn)
|
(unPos' sourceColumn)
|
||||||
]
|
unPos' = fromIntegral . unPos
|
||||||
go (result, state) x =
|
go (result, state) x =
|
||||||
let (_, newState) = reachOffset (errorOffset x) state
|
let (_, newState) = reachOffset (errorOffset x) state
|
||||||
sourcePosition = pstateSourcePos newState
|
sourcePosition = pstateSourcePos newState
|
||||||
in (errorObject x sourcePosition : result, newState)
|
in (result |> errorObject x sourcePosition, newState)
|
||||||
|
|
||||||
-- | A wrapper to pass error messages around.
|
-- | A wrapper to pass error messages around.
|
||||||
type CollectErrsT m = StateT (Resolution m) m
|
type CollectErrsT m = StateT (Resolution m) m
|
||||||
|
|
||||||
-- | Adds an error to the list of errors.
|
-- | Adds an error to the list of errors.
|
||||||
addErr :: Monad m => Aeson.Value -> CollectErrsT m ()
|
addErr :: Monad m => Error -> CollectErrsT m ()
|
||||||
addErr v = modify appender
|
addErr v = modify appender
|
||||||
where
|
where
|
||||||
appender resolution@Resolution{..} = resolution{ errors = v : errors }
|
appender :: Monad m => Resolution m -> Resolution m
|
||||||
|
appender resolution@Resolution{..} = resolution{ errors = errors |> v }
|
||||||
|
|
||||||
makeErrorMessage :: Text -> Aeson.Value
|
makeErrorMessage :: Text -> Error
|
||||||
makeErrorMessage s = Aeson.object [("message", Aeson.toJSON s)]
|
makeErrorMessage s = Error s 0 0
|
||||||
|
|
||||||
-- | Constructs a response object containing only the error with the given
|
-- | Constructs a response object containing only the error with the given
|
||||||
-- message.
|
-- message.
|
||||||
singleError :: Text -> Aeson.Value
|
singleError :: Serialize a => Text -> Response a
|
||||||
singleError message = Aeson.object
|
singleError message = Response null $ Seq.singleton $ makeErrorMessage message
|
||||||
[ ("errors", Aeson.toJSON [makeErrorMessage message])
|
|
||||||
]
|
|
||||||
|
|
||||||
-- | Convenience function for just wrapping an error message.
|
-- | Convenience function for just wrapping an error message.
|
||||||
addErrMsg :: Monad m => Text -> CollectErrsT m ()
|
addErrMsg :: Monad m => Text -> CollectErrsT m ()
|
||||||
addErrMsg = addErr . makeErrorMessage
|
addErrMsg = addErr . makeErrorMessage
|
||||||
|
|
||||||
|
-- | @GraphQL@ error.
|
||||||
|
data Error = Error
|
||||||
|
{ message :: Text
|
||||||
|
, line :: Word
|
||||||
|
, column :: Word
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- | The server\'s response describes the result of executing the requested
|
||||||
|
-- operation if successful, and describes any errors encountered during the
|
||||||
|
-- request.
|
||||||
|
data Response a = Response
|
||||||
|
{ data' :: a
|
||||||
|
, errors :: Seq Error
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
-- | Runs the given query computation, but collects the errors into an error
|
-- | Runs the given query computation, but collects the errors into an error
|
||||||
-- list, which is then sent back with the data.
|
-- list, which is then sent back with the data.
|
||||||
runCollectErrs :: Monad m
|
runCollectErrs :: (Monad m, Serialize a)
|
||||||
=> HashMap Name (Type m)
|
=> HashMap Name (Type m)
|
||||||
-> CollectErrsT m Aeson.Value
|
-> CollectErrsT m a
|
||||||
-> m Aeson.Value
|
-> m (Response a)
|
||||||
runCollectErrs types' res = do
|
runCollectErrs types' res = do
|
||||||
(dat, Resolution{..}) <- runStateT res $ Resolution{ errors = [], types = types' }
|
(dat, Resolution{..}) <- runStateT res
|
||||||
if null errors
|
$ Resolution{ errors = Seq.empty, types = types' }
|
||||||
then return $ Aeson.object [("data", dat)]
|
pure $ Response dat errors
|
||||||
else return $ Aeson.object
|
|
||||||
[ ("data", dat)
|
|
||||||
, ("errors", Aeson.toJSON $ reverse errors)
|
|
||||||
]
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
-- | This module provides functions to execute a @GraphQL@ request.
|
-- | This module provides functions to execute a @GraphQL@ request.
|
||||||
module Language.GraphQL.Execute
|
module Language.GraphQL.Execute
|
||||||
( execute
|
( execute
|
||||||
, executeWithName
|
, module Language.GraphQL.Execute.Coerce
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as Aeson
|
|
||||||
import Data.HashMap.Strict (HashMap)
|
import Data.HashMap.Strict (HashMap)
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
import qualified Data.HashMap.Strict as HashMap
|
||||||
import Data.Sequence (Seq(..))
|
import Data.Sequence (Seq(..))
|
||||||
@ -18,54 +17,38 @@ import qualified Language.GraphQL.Type.Definition as Definition
|
|||||||
import qualified Language.GraphQL.Type.Out as Out
|
import qualified Language.GraphQL.Type.Out as Out
|
||||||
import Language.GraphQL.Type.Schema
|
import Language.GraphQL.Type.Schema
|
||||||
|
|
||||||
-- | The substitution is applied to the document, and the resolvers are applied
|
|
||||||
-- to the resulting fields.
|
|
||||||
--
|
|
||||||
-- Returns the result of the query against the schema wrapped in a /data/
|
|
||||||
-- field, or errors wrapped in an /errors/ field.
|
|
||||||
execute :: (Monad m, VariableValue a)
|
|
||||||
=> Schema m -- ^ Resolvers.
|
|
||||||
-> HashMap.HashMap Name a -- ^ Variable substitution function.
|
|
||||||
-> Document -- @GraphQL@ document.
|
|
||||||
-> m Aeson.Value
|
|
||||||
execute schema = executeRequest schema Nothing
|
|
||||||
|
|
||||||
-- | The substitution is applied to the document, and the resolvers are applied
|
-- | 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
|
-- to the resulting fields. The operation name can be used if the document
|
||||||
-- defines multiple root operations.
|
-- defines multiple root operations.
|
||||||
--
|
--
|
||||||
-- Returns the result of the query against the schema wrapped in a /data/
|
-- Returns the result of the query against the schema wrapped in a /data/
|
||||||
-- field, or errors wrapped in an /errors/ field.
|
-- field, or errors wrapped in an /errors/ field.
|
||||||
executeWithName :: (Monad m, VariableValue a)
|
execute :: (Monad m, VariableValue a, Serialize b)
|
||||||
=> Schema m -- ^ Resolvers
|
=> Schema m -- ^ Resolvers.
|
||||||
-> Text -- ^ Operation name.
|
-> Maybe Text -- ^ Operation name.
|
||||||
-> HashMap.HashMap Name a -- ^ Variable substitution function.
|
-> HashMap.HashMap Name a -- ^ Variable substitution function.
|
||||||
-> Document -- ^ @GraphQL@ Document.
|
-> Document -- @GraphQL@ document.
|
||||||
-> m Aeson.Value
|
-> m (Response b)
|
||||||
executeWithName schema operationName =
|
execute schema operationName subs document =
|
||||||
executeRequest schema (Just operationName)
|
|
||||||
|
|
||||||
executeRequest :: (Monad m, VariableValue a)
|
|
||||||
=> Schema m
|
|
||||||
-> Maybe Text
|
|
||||||
-> HashMap.HashMap Name a
|
|
||||||
-> Document
|
|
||||||
-> m Aeson.Value
|
|
||||||
executeRequest schema operationName subs document =
|
|
||||||
case Transform.document schema operationName subs document of
|
case Transform.document schema operationName subs document of
|
||||||
Left queryError -> pure $ singleError $ Transform.queryError queryError
|
Left queryError -> pure $ singleError $ Transform.queryError queryError
|
||||||
Right (Transform.Document types' rootObjectType operation)
|
Right transformed -> executeRequest transformed
|
||||||
| (Transform.Query _ fields) <- operation ->
|
|
||||||
|
executeRequest :: (Monad m, Serialize a)
|
||||||
|
=> Transform.Document m
|
||||||
|
-> m (Response a)
|
||||||
|
executeRequest (Transform.Document types' rootObjectType operation)
|
||||||
|
| (Transform.Query _ fields) <- operation =
|
||||||
executeOperation types' rootObjectType fields
|
executeOperation types' rootObjectType fields
|
||||||
| (Transform.Mutation _ fields) <- operation ->
|
| (Transform.Mutation _ fields) <- operation =
|
||||||
executeOperation types' rootObjectType fields
|
executeOperation types' rootObjectType fields
|
||||||
|
|
||||||
-- This is actually executeMutation, but we don't distinguish between queries
|
-- This is actually executeMutation, but we don't distinguish between queries
|
||||||
-- and mutations yet.
|
-- and mutations yet.
|
||||||
executeOperation :: Monad m
|
executeOperation :: (Monad m, Serialize a)
|
||||||
=> HashMap Name (Type m)
|
=> HashMap Name (Type m)
|
||||||
-> Out.ObjectType m
|
-> Out.ObjectType m
|
||||||
-> Seq (Transform.Selection m)
|
-> Seq (Transform.Selection m)
|
||||||
-> m Aeson.Value
|
-> m (Response a)
|
||||||
executeOperation types' objectType fields =
|
executeOperation types' objectType fields =
|
||||||
runCollectErrs types' $ executeSelectionSet Definition.Null objectType fields
|
runCollectErrs types' $ executeSelectionSet Definition.Null objectType fields
|
||||||
|
@ -4,6 +4,7 @@ module Language.GraphQL.ErrorSpec
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as Aeson
|
import qualified Data.Aeson as Aeson
|
||||||
|
import qualified Data.Sequence as Seq
|
||||||
import Language.GraphQL.Error
|
import Language.GraphQL.Error
|
||||||
import Test.Hspec ( Spec
|
import Test.Hspec ( Spec
|
||||||
, describe
|
, describe
|
||||||
@ -14,11 +15,6 @@ import Test.Hspec ( Spec
|
|||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = describe "singleError" $
|
spec = describe "singleError" $
|
||||||
it "constructs an error with the given message" $
|
it "constructs an error with the given message" $
|
||||||
let expected = Aeson.object
|
let errors'' = Seq.singleton $ Error "Message." 0 0
|
||||||
[
|
expected = Response Aeson.Null errors''
|
||||||
("errors", Aeson.toJSON
|
|
||||||
[ Aeson.object [("message", "Message.")]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
in singleError "Message." `shouldBe` expected
|
in singleError "Message." `shouldBe` expected
|
||||||
|
@ -47,28 +47,26 @@ spec :: Spec
|
|||||||
spec =
|
spec =
|
||||||
describe "execute" $ do
|
describe "execute" $ do
|
||||||
it "skips unknown fields" $
|
it "skips unknown fields" $
|
||||||
let expected = Aeson.object
|
let data'' = Aeson.object
|
||||||
[ "data" .= Aeson.object
|
|
||||||
[ "philosopher" .= Aeson.object
|
[ "philosopher" .= Aeson.object
|
||||||
[ "firstName" .= ("Friedrich" :: String)
|
[ "firstName" .= ("Friedrich" :: String)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
expected = Response data'' mempty
|
||||||
execute' = execute schema (mempty :: HashMap Name Aeson.Value)
|
execute' = execute schema Nothing (mempty :: HashMap Name Aeson.Value)
|
||||||
actual = runIdentity
|
actual = runIdentity
|
||||||
$ either parseError execute'
|
$ either parseError execute'
|
||||||
$ parse document "" "{ philosopher { firstName surname } }"
|
$ parse document "" "{ philosopher { firstName surname } }"
|
||||||
in actual `shouldBe` expected
|
in actual `shouldBe` expected
|
||||||
it "merges selections" $
|
it "merges selections" $
|
||||||
let expected = Aeson.object
|
let data'' = Aeson.object
|
||||||
[ "data" .= Aeson.object
|
|
||||||
[ "philosopher" .= Aeson.object
|
[ "philosopher" .= Aeson.object
|
||||||
[ "firstName" .= ("Friedrich" :: String)
|
[ "firstName" .= ("Friedrich" :: String)
|
||||||
, "lastName" .= ("Nietzsche" :: String)
|
, "lastName" .= ("Nietzsche" :: String)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
expected = Response data'' mempty
|
||||||
execute' = execute schema (mempty :: HashMap Name Aeson.Value)
|
execute' = execute schema Nothing (mempty :: HashMap Name Aeson.Value)
|
||||||
actual = runIdentity
|
actual = runIdentity
|
||||||
$ either parseError execute'
|
$ either parseError execute'
|
||||||
$ parse document "" "{ philosopher { firstName } philosopher { lastName } }"
|
$ parse document "" "{ philosopher { firstName } philosopher { lastName } }"
|
||||||
|
@ -361,4 +361,4 @@ testQuery q expected = runIdentity (graphql schema q) `shouldBe` expected
|
|||||||
|
|
||||||
testQueryParams :: Aeson.Object -> Text -> Aeson.Value -> Expectation
|
testQueryParams :: Aeson.Object -> Text -> Aeson.Value -> Expectation
|
||||||
testQueryParams f q expected =
|
testQueryParams f q expected =
|
||||||
runIdentity (graphqlSubs schema f q) `shouldBe` expected
|
runIdentity (graphqlSubs schema Nothing f q) `shouldBe` expected
|
||||||
|
Loading…
Reference in New Issue
Block a user