26 Commits

Author SHA1 Message Date
dd6fdf69f6 Release 1.0.0.0 2021-07-04 09:57:17 +02:00
b99bb72272 Report subscription error locations 2021-07-02 09:28:03 +02:00
b580d1a988 Attach the field location to resolver exceptions 2021-06-27 13:42:58 +02:00
c601ccb4ad Add dependency version ranges
Also remove stack.yaml since it isn't used anymore. and adding libraries
to the snapshots doesn't seem to be as easy as I hoped.
2021-06-26 07:35:18 +02:00
96bb061666 Fail with a location for result coercion
The intermediate representation was further modified so that the
operation definitions contain location information. Probably I should
introduce a data type that generalizes fields and operations, so it
contains object type, location and the selection set, so the functions
don't accept so many arguments.
2021-06-24 09:29:24 +02:00
812f6967d4 Provide locations for argument errors
The executor still doesn't give an error per argument, but a single
error per field with locations for all arguments.
If a non-null argument isn't specified, only the error location of the
field is given. If some arguments cannot be coerced, only the locations
of these arguments are given, non-null arguments are ignored. This
should still be improved, so the executor returns all errors at once.
The transformation tree is changed, so that argument map contains
locations of the arguments (but not the locations of the argument values
yet).
2021-06-22 09:13:27 +02:00
6fe9eb72e4 Fix merging fields with arguments
executeField shouldn't assume that a selection has only one field with a
given name, but it should take the first field. The underlying cause is
a wrong pattern, which (because of the laziness) is executed only if the
field has arguments.
2021-06-18 06:51:14 +02:00
2ce2be5d91 Provide location information for interface errors 2021-06-17 08:15:27 +02:00
c311cb0070 Add constructor with additional schema types 2021-05-13 17:40:38 +02:00
1b7cd85216 Add location information to the intermediate tree 2021-05-12 06:51:59 +02:00
f671645043 Remove unused QueryError.TransformationError 2021-05-11 07:11:47 +02:00
1af95345d2 Deprecate internal error generation functions
The functions generating errors in the executor should be changed anyway
when we provide better error messages from the executor, with the error
location and response path. So public definitions of these functions are
deprecated now and they are replaced by more generic functions in the
executor code.
2021-05-10 09:43:39 +02:00
0d23df3da2 Provide an internal function to add errors
The old function, addErrMsg, takes only a string with an error
description, but more information is required for the execution errors:
locations and path. addErrMsg should be deprecated after the switching
to the new addError.
2021-05-09 12:42:02 +02:00
5a5f265fe4 Validate non-nullable values inside lists 2021-05-06 22:23:16 +02:00
2220f0ca56 Remove unused OverloadedStrings pragmas 2021-04-14 07:09:21 +02:00
5654b78935 Traverse input object properties once 2021-04-12 07:09:39 +02:00
d6dda14cfd Remove package.yaml
This reduces duplication between the modified cabal file and
package.yaml.
2021-04-07 10:12:40 +02:00
328e6acdee Emit list item errors once 2021-03-16 10:08:13 +01:00
4d762d6356 Add location information to list values 2021-03-14 12:19:30 +01:00
cbccb9ed0b Add -Wall flags to graphql.cabal 2021-02-22 08:30:36 +11:00
ca0f0bd32d Fix some issues with directive definitions
I found some issues with directive definitions:

- I couldn't use `on FIELD_DEFINITION`, I believe because `FIELD` was parsed
  first in `executableDirectiveLocation`. I've combined both
  `executableDirectiveLocation` and `typetypeSystemDirectiveLocation` into one
  function which can reorder them to ensure every directive location gets a fair
  chance at parsing.

Not actually to do with directives, some literals weren't being parsed
correctly.

- The GraphQL spec defines list to be `[]` or `[Value]`, but empty literal lists
  weren't being parsed correctly because of using `some` instead of `many`.

- The GraphQL spec defines objects to be `{}` or `{Name: Value}`, but empty
  literal objects had the same issue.
2021-02-21 23:35:34 +11:00
10e4d64052 Replace Map with OrderedMap 2021-02-19 08:09:04 +01:00
d74e27e903 traverseMaybe OrderedMap 2021-02-15 09:04:16 +01:00
90d36f66b9 Combine value inserted into the OrderedMap 2021-02-14 14:46:06 +01:00
c1a1b47aea Add OrderedMap prototype 2021-02-13 06:56:10 +01:00
1e8405a6d6 Document AST.Document.escape 2021-02-11 12:02:08 +01:00
25 changed files with 1017 additions and 401 deletions

View File

@ -6,6 +6,36 @@ The format is based on
and this project adheres to and this project adheres to
[Haskell Package Versioning Policy](https://pvp.haskell.org/). [Haskell Package Versioning Policy](https://pvp.haskell.org/).
## [1.0.0.0]
### Added
- `Language.GraphQL.Execute.OrderedMap` is a map data structure, that preserves
insertion order.
- `Language.GraphQL.Schema.schemaWithTypes` constructs a complete schema,
including an optional schema description and user-defined types not referenced
in the schema directly (for example interface implementations).
- `Language.GraphQL.Schema.description` returns the optional schema description.
- All errors that can be associated with a location in the query contain
location information.
### Fixed
- Parser now accepts empty lists and objects.
- Parser now accepts all directive locations.
- `valuesOfCorrectTypeRule` doesn't check lists recursively since the
validation traverser calls it on all list items.
- `valuesOfCorrectTypeRule` doesn't check objects recursively since the
validation traverser calls it on all object properties.
- Validation of non-nullable values inside lists.
- `executeField` shouldn't assume that a selection has only one field with a
given name, but it should take the first field. The underlying cause is a
wrong pattern, which (because of the laziness) is executed only if the field
has arguments.
### Changed
- `AST.Document.Value.List` and `AST.Document.ConstValue.ConstList` contain
location information for each list item.
- `Error`: `singleError`, `addErr` and `addErrMsg` are deprecated. They are
internal functions used by the executor for error handling.
## [0.11.1.0] - 2021-02-07 ## [0.11.1.0] - 2021-02-07
### Added ### Added
- `Validate.Rules`: - `Validate.Rules`:
@ -104,7 +134,7 @@ and this project adheres to
`locations`. `locations`.
- Parsing comments in the front of definitions. - Parsing comments in the front of definitions.
- Some missing labels were added to the parsers, some labels were fixed to - Some missing labels were added to the parsers, some labels were fixed to
refer to the AST nodes being parsed. refer to the AST nodes being parsed.
### Added ### Added
- `AST` reexports `AST.Parser`. - `AST` reexports `AST.Parser`.
@ -413,6 +443,7 @@ and this project adheres to
### Added ### Added
- Data types for the GraphQL language. - Data types for the GraphQL language.
[1.0.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v1.0.0.0&rev_to=v0.11.1.0
[0.11.1.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.11.1.0&rev_to=v0.11.0.0 [0.11.1.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.11.1.0&rev_to=v0.11.0.0
[0.11.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.11.0.0&rev_to=v0.10.0.0 [0.11.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.11.0.0&rev_to=v0.10.0.0
[0.10.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.10.0.0&rev_to=v0.9.0.0 [0.10.0.0]: https://www.caraus.tech/projects/pub-graphql/repository/23/diff?rev=v0.10.0.0&rev_to=v0.9.0.0

View File

@ -1,13 +1,7 @@
cabal-version: 2.2 cabal-version: 2.2
-- This file has been generated from package.yaml by hpack version 0.33.0.
--
-- see: https://github.com/sol/hpack
--
-- hash: 15a0880180192f918ba0bd3b3e955c57232f1efe8993745d505fcb6e1aab1451
name: graphql name: graphql
version: 0.11.1.0 version: 1.0.0.0
synopsis: Haskell GraphQL implementation synopsis: Haskell GraphQL implementation
description: Haskell <https://spec.graphql.org/June2018/ GraphQL> implementation. description: Haskell <https://spec.graphql.org/June2018/ GraphQL> implementation.
category: Language category: Language
@ -26,6 +20,7 @@ build-type: Simple
extra-source-files: extra-source-files:
CHANGELOG.md CHANGELOG.md
README.md README.md
tested-with: GHC == 8.10.4
source-repository head source-repository head
type: git type: git
@ -43,6 +38,7 @@ library
Language.GraphQL.Error Language.GraphQL.Error
Language.GraphQL.Execute Language.GraphQL.Execute
Language.GraphQL.Execute.Coerce Language.GraphQL.Execute.Coerce
Language.GraphQL.Execute.OrderedMap
Language.GraphQL.Type Language.GraphQL.Type
Language.GraphQL.Type.In Language.GraphQL.Type.In
Language.GraphQL.Type.Out Language.GraphQL.Type.Out
@ -52,6 +48,7 @@ library
Test.Hspec.GraphQL Test.Hspec.GraphQL
other-modules: other-modules:
Language.GraphQL.Execute.Execution Language.GraphQL.Execute.Execution
Language.GraphQL.Execute.Internal
Language.GraphQL.Execute.Subscribe Language.GraphQL.Execute.Subscribe
Language.GraphQL.Execute.Transform Language.GraphQL.Execute.Transform
Language.GraphQL.Type.Definition Language.GraphQL.Type.Definition
@ -59,19 +56,21 @@ library
Language.GraphQL.Validate.Rules Language.GraphQL.Validate.Rules
hs-source-dirs: hs-source-dirs:
src src
ghc-options: -Wall
build-depends: build-depends:
aeson aeson >= 1.5.6 && < 1.6
, base >=4.7 && <5 , base >= 4.7 && < 5
, conduit , conduit >= 1.3.4 && < 1.4
, containers , containers >= 0.6.2 && < 0.7
, exceptions , exceptions >= 0.10.4 && < 0.11
, hspec-expectations , hspec-expectations >= 0.8.2 && < 0.9
, megaparsec , megaparsec >= 9.0.1 && < 9.1
, parser-combinators , parser-combinators >= 1.3.0 && < 1.4
, scientific , scientific >= 0.3.7 && < 0.4
, text , text >= 1.2.4 && < 1.3
, transformers , transformers >= 0.5.6 && < 0.6
, unordered-containers , unordered-containers >= 0.2.14 && < 0.3
, vector >= 0.12.3 && < 0.13
default-language: Haskell2010 default-language: Haskell2010
test-suite graphql-test test-suite graphql-test
@ -84,34 +83,28 @@ test-suite graphql-test
Language.GraphQL.AST.ParserSpec Language.GraphQL.AST.ParserSpec
Language.GraphQL.ErrorSpec Language.GraphQL.ErrorSpec
Language.GraphQL.Execute.CoerceSpec Language.GraphQL.Execute.CoerceSpec
Language.GraphQL.Execute.OrderedMapSpec
Language.GraphQL.ExecuteSpec Language.GraphQL.ExecuteSpec
Language.GraphQL.Type.OutSpec Language.GraphQL.Type.OutSpec
Language.GraphQL.Validate.RulesSpec Language.GraphQL.Validate.RulesSpec
Test.DirectiveSpec Test.DirectiveSpec
Test.FragmentSpec Test.FragmentSpec
Test.RootOperationSpec Test.RootOperationSpec
Paths_graphql
autogen-modules:
Paths_graphql
hs-source-dirs: hs-source-dirs:
tests tests
ghc-options: -threaded -rtsopts -with-rtsopts=-N ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
build-depends: build-depends:
QuickCheck QuickCheck >= 2.14.1 && < 2.15
, aeson , aeson
, base >=4.7 && <5 , base >= 4.7 && < 5
, conduit , conduit
, containers
, exceptions , exceptions
, graphql , graphql
, hspec , hspec >= 2.8.2 && < 2.9
, hspec-expectations , hspec-megaparsec >= 2.2.0 && < 2.3
, hspec-megaparsec
, megaparsec , megaparsec
, parser-combinators , raw-strings-qq >= 1.1 && < 1.2
, raw-strings-qq
, scientific , scientific
, text , text
, transformers
, unordered-containers , unordered-containers
default-language: Haskell2010 default-language: Haskell2010

View File

@ -1,67 +0,0 @@
name: graphql
version: 0.11.1.0
synopsis: Haskell GraphQL implementation
description:
Haskell <https://spec.graphql.org/June2018/ GraphQL> implementation.
maintainer: belka@caraus.de
git: git://caraus.tech/pub/graphql.git
homepage: https://www.caraus.tech/projects/pub-graphql
bug-reports: https://www.caraus.tech/projects/pub-graphql/issues
category: Language
license: MPL-2.0 AND BSD-3-Clause
copyright:
- (c) 2019-2021 Eugen Wissner
- (c) 2015-2017 J. Daniel Navarro
author:
- Danny Navarro <j@dannynavarro.net>
- Matthías Páll Gissurarson <mpg@mpg.is>
- Sólrún Halla Einarsdóttir <she@mpg.is>
license-file:
- LICENSE
- LICENSE.MPL
extra-source-files:
- CHANGELOG.md
- README.md
dependencies:
- aeson
- base >= 4.7 && < 5
- conduit
- containers
- exceptions
- hspec-expectations
- megaparsec
- parser-combinators
- scientific
- text
- transformers
- unordered-containers
library:
source-dirs: src
other-modules:
- Language.GraphQL.Execute.Execution
- Language.GraphQL.Execute.Subscribe
- Language.GraphQL.Execute.Transform
- Language.GraphQL.Type.Definition
- Language.GraphQL.Type.Internal
- Language.GraphQL.Validate.Rules
tests:
graphql-test:
main: Spec.hs
source-dirs: tests
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- graphql
- hspec
- hspec-megaparsec
- QuickCheck
- raw-strings-qq
generated-other-modules:
- Paths_graphql

View File

@ -226,6 +226,12 @@ type TypeCondition = Name
-- ** Input Values -- ** Input Values
-- | Escapes a single character according to the GraphQL escaping rules for
-- double-quoted string values.
--
-- Characters, that should be escaped, are written as escaped characters with a
-- backslash or Unicode with an \"\\u\". Other characters are returned as
-- strings.
escape :: Char -> String escape :: Char -> String
escape char' escape char'
| char' == '\\' = "\\\\" | char' == '\\' = "\\\\"
@ -257,7 +263,7 @@ data Value
| Boolean Bool | Boolean Bool
| Null | Null
| Enum Name | Enum Name
| List [Value] | List [Node Value]
| Object [ObjectField Value] | Object [ObjectField Value]
deriving Eq deriving Eq
@ -281,7 +287,7 @@ data ConstValue
| ConstBoolean Bool | ConstBoolean Bool
| ConstNull | ConstNull
| ConstEnum Name | ConstEnum Name
| ConstList [ConstValue] | ConstList [Node ConstValue]
| ConstObject [ObjectField ConstValue] | ConstObject [ObjectField ConstValue]
deriving Eq deriving Eq
@ -318,13 +324,13 @@ instance Functor ObjectField where
-- Each operation can include a list of variables: -- Each operation can include a list of variables:
-- --
-- @ -- @
-- query (protagonist: String = "Zarathustra") { -- query (protagonist: String = \"Zarathustra\") {
-- getAuthor(protagonist: $protagonist) -- getAuthor(protagonist: $protagonist)
-- } -- }
-- @ -- @
-- --
-- This query defines an optional variable @protagonist@ of type @String@, -- This query defines an optional variable @protagonist@ of type @String@,
-- its default value is "Zarathustra". If no default value is defined and no -- its default value is \"Zarathustra\". If no default value is defined and no
-- value is provided, a variable can still be @null@ if its type is nullable. -- value is provided, a variable can still be @null@ if its type is nullable.
-- --
-- Variables are usually passed along with the query, but not in the query -- Variables are usually passed along with the query, but not in the query

View File

@ -219,7 +219,7 @@ fromConstValue (Full.ConstBoolean x) = Full.Boolean x
fromConstValue Full.ConstNull = Full.Null fromConstValue Full.ConstNull = Full.Null
fromConstValue (Full.ConstString string) = Full.String string fromConstValue (Full.ConstString string) = Full.String string
fromConstValue (Full.ConstEnum x) = Full.Enum x fromConstValue (Full.ConstEnum x) = Full.Enum x
fromConstValue (Full.ConstList x) = Full.List $ fromConstValue <$> x fromConstValue (Full.ConstList x) = Full.List $ fmap fromConstValue <$> x
fromConstValue (Full.ConstObject x) = Full.Object $ fromConstObjectField <$> x fromConstValue (Full.ConstObject x) = Full.Object $ fromConstObjectField <$> x
where where
fromConstObjectField Full.ObjectField{value = value', ..} = fromConstObjectField Full.ObjectField{value = value', ..} =
@ -266,8 +266,8 @@ stringValue (Pretty indentation) string =
= Builder.fromLazyText (indent (indentation + 1)) = Builder.fromLazyText (indent (indentation + 1))
<> line' <> newline <> acc <> line' <> newline <> acc
listValue :: Formatter -> [Full.Value] -> Lazy.Text listValue :: Formatter -> [Full.Node Full.Value] -> Lazy.Text
listValue formatter = bracketsCommas formatter $ value formatter listValue formatter = bracketsCommas formatter $ value formatter . Full.node
objectValue :: Formatter -> [Full.ObjectField Full.Value] -> Lazy.Text objectValue :: Formatter -> [Full.ObjectField Full.Value] -> Lazy.Text
objectValue formatter = intercalate $ objectField formatter objectValue formatter = intercalate $ objectField formatter

View File

@ -14,11 +14,7 @@ import qualified Control.Applicative.Combinators.NonEmpty as NonEmpty
import Data.List.NonEmpty (NonEmpty(..)) import Data.List.NonEmpty (NonEmpty(..))
import Data.Text (Text) import Data.Text (Text)
import qualified Language.GraphQL.AST.DirectiveLocation as Directive import qualified Language.GraphQL.AST.DirectiveLocation as Directive
import Language.GraphQL.AST.DirectiveLocation import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation)
( DirectiveLocation
, ExecutableDirectiveLocation
, TypeSystemDirectiveLocation
)
import qualified Language.GraphQL.AST.Document as Full import qualified Language.GraphQL.AST.Document as Full
import Language.GraphQL.AST.Lexer import Language.GraphQL.AST.Lexer
import Text.Megaparsec import Text.Megaparsec
@ -96,34 +92,28 @@ directiveLocations = optional pipe
<?> "DirectiveLocations" <?> "DirectiveLocations"
directiveLocation :: Parser DirectiveLocation directiveLocation :: Parser DirectiveLocation
directiveLocation directiveLocation = e (Directive.Query <$ symbol "QUERY")
= Directive.ExecutableDirectiveLocation <$> executableDirectiveLocation <|> e (Directive.Mutation <$ symbol "MUTATION")
<|> Directive.TypeSystemDirectiveLocation <$> typeSystemDirectiveLocation <|> e (Directive.Subscription <$ symbol "SUBSCRIPTION")
<|> t (Directive.FieldDefinition <$ symbol "FIELD_DEFINITION")
<|> e (Directive.Field <$ symbol "FIELD")
<|> e (Directive.FragmentDefinition <$ "FRAGMENT_DEFINITION")
<|> e (Directive.FragmentSpread <$ "FRAGMENT_SPREAD")
<|> e (Directive.InlineFragment <$ "INLINE_FRAGMENT")
<|> t (Directive.Schema <$ symbol "SCHEMA")
<|> t (Directive.Scalar <$ symbol "SCALAR")
<|> t (Directive.Object <$ symbol "OBJECT")
<|> t (Directive.ArgumentDefinition <$ symbol "ARGUMENT_DEFINITION")
<|> t (Directive.Interface <$ symbol "INTERFACE")
<|> t (Directive.Union <$ symbol "UNION")
<|> t (Directive.EnumValue <$ symbol "ENUM_VALUE")
<|> t (Directive.Enum <$ symbol "ENUM")
<|> t (Directive.InputObject <$ symbol "INPUT_OBJECT")
<|> t (Directive.InputFieldDefinition <$ symbol "INPUT_FIELD_DEFINITION")
<?> "DirectiveLocation" <?> "DirectiveLocation"
where
executableDirectiveLocation :: Parser ExecutableDirectiveLocation e = fmap Directive.ExecutableDirectiveLocation
executableDirectiveLocation = Directive.Query <$ symbol "QUERY" t = fmap Directive.TypeSystemDirectiveLocation
<|> Directive.Mutation <$ symbol "MUTATION"
<|> Directive.Subscription <$ symbol "SUBSCRIPTION"
<|> Directive.Field <$ symbol "FIELD"
<|> Directive.FragmentDefinition <$ "FRAGMENT_DEFINITION"
<|> Directive.FragmentSpread <$ "FRAGMENT_SPREAD"
<|> Directive.InlineFragment <$ "INLINE_FRAGMENT"
<?> "ExecutableDirectiveLocation"
typeSystemDirectiveLocation :: Parser TypeSystemDirectiveLocation
typeSystemDirectiveLocation = Directive.Schema <$ symbol "SCHEMA"
<|> Directive.Scalar <$ symbol "SCALAR"
<|> Directive.Object <$ symbol "OBJECT"
<|> Directive.FieldDefinition <$ symbol "FIELD_DEFINITION"
<|> Directive.ArgumentDefinition <$ symbol "ARGUMENT_DEFINITION"
<|> Directive.Interface <$ symbol "INTERFACE"
<|> Directive.Union <$ symbol "UNION"
<|> Directive.Enum <$ symbol "ENUM"
<|> Directive.EnumValue <$ symbol "ENUM_VALUE"
<|> Directive.InputObject <$ symbol "INPUT_OBJECT"
<|> Directive.InputFieldDefinition <$ symbol "INPUT_FIELD_DEFINITION"
<?> "TypeSystemDirectiveLocation"
typeDefinition :: Full.Description -> Parser Full.TypeDefinition typeDefinition :: Full.Description -> Parser Full.TypeDefinition
typeDefinition description' = scalarTypeDefinition description' typeDefinition description' = scalarTypeDefinition description'
@ -460,7 +450,7 @@ value = Full.Variable <$> variable
<|> Full.Null <$ nullValue <|> Full.Null <$ nullValue
<|> Full.String <$> stringValue <|> Full.String <$> stringValue
<|> Full.Enum <$> try enumValue <|> Full.Enum <$> try enumValue
<|> Full.List <$> brackets (some value) <|> Full.List <$> brackets (some $ valueNode value)
<|> Full.Object <$> braces (some $ objectField $ valueNode value) <|> Full.Object <$> braces (some $ objectField $ valueNode value)
<?> "Value" <?> "Value"
@ -471,8 +461,8 @@ constValue = Full.ConstFloat <$> try float
<|> Full.ConstNull <$ nullValue <|> Full.ConstNull <$ nullValue
<|> Full.ConstString <$> stringValue <|> Full.ConstString <$> stringValue
<|> Full.ConstEnum <$> try enumValue <|> Full.ConstEnum <$> try enumValue
<|> Full.ConstList <$> brackets (some constValue) <|> Full.ConstList <$> brackets (many $ valueNode constValue)
<|> Full.ConstObject <$> braces (some $ objectField $ valueNode constValue) <|> Full.ConstObject <$> braces (many $ objectField $ valueNode constValue)
<?> "Value" <?> "Value"
booleanValue :: Parser Bool booleanValue :: Parser Bool

View File

@ -1,6 +1,5 @@
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
-- | Error handling. -- | Error handling.
@ -70,21 +69,25 @@ parseError ParseErrorBundle{..} =
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.
{-# DEPRECATED #-}
addErr :: Monad m => Error -> CollectErrsT m () addErr :: Monad m => Error -> CollectErrsT m ()
addErr v = modify appender addErr v = modify appender
where where
appender :: Monad m => Resolution m -> Resolution m appender :: Monad m => Resolution m -> Resolution m
appender resolution@Resolution{..} = resolution{ errors = errors |> v } appender resolution@Resolution{..} = resolution{ errors = errors |> v }
{-# DEPRECATED #-}
makeErrorMessage :: Text -> Error makeErrorMessage :: Text -> Error
makeErrorMessage s = Error s [] [] makeErrorMessage s = Error s [] []
-- | Constructs a response object containing only the error with the given -- | Constructs a response object containing only the error with the given
-- message. -- message.
{-# DEPRECATED #-}
singleError :: Serialize a => Text -> Response a singleError :: Serialize a => Text -> Response a
singleError message = Response null $ Seq.singleton $ makeErrorMessage message singleError message = Response null $ Seq.singleton $ Error message [] []
-- | Convenience function for just wrapping an error message. -- | Convenience function for just wrapping an error message.
{-# DEPRECATED #-}
addErrMsg :: (Monad m, Serialize a) => Text -> CollectErrsT m a addErrMsg :: (Monad m, Serialize a) => Text -> CollectErrsT m a
addErrMsg errorMessage = (addErr . makeErrorMessage) errorMessage >> pure null addErrMsg errorMessage = (addErr . makeErrorMessage) errorMessage >> pure null

View File

@ -1,4 +1,4 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ExplicitForAll #-}
-- | 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
@ -10,15 +10,22 @@ import Control.Monad.Catch (MonadCatch)
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import Data.Sequence (Seq(..)) import Data.Sequence (Seq(..))
import Data.Text (Text) import Data.Text (Text)
import Language.GraphQL.AST.Document (Document, Name) import qualified Language.GraphQL.AST.Document as Full
import Language.GraphQL.Execute.Coerce import Language.GraphQL.Execute.Coerce
import Language.GraphQL.Execute.Execution import Language.GraphQL.Execute.Execution
import Language.GraphQL.Execute.Internal
import qualified Language.GraphQL.Execute.Transform as Transform import qualified Language.GraphQL.Execute.Transform as Transform
import qualified Language.GraphQL.Execute.Subscribe as Subscribe import qualified Language.GraphQL.Execute.Subscribe as Subscribe
import Language.GraphQL.Error import Language.GraphQL.Error
( Error
, ResponseEventStream
, Response(..)
, runCollectErrs
)
import qualified Language.GraphQL.Type.Definition as Definition 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
import Prelude hiding (null)
-- | 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
@ -29,35 +36,36 @@ import Language.GraphQL.Type.Schema
execute :: (MonadCatch m, VariableValue a, Serialize b) execute :: (MonadCatch m, VariableValue a, Serialize b)
=> Schema m -- ^ Resolvers. => Schema m -- ^ Resolvers.
-> Maybe Text -- ^ Operation name. -> Maybe Text -- ^ Operation name.
-> HashMap Name a -- ^ Variable substitution function. -> HashMap Full.Name a -- ^ Variable substitution function.
-> Document -- @GraphQL@ document. -> Full.Document -- @GraphQL@ document.
-> m (Either (ResponseEventStream m b) (Response b)) -> m (Either (ResponseEventStream m b) (Response b))
execute schema' operationName subs document = execute schema' operationName subs document
case Transform.document schema' operationName subs document of = either (pure . rightErrorResponse . singleError [] . show) executeRequest
Left queryError -> pure $ Transform.document schema' operationName subs document
$ Right
$ singleError
$ Transform.queryError queryError
Right transformed -> executeRequest transformed
executeRequest :: (MonadCatch m, Serialize a) executeRequest :: (MonadCatch m, Serialize a)
=> Transform.Document m => Transform.Document m
-> m (Either (ResponseEventStream m a) (Response a)) -> m (Either (ResponseEventStream m a) (Response a))
executeRequest (Transform.Document types' rootObjectType operation) executeRequest (Transform.Document types' rootObjectType operation)
| (Transform.Query _ fields) <- operation = | (Transform.Query _ fields objectLocation) <- operation =
Right <$> executeOperation types' rootObjectType fields Right <$> executeOperation types' rootObjectType objectLocation fields
| (Transform.Mutation _ fields) <- operation = | (Transform.Mutation _ fields objectLocation) <- operation =
Right <$> executeOperation types' rootObjectType fields Right <$> executeOperation types' rootObjectType objectLocation fields
| (Transform.Subscription _ fields) <- operation | (Transform.Subscription _ fields objectLocation) <- operation
= either (Right . singleError) Left = either rightErrorResponse Left
<$> Subscribe.subscribe types' rootObjectType fields <$> Subscribe.subscribe types' rootObjectType objectLocation 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 :: (MonadCatch m, Serialize a) executeOperation :: (MonadCatch m, Serialize a)
=> HashMap Name (Type m) => HashMap Full.Name (Type m)
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> m (Response a) -> m (Response a)
executeOperation types' objectType fields = executeOperation types' objectType objectLocation fields
runCollectErrs types' $ executeSelectionSet Definition.Null objectType fields = runCollectErrs types'
$ executeSelectionSet Definition.Null objectType objectLocation fields
rightErrorResponse :: Serialize b => forall a. Error -> Either a (Response b)
rightErrorResponse = Right . Response null . pure

View File

@ -19,7 +19,6 @@ import qualified Data.Aeson as Aeson
import Data.Int (Int32) import Data.Int (Int32)
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.Map.Strict (Map)
import Data.String (IsString(..)) import Data.String (IsString(..))
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text.Lazy as Text.Lazy import qualified Data.Text.Lazy as Text.Lazy
@ -27,6 +26,8 @@ import qualified Data.Text.Lazy.Builder as Text.Builder
import qualified Data.Text.Lazy.Builder.Int as Text.Builder import qualified Data.Text.Lazy.Builder.Int as Text.Builder
import Data.Scientific (toBoundedInteger, toRealFloat) import Data.Scientific (toBoundedInteger, toRealFloat)
import Language.GraphQL.AST (Name) import Language.GraphQL.AST (Name)
import Language.GraphQL.Execute.OrderedMap (OrderedMap)
import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap
import qualified Language.GraphQL.Type as Type import qualified Language.GraphQL.Type as Type
import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out
@ -209,7 +210,7 @@ data Output a
| Boolean Bool | Boolean Bool
| Enum Name | Enum Name
| List [a] | List [a]
| Object (Map Name a) | Object (OrderedMap a)
deriving (Eq, Show) deriving (Eq, Show)
instance forall a. IsString (Output a) where instance forall a. IsString (Output a) where
@ -229,6 +230,9 @@ instance Serialize Aeson.Value where
, Boolean boolean <- value = Just $ Aeson.Bool boolean , Boolean boolean <- value = Just $ Aeson.Bool boolean
serialize _ (Enum enum) = Just $ Aeson.String enum serialize _ (Enum enum) = Just $ Aeson.String enum
serialize _ (List list) = Just $ Aeson.toJSON list serialize _ (List list) = Just $ Aeson.toJSON list
serialize _ (Object object) = Just $ Aeson.toJSON object serialize _ (Object object) = Just
$ Aeson.object
$ OrderedMap.toList
$ Aeson.toJSON <$> object
serialize _ _ = Nothing serialize _ _ = Nothing
null = Aeson.Null null = Aeson.Null

View File

@ -1,4 +1,5 @@
{-# LANGUAGE ExplicitForAll #-} {-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
@ -13,16 +14,18 @@ import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Reader (runReaderT) import Control.Monad.Trans.Reader (runReaderT)
import Control.Monad.Trans.State (gets) import Control.Monad.Trans.State (gets)
import Data.List.NonEmpty (NonEmpty(..)) import Data.List.NonEmpty (NonEmpty(..))
import Data.Map.Strict (Map) import qualified Data.List.NonEmpty as NonEmpty
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 qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Sequence (Seq(..)) import Data.Sequence (Seq(..))
import qualified Data.Text as Text import qualified Data.Text as Text
import Language.GraphQL.AST (Name) import qualified Language.GraphQL.AST as Full
import Language.GraphQL.Error import Language.GraphQL.Error
import Language.GraphQL.Execute.Coerce import Language.GraphQL.Execute.Coerce
import Language.GraphQL.Execute.Internal
import Language.GraphQL.Execute.OrderedMap (OrderedMap)
import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap
import qualified Language.GraphQL.Execute.Transform as Transform import qualified Language.GraphQL.Execute.Transform as Transform
import qualified Language.GraphQL.Type as Type import qualified Language.GraphQL.Type as Type
import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.In as In
@ -34,15 +37,17 @@ resolveFieldValue :: MonadCatch m
=> Type.Value => Type.Value
-> Type.Subs -> Type.Subs
-> Type.Resolve m -> Type.Resolve m
-> Full.Location
-> CollectErrsT m Type.Value -> CollectErrsT m Type.Value
resolveFieldValue result args resolver = resolveFieldValue result args resolver location' =
catch (lift $ runReaderT resolver context) handleFieldError catch (lift $ runReaderT resolver context) handleFieldError
where where
handleFieldError :: MonadCatch m handleFieldError :: MonadCatch m
=> ResolverException => ResolverException
-> CollectErrsT m Type.Value -> CollectErrsT m Type.Value
handleFieldError e = handleFieldError e
addErr (Error (Text.pack $ displayException e) [] []) >> pure Type.Null = addError Type.Null
$ Error (Text.pack $ displayException e) [location'] []
context = Type.Context context = Type.Context
{ Type.arguments = Type.Arguments args { Type.arguments = Type.Arguments args
, Type.values = result , Type.values = result
@ -51,21 +56,21 @@ resolveFieldValue result args resolver =
collectFields :: Monad m collectFields :: Monad m
=> Out.ObjectType m => Out.ObjectType m
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> Map Name (NonEmpty (Transform.Field m)) -> OrderedMap (NonEmpty (Transform.Field m))
collectFields objectType = foldl forEach Map.empty collectFields objectType = foldl forEach OrderedMap.empty
where where
forEach groupedFields (Transform.SelectionField field) = forEach groupedFields (Transform.SelectionField field) =
let responseKey = aliasOrName field let responseKey = aliasOrName field
in Map.insertWith (<>) responseKey (field :| []) groupedFields in OrderedMap.insert responseKey (field :| []) groupedFields
forEach groupedFields (Transform.SelectionFragment selectionFragment) forEach groupedFields (Transform.SelectionFragment selectionFragment)
| Transform.Fragment fragmentType fragmentSelectionSet <- selectionFragment | Transform.Fragment fragmentType fragmentSelectionSet <- selectionFragment
, Internal.doesFragmentTypeApply fragmentType objectType = , Internal.doesFragmentTypeApply fragmentType objectType =
let fragmentGroupedFieldSet = collectFields objectType fragmentSelectionSet let fragmentGroupedFieldSet = collectFields objectType fragmentSelectionSet
in Map.unionWith (<>) groupedFields fragmentGroupedFieldSet in groupedFields <> fragmentGroupedFieldSet
| otherwise = groupedFields | otherwise = groupedFields
aliasOrName :: forall m. Transform.Field m -> Name aliasOrName :: forall m. Transform.Field m -> Full.Name
aliasOrName (Transform.Field alias name _ _) = fromMaybe name alias aliasOrName (Transform.Field alias name _ _ _) = fromMaybe name alias
resolveAbstractType :: Monad m resolveAbstractType :: Monad m
=> Internal.AbstractType m => Internal.AbstractType m
@ -95,11 +100,15 @@ executeField fieldResolver prev fields
where where
executeField' fieldDefinition resolver = do executeField' fieldDefinition resolver = do
let Out.Field _ fieldType argumentDefinitions = fieldDefinition let Out.Field _ fieldType argumentDefinitions = fieldDefinition
let (Transform.Field _ _ arguments' _ :| []) = fields let Transform.Field _ _ arguments' _ location' = NonEmpty.head fields
case coerceArgumentValues argumentDefinitions arguments' of case coerceArgumentValues argumentDefinitions arguments' of
Nothing -> addErrMsg "Argument coercing failed." Left [] ->
Just argumentValues -> do let errorMessage = "Not all required arguments are specified."
answer <- resolveFieldValue prev argumentValues resolver in addError null $ Error errorMessage [location'] []
Left errorLocations -> addError null
$ Error "Argument coercing failed." errorLocations []
Right argumentValues -> do
answer <- resolveFieldValue prev argumentValues resolver location'
completeValue fieldType fields answer completeValue fieldType fields answer
completeValue :: (MonadCatch m, Serialize a) completeValue :: (MonadCatch m, Serialize a)
@ -110,55 +119,67 @@ completeValue :: (MonadCatch m, Serialize a)
completeValue (Out.isNonNullType -> False) _ Type.Null = pure null completeValue (Out.isNonNullType -> False) _ Type.Null = pure null
completeValue outputType@(Out.ListBaseType listType) fields (Type.List list) completeValue outputType@(Out.ListBaseType listType) fields (Type.List list)
= traverse (completeValue listType fields) list = traverse (completeValue listType fields) list
>>= coerceResult outputType . List >>= coerceResult outputType (firstFieldLocation fields) . List
completeValue outputType@(Out.ScalarBaseType _) _ (Type.Int int) = completeValue outputType@(Out.ScalarBaseType _) fields (Type.Int int) =
coerceResult outputType $ Int int coerceResult outputType (firstFieldLocation fields) $ Int int
completeValue outputType@(Out.ScalarBaseType _) _ (Type.Boolean boolean) = completeValue outputType@(Out.ScalarBaseType _) fields (Type.Boolean boolean) =
coerceResult outputType $ Boolean boolean coerceResult outputType (firstFieldLocation fields) $ Boolean boolean
completeValue outputType@(Out.ScalarBaseType _) _ (Type.Float float) = completeValue outputType@(Out.ScalarBaseType _) fields (Type.Float float) =
coerceResult outputType $ Float float coerceResult outputType (firstFieldLocation fields) $ Float float
completeValue outputType@(Out.ScalarBaseType _) _ (Type.String string) = completeValue outputType@(Out.ScalarBaseType _) fields (Type.String string) =
coerceResult outputType $ String string coerceResult outputType (firstFieldLocation fields) $ String string
completeValue outputType@(Out.EnumBaseType enumType) _ (Type.Enum enum) = completeValue outputType@(Out.EnumBaseType enumType) fields (Type.Enum enum) =
let Type.EnumType _ _ enumMembers = enumType let Type.EnumType _ _ enumMembers = enumType
location = firstFieldLocation fields
in if HashMap.member enum enumMembers in if HashMap.member enum enumMembers
then coerceResult outputType $ Enum enum then coerceResult outputType location $ Enum enum
else addErrMsg "Enum value completion failed." else addError null $ Error "Enum value completion failed." [location] []
completeValue (Out.ObjectBaseType objectType) fields result = completeValue (Out.ObjectBaseType objectType) fields result
executeSelectionSet result objectType $ mergeSelectionSets fields = executeSelectionSet result objectType (firstFieldLocation fields)
$ mergeSelectionSets fields
completeValue (Out.InterfaceBaseType interfaceType) fields result completeValue (Out.InterfaceBaseType interfaceType) fields result
| Type.Object objectMap <- result = do | Type.Object objectMap <- result = do
let abstractType = Internal.AbstractInterfaceType interfaceType let abstractType = Internal.AbstractInterfaceType interfaceType
let location = firstFieldLocation fields
concreteType <- resolveAbstractType abstractType objectMap concreteType <- resolveAbstractType abstractType objectMap
case concreteType of case concreteType of
Just objectType -> executeSelectionSet result objectType Just objectType -> executeSelectionSet result objectType location
$ mergeSelectionSets fields $ mergeSelectionSets fields
Nothing -> addErrMsg "Interface value completion failed." Nothing -> addError null
$ Error "Interface value completion failed." [location] []
completeValue (Out.UnionBaseType unionType) fields result completeValue (Out.UnionBaseType unionType) fields result
| Type.Object objectMap <- result = do | Type.Object objectMap <- result = do
let abstractType = Internal.AbstractUnionType unionType let abstractType = Internal.AbstractUnionType unionType
let location = firstFieldLocation fields
concreteType <- resolveAbstractType abstractType objectMap concreteType <- resolveAbstractType abstractType objectMap
case concreteType of case concreteType of
Just objectType -> executeSelectionSet result objectType Just objectType -> executeSelectionSet result objectType
$ mergeSelectionSets fields location $ mergeSelectionSets fields
Nothing -> addErrMsg "Union value completion failed." Nothing -> addError null
completeValue _ _ _ = addErrMsg "Value completion failed." $ Error "Union value completion failed." [location] []
completeValue _ (Transform.Field _ _ _ _ location :| _) _ =
addError null $ Error "Value completion failed." [location] []
mergeSelectionSets :: MonadCatch m mergeSelectionSets :: MonadCatch m
=> NonEmpty (Transform.Field m) => NonEmpty (Transform.Field m)
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
mergeSelectionSets = foldr forEach mempty mergeSelectionSets = foldr forEach mempty
where where
forEach (Transform.Field _ _ _ fieldSelectionSet) selectionSet = forEach (Transform.Field _ _ _ fieldSelectionSet _) selectionSet =
selectionSet <> fieldSelectionSet selectionSet <> fieldSelectionSet
firstFieldLocation :: MonadCatch m => NonEmpty (Transform.Field m) -> Full.Location
firstFieldLocation (Transform.Field _ _ _ _ fieldLocation :| _) = fieldLocation
coerceResult :: (MonadCatch m, Serialize a) coerceResult :: (MonadCatch m, Serialize a)
=> Out.Type m => Out.Type m
-> Full.Location
-> Output a -> Output a
-> CollectErrsT m a -> CollectErrsT m a
coerceResult outputType result coerceResult outputType parentLocation result
| Just serialized <- serialize outputType result = pure serialized | Just serialized <- serialize outputType result = pure serialized
| otherwise = addErrMsg "Result coercion failed." | otherwise = addError null
$ Error "Result coercion failed." [parentLocation] []
-- | Takes an 'Out.ObjectType' and a list of 'Transform.Selection's and applies -- | Takes an 'Out.ObjectType' and a list of 'Transform.Selection's and applies
-- each field to each 'Transform.Selection'. Resolves into a value containing -- each field to each 'Transform.Selection'. Resolves into a value containing
@ -166,29 +187,45 @@ coerceResult outputType result
executeSelectionSet :: (MonadCatch m, Serialize a) executeSelectionSet :: (MonadCatch m, Serialize a)
=> Type.Value => Type.Value
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> CollectErrsT m a -> CollectErrsT m a
executeSelectionSet result objectType@(Out.ObjectType _ _ _ resolvers) selectionSet = do executeSelectionSet result objectType@(Out.ObjectType _ _ _ resolvers) objectLocation selectionSet = do
let fields = collectFields objectType selectionSet let fields = collectFields objectType selectionSet
resolvedValues <- Map.traverseMaybeWithKey forEach fields resolvedValues <- OrderedMap.traverseMaybe forEach fields
coerceResult (Out.NonNullObjectType objectType) $ Object resolvedValues coerceResult (Out.NonNullObjectType objectType) objectLocation
$ Object resolvedValues
where where
forEach _ fields@(field :| _) = forEach fields@(field :| _) =
let Transform.Field _ name _ _ = field let Transform.Field _ name _ _ _ = field
in traverse (tryResolver fields) $ lookupResolver name in traverse (tryResolver fields) $ lookupResolver name
lookupResolver = flip HashMap.lookup resolvers lookupResolver = flip HashMap.lookup resolvers
tryResolver fields resolver = tryResolver fields resolver =
executeField resolver result fields >>= lift . pure executeField resolver result fields >>= lift . pure
coerceArgumentValues coerceArgumentValues
:: HashMap Name In.Argument :: HashMap Full.Name In.Argument
-> HashMap Name Transform.Input -> HashMap Full.Name (Full.Node Transform.Input)
-> Maybe Type.Subs -> Either [Full.Location] Type.Subs
coerceArgumentValues argumentDefinitions argumentValues = coerceArgumentValues argumentDefinitions argumentNodes =
HashMap.foldrWithKey forEach (pure mempty) argumentDefinitions HashMap.foldrWithKey forEach (pure mempty) argumentDefinitions
where where
forEach variableName (In.Argument _ variableType defaultValue) = forEach argumentName (In.Argument _ variableType defaultValue) = \case
matchFieldValues coerceArgumentValue argumentValues variableName variableType defaultValue Right resultMap
| Just matchedValues
<- matchFieldValues' argumentName variableType defaultValue $ Just resultMap
-> Right matchedValues
| otherwise -> Left $ generateError argumentName []
Left errorLocations
| Just _
<- matchFieldValues' argumentName variableType defaultValue $ pure mempty
-> Left errorLocations
| otherwise -> Left $ generateError argumentName errorLocations
generateError argumentName errorLocations =
case HashMap.lookup argumentName argumentNodes of
Just (Full.Node _ errorLocation) -> [errorLocation]
Nothing -> errorLocations
matchFieldValues' = matchFieldValues coerceArgumentValue (Full.node <$> argumentNodes)
coerceArgumentValue inputType (Transform.Int integer) = coerceArgumentValue inputType (Transform.Int integer) =
coerceInputLiteral inputType (Type.Int integer) coerceInputLiteral inputType (Type.Int integer)
coerceArgumentValue inputType (Transform.Boolean boolean) = coerceArgumentValue inputType (Transform.Boolean boolean) =

View File

@ -0,0 +1,31 @@
{- 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 DuplicateRecordFields #-}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE NamedFieldPuns #-}
module Language.GraphQL.Execute.Internal
( addError
, singleError
) where
import Control.Monad.Trans.State (modify)
import Control.Monad.Catch (MonadCatch)
import Data.Sequence ((|>))
import qualified Data.Text as Text
import qualified Language.GraphQL.AST as Full
import Language.GraphQL.Error (CollectErrsT, Error(..), Resolution(..))
import Prelude hiding (null)
addError :: MonadCatch m => forall a. a -> Error -> CollectErrsT m a
addError returnValue error' = modify appender >> pure returnValue
where
appender :: Resolution m -> Resolution m
appender resolution@Resolution{ errors } = resolution
{ errors = errors |> error'
}
singleError :: [Full.Location] -> String -> Error
singleError errorLocations message = Error (Text.pack message) errorLocations []

View File

@ -0,0 +1,148 @@
{- 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 ExplicitForAll #-}
-- | This module contains a map data structure, that preserves insertion order.
-- Some definitions conflict with functions from prelude, so this module should
-- probably be imported qualified.
module Language.GraphQL.Execute.OrderedMap
( OrderedMap
, elems
, empty
, insert
, foldlWithKey'
, keys
, lookup
, replace
, singleton
, size
, toList
, traverseMaybe
) where
import qualified Data.Foldable as Foldable
import Data.HashMap.Strict (HashMap, (!))
import qualified Data.HashMap.Strict as HashMap
import Data.Text (Text)
import Data.Vector (Vector)
import qualified Data.Vector as Vector
import Prelude hiding (filter, lookup)
-- | This map associates values with the given text keys. Insertion order is
-- preserved. When inserting a value with a key, that is already available in
-- the map, the existing value isn't overridden, but combined with the new value
-- using its 'Semigroup' instance.
--
-- Internally this map uses an array with keys to preserve the order and an
-- unorded map with key-value pairs.
data OrderedMap v = OrderedMap (Vector Text) (HashMap Text v)
deriving (Eq)
instance Functor OrderedMap where
fmap f (OrderedMap vector hashMap) = OrderedMap vector $ fmap f hashMap
instance Foldable OrderedMap where
foldr f = foldrWithKey $ const f
null (OrderedMap vector _) = Vector.null vector
instance Semigroup v => Semigroup (OrderedMap v) where
(<>) = foldlWithKey'
$ \accumulator key value -> insert key value accumulator
instance Semigroup v => Monoid (OrderedMap v) where
mempty = empty
instance Traversable OrderedMap where
traverse f (OrderedMap vector hashMap) = OrderedMap vector
<$> traverse f hashMap
instance Show v => Show (OrderedMap v) where
showsPrec precedence map' = showParen (precedence > 10)
$ showString "fromList " . shows (toList map')
-- * Construction
-- | Constructs a map with a single element.
singleton :: forall v. Text -> v -> OrderedMap v
singleton key value = OrderedMap (Vector.singleton key)
$ HashMap.singleton key value
-- | Constructs an empty map.
empty :: forall v. OrderedMap v
empty = OrderedMap mempty mempty
-- * Traversal
-- | Reduces this map by applying a binary operator from right to left to all
-- elements, using the given starting value.
foldrWithKey :: forall v a. (Text -> v -> a -> a) -> a -> OrderedMap v -> a
foldrWithKey f initial (OrderedMap vector hashMap) = foldr go initial vector
where
go key = f key (hashMap ! key)
-- | Reduces this map by applying a binary operator from left to right to all
-- elements, using the given starting value.
foldlWithKey' :: forall v a. (a -> Text -> v -> a) -> a -> OrderedMap v -> a
foldlWithKey' f initial (OrderedMap vector hashMap) =
Vector.foldl' go initial vector
where
go accumulator key = f accumulator key (hashMap ! key)
-- | Traverse over the elements and collect the 'Just' results.
traverseMaybe
:: Applicative f
=> forall a
. (a -> f (Maybe b))
-> OrderedMap a
-> f (OrderedMap b)
traverseMaybe f orderedMap = foldlWithKey' filter empty
<$> traverse f orderedMap
where
filter accumulator key (Just value) = replace key value accumulator
filter accumulator _ Nothing = accumulator
-- * Lists
-- | Converts this map to the list of key-value pairs.
toList :: forall v. OrderedMap v -> [(Text, v)]
toList = foldrWithKey ((.) (:) . (,)) []
-- | Returns a list with all keys in this map.
keys :: forall v. OrderedMap v -> [Text]
keys (OrderedMap vector _) = Foldable.toList vector
-- | Returns a list with all elements in this map.
elems :: forall v. OrderedMap v -> [v]
elems = fmap snd . toList
-- * Basic interface
-- | Associates the specified value with the specified key in this map. If this
-- map previously contained a mapping for the key, the existing and new values
-- are combined.
insert :: Semigroup v => Text -> v -> OrderedMap v -> OrderedMap v
insert key value (OrderedMap vector hashMap)
| Just available <- HashMap.lookup key hashMap = OrderedMap vector
$ HashMap.insert key (available <> value) hashMap
| otherwise = OrderedMap (Vector.snoc vector key)
$ HashMap.insert key value hashMap
-- | Associates the specified value with the specified key in this map. If this
-- map previously contained a mapping for the key, the existing value is
-- replaced by the new one.
replace :: Text -> v -> OrderedMap v -> OrderedMap v
replace key value (OrderedMap vector hashMap)
| HashMap.member key hashMap = OrderedMap vector
$ HashMap.insert key value hashMap
| otherwise = OrderedMap (Vector.snoc vector key)
$ HashMap.insert key value hashMap
-- | Gives the size of this map, i.e. number of elements in it.
size :: forall v. OrderedMap v -> Int
size (OrderedMap vector _) = Vector.length vector
-- | Looks up a value in this map by key.
lookup :: forall v. Text -> OrderedMap v -> Maybe v
lookup key (OrderedMap _ hashMap) = HashMap.lookup key hashMap

View File

@ -9,62 +9,78 @@ module Language.GraphQL.Execute.Subscribe
) where ) where
import Conduit import Conduit
import Control.Arrow (left)
import Control.Monad.Catch (Exception(..), MonadCatch(..)) import Control.Monad.Catch (Exception(..), MonadCatch(..))
import Control.Monad.Trans.Reader (ReaderT(..), runReaderT) import Control.Monad.Trans.Reader (ReaderT(..), runReaderT)
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 qualified Data.Map.Strict as Map
import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List.NonEmpty as NonEmpty
import Data.Sequence (Seq(..)) import Data.Sequence (Seq(..))
import Data.Text (Text) import qualified Language.GraphQL.AST as Full
import qualified Data.Text as Text
import Language.GraphQL.AST (Name)
import Language.GraphQL.Execute.Coerce import Language.GraphQL.Execute.Coerce
import Language.GraphQL.Execute.Execution import Language.GraphQL.Execute.Execution
import Language.GraphQL.Execute.Internal
import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap
import qualified Language.GraphQL.Execute.Transform as Transform import qualified Language.GraphQL.Execute.Transform as Transform
import Language.GraphQL.Error import Language.GraphQL.Error
( Error(..)
, ResolverException
, Response
, ResponseEventStream
, runCollectErrs
)
import qualified Language.GraphQL.Type.Definition as Definition import qualified Language.GraphQL.Type.Definition as Definition
import qualified Language.GraphQL.Type as Type import qualified Language.GraphQL.Type as Type
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
-- This is actually executeMutation, but we don't distinguish between queries
-- and mutations yet.
subscribe :: (MonadCatch m, Serialize a) subscribe :: (MonadCatch m, Serialize a)
=> HashMap Name (Type m) => HashMap Full.Name (Type m)
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> m (Either Text (ResponseEventStream m a)) -> m (Either Error (ResponseEventStream m a))
subscribe types' objectType fields = do subscribe types' objectType objectLocation fields = do
sourceStream <- createSourceEventStream types' objectType fields sourceStream <-
traverse (mapSourceToResponseEvent types' objectType fields) sourceStream createSourceEventStream types' objectType objectLocation fields
let traverser =
mapSourceToResponseEvent types' objectType objectLocation fields
traverse traverser sourceStream
mapSourceToResponseEvent :: (MonadCatch m, Serialize a) mapSourceToResponseEvent :: (MonadCatch m, Serialize a)
=> HashMap Name (Type m) => HashMap Full.Name (Type m)
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> Out.SourceEventStream m -> Out.SourceEventStream m
-> m (ResponseEventStream m a) -> m (ResponseEventStream m a)
mapSourceToResponseEvent types' subscriptionType fields sourceStream = pure mapSourceToResponseEvent types' subscriptionType objectLocation fields sourceStream
= pure
$ sourceStream $ sourceStream
.| mapMC (executeSubscriptionEvent types' subscriptionType fields) .| mapMC (executeSubscriptionEvent types' subscriptionType objectLocation fields)
createSourceEventStream :: MonadCatch m createSourceEventStream :: MonadCatch m
=> HashMap Name (Type m) => HashMap Full.Name (Type m)
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> m (Either Text (Out.SourceEventStream m)) -> m (Either Error (Out.SourceEventStream m))
createSourceEventStream _types subscriptionType@(Out.ObjectType _ _ _ fieldTypes) fields createSourceEventStream _types subscriptionType objectLocation fields
| [fieldGroup] <- Map.elems groupedFieldSet | [fieldGroup] <- OrderedMap.elems groupedFieldSet
, Transform.Field _ fieldName arguments' _ <- NonEmpty.head fieldGroup , Transform.Field _ fieldName arguments' _ errorLocation <- NonEmpty.head fieldGroup
, Out.ObjectType _ _ _ fieldTypes <- subscriptionType
, resolverT <- fieldTypes HashMap.! fieldName , resolverT <- fieldTypes HashMap.! fieldName
, Out.EventStreamResolver fieldDefinition _ resolver <- resolverT , Out.EventStreamResolver fieldDefinition _ resolver <- resolverT
, Out.Field _ _fieldType argumentDefinitions <- fieldDefinition = , Out.Field _ _fieldType argumentDefinitions <- fieldDefinition =
case coerceArgumentValues argumentDefinitions arguments' of case coerceArgumentValues argumentDefinitions arguments' of
Nothing -> pure $ Left "Argument coercion failed." Left _ -> pure
Just argumentValues -> $ Left
resolveFieldEventStream Type.Null argumentValues resolver $ Error "Argument coercion failed." [errorLocation] []
| otherwise = pure $ Left "Subscription contains more than one field." Right argumentValues -> left (singleError [errorLocation])
<$> resolveFieldEventStream Type.Null argumentValues resolver
| otherwise = pure
$ Left
$ Error "Subscription contains more than one field." [objectLocation] []
where where
groupedFieldSet = collectFields subscriptionType fields groupedFieldSet = collectFields subscriptionType fields
@ -72,26 +88,26 @@ resolveFieldEventStream :: MonadCatch m
=> Type.Value => Type.Value
-> Type.Subs -> Type.Subs
-> Out.Subscribe m -> Out.Subscribe m
-> m (Either Text (Out.SourceEventStream m)) -> m (Either String (Out.SourceEventStream m))
resolveFieldEventStream result args resolver = resolveFieldEventStream result args resolver =
catch (Right <$> runReaderT resolver context) handleEventStreamError catch (Right <$> runReaderT resolver context) handleEventStreamError
where where
handleEventStreamError :: MonadCatch m handleEventStreamError :: MonadCatch m
=> ResolverException => ResolverException
-> m (Either Text (Out.SourceEventStream m)) -> m (Either String (Out.SourceEventStream m))
handleEventStreamError = pure . Left . Text.pack . displayException handleEventStreamError = pure . Left . displayException
context = Type.Context context = Type.Context
{ Type.arguments = Type.Arguments args { Type.arguments = Type.Arguments args
, Type.values = result , Type.values = result
} }
-- This is actually executeMutation, but we don't distinguish between queries
-- and mutations yet.
executeSubscriptionEvent :: (MonadCatch m, Serialize a) executeSubscriptionEvent :: (MonadCatch m, Serialize a)
=> HashMap Name (Type m) => HashMap Full.Name (Type m)
-> Out.ObjectType m -> Out.ObjectType m
-> Full.Location
-> Seq (Transform.Selection m) -> Seq (Transform.Selection m)
-> Definition.Value -> Definition.Value
-> m (Response a) -> m (Response a)
executeSubscriptionEvent types' objectType fields initialValue = executeSubscriptionEvent types' objectType objectLocation fields initialValue
runCollectErrs types' $ executeSelectionSet initialValue objectType fields = runCollectErrs types'
$ executeSelectionSet initialValue objectType objectLocation fields

View File

@ -1,3 +1,7 @@
{- 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 ExplicitForAll #-} {-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
@ -25,7 +29,6 @@ module Language.GraphQL.Execute.Transform
, QueryError(..) , QueryError(..)
, Selection(..) , Selection(..)
, document , document
, queryError
) where ) where
import Control.Monad (foldM, unless) import Control.Monad (foldM, unless)
@ -71,16 +74,18 @@ data Selection m
| SelectionField (Field m) | SelectionField (Field m)
-- | GraphQL has 3 operation types: queries, mutations and subscribtions. -- | GraphQL has 3 operation types: queries, mutations and subscribtions.
--
-- Currently only queries and mutations are supported.
data Operation m data Operation m
= Query (Maybe Text) (Seq (Selection m)) = Query (Maybe Text) (Seq (Selection m)) Full.Location
| Mutation (Maybe Text) (Seq (Selection m)) | Mutation (Maybe Text) (Seq (Selection m)) Full.Location
| Subscription (Maybe Text) (Seq (Selection m)) | Subscription (Maybe Text) (Seq (Selection m)) Full.Location
-- | Single GraphQL field. -- | Single GraphQL field.
data Field m = Field data Field m = Field
(Maybe Full.Name) Full.Name (HashMap Full.Name Input) (Seq (Selection m)) (Maybe Full.Name)
Full.Name
(HashMap Full.Name (Full.Node Input))
(Seq (Selection m))
Full.Location
-- | Contains the operation to be executed along with its root type. -- | Contains the operation to be executed along with its root type.
data Document m = Document data Document m = Document
@ -92,16 +97,26 @@ data OperationDefinition = OperationDefinition
[Full.VariableDefinition] [Full.VariableDefinition]
[Full.Directive] [Full.Directive]
Full.SelectionSet Full.SelectionSet
Full.Location
-- | Query error types. -- | Query error types.
data QueryError data QueryError
= OperationNotFound Text = OperationNotFound Text
| OperationNameRequired | OperationNameRequired
| CoercionError | CoercionError
| TransformationError
| EmptyDocument | EmptyDocument
| UnsupportedRootOperation | UnsupportedRootOperation
instance Show QueryError where
show (OperationNotFound operationName) = unwords
["Operation", Text.unpack operationName, "couldn't be found in the document."]
show OperationNameRequired = "Missing operation name."
show CoercionError = "Coercion error."
show EmptyDocument =
"The document doesn't contain any executable operations."
show UnsupportedRootOperation =
"Root operation type couldn't be found in the schema."
data Input data Input
= Int Int32 = Int Int32
| Float Double | Float Double
@ -114,17 +129,6 @@ data Input
| Variable Type.Value | Variable Type.Value
deriving (Eq, Show) deriving (Eq, Show)
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."
queryError UnsupportedRootOperation =
"Root operation type couldn't be found in the schema."
getOperation getOperation
:: Maybe Full.Name :: Maybe Full.Name
-> NonEmpty OperationDefinition -> NonEmpty OperationDefinition
@ -135,7 +139,7 @@ getOperation (Just operationName) operations
| Just operation' <- find matchingName operations = pure operation' | Just operation' <- find matchingName operations = pure operation'
| otherwise = Left $ OperationNotFound operationName | otherwise = Left $ OperationNotFound operationName
where where
matchingName (OperationDefinition _ name _ _ _) = matchingName (OperationDefinition _ name _ _ _ _) =
name == Just operationName name == Just operationName
coerceVariableValues :: Coerce.VariableValue a coerceVariableValues :: Coerce.VariableValue a
@ -145,7 +149,7 @@ coerceVariableValues :: Coerce.VariableValue a
-> HashMap.HashMap Full.Name a -> HashMap.HashMap Full.Name a
-> Either QueryError Type.Subs -> Either QueryError Type.Subs
coerceVariableValues types operationDefinition variableValues = coerceVariableValues types operationDefinition variableValues =
let OperationDefinition _ _ variableDefinitions _ _ = operationDefinition let OperationDefinition _ _ variableDefinitions _ _ _ = operationDefinition
in maybe (Left CoercionError) Right in maybe (Left CoercionError) Right
$ foldr forEach (Just HashMap.empty) variableDefinitions $ foldr forEach (Just HashMap.empty) variableDefinitions
where where
@ -173,7 +177,7 @@ constValue (Full.ConstString x) = Type.String x
constValue (Full.ConstBoolean b) = Type.Boolean b constValue (Full.ConstBoolean b) = Type.Boolean b
constValue Full.ConstNull = Type.Null constValue Full.ConstNull = Type.Null
constValue (Full.ConstEnum e) = Type.Enum e constValue (Full.ConstEnum e) = Type.Enum e
constValue (Full.ConstList l) = Type.List $ constValue <$> l constValue (Full.ConstList list) = Type.List $ constValue . Full.node <$> list
constValue (Full.ConstObject o) = constValue (Full.ConstObject o) =
Type.Object $ HashMap.fromList $ constObjectField <$> o Type.Object $ HashMap.fromList $ constObjectField <$> o
where where
@ -203,14 +207,14 @@ document schema operationName subs ast = do
, types = referencedTypes , types = referencedTypes
} }
case chosenOperation of case chosenOperation of
OperationDefinition Full.Query _ _ _ _ -> OperationDefinition Full.Query _ _ _ _ _ ->
pure $ Document referencedTypes (Schema.query schema) pure $ Document referencedTypes (Schema.query schema)
$ operation chosenOperation replacement $ operation chosenOperation replacement
OperationDefinition Full.Mutation _ _ _ _ OperationDefinition Full.Mutation _ _ _ _ _
| Just mutationType <- Schema.mutation schema -> | Just mutationType <- Schema.mutation schema ->
pure $ Document referencedTypes mutationType pure $ Document referencedTypes mutationType
$ operation chosenOperation replacement $ operation chosenOperation replacement
OperationDefinition Full.Subscription _ _ _ _ OperationDefinition Full.Subscription _ _ _ _ _
| Just subscriptionType <- Schema.subscription schema -> | Just subscriptionType <- Schema.subscription schema ->
pure $ Document referencedTypes subscriptionType pure $ Document referencedTypes subscriptionType
$ operation chosenOperation replacement $ operation chosenOperation replacement
@ -235,10 +239,10 @@ defragment ast =
(operations, HashMap.insert name fragment fragments') (operations, HashMap.insert name fragment fragments')
defragment' _ acc = acc defragment' _ acc = acc
transform = \case transform = \case
Full.OperationDefinition type' name variables directives' selections _ -> Full.OperationDefinition type' name variables directives' selections location ->
OperationDefinition type' name variables directives' selections OperationDefinition type' name variables directives' selections location
Full.SelectionSet selectionSet _ -> Full.SelectionSet selectionSet location ->
OperationDefinition Full.Query Nothing mempty mempty selectionSet OperationDefinition Full.Query Nothing mempty mempty selectionSet location
-- * Operation -- * Operation
@ -247,12 +251,12 @@ operation operationDefinition replacement
= runIdentity = runIdentity
$ evalStateT (collectFragments >> transform operationDefinition) replacement $ evalStateT (collectFragments >> transform operationDefinition) replacement
where where
transform (OperationDefinition Full.Query name _ _ sels) = transform (OperationDefinition Full.Query name _ _ sels location) =
Query name <$> appendSelection sels flip (Query name) location <$> appendSelection sels
transform (OperationDefinition Full.Mutation name _ _ sels) = transform (OperationDefinition Full.Mutation name _ _ sels location) =
Mutation name <$> appendSelection sels flip (Mutation name) location <$> appendSelection sels
transform (OperationDefinition Full.Subscription name _ _ sels) = transform (OperationDefinition Full.Subscription name _ _ sels location) =
Subscription name <$> appendSelection sels flip (Subscription name) location <$> appendSelection sels
-- * Selection -- * Selection
@ -268,15 +272,20 @@ selection (Full.InlineFragmentSelection fragmentSelection) =
inlineFragment fragmentSelection inlineFragment fragmentSelection
field :: Full.Field -> State (Replacement m) (Maybe (Field m)) field :: Full.Field -> State (Replacement m) (Maybe (Field m))
field (Full.Field alias name arguments' directives' selections _) = do field (Full.Field alias name arguments' directives' selections location) = do
fieldArguments <- foldM go HashMap.empty arguments' fieldArguments <- foldM go HashMap.empty arguments'
fieldSelections <- appendSelection selections fieldSelections <- appendSelection selections
fieldDirectives <- Definition.selection <$> directives directives' fieldDirectives <- Definition.selection <$> directives directives'
let field' = Field alias name fieldArguments fieldSelections let field' = Field alias name fieldArguments fieldSelections location
pure $ field' <$ fieldDirectives pure $ field' <$ fieldDirectives
where where
go arguments (Full.Argument name' (Full.Node value' _) _) = go arguments (Full.Argument name' (Full.Node value' _) location') = do
inputField arguments name' value' objectFieldValue <- input value'
case objectFieldValue of
Just fieldValue ->
let argumentNode = Full.Node fieldValue location'
in pure $ HashMap.insert name' argumentNode arguments
Nothing -> pure arguments
fragmentSpread fragmentSpread
:: Full.FragmentSpread :: Full.FragmentSpread
@ -380,7 +389,7 @@ value (Full.String string) = pure $ Type.String string
value (Full.Boolean boolean) = pure $ Type.Boolean boolean value (Full.Boolean boolean) = pure $ Type.Boolean boolean
value Full.Null = pure Type.Null value Full.Null = pure Type.Null
value (Full.Enum enum) = pure $ Type.Enum enum value (Full.Enum enum) = pure $ Type.Enum enum
value (Full.List list) = Type.List <$> traverse value list value (Full.List list) = Type.List <$> traverse (value . Full.node) list
value (Full.Object object) = value (Full.Object object) =
Type.Object . HashMap.fromList <$> traverse objectField object Type.Object . HashMap.fromList <$> traverse objectField object
where where
@ -396,7 +405,7 @@ input (Full.String string) = pure $ pure $ String string
input (Full.Boolean boolean) = pure $ pure $ Boolean boolean input (Full.Boolean boolean) = pure $ pure $ Boolean boolean
input Full.Null = pure $ pure Null input Full.Null = pure $ pure Null
input (Full.Enum enum) = pure $ pure $ Enum enum input (Full.Enum enum) = pure $ pure $ Enum enum
input (Full.List list) = pure . List <$> traverse value list input (Full.List list) = pure . List <$> traverse (value . Full.node) list
input (Full.Object object) = do input (Full.Object object) = do
objectFields <- foldM objectField HashMap.empty object objectFields <- foldM objectField HashMap.empty object
pure $ pure $ Object objectFields pure $ pure $ Object objectFields

View File

@ -21,6 +21,6 @@ module Language.GraphQL.Type
) where ) where
import Language.GraphQL.Type.Definition import Language.GraphQL.Type.Definition
import Language.GraphQL.Type.Schema (Schema, schema) import Language.GraphQL.Type.Schema (Schema, schema, schemaWithTypes)
import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out

View File

@ -12,6 +12,7 @@ module Language.GraphQL.Type.Internal
, Directives , Directives
, Schema(..) , Schema(..)
, Type(..) , Type(..)
, description
, directives , directives
, doesFragmentTypeApply , doesFragmentTypeApply
, implementations , implementations
@ -55,41 +56,43 @@ type Directives = HashMap Full.Name Directive
-- | A Schema is created by supplying the root types of each type of operation, -- | A Schema is created by supplying the root types of each type of operation,
-- query and mutation (optional). A schema definition is then supplied to the -- query and mutation (optional). A schema definition is then supplied to the
-- validator and executor. -- validator and executor.
--
-- __Note:__ When the schema is constructed, by default only the types that
-- are reachable by traversing the root types are included, other types must
-- be explicitly referenced.
data Schema m = Schema data Schema m = Schema
(Out.ObjectType m) (Maybe Text) -- ^ Description.
(Maybe (Out.ObjectType m)) (Out.ObjectType m) -- ^ Query.
(Maybe (Out.ObjectType m)) (Maybe (Out.ObjectType m)) -- ^ Mutation.
Directives (Maybe (Out.ObjectType m)) -- ^ Subscription.
(HashMap Full.Name (Type m)) Directives -- ^ Directives
(HashMap Full.Name (Type m)) -- ^ Types.
-- Interface implementations (used only for faster access).
(HashMap Full.Name [Type m]) (HashMap Full.Name [Type m])
-- | Schema description.
description :: forall m. Schema m -> Maybe Text
description (Schema description' _ _ _ _ _ _) = description'
-- | Schema query type. -- | Schema query type.
query :: forall m. Schema m -> Out.ObjectType m query :: forall m. Schema m -> Out.ObjectType m
query (Schema query' _ _ _ _ _) = query' query (Schema _ query' _ _ _ _ _) = query'
-- | Schema mutation type. -- | Schema mutation type.
mutation :: forall m. Schema m -> Maybe (Out.ObjectType m) mutation :: forall m. Schema m -> Maybe (Out.ObjectType m)
mutation (Schema _ mutation' _ _ _ _) = mutation' mutation (Schema _ _ mutation' _ _ _ _) = mutation'
-- | Schema subscription type. -- | Schema subscription type.
subscription :: forall m. Schema m -> Maybe (Out.ObjectType m) subscription :: forall m. Schema m -> Maybe (Out.ObjectType m)
subscription (Schema _ _ subscription' _ _ _) = subscription' subscription (Schema _ _ _ subscription' _ _ _) = subscription'
-- | Schema directive definitions. -- | Schema directive definitions.
directives :: forall m. Schema m -> Directives directives :: forall m. Schema m -> Directives
directives (Schema _ _ _ directives' _ _) = directives' directives (Schema _ _ _ _ directives' _ _) = directives'
-- | Types referenced by the schema. -- | Types referenced by the schema.
types :: forall m. Schema m -> HashMap Full.Name (Type m) types :: forall m. Schema m -> HashMap Full.Name (Type m)
types (Schema _ _ _ _ types' _) = types' types (Schema _ _ _ _ _ types' _) = types'
-- | Interface implementations. -- | Interface implementations.
implementations :: forall m. Schema m -> HashMap Full.Name [Type m] implementations :: forall m. Schema m -> HashMap Full.Name [Type m]
implementations (Schema _ _ _ _ _ implementations') = implementations' implementations (Schema _ _ _ _ _ _ implementations') = implementations'
-- | These types may describe the parent context of a selection set. -- | These types may describe the parent context of a selection set.
data CompositeType m data CompositeType m

View File

@ -9,11 +9,13 @@
-- functions for defining and manipulating schemas. -- functions for defining and manipulating schemas.
module Language.GraphQL.Type.Schema module Language.GraphQL.Type.Schema
( schema ( schema
, schemaWithTypes
, module Language.GraphQL.Type.Internal , module Language.GraphQL.Type.Internal
) where ) where
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.Text (Text)
import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation(..)) import Language.GraphQL.AST.DirectiveLocation (DirectiveLocation(..))
import qualified Language.GraphQL.AST.DirectiveLocation as DirectiveLocation import qualified Language.GraphQL.AST.DirectiveLocation as DirectiveLocation
import qualified Language.GraphQL.AST as Full import qualified Language.GraphQL.AST as Full
@ -22,6 +24,7 @@ import Language.GraphQL.Type.Internal
, Directives , Directives
, Schema , Schema
, Type(..) , Type(..)
, description
, directives , directives
, implementations , implementations
, mutation , mutation
@ -35,17 +38,47 @@ import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out import qualified Language.GraphQL.Type.Out as Out
-- | Schema constructor. -- | Schema constructor.
--
-- __Note:__ When the schema is constructed, by default only the types that
-- are reachable by traversing the root types are included, other types must
-- be explicitly referenced using 'schemaWithTypes' instead.
schema :: forall m schema :: forall m
. Out.ObjectType m -- ^ Query type. . Out.ObjectType m -- ^ Query type.
-> Maybe (Out.ObjectType m) -- ^ Mutation type. -> Maybe (Out.ObjectType m) -- ^ Mutation type.
-> Maybe (Out.ObjectType m) -- ^ Subscription type. -> Maybe (Out.ObjectType m) -- ^ Subscription type.
-> Directives -- ^ Directive definitions. -> Directives -- ^ Directive definitions.
-> Schema m -- ^ Schema. -> Schema m -- ^ Schema.
schema queryRoot mutationRoot subscriptionRoot directiveDefinitions = schema queryRoot mutationRoot subscriptionRoot =
Internal.Schema queryRoot mutationRoot subscriptionRoot schemaWithTypes Nothing queryRoot mutationRoot subscriptionRoot mempty
-- | Constructs a complete schema, including user-defined types not referenced
-- in the schema directly (for example interface implementations).
schemaWithTypes :: forall m
. Maybe Text -- ^ Schema description
-> Out.ObjectType m -- ^ Query type.
-> Maybe (Out.ObjectType m) -- ^ Mutation type.
-> Maybe (Out.ObjectType m) -- ^ Subscription type.
-> [Type m] -- ^ Additional types.
-> Directives -- ^ Directive definitions.
-> Schema m -- ^ Schema.
schemaWithTypes description' queryRoot mutationRoot subscriptionRoot types' directiveDefinitions =
Internal.Schema description' queryRoot mutationRoot subscriptionRoot
allDirectives collectedTypes collectedImplementations allDirectives collectedTypes collectedImplementations
where where
collectedTypes = collectReferencedTypes queryRoot mutationRoot subscriptionRoot allTypes = foldr addTypeDefinition HashMap.empty types'
addTypeDefinition type'@(ScalarType (Definition.ScalarType typeName _)) accumulator =
HashMap.insert typeName type' accumulator
addTypeDefinition type'@(EnumType (Definition.EnumType typeName _ _)) accumulator =
HashMap.insert typeName type' accumulator
addTypeDefinition type'@(ObjectType (Out.ObjectType typeName _ _ _)) accumulator =
HashMap.insert typeName type' accumulator
addTypeDefinition type'@(InputObjectType (In.InputObjectType typeName _ _)) accumulator =
HashMap.insert typeName type' accumulator
addTypeDefinition type'@(InterfaceType (Out.InterfaceType typeName _ _ _)) accumulator =
HashMap.insert typeName type' accumulator
addTypeDefinition type'@(UnionType (Out.UnionType typeName _ _)) accumulator =
HashMap.insert typeName type' accumulator
collectedTypes = collectReferencedTypes queryRoot mutationRoot subscriptionRoot allTypes
collectedImplementations = collectImplementations collectedTypes collectedImplementations = collectImplementations collectedTypes
allDirectives = HashMap.union directiveDefinitions defaultDirectives allDirectives = HashMap.union directiveDefinitions defaultDirectives
defaultDirectives = HashMap.fromList defaultDirectives = HashMap.fromList
@ -98,11 +131,12 @@ collectReferencedTypes :: forall m
-> Maybe (Out.ObjectType m) -> Maybe (Out.ObjectType m)
-> Maybe (Out.ObjectType m) -> Maybe (Out.ObjectType m)
-> HashMap Full.Name (Type m) -> HashMap Full.Name (Type m)
collectReferencedTypes queryRoot mutationRoot subscriptionRoot = -> HashMap Full.Name (Type m)
let queryTypes = traverseObjectType queryRoot HashMap.empty collectReferencedTypes queryRoot mutationRoot subscriptionRoot extraTypes =
let queryTypes = traverseObjectType queryRoot extraTypes
mutationTypes = maybe queryTypes (`traverseObjectType` queryTypes) mutationTypes = maybe queryTypes (`traverseObjectType` queryTypes)
mutationRoot mutationRoot
in maybe mutationTypes (`traverseObjectType` queryTypes) subscriptionRoot in maybe mutationTypes (`traverseObjectType` mutationTypes) subscriptionRoot
where where
collect traverser typeName element foundTypes collect traverser typeName element foundTypes
| HashMap.member typeName foundTypes = foundTypes | HashMap.member typeName foundTypes = foundTypes

View File

@ -4,7 +4,6 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
-- | GraphQL validator. -- | GraphQL validator.
@ -315,9 +314,6 @@ constValue (Validation.ValueRule _ rule) valueType = go valueType
go inputObjectType value'@(Full.Node (Full.ConstObject fields) _) go inputObjectType value'@(Full.Node (Full.ConstObject fields) _)
= foldMap (forEach inputObjectType) (Seq.fromList fields) = foldMap (forEach inputObjectType) (Seq.fromList fields)
|> rule inputObjectType value' |> rule inputObjectType value'
go listType value'@(Full.Node (Full.ConstList values) location')
= embedListLocation go listType values location'
|> rule listType value'
go anotherValue value' = pure $ rule anotherValue value' go anotherValue value' = pure $ rule anotherValue value'
forEach inputObjectType Full.ObjectField{value = value', ..} = forEach inputObjectType Full.ObjectField{value = value', ..} =
go (valueTypeByName name inputObjectType) value' go (valueTypeByName name inputObjectType) value'
@ -421,20 +417,6 @@ argument rule argumentType (Full.Argument _ value' _) =
where where
valueType (In.Argument _ valueType' _) = valueType' valueType (In.Argument _ valueType' _) = valueType'
-- valueTypeFromList :: Maybe In.Type -> Maybe In.Type
embedListLocation :: forall a m
. (Maybe In.Type -> Full.Node a -> Seq m)
-> Maybe In.Type
-> [a]
-> Full.Location
-> Seq m
embedListLocation go listType values location'
= foldMap (go $ valueTypeFromList listType)
$ flip Full.Node location' <$> Seq.fromList values
where
valueTypeFromList (Just (In.ListBaseType baseType)) = Just baseType
valueTypeFromList _ = Nothing
value :: forall m value :: forall m
. Validation.Rule m . Validation.Rule m
-> Maybe In.Type -> Maybe In.Type
@ -445,9 +427,6 @@ value (Validation.ValueRule rule _) valueType = go valueType
go inputObjectType value'@(Full.Node (Full.Object fields) _) go inputObjectType value'@(Full.Node (Full.Object fields) _)
= foldMap (forEach inputObjectType) (Seq.fromList fields) = foldMap (forEach inputObjectType) (Seq.fromList fields)
|> rule inputObjectType value' |> rule inputObjectType value'
go listType value'@(Full.Node (Full.List values) location')
= embedListLocation go listType values location'
|> rule listType value'
go anotherValue value' = pure $ rule anotherValue value' go anotherValue value' = pure $ rule anotherValue value'
forEach inputObjectType Full.ObjectField{value = value', ..} = forEach inputObjectType Full.ObjectField{value = value', ..} =
go (valueTypeByName name inputObjectType) value' go (valueTypeByName name inputObjectType) value'

View File

@ -1502,15 +1502,6 @@ variablesInAllowedPositionRule = OperationDefinitionRule $ \case
hasNonNullVariableDefaultValue (Just (Full.Node Full.ConstNull _)) = False hasNonNullVariableDefaultValue (Just (Full.Node Full.ConstNull _)) = False
hasNonNullVariableDefaultValue Nothing = False hasNonNullVariableDefaultValue Nothing = False
hasNonNullVariableDefaultValue _ = True hasNonNullVariableDefaultValue _ = True
unwrapInType (In.NonNullScalarType nonNullType) =
Just $ In.NamedScalarType nonNullType
unwrapInType (In.NonNullEnumType nonNullType) =
Just $ In.NamedEnumType nonNullType
unwrapInType (In.NonNullInputObjectType nonNullType) =
Just $ In.NamedInputObjectType nonNullType
unwrapInType (In.NonNullListType nonNullType) =
Just $ In.ListType nonNullType
unwrapInType _ = Nothing
makeError variableDefinition expectedType = makeError variableDefinition expectedType =
let Full.VariableDefinition variableName variableType _ location' = let Full.VariableDefinition variableName variableType _ location' =
variableDefinition variableDefinition
@ -1527,6 +1518,17 @@ variablesInAllowedPositionRule = OperationDefinitionRule $ \case
, locations = [location'] , locations = [location']
} }
unwrapInType :: In.Type -> Maybe In.Type
unwrapInType (In.NonNullScalarType nonNullType) =
Just $ In.NamedScalarType nonNullType
unwrapInType (In.NonNullEnumType nonNullType) =
Just $ In.NamedEnumType nonNullType
unwrapInType (In.NonNullInputObjectType nonNullType) =
Just $ In.NamedInputObjectType nonNullType
unwrapInType (In.NonNullListType nonNullType) =
Just $ In.ListType nonNullType
unwrapInType _ = Nothing
-- | Literal values must be compatible with the type expected in the position -- | Literal values must be compatible with the type expected in the position
-- they are found as per the coercion rules. -- they are found as per the coercion rules.
-- --
@ -1540,7 +1542,7 @@ valuesOfCorrectTypeRule = ValueRule go constGo
go (Just inputType) value go (Just inputType) value
| Just constValue <- toConstNode value = | Just constValue <- toConstNode value =
lift $ check inputType constValue lift $ check inputType constValue
go _ _ = lift mempty go _ _ = lift mempty -- This rule checks only literals.
toConstNode Full.Node{..} = flip Full.Node location <$> toConst node toConstNode Full.Node{..} = flip Full.Node location <$> toConst node
toConst (Full.Variable _) = Nothing toConst (Full.Variable _) = Nothing
toConst (Full.Int integer) = Just $ Full.ConstInt integer toConst (Full.Int integer) = Just $ Full.ConstInt integer
@ -1550,7 +1552,7 @@ valuesOfCorrectTypeRule = ValueRule go constGo
toConst Full.Null = Just Full.ConstNull toConst Full.Null = Just Full.ConstNull
toConst (Full.Enum enum) = Just $ Full.ConstEnum enum toConst (Full.Enum enum) = Just $ Full.ConstEnum enum
toConst (Full.List values) = toConst (Full.List values) =
Just $ Full.ConstList $ catMaybes $ toConst <$> values Just $ Full.ConstList $ catMaybes $ toConstNode <$> values
toConst (Full.Object fields) = Just $ Full.ConstObject toConst (Full.Object fields) = Just $ Full.ConstObject
$ catMaybes $ constObjectField <$> fields $ catMaybes $ constObjectField <$> fields
constObjectField Full.ObjectField{..} constObjectField Full.ObjectField{..}
@ -1582,24 +1584,37 @@ valuesOfCorrectTypeRule = ValueRule go constGo
, Full.ConstEnum memberValue <- node , Full.ConstEnum memberValue <- node
, HashMap.member memberValue members = mempty , HashMap.member memberValue members = mempty
check (In.InputObjectBaseType objectType) Full.Node{ node } check (In.InputObjectBaseType objectType) Full.Node{ node }
| In.InputObjectType _ _ typeFields <- objectType -- Skip, objects are checked recursively by the validation traverser.
, Full.ConstObject valueFields <- node = | In.InputObjectType{} <- objectType
foldMap (checkObjectField typeFields) valueFields , Full.ConstObject{} <- node = mempty
check (In.ListBaseType listType) constValue@Full.Node{ .. } check (In.ListBaseType listType) constValue@Full.Node{ .. }
| Full.ConstList listValues <- node = | Full.ConstList values <- node =
foldMap (check listType) $ flip Full.Node location <$> listValues foldMap (checkNull listType) values
| otherwise = check listType constValue | otherwise = check listType constValue
check inputType Full.Node{ .. } = pure $ Error check inputType Full.Node{ .. } = pure $ Error
{ message = concat { message = concat
[ "Value " [ "Value "
, show node, " cannot be coerced to type \"" , show node
, " cannot be coerced to type \""
, show inputType , show inputType
, "\"." , "\"."
] ]
, locations = [location] , locations = [location]
} }
checkObjectField typeFields Full.ObjectField{..} checkNull inputType constValue =
| Just typeField <- HashMap.lookup name typeFields let checkResult = check inputType constValue
, In.InputField _ fieldType _ <- typeField = in case null checkResult of
check fieldType value True
checkObjectField _ _ = mempty | Just unwrappedType <- unwrapInType inputType
, Full.Node{ node = Full.ConstNull, .. } <- constValue ->
pure $ Error
{ message = concat
[ "List of non-null values of type \""
, show unwrappedType
, "\" cannot contain null values."
]
, locations = [location]
}
| otherwise -> mempty
_ -> checkResult

View File

@ -1,9 +0,0 @@
resolver: lts-17.2
packages:
- .
extra-deps: []
flags: {}
pvp-bounds: lower

View File

@ -6,6 +6,7 @@ module Language.GraphQL.AST.ParserSpec
import Data.List.NonEmpty (NonEmpty(..)) import Data.List.NonEmpty (NonEmpty(..))
import Language.GraphQL.AST.Document import Language.GraphQL.AST.Document
import qualified Language.GraphQL.AST.DirectiveLocation as DirLoc
import Language.GraphQL.AST.Parser import Language.GraphQL.AST.Parser
import Test.Hspec (Spec, describe, it) import Test.Hspec (Spec, describe, it)
import Test.Hspec.Megaparsec (shouldParse, shouldFailOn, shouldSucceedOn) import Test.Hspec.Megaparsec (shouldParse, shouldFailOn, shouldSucceedOn)
@ -119,6 +120,56 @@ spec = describe "Parser" $ do
| FRAGMENT_SPREAD | FRAGMENT_SPREAD
|] |]
it "parses two minimal directive definitions" $
let directive nm loc =
TypeSystemDefinition
(DirectiveDefinition
(Description Nothing)
nm
(ArgumentsDefinition [])
(loc :| []))
example1 =
directive "example1"
(DirLoc.TypeSystemDirectiveLocation DirLoc.FieldDefinition)
(Location {line = 2, column = 17})
example2 =
directive "example2"
(DirLoc.ExecutableDirectiveLocation DirLoc.Field)
(Location {line = 3, column = 17})
testSchemaExtension = example1 :| [ example2 ]
query = [r|
directive @example1 on FIELD_DEFINITION
directive @example2 on FIELD
|]
in parse document "" query `shouldParse` testSchemaExtension
it "parses a directive definition with a default empty list argument" $
let directive nm loc args =
TypeSystemDefinition
(DirectiveDefinition
(Description Nothing)
nm
(ArgumentsDefinition
[ InputValueDefinition
(Description Nothing)
argName
argType
argValue
[]
| (argName, argType, argValue) <- args])
(loc :| []))
defn =
directive "test"
(DirLoc.TypeSystemDirectiveLocation DirLoc.FieldDefinition)
[("foo",
TypeList (TypeNamed "String"),
Just
$ Node (ConstList [])
$ Location {line = 1, column = 33})]
(Location {line = 1, column = 1})
query = [r|directive @test(foo: [String] = []) on FIELD_DEFINITION|]
in parse document "" query `shouldParse` (defn :| [ ])
it "parses schema extension with a new directive" $ it "parses schema extension with a new directive" $
parse document "" `shouldSucceedOn`[r| parse document "" `shouldSucceedOn`[r|
extend schema @newDirective extend schema @newDirective

View File

@ -8,17 +8,29 @@ 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 Data.List.NonEmpty (NonEmpty (..))
import Language.GraphQL.Error import Language.GraphQL.Error
import Test.Hspec ( Spec import Test.Hspec
, describe ( Spec
, it , describe
, shouldBe , it
) , shouldBe
)
import Text.Megaparsec (PosState(..))
import Text.Megaparsec.Error (ParseError(..), ParseErrorBundle(..))
import Text.Megaparsec.Pos (SourcePos(..), mkPos)
spec :: Spec spec :: Spec
spec = describe "singleError" $ spec = describe "parseError" $
it "constructs an error with the given message" $ it "generates response with a single error" $ do
let errors'' = Seq.singleton $ Error "Message." [] [] let parseErrors = TrivialError 0 Nothing mempty :| []
expected = Response Aeson.Null errors'' posState = PosState
in singleError "Message." `shouldBe` expected { pstateInput = ""
, pstateOffset = 0
, pstateSourcePos = SourcePos "" (mkPos 1) (mkPos 1)
, pstateTabWidth = mkPos 1
, pstateLinePrefix = ""
}
Response Aeson.Null actual <-
parseError (ParseErrorBundle parseErrors posState)
length actual `shouldBe` 1

View File

@ -0,0 +1,72 @@
{- 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 OverloadedStrings #-}
module Language.GraphQL.Execute.OrderedMapSpec
( spec
) where
import Language.GraphQL.Execute.OrderedMap (OrderedMap)
import qualified Language.GraphQL.Execute.OrderedMap as OrderedMap
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)
spec :: Spec
spec =
describe "OrderedMap" $ do
it "creates an empty map" $
(mempty :: OrderedMap String) `shouldSatisfy` null
it "creates a singleton" $
let value :: String
value = "value"
in OrderedMap.size (OrderedMap.singleton "key" value) `shouldBe` 1
it "combines inserted vales" $
let key = "key"
map1 = OrderedMap.singleton key ("1" :: String)
map2 = OrderedMap.singleton key ("2" :: String)
in OrderedMap.lookup key (map1 <> map2) `shouldBe` Just "12"
it "shows the map" $
let actual = show
$ OrderedMap.insert "key1" "1"
$ OrderedMap.singleton "key2" ("2" :: String)
expected = "fromList [(\"key2\",\"2\"),(\"key1\",\"1\")]"
in actual `shouldBe` expected
it "traverses a map of just values" $
let actual = sequence
$ OrderedMap.insert "key1" (Just "2")
$ OrderedMap.singleton "key2" $ Just ("1" :: String)
expected = Just
$ OrderedMap.insert "key1" "2"
$ OrderedMap.singleton "key2" ("1" :: String)
in actual `shouldBe` expected
it "traverses a map with a Nothing" $
let actual = sequence
$ OrderedMap.insert "key1" Nothing
$ OrderedMap.singleton "key2" $ Just ("1" :: String)
expected = Nothing
in actual `shouldBe` expected
it "combines two maps preserving the order of the second one" $
let map1 :: OrderedMap String
map1 = OrderedMap.insert "key2" "2"
$ OrderedMap.singleton "key1" "1"
map2 :: OrderedMap String
map2 = OrderedMap.insert "key4" "4"
$ OrderedMap.singleton "key3" "3"
expected = OrderedMap.insert "key4" "4"
$ OrderedMap.insert "key3" "3"
$ OrderedMap.insert "key2" "2"
$ OrderedMap.singleton "key1" "1"
in (map1 <> map2) `shouldBe` expected
it "replaces existing values" $
let key = "key"
actual = OrderedMap.replace key ("2" :: String)
$ OrderedMap.singleton key ("1" :: String)
in OrderedMap.lookup key actual `shouldBe` Just "2"

View File

@ -8,34 +8,87 @@ module Language.GraphQL.ExecuteSpec
( spec ( spec
) where ) where
import Control.Exception (SomeException) import Control.Exception (Exception(..), SomeException)
import Control.Monad.Catch (throwM)
import Data.Aeson ((.=)) import Data.Aeson ((.=))
import qualified Data.Aeson as Aeson import qualified Data.Aeson as Aeson
import Data.Aeson.Types (emptyObject) import Data.Aeson.Types (emptyObject)
import Data.Conduit import Data.Conduit
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 Language.GraphQL.AST (Document, Name) import Data.Typeable (cast)
import Language.GraphQL.AST (Document, Location(..), Name)
import Language.GraphQL.AST.Parser (document) import Language.GraphQL.AST.Parser (document)
import Language.GraphQL.Error import Language.GraphQL.Error
import Language.GraphQL.Execute import Language.GraphQL.Execute (execute)
import Language.GraphQL.Type as Type import qualified Language.GraphQL.Type.Schema as Schema
import Language.GraphQL.Type.Out as Out import Language.GraphQL.Type
import qualified Language.GraphQL.Type.In as In
import qualified Language.GraphQL.Type.Out as Out
import Prelude hiding (id)
import Test.Hspec (Spec, context, describe, it, shouldBe) import Test.Hspec (Spec, context, describe, it, shouldBe)
import Text.Megaparsec (parse) import Text.Megaparsec (parse)
import Text.RawString.QQ (r) import Text.RawString.QQ (r)
data PhilosopherException = PhilosopherException
deriving Show
instance Exception PhilosopherException where
toException = toException. ResolverException
fromException e = do
ResolverException resolverException <- fromException e
cast resolverException
philosopherSchema :: Schema (Either SomeException) philosopherSchema :: Schema (Either SomeException)
philosopherSchema = schema queryType Nothing (Just subscriptionType) mempty philosopherSchema =
schemaWithTypes Nothing queryType Nothing subscriptionRoot extraTypes mempty
where
subscriptionRoot = Just subscriptionType
extraTypes =
[ Schema.ObjectType bookType
, Schema.ObjectType bookCollectionType
]
queryType :: Out.ObjectType (Either SomeException) queryType :: Out.ObjectType (Either SomeException)
queryType = Out.ObjectType "Query" Nothing [] queryType = Out.ObjectType "Query" Nothing []
$ HashMap.singleton "philosopher" $ HashMap.fromList
$ ValueResolver philosopherField [ ("philosopher", ValueResolver philosopherField philosopherResolver)
$ pure $ Type.Object mempty , ("genres", ValueResolver genresField genresResolver)
]
where where
philosopherField = philosopherField =
Out.Field Nothing (Out.NonNullObjectType philosopherType) HashMap.empty Out.Field Nothing (Out.NonNullObjectType philosopherType)
$ HashMap.singleton "id"
$ In.Argument Nothing (In.NamedScalarType id) Nothing
philosopherResolver = pure $ Object mempty
genresField =
let fieldType = Out.ListType $ Out.NonNullScalarType string
in Out.Field Nothing fieldType HashMap.empty
genresResolver :: Resolve (Either SomeException)
genresResolver = throwM PhilosopherException
musicType :: Out.ObjectType (Either SomeException)
musicType = Out.ObjectType "Music" Nothing []
$ HashMap.fromList resolvers
where
resolvers =
[ ("instrument", ValueResolver instrumentField instrumentResolver)
]
instrumentResolver = pure $ String "piano"
instrumentField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
poetryType :: Out.ObjectType (Either SomeException)
poetryType = Out.ObjectType "Poetry" Nothing []
$ HashMap.fromList resolvers
where
resolvers =
[ ("genre", ValueResolver genreField genreResolver)
]
genreResolver = pure $ String "Futurism"
genreField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
interestType :: Out.UnionType (Either SomeException)
interestType = Out.UnionType "Interest" Nothing [musicType, poetryType]
philosopherType :: Out.ObjectType (Either SomeException) philosopherType :: Out.ObjectType (Either SomeException)
philosopherType = Out.ObjectType "Philosopher" Nothing [] philosopherType = Out.ObjectType "Philosopher" Nothing []
@ -44,19 +97,68 @@ philosopherType = Out.ObjectType "Philosopher" Nothing []
resolvers = resolvers =
[ ("firstName", ValueResolver firstNameField firstNameResolver) [ ("firstName", ValueResolver firstNameField firstNameResolver)
, ("lastName", ValueResolver lastNameField lastNameResolver) , ("lastName", ValueResolver lastNameField lastNameResolver)
, ("school", ValueResolver schoolField schoolResolver)
, ("interest", ValueResolver interestField interestResolver)
, ("majorWork", ValueResolver majorWorkField majorWorkResolver)
, ("century", ValueResolver centuryField centuryResolver)
] ]
firstNameField = firstNameField =
Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
firstNameResolver = pure $ Type.String "Friedrich" firstNameResolver = pure $ String "Friedrich"
lastNameField lastNameField
= Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
lastNameResolver = pure $ Type.String "Nietzsche" lastNameResolver = pure $ String "Nietzsche"
schoolField
= Out.Field Nothing (Out.NonNullEnumType schoolType) HashMap.empty
schoolResolver = pure $ Enum "EXISTENTIALISM"
interestField
= Out.Field Nothing (Out.NonNullUnionType interestType) HashMap.empty
interestResolver = pure
$ Object
$ HashMap.fromList [("instrument", "piano")]
majorWorkField
= Out.Field Nothing (Out.NonNullInterfaceType workType) HashMap.empty
majorWorkResolver = pure
$ Object
$ HashMap.fromList
[ ("title", "Also sprach Zarathustra: Ein Buch für Alle und Keinen")
]
centuryField =
Out.Field Nothing (Out.NonNullScalarType int) HashMap.empty
centuryResolver = pure $ Float 18.5
workType :: Out.InterfaceType (Either SomeException)
workType = Out.InterfaceType "Work" Nothing []
$ HashMap.fromList fields
where
fields = [("title", titleField)]
titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
bookType :: Out.ObjectType (Either SomeException)
bookType = Out.ObjectType "Book" Nothing [workType]
$ HashMap.fromList resolvers
where
resolvers =
[ ("title", ValueResolver titleField titleResolver)
]
titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
titleResolver = pure "Also sprach Zarathustra: Ein Buch für Alle und Keinen"
bookCollectionType :: Out.ObjectType (Either SomeException)
bookCollectionType = Out.ObjectType "Book" Nothing [workType]
$ HashMap.fromList resolvers
where
resolvers =
[ ("title", ValueResolver titleField titleResolver)
]
titleField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
titleResolver = pure "The Three Critiques"
subscriptionType :: Out.ObjectType (Either SomeException) subscriptionType :: Out.ObjectType (Either SomeException)
subscriptionType = Out.ObjectType "Subscription" Nothing [] subscriptionType = Out.ObjectType "Subscription" Nothing []
$ HashMap.singleton "newQuote" $ HashMap.singleton "newQuote"
$ EventStreamResolver quoteField (pure $ Type.Object mempty) $ EventStreamResolver quoteField (pure $ Object mempty)
$ pure $ yield $ Type.Object mempty $ pure $ yield $ Object mempty
where where
quoteField = quoteField =
Out.Field Nothing (Out.NonNullObjectType quoteType) HashMap.empty Out.Field Nothing (Out.NonNullObjectType quoteType) HashMap.empty
@ -70,6 +172,13 @@ quoteType = Out.ObjectType "Quote" Nothing []
quoteField = quoteField =
Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
schoolType :: EnumType
schoolType = EnumType "School" Nothing $ HashMap.fromList
[ ("NOMINALISM", EnumValue Nothing)
, ("REALISM", EnumValue Nothing)
, ("IDEALISM", EnumValue Nothing)
]
type EitherStreamOrValue = Either type EitherStreamOrValue = Either
(ResponseEventStream (Either SomeException) Aeson.Value) (ResponseEventStream (Either SomeException) Aeson.Value)
(Response Aeson.Value) (Response Aeson.Value)
@ -118,6 +227,99 @@ spec =
Right (Right actual) = either (pure . parseError) execute' Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher { firstName } philosopher { lastName } }" $ parse document "" "{ philosopher { firstName } philosopher { lastName } }"
in actual `shouldBe` expected in actual `shouldBe` expected
it "errors on invalid output enum values" $
let data'' = Aeson.object
[ "philosopher" .= Aeson.object
[ "school" .= Aeson.Null
]
]
executionErrors = pure $ Error
{ message = "Enum value completion failed."
, locations = [Location 1 17]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher { school } }"
in actual `shouldBe` expected
it "gives location information for non-null unions" $
let data'' = Aeson.object
[ "philosopher" .= Aeson.object
[ "interest" .= Aeson.Null
]
]
executionErrors = pure $ Error
{ message = "Union value completion failed."
, locations = [Location 1 17]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher { interest } }"
in actual `shouldBe` expected
it "gives location information for invalid interfaces" $
let data'' = Aeson.object
[ "philosopher" .= Aeson.object
[ "majorWork" .= Aeson.Null
]
]
executionErrors = pure $ Error
{ message = "Interface value completion failed."
, locations = [Location 1 17]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher { majorWork { title } } }"
in actual `shouldBe` expected
it "gives location information for invalid scalar arguments" $
let data'' = Aeson.object
[ "philosopher" .= Aeson.Null
]
executionErrors = pure $ Error
{ message = "Argument coercing failed."
, locations = [Location 1 15]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher(id: true) { lastName } }"
in actual `shouldBe` expected
it "gives location information for failed result coercion" $
let data'' = Aeson.object
[ "philosopher" .= Aeson.object
[ "century" .= Aeson.Null
]
]
executionErrors = pure $ Error
{ message = "Result coercion failed."
, locations = [Location 1 26]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ philosopher(id: \"1\") { century } }"
in actual `shouldBe` expected
it "gives location information for failed result coercion" $
let data'' = Aeson.object
[ "genres" .= Aeson.Null
]
executionErrors = pure $ Error
{ message = "PhilosopherException"
, locations = [Location 1 3]
, path = []
}
expected = Response data'' executionErrors
Right (Right actual) = either (pure . parseError) execute'
$ parse document "" "{ genres }"
in actual `shouldBe` expected
context "Subscription" $ context "Subscription" $
it "subscribes" $ it "subscribes" $
let data'' = Aeson.object let data'' = Aeson.object

View File

@ -49,16 +49,18 @@ catType :: ObjectType IO
catType = ObjectType "Cat" Nothing [petType] $ HashMap.fromList catType = ObjectType "Cat" Nothing [petType] $ HashMap.fromList
[ ("name", nameResolver) [ ("name", nameResolver)
, ("nickname", nicknameResolver) , ("nickname", nicknameResolver)
, ("doesKnowCommand", doesKnowCommandResolver) , ("doesKnowCommands", doesKnowCommandsResolver)
, ("meowVolume", meowVolumeResolver) , ("meowVolume", meowVolumeResolver)
] ]
where where
meowVolumeField = Field Nothing (Out.NamedScalarType int) mempty meowVolumeField = Field Nothing (Out.NamedScalarType int) mempty
meowVolumeResolver = ValueResolver meowVolumeField $ pure $ Int 3 meowVolumeResolver = ValueResolver meowVolumeField $ pure $ Int 3
doesKnowCommandField = Field Nothing (Out.NonNullScalarType boolean) doesKnowCommandsType = In.NonNullListType
$ HashMap.singleton "catCommand" $ In.NonNullEnumType catCommandType
$ In.Argument Nothing (In.NonNullEnumType catCommandType) Nothing doesKnowCommandsField = Field Nothing (Out.NonNullScalarType boolean)
doesKnowCommandResolver = ValueResolver doesKnowCommandField $ HashMap.singleton "catCommands"
$ In.Argument Nothing doesKnowCommandsType Nothing
doesKnowCommandsResolver = ValueResolver doesKnowCommandsField
$ pure $ Boolean True $ pure $ Boolean True
nameResolver :: Resolver IO nameResolver :: Resolver IO
@ -845,7 +847,7 @@ spec =
} }
in validate queryString `shouldBe` [expected] in validate queryString `shouldBe` [expected]
context "providedRequiredArgumentsRule" $ context "providedRequiredArgumentsRule" $ do
it "checks for (non-)nullable arguments" $ it "checks for (non-)nullable arguments" $
let queryString = [r| let queryString = [r|
{ {
@ -866,17 +868,17 @@ spec =
context "variablesInAllowedPositionRule" $ do context "variablesInAllowedPositionRule" $ do
it "rejects wrongly typed variable arguments" $ it "rejects wrongly typed variable arguments" $
let queryString = [r| let queryString = [r|
query catCommandArgQuery($catCommandArg: CatCommand) { query dogCommandArgQuery($dogCommandArg: DogCommand) {
cat { dog {
doesKnowCommand(catCommand: $catCommandArg) doesKnowCommand(dogCommand: $dogCommandArg)
} }
} }
|] |]
expected = Error expected = Error
{ message = { message =
"Variable \"$catCommandArg\" of type \ "Variable \"$dogCommandArg\" of type \
\\"CatCommand\" used in position expecting type \ \\"DogCommand\" used in position expecting type \
\\"!CatCommand\"." \\"!DogCommand\"."
, locations = [AST.Location 2 44] , locations = [AST.Location 2 44]
} }
in validate queryString `shouldBe` [expected] in validate queryString `shouldBe` [expected]
@ -897,7 +899,7 @@ spec =
} }
in validate queryString `shouldBe` [expected] in validate queryString `shouldBe` [expected]
context "valuesOfCorrectTypeRule" $ context "valuesOfCorrectTypeRule" $ do
it "rejects values of incorrect types" $ it "rejects values of incorrect types" $
let queryString = [r| let queryString = [r|
{ {
@ -912,3 +914,49 @@ spec =
, locations = [AST.Location 4 52] , locations = [AST.Location 4 52]
} }
in validate queryString `shouldBe` [expected] in validate queryString `shouldBe` [expected]
it "uses the location of a single list value" $
let queryString = [r|
{
cat {
doesKnowCommands(catCommands: [3])
}
}
|]
expected = Error
{ message =
"Value 3 cannot be coerced to type \"!CatCommand\"."
, locations = [AST.Location 4 54]
}
in validate queryString `shouldBe` [expected]
it "validates input object properties once" $
let queryString = [r|
{
findDog(complex: { name: 3 }) {
name
}
}
|]
expected = Error
{ message =
"Value 3 cannot be coerced to type \"!String\"."
, locations = [AST.Location 3 46]
}
in validate queryString `shouldBe` [expected]
it "checks for required list members" $
let queryString = [r|
{
cat {
doesKnowCommands(catCommands: [null])
}
}
|]
expected = Error
{ message =
"List of non-null values of type \"CatCommand\" \
\cannot contain null values."
, locations = [AST.Location 4 54]
}
in validate queryString `shouldBe` [expected]