Skip to content

Commit

Permalink
Feature/validate single top level field on subscription (#428)
Browse files Browse the repository at this point in the history
* implement validation

* subscription

* liftWS

* single selection

* add tests

* withScope

* fix tests

* city

* update

* update changelog

* fix wrong field name on missing argument

* update changelog
nalchevanidze authored Apr 25, 2020
1 parent edd66a1 commit 680eee1
Showing 16 changed files with 125 additions and 24 deletions.
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -36,6 +36,8 @@
websockets and http app do not have to be on the same server.
e.g. you can pass events between servers with webhooks.

- subscription can select only one top level field (based on the GraphQL specification).

### New features

- Instead of rejecting conflicting selections, they are merged (based on the GraphQL specification).
@@ -48,6 +50,7 @@
- changes to internal types
- fixed validation of apollo websockets requests


## 0.10.0 - 07.01.2020

### Breaking Changes
6 changes: 5 additions & 1 deletion morpheus-graphql.cabal
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ cabal-version: 1.12
--
-- see: https://github.com/sol/hpack
--
-- hash: 32c2b16f72c001cbe4fe83808b82db4070624d4ec66a85990176737bc0ceee8c
-- hash: daf2c440da655b06dcebb11fdd64060a04fee75fd9588a437428db79c05c7b4d

name: morpheus-graphql
version: 0.10.1
@@ -89,6 +89,8 @@ data-files:
test/Feature/Holistic/selection/mergeSelection/query.gql
test/Feature/Holistic/selection/mergeUnionSelection/query.gql
test/Feature/Holistic/selection/mustHaveSubFields/query.gql
test/Feature/Holistic/selection/subscription/singleTopLevelField/fail/query.gql
test/Feature/Holistic/selection/subscription/singleTopLevelField/failAnonymous/query.gql
test/Feature/Holistic/selection/unknownField/query.gql
test/Feature/Input/Enum/decode2Con/query.gql
test/Feature/Input/Enum/decode3Con/query.gql
@@ -223,6 +225,8 @@ data-files:
test/Feature/Holistic/selection/mergeSelection/response.json
test/Feature/Holistic/selection/mergeUnionSelection/response.json
test/Feature/Holistic/selection/mustHaveSubFields/response.json
test/Feature/Holistic/selection/subscription/singleTopLevelField/fail/response.json
test/Feature/Holistic/selection/subscription/singleTopLevelField/failAnonymous/response.json
test/Feature/Holistic/selection/unknownField/response.json
test/Feature/Input/Enum/cases.json
test/Feature/Input/Enum/decode2Con/response.json
3 changes: 1 addition & 2 deletions src/Data/Morpheus/Types/Internal/Subscription/Stream.hs
Original file line number Diff line number Diff line change
@@ -22,7 +22,6 @@ module Data.Morpheus.Types.Internal.Subscription.Stream
)
where

import Data.Semigroup ( (<>) )
import Data.Foldable ( traverse_ )
import Data.ByteString.Lazy.Char8 (ByteString)

@@ -137,7 +136,7 @@ handleWSRequest
handleWSRequest gqlApp clientId = handle . apolloFormat
where
--handle :: Applicative m => Validation ApolloAction -> Stream WS e m
handle = either (liftWS . Left . ("Error: " <>)) handleAction
handle = either (liftWS . Left) handleAction
--------------------------------------------------
-- handleAction :: ApolloAction -> Stream WS e m
handleAction ConnectionInit = liftWS $ Right []
6 changes: 3 additions & 3 deletions src/Data/Morpheus/Types/Internal/Validation.hs
Original file line number Diff line number Diff line change
@@ -308,10 +308,10 @@ setGlobalContext
-> Validator c a
setGlobalContext f = Validator . withReaderT ( \(x,y) -> (f x,y)) . _runValidator

withScope :: Name -> Position -> Validator ctx a -> Validator ctx a
withScope scopeTypeName scopePosition = setGlobalContext update
withScope :: Name -> Ref -> Validator ctx a -> Validator ctx a
withScope scopeTypeName (Ref scopeSelectionName scopePosition) = setGlobalContext update
where
update ctx = ctx { scopeTypeName , scopePosition }
update ctx = ctx { scopeTypeName , scopePosition , scopeSelectionName}

withScopePosition :: Position -> Validator ctx a -> Validator ctx a
withScopePosition scopePosition = setGlobalContext update
4 changes: 2 additions & 2 deletions src/Data/Morpheus/Types/Internal/Validation/Error.hs
Original file line number Diff line number Diff line change
@@ -100,12 +100,12 @@ class MissingRequired c ctx where

instance MissingRequired (Arguments s) ctx where
missingRequired
Context { scopePosition , scopeTypeName }
Context { scopePosition , scopeSelectionName }
_
Ref { refName } _
= GQLError
{ message
= "Field \"" <> scopeTypeName <> "\" argument \""
= "Field \"" <> scopeSelectionName <> "\" argument \""
<> refName <> "\" is required but not provided."
, locations = [scopePosition]
}
11 changes: 6 additions & 5 deletions src/Data/Morpheus/Types/Internal/Validation/Validator.hs
Original file line number Diff line number Diff line change
@@ -96,11 +96,12 @@ renderSource (SourceVariable Variable { variableName })
= "Variable \"$" <> variableName <>"\" got invalid value. "

data Context = Context
{ schema :: Schema
, fragments :: Fragments
, scopePosition :: Position
, scopeTypeName :: Name
, operationName :: Maybe Name
{ schema :: Schema
, fragments :: Fragments
, scopePosition :: Position
, scopeTypeName :: Name
, operationName :: Maybe Name
, scopeSelectionName :: Name
} deriving (Show)

data InputContext
37 changes: 33 additions & 4 deletions src/Data/Morpheus/Validation/Query/Selection.hs
Original file line number Diff line number Diff line change
@@ -36,13 +36,17 @@ import Data.Morpheus.Types.Internal.AST
, Arguments
, isEntNode
, getOperationDataType
, GQLError(..)
, OperationType(..)
)
import Data.Morpheus.Types.Internal.AST.MergeSet
( concatTraverse )
import Data.Morpheus.Types.Internal.Operation
( empty
, singleton
, Failure(..)
, keyOf
, toList
)
import Data.Morpheus.Types.Internal.Validation
( SelectionValidator
@@ -75,6 +79,27 @@ getOperationObject operation = do
<> typeName
<> "\" must be an Object"


selectionsWitoutTypename :: SelectionSet VALID -> [Selection VALID]
selectionsWitoutTypename = filter (("__typename" /=) . keyOf) . toList

singleTopLevelSelection :: Operation RAW -> SelectionSet VALID -> SelectionValidator ()
singleTopLevelSelection Operation { operationType = Subscription , operationName } selSet =
case selectionsWitoutTypename selSet of
(_:xs) | not (null xs) -> failure $ map (singleTopLevelSelectionError operationName) xs
_ -> pure ()
singleTopLevelSelection _ _ = pure ()

singleTopLevelSelectionError :: Maybe Name -> Selection VALID -> GQLError
singleTopLevelSelectionError name Selection { selectionPosition } = GQLError
{ message
= subscriptionName <> " must select "
<> "only one top level field."
, locations = [selectionPosition]
}
where
subscriptionName = maybe "Anonymous Subscription" (("Subscription \"" <>) . (<> "\"")) name

validateOperation
:: Operation RAW
-> SelectionValidator (Operation VALID)
@@ -88,6 +113,7 @@ validateOperation
= do
typeDef <- getOperationObject rawOperation
selection <- validateSelectionSet typeDef operationSelection
singleTopLevelSelection rawOperation selection
pure $ Operation
{ operationName
, operationType
@@ -96,6 +122,7 @@ validateOperation
, operationPosition
}


validateSelectionSet
:: TypeDef -> SelectionSet RAW -> SelectionValidator (SelectionSet VALID)
validateSelectionSet dataType@(typeName,fieldsDef) =
@@ -111,10 +138,12 @@ validateSelectionSet dataType@(typeName,fieldsDef) =
, selectionPosition
}
= withScope
typeName
selectionPosition $
validateSelectionContent selectionContent
typeName
currentSelectionRef
$ validateSelectionContent
selectionContent
where
currentSelectionRef = Ref selectionName selectionPosition
commonValidation :: SelectionValidator (TypeDefinition, Arguments VALID)
commonValidation = do
(fieldDef :: FieldDefinition) <- selectKnown (Ref selectionName selectionPosition) fieldsDef
@@ -145,7 +174,7 @@ validateSelectionSet dataType@(typeName,fieldsDef) =
validateSelectionContent (SelectionSet rawSelectionSet)
= do
(TypeDefinition { typeName = name , typeContent}, validArgs) <- commonValidation
selContent <- withScope name selectionPosition $ validateByTypeContent name typeContent
selContent <- withScope name currentSelectionRef $ validateByTypeContent name typeContent
pure $ singleton $ sel { selectionArguments = validArgs, selectionContent = selContent }
where
validateByTypeContent :: Name -> TypeContent -> SelectionValidator (SelectionContent VALID)
1 change: 1 addition & 0 deletions src/Data/Morpheus/Validation/Query/Validation.hs
Original file line number Diff line number Diff line change
@@ -62,6 +62,7 @@ validateRequest
{ schema
, fragments
, scopeTypeName = "Root"
, scopeSelectionName = "Root"
, scopePosition = operationPosition
, operationName
}
13 changes: 7 additions & 6 deletions test/Feature/Holistic/API.hs
Original file line number Diff line number Diff line change
@@ -54,8 +54,6 @@ type EVENT = Event Channel ()

importGQLDocument "test/Feature/Holistic/schema.gql"



alwaysFail :: IO (Either String a)
alwaysFail = pure $ Left "fail with Either"

@@ -68,7 +66,10 @@ rootResolver = GQLRootResolver
, fail2 = failRes "fail with failRes"
}
, mutationResolver = Mutation { createUser = const user }
, subscriptionResolver = Subscription { newUser = subscribe [Channel] (pure $ const user)}
, subscriptionResolver = Subscription
{ newUser = subscribe [Channel] (pure $ const user)
, newAddress = subscribe [Channel] (pure resolveAddress)
}
}
where
user :: Applicative m => m (User m)
@@ -78,9 +79,9 @@ rootResolver = GQLRootResolver
, office = resolveAddress
, friend = pure Nothing
}
where
resolveAddress :: Applicative m => a -> m (Address m)
resolveAddress _ = pure Address { city = pure ""
-----------------------------------------------------
resolveAddress :: Applicative m => a -> m (Address m)
resolveAddress _ = pure Address { city = pure ""
, houseNumber = pure 0
, street = const $ pure Nothing
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"errors": [
{
"message": "Field \"User\" argument \"coordinates\" is required but not provided.",
"message": "Field \"address\" argument \"coordinates\" is required but not provided.",
"locations": [
{
"line": 3,
8 changes: 8 additions & 0 deletions test/Feature/Holistic/cases.json
Original file line number Diff line number Diff line change
@@ -258,5 +258,13 @@
{
"path": "selection/mergeSelection",
"description": "merge selection on same fields"
},
{
"path": "selection/subscription/singleTopLevelField/fail",
"description": "fail if subscription selects more then one top level field."
},
{
"path": "selection/subscription/singleTopLevelField/failAnonymous",
"description": "fail if subscription selects more then one top level field. (for anonymous subscription)"
}
]
1 change: 1 addition & 0 deletions test/Feature/Holistic/schema.gql
Original file line number Diff line number Diff line change
@@ -102,4 +102,5 @@ type Mutation {

type Subscription {
newUser: User!
newAddress: Address!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
subscription SomeTestSubscription {
newUser {
name
}
newAddress {
houseNumber
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"errors": [
{
"message": "Subscription \"SomeTestSubscription\" must select only one top level field.",
"locations": [
{
"line": 5,
"column": 14
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
subscription {
newUser {
name
}
newAddress {
houseNumber
}
address2: newAddress {
houseNumber
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"errors": [
{
"message": "Anonymous Subscription must select only one top level field.",
"locations": [
{
"line": 5,
"column": 14
}
]
},
{
"message": "Anonymous Subscription must select only one top level field.",
"locations": [
{
"line": 8,
"column": 24
}
]
}
]
}

0 comments on commit 680eee1

Please sign in to comment.