2020-07-20 21:29:12 +02:00
|
|
|
|
{- 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/. -}
|
|
|
|
|
|
2020-08-26 18:58:48 +02:00
|
|
|
|
{-# LANGUAGE LambdaCase #-}
|
2020-08-25 21:03:42 +02:00
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2020-09-21 07:28:40 +02:00
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
2020-08-27 09:04:31 +02:00
|
|
|
|
{-# LANGUAGE ViewPatterns #-}
|
2020-08-25 21:03:42 +02:00
|
|
|
|
|
2020-07-24 21:34:31 +02:00
|
|
|
|
-- | This module contains default rules defined in the GraphQL specification.
|
2020-07-20 21:29:12 +02:00
|
|
|
|
module Language.GraphQL.Validate.Rules
|
2020-08-25 21:03:42 +02:00
|
|
|
|
( executableDefinitionsRule
|
2020-09-25 21:57:25 +02:00
|
|
|
|
, fieldsOnCorrectTypeRule
|
2020-09-07 22:01:49 +02:00
|
|
|
|
, fragmentsOnCompositeTypesRule
|
2020-09-04 19:12:19 +02:00
|
|
|
|
, fragmentSpreadTargetDefinedRule
|
|
|
|
|
, fragmentSpreadTypeExistenceRule
|
2020-08-27 09:04:31 +02:00
|
|
|
|
, loneAnonymousOperationRule
|
2020-09-11 08:03:49 +02:00
|
|
|
|
, noFragmentCyclesRule
|
2020-09-21 07:28:40 +02:00
|
|
|
|
, noUndefinedVariablesRule
|
2020-09-09 17:04:31 +02:00
|
|
|
|
, noUnusedFragmentsRule
|
2020-09-22 04:42:25 +02:00
|
|
|
|
, noUnusedVariablesRule
|
2020-08-27 09:04:31 +02:00
|
|
|
|
, singleFieldSubscriptionsRule
|
2020-07-20 21:29:12 +02:00
|
|
|
|
, specifiedRules
|
2020-09-17 10:33:37 +02:00
|
|
|
|
, uniqueArgumentNamesRule
|
2020-09-18 07:32:58 +02:00
|
|
|
|
, uniqueDirectiveNamesRule
|
2020-08-28 08:32:21 +02:00
|
|
|
|
, uniqueFragmentNamesRule
|
2020-09-24 05:47:31 +02:00
|
|
|
|
, uniqueInputFieldNamesRule
|
2020-08-27 09:04:31 +02:00
|
|
|
|
, uniqueOperationNamesRule
|
2020-09-19 18:18:26 +02:00
|
|
|
|
, uniqueVariableNamesRule
|
2020-09-20 06:59:27 +02:00
|
|
|
|
, variablesAreInputTypesRule
|
2020-07-20 21:29:12 +02:00
|
|
|
|
) where
|
|
|
|
|
|
2020-09-20 06:59:27 +02:00
|
|
|
|
import Control.Monad ((>=>), foldM)
|
2020-08-25 21:03:42 +02:00
|
|
|
|
import Control.Monad.Trans.Class (MonadTrans(..))
|
2020-09-21 07:28:40 +02:00
|
|
|
|
import Control.Monad.Trans.Reader (ReaderT(..), asks, mapReaderT)
|
2020-09-11 08:03:49 +02:00
|
|
|
|
import Control.Monad.Trans.State (StateT, evalStateT, gets, modify)
|
|
|
|
|
import Data.Bifunctor (first)
|
2020-09-21 07:28:40 +02:00
|
|
|
|
import Data.Foldable (find, toList)
|
2020-09-04 19:12:19 +02:00
|
|
|
|
import qualified Data.HashMap.Strict as HashMap
|
2020-09-11 08:03:49 +02:00
|
|
|
|
import Data.HashMap.Strict (HashMap)
|
2020-09-21 07:28:40 +02:00
|
|
|
|
import Data.HashSet (HashSet)
|
2020-08-25 21:03:42 +02:00
|
|
|
|
import qualified Data.HashSet as HashSet
|
2020-09-17 10:33:37 +02:00
|
|
|
|
import Data.List (groupBy, sortBy, sortOn)
|
2020-09-25 21:57:25 +02:00
|
|
|
|
import Data.Maybe (isJust, mapMaybe)
|
2020-09-11 08:03:49 +02:00
|
|
|
|
import Data.Ord (comparing)
|
2020-09-14 07:49:33 +02:00
|
|
|
|
import Data.Sequence (Seq(..))
|
2020-09-17 10:33:37 +02:00
|
|
|
|
import qualified Data.Sequence as Seq
|
2020-09-04 19:12:19 +02:00
|
|
|
|
import Data.Text (Text)
|
2020-08-25 21:03:42 +02:00
|
|
|
|
import qualified Data.Text as Text
|
2020-07-20 21:29:12 +02:00
|
|
|
|
import Language.GraphQL.AST.Document
|
2020-09-25 21:57:25 +02:00
|
|
|
|
import qualified Language.GraphQL.Type.Definition as Definition
|
2020-08-25 21:03:42 +02:00
|
|
|
|
import Language.GraphQL.Type.Internal
|
2020-09-25 21:57:25 +02:00
|
|
|
|
import qualified Language.GraphQL.Type.Out as Out
|
2020-08-25 21:03:42 +02:00
|
|
|
|
import qualified Language.GraphQL.Type.Schema as Schema
|
|
|
|
|
import Language.GraphQL.Validate.Validation
|
2020-07-20 21:29:12 +02:00
|
|
|
|
|
2020-09-21 07:28:40 +02:00
|
|
|
|
-- Local help type that contains a hash set to track visited fragments.
|
|
|
|
|
type ValidationState m a = StateT (HashSet Name) (ReaderT (Validation m) Seq) a
|
|
|
|
|
|
2020-08-22 06:39:52 +02:00
|
|
|
|
-- | Default rules given in the specification.
|
2020-08-25 21:03:42 +02:00
|
|
|
|
specifiedRules :: forall m. [Rule m]
|
2020-07-20 21:29:12 +02:00
|
|
|
|
specifiedRules =
|
2020-09-04 19:12:19 +02:00
|
|
|
|
-- Documents.
|
2020-07-20 21:29:12 +02:00
|
|
|
|
[ executableDefinitionsRule
|
2020-09-04 19:12:19 +02:00
|
|
|
|
-- Operations.
|
2020-08-25 21:03:42 +02:00
|
|
|
|
, singleFieldSubscriptionsRule
|
2020-08-26 18:58:48 +02:00
|
|
|
|
, loneAnonymousOperationRule
|
2020-08-27 09:04:31 +02:00
|
|
|
|
, uniqueOperationNamesRule
|
2020-09-25 21:57:25 +02:00
|
|
|
|
-- Fields
|
|
|
|
|
, fieldsOnCorrectTypeRule
|
2020-09-17 10:33:37 +02:00
|
|
|
|
-- Arguments.
|
|
|
|
|
, uniqueArgumentNamesRule
|
2020-09-04 19:12:19 +02:00
|
|
|
|
-- Fragments.
|
2020-08-28 08:32:21 +02:00
|
|
|
|
, uniqueFragmentNamesRule
|
2020-09-04 19:12:19 +02:00
|
|
|
|
, fragmentSpreadTypeExistenceRule
|
2020-09-07 22:01:49 +02:00
|
|
|
|
, fragmentsOnCompositeTypesRule
|
2020-09-09 17:04:31 +02:00
|
|
|
|
, noUnusedFragmentsRule
|
|
|
|
|
, fragmentSpreadTargetDefinedRule
|
2020-09-11 08:03:49 +02:00
|
|
|
|
, noFragmentCyclesRule
|
2020-09-24 05:47:31 +02:00
|
|
|
|
-- Values
|
|
|
|
|
, uniqueInputFieldNamesRule
|
2020-09-18 07:32:58 +02:00
|
|
|
|
-- Directives.
|
|
|
|
|
, uniqueDirectiveNamesRule
|
2020-09-19 18:18:26 +02:00
|
|
|
|
-- Variables.
|
|
|
|
|
, uniqueVariableNamesRule
|
2020-09-20 06:59:27 +02:00
|
|
|
|
, variablesAreInputTypesRule
|
2020-09-21 07:28:40 +02:00
|
|
|
|
, noUndefinedVariablesRule
|
2020-09-22 04:42:25 +02:00
|
|
|
|
, noUnusedVariablesRule
|
2020-07-20 21:29:12 +02:00
|
|
|
|
]
|
|
|
|
|
|
2020-07-24 21:34:31 +02:00
|
|
|
|
-- | Definition must be OperationDefinition or FragmentDefinition.
|
2020-08-25 21:03:42 +02:00
|
|
|
|
executableDefinitionsRule :: forall m. Rule m
|
2020-08-26 18:58:48 +02:00
|
|
|
|
executableDefinitionsRule = DefinitionRule $ \case
|
2020-09-14 07:49:33 +02:00
|
|
|
|
ExecutableDefinition _ -> lift mempty
|
2020-08-27 09:04:31 +02:00
|
|
|
|
TypeSystemDefinition _ location -> pure $ error' location
|
|
|
|
|
TypeSystemExtension _ location -> pure $ error' location
|
|
|
|
|
where
|
|
|
|
|
error' location = Error
|
|
|
|
|
{ message =
|
|
|
|
|
"Definition must be OperationDefinition or FragmentDefinition."
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
2020-08-25 21:03:42 +02:00
|
|
|
|
|
|
|
|
|
-- | Subscription operations must have exactly one root field.
|
|
|
|
|
singleFieldSubscriptionsRule :: forall m. Rule m
|
2020-08-26 18:58:48 +02:00
|
|
|
|
singleFieldSubscriptionsRule = OperationDefinitionRule $ \case
|
2020-08-27 09:04:31 +02:00
|
|
|
|
OperationDefinition Subscription name' _ _ rootFields location -> do
|
2020-08-25 21:03:42 +02:00
|
|
|
|
groupedFieldSet <- evalStateT (collectFields rootFields) HashSet.empty
|
|
|
|
|
case HashSet.size groupedFieldSet of
|
2020-09-14 07:49:33 +02:00
|
|
|
|
1 -> lift mempty
|
2020-08-25 21:03:42 +02:00
|
|
|
|
_
|
2020-08-27 09:04:31 +02:00
|
|
|
|
| Just name <- name' -> pure $ Error
|
|
|
|
|
{ message = unwords
|
|
|
|
|
[ "Subscription"
|
|
|
|
|
, Text.unpack name
|
|
|
|
|
, "must select only one top level field."
|
|
|
|
|
]
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
|
|
|
|
| otherwise -> pure $ Error
|
|
|
|
|
{ message = errorMessage
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
_ -> lift mempty
|
2020-08-26 18:58:48 +02:00
|
|
|
|
where
|
2020-08-27 09:04:31 +02:00
|
|
|
|
errorMessage =
|
|
|
|
|
"Anonymous Subscription must select only one top level field."
|
2020-08-25 21:03:42 +02:00
|
|
|
|
collectFields selectionSet = foldM forEach HashSet.empty selectionSet
|
2020-09-07 22:01:49 +02:00
|
|
|
|
forEach accumulator = \case
|
2020-09-09 17:04:31 +02:00
|
|
|
|
FieldSelection fieldSelection -> forField accumulator fieldSelection
|
2020-09-07 22:01:49 +02:00
|
|
|
|
FragmentSpreadSelection fragmentSelection ->
|
|
|
|
|
forSpread accumulator fragmentSelection
|
|
|
|
|
InlineFragmentSelection fragmentSelection ->
|
|
|
|
|
forInline accumulator fragmentSelection
|
2020-09-09 17:04:31 +02:00
|
|
|
|
forField accumulator (Field alias name _ directives _ _)
|
|
|
|
|
| any skip directives = pure accumulator
|
|
|
|
|
| Just aliasedName <- alias = pure
|
|
|
|
|
$ HashSet.insert aliasedName accumulator
|
|
|
|
|
| otherwise = pure $ HashSet.insert name accumulator
|
2020-09-07 22:01:49 +02:00
|
|
|
|
forSpread accumulator (FragmentSpread fragmentName directives _)
|
2020-08-25 21:03:42 +02:00
|
|
|
|
| any skip directives = pure accumulator
|
|
|
|
|
| otherwise = do
|
|
|
|
|
inVisitetFragments <- gets $ HashSet.member fragmentName
|
|
|
|
|
if inVisitetFragments
|
|
|
|
|
then pure accumulator
|
|
|
|
|
else collectFromSpread fragmentName accumulator
|
2020-09-07 22:01:49 +02:00
|
|
|
|
forInline accumulator (InlineFragment maybeType directives selections _)
|
2020-08-25 21:03:42 +02:00
|
|
|
|
| any skip directives = pure accumulator
|
2020-09-07 22:01:49 +02:00
|
|
|
|
| Just typeCondition <- maybeType =
|
|
|
|
|
collectFromFragment typeCondition selections accumulator
|
2020-08-25 21:03:42 +02:00
|
|
|
|
| otherwise = HashSet.union accumulator
|
2020-09-07 22:01:49 +02:00
|
|
|
|
<$> collectFields selections
|
2020-09-21 07:28:40 +02:00
|
|
|
|
skip (Directive "skip" [Argument "if" (Node argumentValue _) _] _) =
|
|
|
|
|
Boolean True == argumentValue
|
|
|
|
|
skip (Directive "include" [Argument "if" (Node argumentValue _) _] _) =
|
|
|
|
|
Boolean False == argumentValue
|
2020-08-25 21:03:42 +02:00
|
|
|
|
skip _ = False
|
|
|
|
|
findFragmentDefinition (ExecutableDefinition executableDefinition) Nothing
|
|
|
|
|
| DefinitionFragment fragmentDefinition <- executableDefinition =
|
|
|
|
|
Just fragmentDefinition
|
|
|
|
|
findFragmentDefinition _ accumulator = accumulator
|
|
|
|
|
collectFromFragment typeCondition selectionSet accumulator = do
|
|
|
|
|
types' <- lift $ asks types
|
|
|
|
|
schema' <- lift $ asks schema
|
|
|
|
|
case lookupTypeCondition typeCondition types' of
|
|
|
|
|
Nothing -> pure accumulator
|
|
|
|
|
Just compositeType
|
|
|
|
|
| Just objectType <- Schema.subscription schema'
|
|
|
|
|
, True <- doesFragmentTypeApply compositeType objectType ->
|
2020-09-09 17:04:31 +02:00
|
|
|
|
HashSet.union accumulator <$> collectFields selectionSet
|
2020-08-25 21:03:42 +02:00
|
|
|
|
| otherwise -> pure accumulator
|
|
|
|
|
collectFromSpread fragmentName accumulator = do
|
|
|
|
|
modify $ HashSet.insert fragmentName
|
|
|
|
|
ast' <- lift $ asks ast
|
|
|
|
|
case foldr findFragmentDefinition Nothing ast' of
|
|
|
|
|
Nothing -> pure accumulator
|
|
|
|
|
Just (FragmentDefinition _ typeCondition _ selectionSet _) ->
|
|
|
|
|
collectFromFragment typeCondition selectionSet accumulator
|
2020-08-26 18:58:48 +02:00
|
|
|
|
|
|
|
|
|
-- | GraphQL allows a short‐hand form for defining query operations when only
|
|
|
|
|
-- that one operation exists in the document.
|
|
|
|
|
loneAnonymousOperationRule :: forall m. Rule m
|
|
|
|
|
loneAnonymousOperationRule = OperationDefinitionRule $ \case
|
|
|
|
|
SelectionSet _ thisLocation -> check thisLocation
|
|
|
|
|
OperationDefinition _ Nothing _ _ _ thisLocation -> check thisLocation
|
2020-09-14 07:49:33 +02:00
|
|
|
|
_ -> lift mempty
|
2020-08-26 18:58:48 +02:00
|
|
|
|
where
|
|
|
|
|
check thisLocation = asks ast
|
2020-09-14 07:49:33 +02:00
|
|
|
|
>>= lift . foldr (filterAnonymousOperations thisLocation) mempty
|
|
|
|
|
filterAnonymousOperations thisLocation definition Empty
|
2020-08-27 09:04:31 +02:00
|
|
|
|
| (viewOperation -> Just operationDefinition) <- definition =
|
2020-08-26 18:58:48 +02:00
|
|
|
|
compareAnonymousOperations thisLocation operationDefinition
|
|
|
|
|
filterAnonymousOperations _ _ accumulator = accumulator
|
2020-08-27 09:04:31 +02:00
|
|
|
|
compareAnonymousOperations thisLocation = \case
|
|
|
|
|
OperationDefinition _ _ _ _ _ thatLocation
|
|
|
|
|
| thisLocation /= thatLocation -> pure $ error' thisLocation
|
|
|
|
|
SelectionSet _ thatLocation
|
|
|
|
|
| thisLocation /= thatLocation -> pure $ error' thisLocation
|
2020-09-14 07:49:33 +02:00
|
|
|
|
_ -> mempty
|
2020-08-27 09:04:31 +02:00
|
|
|
|
error' location = Error
|
|
|
|
|
{ message =
|
|
|
|
|
"This anonymous operation must be the only defined operation."
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
-- | Each named operation definition must be unique within a document when
|
|
|
|
|
-- referred to by its name.
|
|
|
|
|
uniqueOperationNamesRule :: forall m. Rule m
|
|
|
|
|
uniqueOperationNamesRule = OperationDefinitionRule $ \case
|
2020-08-28 08:32:21 +02:00
|
|
|
|
OperationDefinition _ (Just thisName) _ _ _ thisLocation ->
|
|
|
|
|
findDuplicates (filterByName thisName) thisLocation (error' thisName)
|
2020-09-14 07:49:33 +02:00
|
|
|
|
_ -> lift mempty
|
2020-08-27 09:04:31 +02:00
|
|
|
|
where
|
2020-08-28 08:32:21 +02:00
|
|
|
|
error' operationName = concat
|
|
|
|
|
[ "There can be only one operation named \""
|
|
|
|
|
, Text.unpack operationName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-08-27 09:04:31 +02:00
|
|
|
|
filterByName thisName definition' accumulator
|
|
|
|
|
| (viewOperation -> Just operationDefinition) <- definition'
|
|
|
|
|
, OperationDefinition _ (Just thatName) _ _ _ thatLocation <- operationDefinition
|
|
|
|
|
, thisName == thatName = thatLocation : accumulator
|
|
|
|
|
| otherwise = accumulator
|
|
|
|
|
|
2020-08-28 08:32:21 +02:00
|
|
|
|
findDuplicates :: (Definition -> [Location] -> [Location])
|
|
|
|
|
-> Location
|
|
|
|
|
-> String
|
|
|
|
|
-> RuleT m
|
|
|
|
|
findDuplicates filterByName thisLocation errorMessage = do
|
|
|
|
|
ast' <- asks ast
|
|
|
|
|
let locations' = foldr filterByName [] ast'
|
|
|
|
|
if length locations' > 1 && head locations' == thisLocation
|
|
|
|
|
then pure $ error' locations'
|
2020-09-14 07:49:33 +02:00
|
|
|
|
else lift mempty
|
2020-08-28 08:32:21 +02:00
|
|
|
|
where
|
|
|
|
|
error' locations' = Error
|
|
|
|
|
{ message = errorMessage
|
|
|
|
|
, locations = locations'
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-27 09:04:31 +02:00
|
|
|
|
viewOperation :: Definition -> Maybe OperationDefinition
|
|
|
|
|
viewOperation definition
|
|
|
|
|
| ExecutableDefinition executableDefinition <- definition
|
|
|
|
|
, DefinitionOperation operationDefinition <- executableDefinition =
|
|
|
|
|
Just operationDefinition
|
|
|
|
|
viewOperation _ = Nothing
|
2020-08-28 08:32:21 +02:00
|
|
|
|
|
2020-08-31 11:06:27 +02:00
|
|
|
|
viewFragment :: Definition -> Maybe FragmentDefinition
|
|
|
|
|
viewFragment definition
|
|
|
|
|
| ExecutableDefinition executableDefinition <- definition
|
|
|
|
|
, DefinitionFragment fragmentDefinition <- executableDefinition =
|
|
|
|
|
Just fragmentDefinition
|
|
|
|
|
viewFragment _ = Nothing
|
|
|
|
|
|
2020-08-28 08:32:21 +02:00
|
|
|
|
-- | Fragment definitions are referenced in fragment spreads by name. To avoid
|
|
|
|
|
-- ambiguity, each fragment’s name must be unique within a document.
|
|
|
|
|
--
|
|
|
|
|
-- Inline fragments are not considered fragment definitions, and are unaffected
|
|
|
|
|
-- by this validation rule.
|
|
|
|
|
uniqueFragmentNamesRule :: forall m. Rule m
|
|
|
|
|
uniqueFragmentNamesRule = FragmentDefinitionRule $ \case
|
|
|
|
|
FragmentDefinition thisName _ _ _ thisLocation ->
|
|
|
|
|
findDuplicates (filterByName thisName) thisLocation (error' thisName)
|
|
|
|
|
where
|
|
|
|
|
error' fragmentName = concat
|
|
|
|
|
[ "There can be only one fragment named \""
|
|
|
|
|
, Text.unpack fragmentName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
|
|
|
|
filterByName thisName definition accumulator
|
2020-08-31 11:06:27 +02:00
|
|
|
|
| Just fragmentDefinition <- viewFragment definition
|
2020-08-28 08:32:21 +02:00
|
|
|
|
, FragmentDefinition thatName _ _ _ thatLocation <- fragmentDefinition
|
|
|
|
|
, thisName == thatName = thatLocation : accumulator
|
|
|
|
|
| otherwise = accumulator
|
2020-08-31 11:06:27 +02:00
|
|
|
|
|
|
|
|
|
-- | 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
|
2020-09-07 22:01:49 +02:00
|
|
|
|
fragmentSpreadTargetDefinedRule = FragmentSpreadRule $ \case
|
2020-08-31 11:06:27 +02:00
|
|
|
|
FragmentSpread fragmentName _ location -> do
|
|
|
|
|
ast' <- asks ast
|
2020-09-04 19:12:19 +02:00
|
|
|
|
case find (isSpreadTarget fragmentName) ast' of
|
2020-08-31 11:06:27 +02:00
|
|
|
|
Nothing -> pure $ Error
|
|
|
|
|
{ message = error' fragmentName
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
Just _ -> lift mempty
|
2020-08-31 11:06:27 +02:00
|
|
|
|
where
|
|
|
|
|
error' fragmentName = concat
|
|
|
|
|
[ "Fragment target \""
|
|
|
|
|
, Text.unpack fragmentName
|
|
|
|
|
, "\" is undefined."
|
|
|
|
|
]
|
2020-09-04 19:12:19 +02:00
|
|
|
|
|
|
|
|
|
isSpreadTarget :: Text -> Definition -> Bool
|
|
|
|
|
isSpreadTarget thisName (viewFragment -> Just fragmentDefinition)
|
|
|
|
|
| FragmentDefinition thatName _ _ _ _ <- fragmentDefinition
|
|
|
|
|
, thisName == thatName = True
|
|
|
|
|
isSpreadTarget _ _ = False
|
|
|
|
|
|
|
|
|
|
-- | Fragments must be specified on types that exist in the schema. This applies
|
|
|
|
|
-- for both named and inline fragments. If they are not defined in the schema,
|
|
|
|
|
-- the query does not validate.
|
|
|
|
|
fragmentSpreadTypeExistenceRule :: forall m. Rule m
|
2020-09-25 21:57:25 +02:00
|
|
|
|
fragmentSpreadTypeExistenceRule = SelectionRule $ const $ \case
|
2020-09-07 22:01:49 +02:00
|
|
|
|
FragmentSpreadSelection fragmentSelection
|
|
|
|
|
| FragmentSpread fragmentName _ location <- fragmentSelection -> do
|
|
|
|
|
ast' <- asks ast
|
2020-09-14 07:49:33 +02:00
|
|
|
|
let target = find (isSpreadTarget fragmentName) ast'
|
|
|
|
|
typeCondition <- lift $ maybeToSeq $ target >>= extractTypeCondition
|
2020-09-07 22:01:49 +02:00
|
|
|
|
types' <- asks types
|
|
|
|
|
case HashMap.lookup typeCondition types' of
|
|
|
|
|
Nothing -> pure $ Error
|
|
|
|
|
{ message = spreadError fragmentName typeCondition
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
Just _ -> lift mempty
|
2020-09-07 22:01:49 +02:00
|
|
|
|
InlineFragmentSelection fragmentSelection
|
|
|
|
|
| InlineFragment maybeType _ _ location <- fragmentSelection
|
|
|
|
|
, Just typeCondition <- maybeType -> do
|
|
|
|
|
types' <- asks types
|
|
|
|
|
case HashMap.lookup typeCondition types' of
|
|
|
|
|
Nothing -> pure $ Error
|
|
|
|
|
{ message = inlineError typeCondition
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
Just _ -> lift mempty
|
|
|
|
|
_ -> lift mempty
|
2020-09-04 19:12:19 +02:00
|
|
|
|
where
|
|
|
|
|
extractTypeCondition (viewFragment -> Just fragmentDefinition) =
|
|
|
|
|
let FragmentDefinition _ typeCondition _ _ _ = fragmentDefinition
|
2020-09-14 07:49:33 +02:00
|
|
|
|
in Just typeCondition
|
|
|
|
|
extractTypeCondition _ = Nothing
|
2020-09-05 10:00:58 +02:00
|
|
|
|
spreadError fragmentName typeCondition = concat
|
2020-09-04 19:12:19 +02:00
|
|
|
|
[ "Fragment \""
|
|
|
|
|
, Text.unpack fragmentName
|
|
|
|
|
, "\" is specified on type \""
|
|
|
|
|
, Text.unpack typeCondition
|
|
|
|
|
, "\" which doesn't exist in the schema."
|
|
|
|
|
]
|
2020-09-05 10:00:58 +02:00
|
|
|
|
inlineError typeCondition = concat
|
|
|
|
|
[ "Inline fragment is specified on type \""
|
|
|
|
|
, Text.unpack typeCondition
|
|
|
|
|
, "\" which doesn't exist in the schema."
|
|
|
|
|
]
|
2020-09-07 22:01:49 +02:00
|
|
|
|
|
2020-09-14 07:49:33 +02:00
|
|
|
|
maybeToSeq :: forall a. Maybe a -> Seq a
|
|
|
|
|
maybeToSeq (Just x) = pure x
|
|
|
|
|
maybeToSeq Nothing = mempty
|
|
|
|
|
|
2020-09-07 22:01:49 +02:00
|
|
|
|
-- | Fragments can only be declared on unions, interfaces, and objects. They are
|
|
|
|
|
-- invalid on scalars. They can only be applied on non‐leaf fields. This rule
|
|
|
|
|
-- applies to both inline and named fragments.
|
|
|
|
|
fragmentsOnCompositeTypesRule :: forall m. Rule m
|
|
|
|
|
fragmentsOnCompositeTypesRule = FragmentRule definitionRule inlineRule
|
|
|
|
|
where
|
|
|
|
|
inlineRule (InlineFragment (Just typeCondition) _ _ location) =
|
|
|
|
|
check typeCondition location
|
2020-09-14 07:49:33 +02:00
|
|
|
|
inlineRule _ = lift mempty
|
2020-09-07 22:01:49 +02:00
|
|
|
|
definitionRule (FragmentDefinition _ typeCondition _ _ location) =
|
|
|
|
|
check typeCondition location
|
|
|
|
|
check typeCondition location = do
|
|
|
|
|
types' <- asks types
|
|
|
|
|
-- Skip unknown types, they are checked by another rule.
|
2020-09-14 07:49:33 +02:00
|
|
|
|
_ <- lift $ maybeToSeq $ HashMap.lookup typeCondition types'
|
2020-09-07 22:01:49 +02:00
|
|
|
|
case lookupTypeCondition typeCondition types' of
|
|
|
|
|
Nothing -> pure $ Error
|
|
|
|
|
{ message = errorMessage typeCondition
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
Just _ -> lift mempty
|
2020-09-07 22:01:49 +02:00
|
|
|
|
errorMessage typeCondition = concat
|
|
|
|
|
[ "Fragment cannot condition on non composite type \""
|
|
|
|
|
, Text.unpack typeCondition,
|
|
|
|
|
"\"."
|
|
|
|
|
]
|
2020-09-09 17:04:31 +02:00
|
|
|
|
|
|
|
|
|
-- | Defined fragments must be used within a document.
|
|
|
|
|
noUnusedFragmentsRule :: forall m. Rule m
|
2020-09-21 07:28:40 +02:00
|
|
|
|
noUnusedFragmentsRule = FragmentDefinitionRule $ \fragment -> do
|
|
|
|
|
let FragmentDefinition fragmentName _ _ _ location = fragment
|
|
|
|
|
in mapReaderT (checkFragmentName fragmentName location)
|
|
|
|
|
$ asks ast
|
|
|
|
|
>>= flip evalStateT HashSet.empty
|
|
|
|
|
. filterSelections evaluateSelection
|
|
|
|
|
. foldMap definitionSelections
|
2020-09-09 17:04:31 +02:00
|
|
|
|
where
|
2020-09-21 07:28:40 +02:00
|
|
|
|
checkFragmentName fragmentName location elements
|
|
|
|
|
| fragmentName `elem` elements = mempty
|
|
|
|
|
| otherwise = pure $ makeError fragmentName location
|
|
|
|
|
makeError fragName location = Error
|
|
|
|
|
{ message = errorMessage fragName
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
2020-09-09 17:04:31 +02:00
|
|
|
|
errorMessage fragName = concat
|
|
|
|
|
[ "Fragment \""
|
|
|
|
|
, Text.unpack fragName
|
|
|
|
|
, "\" is never used."
|
|
|
|
|
]
|
2020-09-21 07:28:40 +02:00
|
|
|
|
evaluateSelection selection
|
2020-09-09 17:04:31 +02:00
|
|
|
|
| FragmentSpreadSelection spreadSelection <- selection
|
2020-09-21 07:28:40 +02:00
|
|
|
|
, FragmentSpread spreadName _ _ <- spreadSelection =
|
|
|
|
|
lift $ pure spreadName
|
|
|
|
|
evaluateSelection _ = lift $ lift mempty
|
|
|
|
|
|
|
|
|
|
definitionSelections :: Definition -> SelectionSetOpt
|
|
|
|
|
definitionSelections (viewOperation -> Just operation)
|
|
|
|
|
| OperationDefinition _ _ _ _ selections _ <- operation = toList selections
|
|
|
|
|
| SelectionSet selections _ <- operation = toList selections
|
|
|
|
|
definitionSelections (viewFragment -> Just fragment)
|
|
|
|
|
| FragmentDefinition _ _ _ selections _ <- fragment = toList selections
|
|
|
|
|
definitionSelections _ = []
|
|
|
|
|
|
|
|
|
|
filterSelections :: Foldable t
|
|
|
|
|
=> forall a m
|
|
|
|
|
. (Selection -> ValidationState m a)
|
|
|
|
|
-> t Selection
|
|
|
|
|
-> ValidationState m a
|
|
|
|
|
filterSelections applyFilter selections
|
|
|
|
|
= (lift . lift) (Seq.fromList $ foldr evaluateSelection mempty selections)
|
|
|
|
|
>>= applyFilter
|
|
|
|
|
where
|
|
|
|
|
evaluateSelection selection accumulator
|
|
|
|
|
| FragmentSpreadSelection{} <- selection = selection : accumulator
|
2020-09-09 17:04:31 +02:00
|
|
|
|
| FieldSelection fieldSelection <- selection
|
2020-09-21 07:28:40 +02:00
|
|
|
|
, Field _ _ _ _ subselections _ <- fieldSelection =
|
|
|
|
|
selection : foldr evaluateSelection accumulator subselections
|
2020-09-09 17:04:31 +02:00
|
|
|
|
| InlineFragmentSelection inlineSelection <- selection
|
2020-09-21 07:28:40 +02:00
|
|
|
|
, InlineFragment _ _ subselections _ <- inlineSelection =
|
|
|
|
|
selection : foldr evaluateSelection accumulator subselections
|
2020-09-11 08:03:49 +02:00
|
|
|
|
|
|
|
|
|
-- | The graph of fragment spreads must not form any cycles including spreading
|
|
|
|
|
-- itself. Otherwise an operation could infinitely spread or infinitely execute
|
|
|
|
|
-- on cycles in the underlying data.
|
|
|
|
|
noFragmentCyclesRule :: forall m. Rule m
|
|
|
|
|
noFragmentCyclesRule = FragmentDefinitionRule $ \case
|
|
|
|
|
FragmentDefinition fragmentName _ _ selections location -> do
|
|
|
|
|
state <- evalStateT (collectFields selections)
|
|
|
|
|
(0, fragmentName)
|
|
|
|
|
let spreadPath = fst <$> sortBy (comparing snd) (HashMap.toList state)
|
|
|
|
|
case reverse spreadPath of
|
|
|
|
|
x : _ | x == fragmentName -> pure $ Error
|
|
|
|
|
{ message = concat
|
|
|
|
|
[ "Cannot spread fragment \""
|
|
|
|
|
, Text.unpack fragmentName
|
|
|
|
|
, "\" within itself (via "
|
|
|
|
|
, Text.unpack $ Text.intercalate " -> " $ fragmentName : spreadPath
|
|
|
|
|
, ")."
|
|
|
|
|
]
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
_ -> lift mempty
|
2020-09-11 08:03:49 +02:00
|
|
|
|
where
|
|
|
|
|
collectFields :: Traversable t
|
2020-09-21 07:28:40 +02:00
|
|
|
|
=> t Selection
|
2020-09-14 07:49:33 +02:00
|
|
|
|
-> StateT (Int, Name) (ReaderT (Validation m) Seq) (HashMap Name Int)
|
2020-09-11 08:03:49 +02:00
|
|
|
|
collectFields selectionSet = foldM forEach HashMap.empty selectionSet
|
|
|
|
|
forEach accumulator = \case
|
|
|
|
|
FieldSelection fieldSelection -> forField accumulator fieldSelection
|
|
|
|
|
InlineFragmentSelection fragmentSelection ->
|
|
|
|
|
forInline accumulator fragmentSelection
|
|
|
|
|
FragmentSpreadSelection fragmentSelection ->
|
|
|
|
|
forSpread accumulator fragmentSelection
|
|
|
|
|
forSpread accumulator (FragmentSpread fragmentName _ _) = do
|
|
|
|
|
firstFragmentName <- gets snd
|
|
|
|
|
modify $ first (+ 1)
|
|
|
|
|
lastIndex <- gets fst
|
|
|
|
|
let newAccumulator = HashMap.insert fragmentName lastIndex accumulator
|
2020-09-21 07:28:40 +02:00
|
|
|
|
let inVisitetFragment = HashMap.member fragmentName accumulator
|
2020-09-11 08:03:49 +02:00
|
|
|
|
if fragmentName == firstFragmentName || inVisitetFragment
|
|
|
|
|
then pure newAccumulator
|
|
|
|
|
else collectFromSpread fragmentName newAccumulator
|
|
|
|
|
forInline accumulator (InlineFragment _ _ selections _) =
|
|
|
|
|
(accumulator <>) <$> collectFields selections
|
|
|
|
|
forField accumulator (Field _ _ _ _ selections _) =
|
|
|
|
|
(accumulator <>) <$> collectFields selections
|
|
|
|
|
findFragmentDefinition n (ExecutableDefinition executableDefinition) Nothing
|
|
|
|
|
| DefinitionFragment fragmentDefinition <- executableDefinition
|
|
|
|
|
, FragmentDefinition fragmentName _ _ _ _ <- fragmentDefinition
|
|
|
|
|
, fragmentName == n = Just fragmentDefinition
|
|
|
|
|
findFragmentDefinition _ _ accumulator = accumulator
|
|
|
|
|
collectFromSpread _fragmentName accumulator = do
|
|
|
|
|
ast' <- lift $ asks ast
|
|
|
|
|
case foldr (findFragmentDefinition _fragmentName) Nothing ast' of
|
|
|
|
|
Nothing -> pure accumulator
|
|
|
|
|
Just (FragmentDefinition _ _ _ selections _) ->
|
|
|
|
|
(accumulator <>) <$> collectFields selections
|
2020-09-17 10:33:37 +02:00
|
|
|
|
|
|
|
|
|
-- | Fields and directives treat arguments as a mapping of argument name to
|
|
|
|
|
-- value. More than one argument with the same name in an argument set is
|
|
|
|
|
-- ambiguous and invalid.
|
|
|
|
|
uniqueArgumentNamesRule :: forall m. Rule m
|
|
|
|
|
uniqueArgumentNamesRule = ArgumentsRule fieldRule directiveRule
|
|
|
|
|
where
|
2020-09-18 07:32:58 +02:00
|
|
|
|
fieldRule (Field _ _ arguments _ _ _) =
|
2020-09-24 05:47:31 +02:00
|
|
|
|
lift $ filterDuplicates extract "argument" arguments
|
2020-09-18 07:32:58 +02:00
|
|
|
|
directiveRule (Directive _ arguments _) =
|
2020-09-24 05:47:31 +02:00
|
|
|
|
lift $ filterDuplicates extract "argument" arguments
|
2020-09-18 07:32:58 +02:00
|
|
|
|
extract (Argument argumentName _ location) = (argumentName, location)
|
|
|
|
|
|
|
|
|
|
-- | Directives are used to describe some metadata or behavioral change on the
|
|
|
|
|
-- definition they apply to. When more than one directive of the same name is
|
|
|
|
|
-- used, the expected metadata or behavior becomes ambiguous, therefore only one
|
|
|
|
|
-- of each directive is allowed per location.
|
|
|
|
|
uniqueDirectiveNamesRule :: forall m. Rule m
|
|
|
|
|
uniqueDirectiveNamesRule = DirectivesRule
|
2020-09-24 05:47:31 +02:00
|
|
|
|
$ lift . filterDuplicates extract "directive"
|
2020-09-18 07:32:58 +02:00
|
|
|
|
where
|
|
|
|
|
extract (Directive directiveName _ location) = (directiveName, location)
|
|
|
|
|
|
2020-09-24 05:47:31 +02:00
|
|
|
|
filterDuplicates :: (a -> (Text, Location)) -> String -> [a] -> Seq Error
|
|
|
|
|
filterDuplicates extract nodeType = Seq.fromList
|
2020-09-18 07:32:58 +02:00
|
|
|
|
. fmap makeError
|
|
|
|
|
. filter ((> 1) . length)
|
|
|
|
|
. groupBy equalByName
|
|
|
|
|
. sortOn getName
|
|
|
|
|
where
|
|
|
|
|
getName = fst . extract
|
|
|
|
|
equalByName lhs rhs = getName lhs == getName rhs
|
|
|
|
|
makeError directives = Error
|
|
|
|
|
{ message = makeMessage $ head directives
|
|
|
|
|
, locations = snd . extract <$> directives
|
2020-09-17 10:33:37 +02:00
|
|
|
|
}
|
2020-09-18 07:32:58 +02:00
|
|
|
|
makeMessage directive = concat
|
|
|
|
|
[ "There can be only one "
|
|
|
|
|
, nodeType
|
|
|
|
|
, " named \""
|
|
|
|
|
, Text.unpack $ fst $ extract directive
|
2020-09-17 10:33:37 +02:00
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-09-19 18:18:26 +02:00
|
|
|
|
|
|
|
|
|
-- | If any operation defines more than one variable with the same name, it is
|
|
|
|
|
-- ambiguous and invalid. It is invalid even if the type of the duplicate
|
|
|
|
|
-- variable is the same.
|
|
|
|
|
uniqueVariableNamesRule :: forall m. Rule m
|
|
|
|
|
uniqueVariableNamesRule = VariablesRule
|
2020-09-24 05:47:31 +02:00
|
|
|
|
$ lift . filterDuplicates extract "variable"
|
2020-09-19 18:18:26 +02:00
|
|
|
|
where
|
|
|
|
|
extract (VariableDefinition variableName _ _ location) =
|
|
|
|
|
(variableName, location)
|
2020-09-20 06:59:27 +02:00
|
|
|
|
|
|
|
|
|
-- | Variables can only be input types. Objects, unions and interfaces cannot be
|
|
|
|
|
-- used as inputs.
|
|
|
|
|
variablesAreInputTypesRule :: forall m. Rule m
|
|
|
|
|
variablesAreInputTypesRule = VariablesRule
|
|
|
|
|
$ (traverse check . Seq.fromList) >=> lift
|
|
|
|
|
where
|
|
|
|
|
check (VariableDefinition name typeName _ location)
|
|
|
|
|
= asks types
|
|
|
|
|
>>= lift
|
|
|
|
|
. maybe (makeError name typeName location) (const mempty)
|
|
|
|
|
. lookupInputType typeName
|
|
|
|
|
makeError name typeName location = pure $ Error
|
|
|
|
|
{ message = concat
|
|
|
|
|
[ "Variable \"$"
|
|
|
|
|
, Text.unpack name
|
|
|
|
|
, "\" cannot be non-input type \""
|
|
|
|
|
, Text.unpack $ getTypeName typeName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
|
|
|
|
getTypeName (TypeNamed name) = name
|
|
|
|
|
getTypeName (TypeList name) = getTypeName name
|
|
|
|
|
getTypeName (TypeNonNull (NonNullTypeNamed nonNull)) = nonNull
|
|
|
|
|
getTypeName (TypeNonNull (NonNullTypeList nonNull)) = getTypeName nonNull
|
2020-09-21 07:28:40 +02:00
|
|
|
|
|
|
|
|
|
-- | Variables are scoped on a per‐operation basis. That means that any variable
|
|
|
|
|
-- used within the context of an operation must be defined at the top level of
|
|
|
|
|
-- that operation.
|
|
|
|
|
noUndefinedVariablesRule :: forall m. Rule m
|
2020-09-22 04:42:25 +02:00
|
|
|
|
noUndefinedVariablesRule =
|
|
|
|
|
variableUsageDifference (flip HashMap.difference) errorMessage
|
|
|
|
|
where
|
|
|
|
|
errorMessage Nothing variableName = concat
|
|
|
|
|
[ "Variable \"$"
|
|
|
|
|
, Text.unpack variableName
|
|
|
|
|
, "\" is not defined."
|
|
|
|
|
]
|
|
|
|
|
errorMessage (Just operationName) variableName = concat
|
|
|
|
|
[ "Variable \"$"
|
|
|
|
|
, Text.unpack variableName
|
|
|
|
|
, "\" is not defined by operation \""
|
|
|
|
|
, Text.unpack operationName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
variableUsageDifference :: forall m
|
|
|
|
|
. (HashMap Name [Location] -> HashMap Name [Location] -> HashMap Name [Location])
|
|
|
|
|
-> (Maybe Name -> Name -> String)
|
|
|
|
|
-> Rule m
|
|
|
|
|
variableUsageDifference difference errorMessage = OperationDefinitionRule $ \case
|
2020-09-21 07:28:40 +02:00
|
|
|
|
SelectionSet _ _ -> lift mempty
|
|
|
|
|
OperationDefinition _ operationName variables _ selections _ ->
|
|
|
|
|
let variableNames = HashMap.fromList $ getVariableName <$> variables
|
|
|
|
|
in mapReaderT (readerMapper operationName variableNames)
|
|
|
|
|
$ flip evalStateT HashSet.empty
|
|
|
|
|
$ filterSelections'
|
|
|
|
|
$ toList selections
|
|
|
|
|
where
|
|
|
|
|
readerMapper operationName variableNames' = Seq.fromList
|
|
|
|
|
. fmap (makeError operationName)
|
|
|
|
|
. HashMap.toList
|
2020-09-22 04:42:25 +02:00
|
|
|
|
. difference variableNames'
|
2020-09-21 07:28:40 +02:00
|
|
|
|
. HashMap.fromListWith (++)
|
|
|
|
|
. toList
|
2020-09-22 04:42:25 +02:00
|
|
|
|
getVariableName (VariableDefinition variableName _ _ location) =
|
|
|
|
|
(variableName, [location])
|
2020-09-21 07:28:40 +02:00
|
|
|
|
filterSelections' :: Foldable t
|
|
|
|
|
=> t Selection
|
|
|
|
|
-> ValidationState m (Name, [Location])
|
|
|
|
|
filterSelections' = filterSelections variableFilter
|
|
|
|
|
variableFilter :: Selection -> ValidationState m (Name, [Location])
|
|
|
|
|
variableFilter (InlineFragmentSelection inline)
|
|
|
|
|
| InlineFragment _ directives _ _ <- inline =
|
|
|
|
|
lift $ lift $ mapDirectives directives
|
|
|
|
|
variableFilter (FieldSelection fieldSelection)
|
|
|
|
|
| Field _ _ arguments directives _ _ <- fieldSelection =
|
|
|
|
|
lift $ lift $ mapArguments arguments <> mapDirectives directives
|
|
|
|
|
variableFilter (FragmentSpreadSelection spread)
|
|
|
|
|
| FragmentSpread fragmentName _ _ <- spread = do
|
|
|
|
|
definitions <- lift $ asks ast
|
|
|
|
|
visited <- gets (HashSet.member fragmentName)
|
|
|
|
|
modify (HashSet.insert fragmentName)
|
|
|
|
|
case find (isSpreadTarget fragmentName) definitions of
|
|
|
|
|
Just (viewFragment -> Just fragmentDefinition)
|
|
|
|
|
| not visited -> diveIntoSpread fragmentDefinition
|
|
|
|
|
_ -> lift $ lift mempty
|
|
|
|
|
diveIntoSpread (FragmentDefinition _ _ directives selections _)
|
|
|
|
|
= filterSelections' selections
|
|
|
|
|
>>= lift . mapReaderT (<> mapDirectives directives) . pure
|
|
|
|
|
findDirectiveVariables (Directive _ arguments _) = mapArguments arguments
|
|
|
|
|
mapArguments = Seq.fromList . mapMaybe findArgumentVariables
|
|
|
|
|
mapDirectives = foldMap findDirectiveVariables
|
|
|
|
|
findArgumentVariables (Argument _ (Node (Variable value) location) _) =
|
|
|
|
|
Just (value, [location])
|
|
|
|
|
findArgumentVariables _ = Nothing
|
|
|
|
|
makeError operationName (variableName, locations') = Error
|
|
|
|
|
{ message = errorMessage operationName variableName
|
|
|
|
|
, locations = locations'
|
|
|
|
|
}
|
2020-09-22 04:42:25 +02:00
|
|
|
|
|
|
|
|
|
-- | All variables defined by an operation must be used in that operation or a
|
|
|
|
|
-- fragment transitively included by that operation. Unused variables cause a
|
|
|
|
|
-- validation error.
|
|
|
|
|
noUnusedVariablesRule :: forall m. Rule m
|
|
|
|
|
noUnusedVariablesRule = variableUsageDifference HashMap.difference errorMessage
|
|
|
|
|
where
|
2020-09-21 07:28:40 +02:00
|
|
|
|
errorMessage Nothing variableName = concat
|
|
|
|
|
[ "Variable \"$"
|
|
|
|
|
, Text.unpack variableName
|
2020-09-22 04:42:25 +02:00
|
|
|
|
, "\" is never used."
|
2020-09-21 07:28:40 +02:00
|
|
|
|
]
|
|
|
|
|
errorMessage (Just operationName) variableName = concat
|
|
|
|
|
[ "Variable \"$"
|
|
|
|
|
, Text.unpack variableName
|
2020-09-22 04:42:25 +02:00
|
|
|
|
, "\" is never used in operation \""
|
2020-09-21 07:28:40 +02:00
|
|
|
|
, Text.unpack operationName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-09-24 05:47:31 +02:00
|
|
|
|
|
|
|
|
|
-- | Input objects must not contain more than one field of the same name,
|
|
|
|
|
-- otherwise an ambiguity would exist which includes an ignored portion of
|
|
|
|
|
-- syntax.
|
|
|
|
|
uniqueInputFieldNamesRule :: forall m. Rule m
|
|
|
|
|
uniqueInputFieldNamesRule = ValueRule (lift . go) (lift . constGo)
|
|
|
|
|
where
|
|
|
|
|
go (Object fields) = foldMap (objectField go) fields
|
|
|
|
|
<> filterFieldDuplicates fields
|
|
|
|
|
go (List values) = foldMap go values
|
|
|
|
|
go _ = mempty
|
|
|
|
|
objectField go' (ObjectField _ fieldValue _) = go' fieldValue
|
|
|
|
|
filterFieldDuplicates fields =
|
|
|
|
|
filterDuplicates getFieldName "input field" fields
|
|
|
|
|
getFieldName (ObjectField fieldName _ location) = (fieldName, location)
|
|
|
|
|
constGo (ConstObject fields) = foldMap (objectField constGo) fields
|
|
|
|
|
<> filterFieldDuplicates fields
|
|
|
|
|
constGo (ConstList values) = foldMap constGo values
|
|
|
|
|
constGo _ = mempty
|
2020-09-25 21:57:25 +02:00
|
|
|
|
|
|
|
|
|
-- | The target field of a field selection must be defined on the scoped type of
|
|
|
|
|
-- the selection set. There are no limitations on alias names.
|
|
|
|
|
fieldsOnCorrectTypeRule :: forall m. Rule m
|
|
|
|
|
fieldsOnCorrectTypeRule = SelectionRule go
|
|
|
|
|
where
|
|
|
|
|
go (Just objectType) (FieldSelection fieldSelection) =
|
|
|
|
|
fieldRule objectType fieldSelection
|
|
|
|
|
go _ _ = lift mempty
|
|
|
|
|
fieldRule objectType (Field _ fieldName _ _ _ location)
|
|
|
|
|
| isJust (lookupTypeField fieldName objectType) = lift mempty
|
|
|
|
|
| otherwise = pure $ Error
|
|
|
|
|
{ message = errorMessage fieldName objectType
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
|
|
|
|
errorMessage fieldName objectType = concat
|
|
|
|
|
[ "Cannot query field \""
|
|
|
|
|
, Text.unpack fieldName
|
|
|
|
|
, "\" on type \""
|
|
|
|
|
, Text.unpack $ outputTypeName objectType
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
|
|
|
|
outputTypeName (Out.ObjectBaseType (Out.ObjectType typeName _ _ _)) =
|
|
|
|
|
typeName
|
|
|
|
|
outputTypeName (Out.InterfaceBaseType (Out.InterfaceType typeName _ _ _)) =
|
|
|
|
|
typeName
|
|
|
|
|
outputTypeName (Out.UnionBaseType (Out.UnionType typeName _ _)) =
|
|
|
|
|
typeName
|
|
|
|
|
outputTypeName (Out.ScalarBaseType (Definition.ScalarType typeName _)) =
|
|
|
|
|
typeName
|
|
|
|
|
outputTypeName (Out.EnumBaseType (Definition.EnumType typeName _ _)) =
|
|
|
|
|
typeName
|
|
|
|
|
outputTypeName (Out.ListBaseType wrappedType) = outputTypeName wrappedType
|