{-# LANGUAGE GADTs #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE MultiParamTypeClasses #-} module Data.Morpheus.Validation.Query.Selection ( validateOperation ) where import Data.Semigroup ( (<>) ) -- MORPHEUS import Data.Morpheus.Error.Selection ( hasNoSubfields , subfieldsNotSelected ) import Data.Morpheus.Types.Internal.AST ( Selection(..) , SelectionContent(..) , Fragment(..) , SelectionSet , FieldDefinition(..) , FieldsDefinition(..) , TypeContent(..) , TypeDefinition(..) , Operation(..) , Ref(..) , Name , RAW , VALID , 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 , askFieldType , selectKnown , withScope , askSchema ) import Data.Morpheus.Validation.Query.UnionSelection (validateUnionSelection) import Data.Morpheus.Validation.Query.Arguments ( validateArguments ) import Data.Morpheus.Validation.Query.Fragment ( castFragmentType , resolveSpread ) type TypeDef = (Name, FieldsDefinition) getOperationObject :: Operation a -> SelectionValidator (Name, FieldsDefinition) getOperationObject operation = do dt <- askSchema >>= getOperationDataType operation case dt of TypeDefinition { typeContent = DataObject { objectFields }, typeName } -> pure (typeName, objectFields) TypeDefinition { typeName } -> failure $ "Type Mismatch: operation \"" <> 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) validateOperation rawOperation@Operation { operationName , operationType , operationSelection , operationPosition } = do typeDef <- getOperationObject rawOperation selection <- validateSelectionSet typeDef operationSelection singleTopLevelSelection rawOperation selection pure $ Operation { operationName , operationType , operationArguments = empty , operationSelection = selection , operationPosition } validateSelectionSet :: TypeDef -> SelectionSet RAW -> SelectionValidator (SelectionSet VALID) validateSelectionSet dataType@(typeName,fieldsDef) = concatTraverse validateSelection where -- validate single selection: InlineFragments and Spreads will Be resolved and included in SelectionSet validateSelection :: Selection RAW -> SelectionValidator (SelectionSet VALID) validateSelection sel@Selection { selectionName , selectionArguments , selectionContent , selectionPosition } = withScope typeName currentSelectionRef $ validateSelectionContent selectionContent where currentSelectionRef = Ref selectionName selectionPosition commonValidation :: SelectionValidator (TypeDefinition, Arguments VALID) commonValidation = do (fieldDef :: FieldDefinition) <- selectKnown (Ref selectionName selectionPosition) fieldsDef -- validate field Argument ----- arguments <- validateArguments fieldDef selectionArguments -- check field Type existence ----- (typeDef :: TypeDefinition) <- askFieldType fieldDef pure (typeDef, arguments) ----------------------------------------------------------------------------------- validateSelectionContent :: SelectionContent RAW -> SelectionValidator (SelectionSet VALID) validateSelectionContent SelectionField | null selectionArguments && selectionName == "__typename" = pure $ singleton $ sel { selectionArguments = empty, selectionContent = SelectionField } | otherwise = do (datatype, validArgs) <- commonValidation isLeaf datatype pure $ singleton $ sel { selectionArguments = validArgs, selectionContent = SelectionField } where ------------------------------------------------------------ isLeaf :: TypeDefinition -> SelectionValidator () isLeaf TypeDefinition { typeName = typename, typeContent } | isEntNode typeContent = pure () | otherwise = failure $ subfieldsNotSelected selectionName typename selectionPosition ----- SelectionSet validateSelectionContent (SelectionSet rawSelectionSet) = do (TypeDefinition { typeName = name , typeContent}, validArgs) <- commonValidation selContent <- withScope name currentSelectionRef $ validateByTypeContent name typeContent pure $ singleton $ sel { selectionArguments = validArgs, selectionContent = selContent } where validateByTypeContent :: Name -> TypeContent -> SelectionValidator (SelectionContent VALID) -- Validate UnionSelection validateByTypeContent _ DataUnion { unionMembers } = validateUnionSelection validateSelectionSet rawSelectionSet unionMembers -- Validate Regular selection set validateByTypeContent typename DataObject { objectFields } = SelectionSet <$> validateSelectionSet (typename, objectFields) rawSelectionSet validateByTypeContent typename _ = failure $ hasNoSubfields (Ref selectionName selectionPosition) typename validateSelection (Spread ref) = resolveSpread [typeName] ref >>= validateFragment validateSelection (InlineFragment fragment') = castFragmentType Nothing (fragmentPosition fragment') [typeName] fragment' >>= validateFragment -------------------------------------------------------------------------------- validateFragment Fragment { fragmentSelection } = validateSelectionSet dataType fragmentSelection