7 Commits

Author SHA1 Message Date
2215799cf7 Release 1.5.0.1
All checks were successful
Build / audit (push) Successful in 17s
Build / test (push) Successful in 6m19s
Build / doc (push) Successful in 5m18s
Release / release (push) Successful in 5s
2025-06-19 13:50:45 +02:00
497edf326d Add --enable-tests to test
All checks were successful
Build / audit (push) Successful in 16s
Build / test (push) Successful in 6m26s
Build / doc (push) Successful in 5m23s
2025-01-18 16:54:11 +01:00
663e4f3521 Make the lexer and parser safe
Some checks failed
Build / audit (push) Has been cancelled
Build / test (push) Has been cancelled
Build / doc (push) Has been cancelled
2025-01-18 16:33:13 +01:00
324a4c55ff Release 1.5.0.0
All checks were successful
Build / audit (push) Successful in 17s
Build / test (push) Successful in 6m27s
Build / doc (push) Successful in 5m32s
Release / release (push) Successful in 5s
2024-12-03 19:49:33 +01:00
7ea76865e6 Validate the subscription root
All checks were successful
Build / audit (push) Successful in 17s
Build / test (push) Successful in 6m33s
Build / doc (push) Successful in 5m45s
…not to be an introspection field.
2024-12-01 21:53:01 +01:00
2dcefff76a Add test for introspection as subscription root
All checks were successful
Build / audit (push) Successful in 18s
Build / test (push) Successful in 6m19s
Build / doc (push) Successful in 5m30s
Add a pending test checking that an introspection field cannot be
subscription root.
2024-11-10 22:23:54 +01:00
27a5a0b44e Adjust wording according to the 2021 specification
All checks were successful
Build / audit (push) Successful in 17s
Build / test (push) Successful in 6m33s
Build / doc (push) Successful in 5m37s
2024-11-07 18:18:12 +01:00
14 changed files with 93 additions and 69 deletions

View File

@ -21,8 +21,8 @@ jobs:
- name: Install dependencies
run: cabal update
- name: Prepare system
run: cabal build graphql-test
- run: cabal test --test-show-details=streaming
run: cabal build graphql-test --enable-tests
- run: cabal test --test-show-details=streaming --enable-tests
doc:
runs-on: buildenv

View File

@ -6,10 +6,19 @@ The format is based on
and this project adheres to
[Haskell Package Versioning Policy](https://pvp.haskell.org/).
## [Unreleased]
### Changed
## [1.5.0.1] - 2025-06-19
### Fixed
- Allow any 2.x QuickCheck version.
- Make the lexer and parser safe.
## [1.5.0.0] - 2024-12-03
### Removed
- Remove deprecated 'gql' quasi quoter.
### Changed
- Validate the subscription root not to be an introspection field
(`singleFieldSubscriptionsRule`).
## [1.4.0.0] - 2024-10-26
### Changed
- `Schema.Directive` is extended to contain a boolean argument, representing
@ -542,7 +551,8 @@ and this project adheres to
### Added
- Data types for the GraphQL language.
[Unreleased]: https://git.caraus.tech/OSS/graphql/compare/v1.4.0.0...master
[1.5.0.1]: https://git.caraus.tech/OSS/graphql/compare/v1.5.0.0...v1.5.0.1
[1.5.0.0]: https://git.caraus.tech/OSS/graphql/compare/v1.4.0.0...v1.5.0.0
[1.4.0.0]: https://git.caraus.tech/OSS/graphql/compare/v1.3.0.0...v1.4.0.0
[1.3.0.0]: https://git.caraus.tech/OSS/graphql/compare/v1.2.0.3...v1.3.0.0
[1.2.0.3]: https://git.caraus.tech/OSS/graphql/compare/v1.2.0.2...v1.2.0.3

View File

@ -1,7 +1,7 @@
cabal-version: 3.0
name: graphql
version: 1.4.0.0
version: 1.5.0.1
synopsis: Haskell GraphQL implementation
description: Haskell <https://spec.graphql.org/June2018/ GraphQL> implementation.
category: Language
@ -11,7 +11,7 @@ author: Danny Navarro <j@dannynavarro.net>,
Matthías Páll Gissurarson <mpg@mpg.is>,
Sólrún Halla Einarsdóttir <she@mpg.is>
maintainer: belka@caraus.de
copyright: (c) 2019-2024 Eugen Wissner,
copyright: (c) 2019-2025 Eugen Wissner,
(c) 2015-2017 J. Daniel Navarro
license: MPL-2.0 AND BSD-3-Clause
license-files: LICENSE,
@ -21,7 +21,7 @@ extra-source-files:
CHANGELOG.md
README.md
tested-with:
GHC == 9.8.2
GHC == 9.10.1
source-repository head
type: git
@ -90,7 +90,7 @@ test-suite graphql-test
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
build-depends:
QuickCheck >= 2.14 && < 2.16,
QuickCheck >= 2.14 && < 3,
base,
conduit,
exceptions,

View File

@ -1,3 +1,4 @@
{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}
{-# LANGUAGE Safe #-}
-- | Target AST for parser.

View File

@ -1,3 +1,4 @@
{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}
{-# LANGUAGE Safe #-}
-- | Various parts of a GraphQL document can be annotated with directives.

View File

@ -3,6 +3,7 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}
{-# LANGUAGE Safe #-}
-- | This module defines an abstract syntax tree for the @GraphQL@ language. It

View File

@ -2,6 +2,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}
{-# LANGUAGE Safe #-}
-- | This module defines a minifier and a printer for the @GraphQL@ language.

View File

@ -1,5 +1,7 @@
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}
{-# LANGUAGE Safe #-}
-- | This module defines a bunch of small parsers used to parse individual
-- lexemes.

View File

@ -2,6 +2,8 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}
{-# LANGUAGE Safe #-}
-- | @GraphQL@ document parser.
module Language.GraphQL.AST.Parser

View File

@ -189,6 +189,8 @@ data QueryError
| CoercionError Full.VariableDefinition
| UnknownInputType Full.VariableDefinition
type ExecuteHandler m a e = e -> ExecutorT m a
tell :: Monad m => Seq Error -> ExecutorT m ()
tell = ExecutorT . lift . Writer.tell
@ -313,8 +315,7 @@ executeQuery topSelections schema = do
pure $ Response data' errors
handleException :: (MonadCatch m, Serialize a)
=> FieldException
-> ExecutorT m a
=> ExecuteHandler m a FieldException
handleException (FieldException fieldLocation errorPath next) =
let newError = constructError next fieldLocation errorPath
in tell (Seq.singleton newError) >> pure null
@ -390,30 +391,28 @@ executeField objectValue fields (viewResolver -> resolverPair) errorPath =
fieldErrorPath = fieldsSegment fields : errorPath
inputCoercionHandler :: (MonadCatch m, Serialize a)
=> Full.Location
-> InputCoercionException
-> ExecutorT m a
-> ExecuteHandler m a InputCoercionException
inputCoercionHandler _ e@(InputCoercionException _ _ (Just valueNode)) =
let argumentLocation = getField @"location" valueNode
in exceptionHandler argumentLocation e
inputCoercionHandler fieldLocation e = exceptionHandler fieldLocation e
resultHandler :: (MonadCatch m, Serialize a)
=> Full.Location
-> ResultException
-> ExecutorT m a
-> ExecuteHandler m a ResultException
resultHandler = exceptionHandler
resolverHandler :: (MonadCatch m, Serialize a)
=> Full.Location
-> ResolverException
-> ExecutorT m a
-> ExecuteHandler m a ResolverException
resolverHandler = exceptionHandler
nullResultHandler :: (MonadCatch m, Serialize a)
=> FieldException
-> ExecutorT m a
nullResultHandler :: (MonadCatch m, Serialize a) => ExecuteHandler m a FieldException
nullResultHandler e@(FieldException fieldLocation errorPath' next) =
let newError = constructError next fieldLocation errorPath'
in if Out.isNonNullType fieldType
then throwM e
else returnError newError
exceptionHandler :: (Exception e, MonadCatch m, Serialize a)
=> Full.Location
-> ExecuteHandler m a e
exceptionHandler errorLocation e =
let newError = constructError e errorLocation fieldErrorPath
in if Out.isNonNullType fieldType

View File

@ -3,6 +3,7 @@
obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}
{-# LANGUAGE Safe #-}
-- | Types that can be used as both input and output types.

View File

@ -3,6 +3,7 @@
obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}
{-# LANGUAGE Safe #-}
{-# LANGUAGE ViewPatterns #-}

View File

@ -137,25 +137,28 @@ singleFieldSubscriptionsRule :: forall m. Rule m
singleFieldSubscriptionsRule = OperationDefinitionRule $ \case
Full.OperationDefinition Full.Subscription name' _ _ rootFields location' -> do
groupedFieldSet <- evalStateT (collectFields rootFields) HashSet.empty
case HashSet.size groupedFieldSet of
1 -> lift mempty
_
| Just name <- name' -> pure $ Error
{ message = concat
[ "Subscription \""
, Text.unpack name
, "\" must select only one top level field."
]
, locations = [location']
}
| otherwise -> pure $ Error
{ message = errorMessage
, locations = [location']
}
case HashSet.toList groupedFieldSet of
[rootName]
| Text.isPrefixOf "__" rootName -> makeError location' name'
"exactly one top level field, which must not be an introspection field."
| otherwise -> lift mempty
[] -> makeError location' name' "exactly one top level field."
_ -> makeError location' name' "only one top level field."
_ -> lift mempty
where
errorMessage =
"Anonymous Subscription must select only one top level field."
makeError location' (Just operationName) errorLine = pure $ Error
{ message = concat
[ "Subscription \""
, Text.unpack operationName
, "\" must select "
, errorLine
]
, locations = [location']
}
makeError location' Nothing errorLine = pure $ Error
{ message = "Anonymous Subscription must select " <> errorLine
, locations = [location']
}
collectFields = foldM forEach HashSet.empty
forEach accumulator = \case
Full.FieldSelection fieldSelection -> forField accumulator fieldSelection
@ -856,8 +859,8 @@ knownArgumentNamesRule = ArgumentsRule fieldRule directiveRule
, "\"."
]
-- | GraphQL servers define what directives they support. For each usage of a
-- directive, the directive must be available on that server.
-- | GraphQL services define what directives they support. For each usage of a
-- directive, the directive must be available on that service.
knownDirectiveNamesRule :: Rule m
knownDirectiveNamesRule = DirectivesRule $ const $ \directives' -> do
definitions' <- asks $ Schema.directives . schema
@ -909,9 +912,9 @@ knownInputFieldNamesRule = ValueRule go constGo
, "\"."
]
-- | GraphQL servers define what directives they support and where they support
-- | GraphQL services define what directives they support and where they support
-- them. For each usage of a directive, the directive must be used in a location
-- that the server has declared support for.
-- that the service has declared support for.
directivesInValidLocationsRule :: Rule m
directivesInValidLocationsRule = DirectivesRule directivesRule
where
@ -1064,18 +1067,12 @@ overlappingFieldsCanBeMergedRule = OperationDefinitionRule $ \case
go selectionSet selectionType = do
fieldTuples <- evalStateT (collectFields selectionType selectionSet) HashSet.empty
fieldsInSetCanMerge fieldTuples
fieldsInSetCanMerge :: forall m
. HashMap Full.Name (NonEmpty (Full.Field, Type.CompositeType m))
-> ReaderT (Validation m) Seq Error
fieldsInSetCanMerge fieldTuples = do
validation <- ask
let (lonely, paired) = flattenPairs fieldTuples
let reader = flip runReaderT validation
lift $ foldMap (reader . visitLonelyFields) lonely
<> foldMap (reader . forEachFieldTuple) paired
forEachFieldTuple :: forall m
. (FieldInfo m, FieldInfo m)
-> ReaderT (Validation m) Seq Error
forEachFieldTuple (fieldA, fieldB) =
case (parent fieldA, parent fieldB) of
(parentA@Type.CompositeObjectType{}, parentB@Type.CompositeObjectType{})
@ -1102,10 +1099,6 @@ overlappingFieldsCanBeMergedRule = OperationDefinitionRule $ \case
let Full.Field _ _ _ _ subSelections _ = node
compositeFieldType = Type.outToComposite type'
in maybe (lift Seq.empty) (go subSelections) compositeFieldType
sameResponseShape :: forall m
. FieldInfo m
-> FieldInfo m
-> ReaderT (Validation m) Seq Error
sameResponseShape fieldA fieldB =
let Full.Field _ _ _ _ selectionsA _ = node fieldA
Full.Field _ _ _ _ selectionsB _ = node fieldB

View File

@ -94,7 +94,7 @@ dogType = ObjectType "Dog" Nothing [petType] $ HashMap.fromList
, ("nickname", nicknameResolver)
, ("barkVolume", barkVolumeResolver)
, ("doesKnowCommand", doesKnowCommandResolver)
, ("isHousetrained", isHousetrainedResolver)
, ("isHouseTrained", isHouseTrainedResolver)
, ("owner", ownerResolver)
]
where
@ -105,10 +105,10 @@ dogType = ObjectType "Dog" Nothing [petType] $ HashMap.fromList
$ In.Argument Nothing (In.NonNullEnumType dogCommandType) Nothing
doesKnowCommandResolver = ValueResolver doesKnowCommandField
$ pure $ Boolean True
isHousetrainedField = Field Nothing (Out.NonNullScalarType boolean)
isHouseTrainedField = Field Nothing (Out.NonNullScalarType boolean)
$ HashMap.singleton "atOtherHomes"
$ In.Argument Nothing (In.NamedScalarType boolean) Nothing
isHousetrainedResolver = ValueResolver isHousetrainedField
isHouseTrainedResolver = ValueResolver isHouseTrainedField
$ pure $ Boolean True
ownerField = Field Nothing (Out.NamedObjectType humanType) mempty
ownerResolver = ValueResolver ownerField $ pure Null
@ -206,6 +206,18 @@ spec =
}
in validate queryString `shouldContain` [expected]
it "rejects an introspection field as the subscription root" $
let queryString = "subscription sub {\n\
\ __typename\n\
\}"
expected = Error
{ message =
"Subscription \"sub\" must select exactly one top \
\level field, which must not be an introspection field."
, locations = [AST.Location 1 1]
}
in validate queryString `shouldContain` [expected]
it "rejects multiple subscription root fields coming from a fragment" $
let queryString = "subscription sub {\n\
\ ...multipleSubscriptions\n\
@ -455,7 +467,7 @@ spec =
it "rejects duplicate field arguments" $
let queryString = "{\n\
\ dog {\n\
\ isHousetrained(atOtherHomes: true, atOtherHomes: true)\n\
\ isHouseTrained(atOtherHomes: true, atOtherHomes: true)\n\
\ }\n\
\}"
expected = Error
@ -492,7 +504,7 @@ spec =
it "rejects duplicate variables" $
let queryString = "query houseTrainedQuery($atOtherHomes: Boolean, $atOtherHomes: Boolean) {\n\
\ dog {\n\
\ isHousetrained(atOtherHomes: $atOtherHomes)\n\
\ isHouseTrained(atOtherHomes: $atOtherHomes)\n\
\ }\n\
\}"
expected = Error
@ -507,7 +519,7 @@ spec =
it "rejects non-input types as variables" $
let queryString = "query takesDogBang($dog: Dog!) {\n\
\ dog {\n\
\ isHousetrained(atOtherHomes: $dog)\n\
\ isHouseTrained(atOtherHomes: $dog)\n\
\ }\n\
\}"
expected = Error
@ -522,12 +534,12 @@ spec =
it "rejects undefined variables" $
let queryString = "query variableIsNotDefinedUsedInSingleFragment {\n\
\ dog {\n\
\ ...isHousetrainedFragment\n\
\ ...isHouseTrainedFragment\n\
\ }\n\
\}\n\
\\n\
\fragment isHousetrainedFragment on Dog {\n\
\ isHousetrained(atOtherHomes: $atOtherHomes)\n\
\fragment isHouseTrainedFragment on Dog {\n\
\ isHouseTrained(atOtherHomes: $atOtherHomes)\n\
\}"
expected = Error
{ message =
@ -566,7 +578,7 @@ spec =
it "rejects unused variables" $
let queryString = "query variableUnused($atOtherHomes: Boolean) {\n\
\ dog {\n\
\ isHousetrained\n\
\ isHouseTrained\n\
\ }\n\
\}"
expected = Error
@ -648,7 +660,7 @@ spec =
it "rejects directive arguments missing in the definition" $
let queryString = "{\n\
\ dog {\n\
\ isHousetrained(atOtherHomes: true) @include(unless: false, if: true)\n\
\ isHouseTrained(atOtherHomes: true) @include(unless: false, if: true)\n\
\ }\n\
\}"
expected = Error
@ -663,7 +675,7 @@ spec =
it "rejects undefined directives" $
let queryString = "{\n\
\ dog {\n\
\ isHousetrained(atOtherHomes: true) @ignore(if: true)\n\
\ isHouseTrained(atOtherHomes: true) @ignore(if: true)\n\
\ }\n\
\}"
expected = Error
@ -740,13 +752,13 @@ spec =
let queryString = "{\n\
\ dog {\n\
\ doesKnowCommand(dogCommand: SIT)\n\
\ doesKnowCommand: isHousetrained(atOtherHomes: true)\n\
\ doesKnowCommand: isHouseTrained(atOtherHomes: true)\n\
\ }\n\
\}"
expected = Error
{ message =
"Fields \"doesKnowCommand\" conflict because \
\\"doesKnowCommand\" and \"isHousetrained\" are \
\\"doesKnowCommand\" and \"isHouseTrained\" are \
\different fields. Use different aliases on the \
\fields to fetch both if this was intentional."
, locations = [AST.Location 3 5, AST.Location 4 5]
@ -761,13 +773,13 @@ spec =
\ }\n\
\ dog {\n\
\ name\n\
\ doesKnowCommand: isHousetrained(atOtherHomes: true)\n\
\ doesKnowCommand: isHouseTrained(atOtherHomes: true)\n\
\ }\n\
\}"
expected = Error
{ message =
"Fields \"doesKnowCommand\" conflict because \
\\"doesKnowCommand\" and \"isHousetrained\" are \
\\"doesKnowCommand\" and \"isHouseTrained\" are \
\different fields. Use different aliases on the \
\fields to fetch both if this was intentional."
, locations = [AST.Location 4 5, AST.Location 8 5]
@ -860,7 +872,7 @@ spec =
it "rejects wrongly typed variable arguments" $
let queryString = "query intCannotGoIntoBoolean($intArg: Int) {\n\
\ dog {\n\
\ isHousetrained(atOtherHomes: $intArg)\n\
\ isHouseTrained(atOtherHomes: $intArg)\n\
\ }\n\
\}"
expected = Error
@ -875,7 +887,7 @@ spec =
it "rejects values of incorrect types" $
let queryString = "{\n\
\ dog {\n\
\ isHousetrained(atOtherHomes: 3)\n\
\ isHouseTrained(atOtherHomes: 3)\n\
\ }\n\
\}"
expected = Error