forked from OSS/graphql
Validate fragment spread target existence
This commit is contained in:
parent
4b59da2fcb
commit
33318a3b01
21
CHANGELOG.md
21
CHANGELOG.md
@ -6,28 +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/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
### Changed
|
||||||
|
- Added location information to `AST.Document.Selection.FragmentSpread`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `Validate.Validation.Rule`: `SelectionRule` constructor.
|
||||||
|
- `Validate.Rules`: `fragmentSpreadTargetDefinedRule`.
|
||||||
|
|
||||||
## [0.10.0.0] - 2020-08-29
|
## [0.10.0.0] - 2020-08-29
|
||||||
## Changed
|
### Changed
|
||||||
- `Test.Hspec.GraphQL.*`: replace `IO` in the resolver with any `MonadCatch`.
|
- `Test.Hspec.GraphQL.*`: replace `IO` in the resolver with any `MonadCatch`.
|
||||||
- The `Location` argument of `AST.Document.Definition.ExecutableDefinition` was
|
- The `Location` argument of `AST.Document.Definition.ExecutableDefinition` was
|
||||||
moved to `OperationDefinition` and `FragmentDefinition` since these are the
|
moved to `OperationDefinition` and `FragmentDefinition` since these are the
|
||||||
actual elements that have a location in the document.
|
actual elements that have a location in the document.
|
||||||
- `Validate.Rules` get the whole validation context (AST and schema).
|
- `Validate.Rules` get the whole validation context (AST and schema).
|
||||||
|
|
||||||
## Added
|
### Added
|
||||||
- `Validate.Validation` contains data structures and functions used by the
|
- `Validate.Validation` contains data structures and functions used by the
|
||||||
validator and concretet rules.
|
validator and concretet rules.
|
||||||
- `Validate.Rules`: operation validation rules.
|
- `Validate.Rules`: operation validation rules.
|
||||||
|
|
||||||
## [0.9.0.0] - 2020-07-24
|
## [0.9.0.0] - 2020-07-24
|
||||||
## Fixed
|
### Fixed
|
||||||
- Location of a parse error is returned in a singleton array with key
|
- Location of a parse error is returned in a singleton array with key
|
||||||
`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`.
|
||||||
- `AST.Document.Location` is a token location as a line and column pair.
|
- `AST.Document.Location` is a token location as a line and column pair.
|
||||||
- `Execute` reexports `Execute.Coerce`.
|
- `Execute` reexports `Execute.Coerce`.
|
||||||
@ -43,7 +51,7 @@ and this project adheres to
|
|||||||
- `Test.Hspec.GraphQL` contains some test helpers.
|
- `Test.Hspec.GraphQL` contains some test helpers.
|
||||||
- `Validate` contains the validator and standard rules.
|
- `Validate` contains the validator and standard rules.
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
- `Type.Out.Resolver`: Interface fields don't have resolvers, object fields
|
- `Type.Out.Resolver`: Interface fields don't have resolvers, object fields
|
||||||
have value resolvers, root subscription type resolvers need an additional
|
have value resolvers, root subscription type resolvers need an additional
|
||||||
resolver that creates an event stream. `Resolver` represents these differences
|
resolver that creates an event stream. `Resolver` represents these differences
|
||||||
@ -67,7 +75,7 @@ and this project adheres to
|
|||||||
- The constraint of the base monad was changed to `MonadCatch` (and it implies
|
- The constraint of the base monad was changed to `MonadCatch` (and it implies
|
||||||
`MonadThrow`).
|
`MonadThrow`).
|
||||||
|
|
||||||
## Removed
|
### Removed
|
||||||
- `Trans.ActionT` is an unneeded layer of complexity. `Type.Out.Resolver`
|
- `Trans.ActionT` is an unneeded layer of complexity. `Type.Out.Resolver`
|
||||||
represents possible resolver configurations.
|
represents possible resolver configurations.
|
||||||
- `Execute.executeWithName`. `Execute.execute` takes the operation name and
|
- `Execute.executeWithName`. `Execute.execute` takes the operation name and
|
||||||
@ -334,6 +342,7 @@ and this project adheres to
|
|||||||
### Added
|
### Added
|
||||||
- Data types for the GraphQL language.
|
- Data types for the GraphQL language.
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/caraus-ecms/graphql/compare/v0.10.0.0...HEAD
|
||||||
[0.10.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.9.0.0...v0.10.0.0
|
[0.10.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.9.0.0...v0.10.0.0
|
||||||
[0.9.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.8.0.0...v0.9.0.0
|
[0.9.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.8.0.0...v0.9.0.0
|
||||||
[0.8.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.7.0.0...v0.8.0.0
|
[0.8.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.7.0.0...v0.8.0.0
|
||||||
|
@ -163,7 +163,7 @@ type SelectionSetOpt = [Selection]
|
|||||||
-- @
|
-- @
|
||||||
data Selection
|
data Selection
|
||||||
= Field (Maybe Alias) Name [Argument] [Directive] SelectionSetOpt
|
= Field (Maybe Alias) Name [Argument] [Directive] SelectionSetOpt
|
||||||
| FragmentSpread Name [Directive]
|
| FragmentSpread Name [Directive] Location
|
||||||
| InlineFragment (Maybe TypeCondition) [Directive] SelectionSet
|
| InlineFragment (Maybe TypeCondition) [Directive] SelectionSet
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ selection formatter = Lazy.Text.append indent' . encodeSelection
|
|||||||
field incrementIndent alias name args directives' selections
|
field incrementIndent alias name args directives' selections
|
||||||
encodeSelection (InlineFragment typeCondition directives' selections) =
|
encodeSelection (InlineFragment typeCondition directives' selections) =
|
||||||
inlineFragment incrementIndent typeCondition directives' selections
|
inlineFragment incrementIndent typeCondition directives' selections
|
||||||
encodeSelection (FragmentSpread name directives') =
|
encodeSelection (FragmentSpread name directives' _) =
|
||||||
fragmentSpread incrementIndent name directives'
|
fragmentSpread incrementIndent name directives'
|
||||||
incrementIndent
|
incrementIndent
|
||||||
| Pretty indentation <- formatter = Pretty $ indentation + 1
|
| Pretty indentation <- formatter = Pretty $ indentation + 1
|
||||||
|
@ -400,11 +400,12 @@ argument :: Parser Argument
|
|||||||
argument = Argument <$> name <* colon <*> value <?> "Argument"
|
argument = Argument <$> name <* colon <*> value <?> "Argument"
|
||||||
|
|
||||||
fragmentSpread :: Parser Selection
|
fragmentSpread :: Parser Selection
|
||||||
fragmentSpread = FragmentSpread
|
fragmentSpread = label "FragmentSpread" $ do
|
||||||
<$ spread
|
location <- getLocation
|
||||||
<*> fragmentName
|
_ <- spread
|
||||||
<*> directives
|
fragmentName' <- fragmentName
|
||||||
<?> "FragmentSpread"
|
directives' <- directives
|
||||||
|
pure $ FragmentSpread fragmentName' directives' location
|
||||||
|
|
||||||
inlineFragment :: Parser Selection
|
inlineFragment :: Parser Selection
|
||||||
inlineFragment = InlineFragment
|
inlineFragment = InlineFragment
|
||||||
|
@ -299,7 +299,7 @@ selection (Full.Field alias name arguments' directives' selections) =
|
|||||||
go arguments (Full.Argument name' value') =
|
go arguments (Full.Argument name' value') =
|
||||||
inputField arguments name' value'
|
inputField arguments name' value'
|
||||||
|
|
||||||
selection (Full.FragmentSpread name directives') =
|
selection (Full.FragmentSpread name directives' _) =
|
||||||
maybe (Left mempty) (Right . SelectionFragment) <$> do
|
maybe (Left mempty) (Right . SelectionFragment) <$> do
|
||||||
spreadDirectives <- Definition.selection <$> directives directives'
|
spreadDirectives <- Definition.selection <$> directives directives'
|
||||||
fragments' <- gets fragments
|
fragments' <- gets fragments
|
||||||
|
@ -15,7 +15,7 @@ module Language.GraphQL.Validate
|
|||||||
|
|
||||||
import Control.Monad (foldM)
|
import Control.Monad (foldM)
|
||||||
import Control.Monad.Trans.Reader (Reader, asks, mapReaderT, runReader)
|
import Control.Monad.Trans.Reader (Reader, asks, mapReaderT, runReader)
|
||||||
import Data.Foldable (foldrM)
|
import Data.Foldable (fold, foldrM)
|
||||||
import Data.Sequence (Seq(..), (><), (|>))
|
import Data.Sequence (Seq(..), (><), (|>))
|
||||||
import qualified Data.Sequence as Seq
|
import qualified Data.Sequence as Seq
|
||||||
import Language.GraphQL.AST.Document
|
import Language.GraphQL.AST.Document
|
||||||
@ -66,15 +66,41 @@ executableDefinition (DefinitionFragment definition') =
|
|||||||
|
|
||||||
operationDefinition :: forall m. OperationDefinition -> ValidateT m
|
operationDefinition :: forall m. OperationDefinition -> ValidateT m
|
||||||
operationDefinition operation =
|
operationDefinition operation =
|
||||||
asks rules >>= foldM ruleFilter Seq.empty
|
let selectionSet = getSelectionSet operation
|
||||||
|
in visitChildSelections ruleFilter selectionSet
|
||||||
where
|
where
|
||||||
ruleFilter accumulator (OperationDefinitionRule rule) =
|
ruleFilter accumulator (OperationDefinitionRule rule) =
|
||||||
mapReaderT (runRule accumulator) $ rule operation
|
mapReaderT (runRule accumulator) $ rule operation
|
||||||
ruleFilter accumulator _ = pure accumulator
|
ruleFilter accumulator _ = pure accumulator
|
||||||
|
getSelectionSet (SelectionSet selectionSet _) = selectionSet
|
||||||
|
getSelectionSet (OperationDefinition _ _ _ _ selectionSet _) = selectionSet
|
||||||
|
|
||||||
|
selection :: forall m. Selection -> ValidateT m
|
||||||
|
selection selection'@FragmentSpread{} =
|
||||||
|
asks rules >>= foldM ruleFilter Seq.empty
|
||||||
|
where
|
||||||
|
ruleFilter accumulator (SelectionRule rule) =
|
||||||
|
mapReaderT (runRule accumulator) $ rule selection'
|
||||||
|
ruleFilter accumulator _ = pure accumulator
|
||||||
|
selection (Field _ _ _ _ selectionSet) = traverseSelectionSet selectionSet
|
||||||
|
selection (InlineFragment _ _ selectionSet) = traverseSelectionSet selectionSet
|
||||||
|
|
||||||
|
traverseSelectionSet :: Traversable t => forall m. t Selection -> ValidateT m
|
||||||
|
traverseSelectionSet = fmap fold . traverse selection
|
||||||
|
|
||||||
|
visitChildSelections :: Traversable t
|
||||||
|
=> (Seq Error -> Rule m -> ValidateT m)
|
||||||
|
-> t Selection
|
||||||
|
-> ValidateT m
|
||||||
|
visitChildSelections ruleFilter selectionSet = do
|
||||||
|
rules' <- asks rules
|
||||||
|
applied <- foldM ruleFilter Seq.empty rules'
|
||||||
|
children <- traverseSelectionSet selectionSet
|
||||||
|
pure $ children >< applied
|
||||||
|
|
||||||
fragmentDefinition :: forall m. FragmentDefinition -> ValidateT m
|
fragmentDefinition :: forall m. FragmentDefinition -> ValidateT m
|
||||||
fragmentDefinition fragment =
|
fragmentDefinition fragment@(FragmentDefinition _ _ _ selectionSet _) =
|
||||||
asks rules >>= foldM ruleFilter Seq.empty
|
visitChildSelections ruleFilter selectionSet
|
||||||
where
|
where
|
||||||
ruleFilter accumulator (FragmentDefinitionRule rule) =
|
ruleFilter accumulator (FragmentDefinitionRule rule) =
|
||||||
mapReaderT (runRule accumulator) $ rule fragment
|
mapReaderT (runRule accumulator) $ rule fragment
|
||||||
|
@ -21,6 +21,7 @@ import Control.Monad (foldM)
|
|||||||
import Control.Monad.Trans.Class (MonadTrans(..))
|
import Control.Monad.Trans.Class (MonadTrans(..))
|
||||||
import Control.Monad.Trans.Reader (asks)
|
import Control.Monad.Trans.Reader (asks)
|
||||||
import Control.Monad.Trans.State (evalStateT, gets, modify)
|
import Control.Monad.Trans.State (evalStateT, gets, modify)
|
||||||
|
import Data.Foldable (find)
|
||||||
import qualified Data.HashSet as HashSet
|
import qualified Data.HashSet as HashSet
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import Language.GraphQL.AST.Document
|
import Language.GraphQL.AST.Document
|
||||||
@ -36,6 +37,7 @@ specifiedRules =
|
|||||||
, loneAnonymousOperationRule
|
, loneAnonymousOperationRule
|
||||||
, uniqueOperationNamesRule
|
, uniqueOperationNamesRule
|
||||||
, uniqueFragmentNamesRule
|
, uniqueFragmentNamesRule
|
||||||
|
, fragmentSpreadTargetDefinedRule
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | Definition must be OperationDefinition or FragmentDefinition.
|
-- | Definition must be OperationDefinition or FragmentDefinition.
|
||||||
@ -84,7 +86,7 @@ singleFieldSubscriptionsRule = OperationDefinitionRule $ \case
|
|||||||
| Just aliasedName <- alias = pure
|
| Just aliasedName <- alias = pure
|
||||||
$ HashSet.insert aliasedName accumulator
|
$ HashSet.insert aliasedName accumulator
|
||||||
| otherwise = pure $ HashSet.insert name accumulator
|
| otherwise = pure $ HashSet.insert name accumulator
|
||||||
forEach accumulator (FragmentSpread fragmentName directives)
|
forEach accumulator (FragmentSpread fragmentName directives _)
|
||||||
| any skip directives = pure accumulator
|
| any skip directives = pure accumulator
|
||||||
| otherwise = do
|
| otherwise = do
|
||||||
inVisitetFragments <- gets $ HashSet.member fragmentName
|
inVisitetFragments <- gets $ HashSet.member fragmentName
|
||||||
@ -192,6 +194,13 @@ viewOperation definition
|
|||||||
Just operationDefinition
|
Just operationDefinition
|
||||||
viewOperation _ = Nothing
|
viewOperation _ = Nothing
|
||||||
|
|
||||||
|
viewFragment :: Definition -> Maybe FragmentDefinition
|
||||||
|
viewFragment definition
|
||||||
|
| ExecutableDefinition executableDefinition <- definition
|
||||||
|
, DefinitionFragment fragmentDefinition <- executableDefinition =
|
||||||
|
Just fragmentDefinition
|
||||||
|
viewFragment _ = Nothing
|
||||||
|
|
||||||
-- | Fragment definitions are referenced in fragment spreads by name. To avoid
|
-- | Fragment definitions are referenced in fragment spreads by name. To avoid
|
||||||
-- ambiguity, each fragment’s name must be unique within a document.
|
-- ambiguity, each fragment’s name must be unique within a document.
|
||||||
--
|
--
|
||||||
@ -208,8 +217,32 @@ uniqueFragmentNamesRule = FragmentDefinitionRule $ \case
|
|||||||
, "\"."
|
, "\"."
|
||||||
]
|
]
|
||||||
filterByName thisName definition accumulator
|
filterByName thisName definition accumulator
|
||||||
| ExecutableDefinition executableDefinition <- definition
|
| Just fragmentDefinition <- viewFragment definition
|
||||||
, DefinitionFragment fragmentDefinition <- executableDefinition
|
|
||||||
, FragmentDefinition thatName _ _ _ thatLocation <- fragmentDefinition
|
, FragmentDefinition thatName _ _ _ thatLocation <- fragmentDefinition
|
||||||
, thisName == thatName = thatLocation : accumulator
|
, thisName == thatName = thatLocation : accumulator
|
||||||
| otherwise = accumulator
|
| otherwise = accumulator
|
||||||
|
|
||||||
|
-- | Named fragment spreads must refer to fragments defined within the document.
|
||||||
|
-- It is a validation error if the target of a spread is not defined.
|
||||||
|
fragmentSpreadTargetDefinedRule :: forall m. Rule m
|
||||||
|
fragmentSpreadTargetDefinedRule = SelectionRule $ \case
|
||||||
|
FragmentSpread fragmentName _ location -> do
|
||||||
|
ast' <- asks ast
|
||||||
|
case find (findTarget fragmentName) ast' of
|
||||||
|
Nothing -> pure $ Error
|
||||||
|
{ message = error' fragmentName
|
||||||
|
, locations = [location]
|
||||||
|
, path = []
|
||||||
|
}
|
||||||
|
Just _ -> lift Nothing
|
||||||
|
_ -> lift Nothing
|
||||||
|
where
|
||||||
|
error' fragmentName = concat
|
||||||
|
[ "Fragment target \""
|
||||||
|
, Text.unpack fragmentName
|
||||||
|
, "\" is undefined."
|
||||||
|
]
|
||||||
|
findTarget thisName (viewFragment -> Just fragmentDefinition)
|
||||||
|
| FragmentDefinition thatName _ _ _ _ <- fragmentDefinition
|
||||||
|
, thisName == thatName = True
|
||||||
|
findTarget _ _ = False
|
||||||
|
@ -49,6 +49,7 @@ data Rule m
|
|||||||
= DefinitionRule (Definition -> RuleT m)
|
= DefinitionRule (Definition -> RuleT m)
|
||||||
| OperationDefinitionRule (OperationDefinition -> RuleT m)
|
| OperationDefinitionRule (OperationDefinition -> RuleT m)
|
||||||
| FragmentDefinitionRule (FragmentDefinition -> RuleT m)
|
| FragmentDefinitionRule (FragmentDefinition -> RuleT m)
|
||||||
|
| SelectionRule (Selection -> RuleT m)
|
||||||
|
|
||||||
-- | Monad transformer used by the rules.
|
-- | Monad transformer used by the rules.
|
||||||
type RuleT m = ReaderT (Validation m) Maybe Error
|
type RuleT m = ReaderT (Validation m) Maybe Error
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
resolver: lts-16.11
|
resolver: lts-16.12
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
- .
|
- .
|
||||||
@ -6,4 +6,4 @@ packages:
|
|||||||
extra-deps: []
|
extra-deps: []
|
||||||
flags: {}
|
flags: {}
|
||||||
|
|
||||||
pvp-bounds: both
|
pvp-bounds: lower
|
||||||
|
@ -281,3 +281,19 @@ spec =
|
|||||||
, path = []
|
, path = []
|
||||||
}
|
}
|
||||||
in validate queryString `shouldBe` Seq.singleton expected
|
in validate queryString `shouldBe` Seq.singleton expected
|
||||||
|
|
||||||
|
it "rejects the fragment spread without a target" $
|
||||||
|
let queryString = [r|
|
||||||
|
{
|
||||||
|
dog {
|
||||||
|
...undefinedFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expected = Error
|
||||||
|
{ message =
|
||||||
|
"Fragment target \"undefinedFragment\" is undefined."
|
||||||
|
, locations = [AST.Location 4 19]
|
||||||
|
, path = []
|
||||||
|
}
|
||||||
|
in validate queryString `shouldBe` Seq.singleton expected
|
||||||
|
Loading…
Reference in New Issue
Block a user