{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} module Hasura.GraphQL.Validate.Field ( ArgsMap , Field(..) , SelSet , denormSelSet ) where import Hasura.Prelude import qualified Data.Aeson as J import qualified Data.Aeson.Casing as J import qualified Data.Aeson.TH as J import qualified Data.HashMap.Strict as Map import qualified Data.List as L import qualified Data.Sequence as Seq import qualified Data.Text as T import qualified Hasura.GraphQL.NonEmptySeq as NE import qualified Hasura.GraphQL.OrderedMap as OMap import qualified Language.GraphQL.Draft.Syntax as G import Hasura.GraphQL.Validate.Context import Hasura.GraphQL.Validate.InputValue import Hasura.GraphQL.Validate.Types import Hasura.RQL.Types -- data ScalarInfo -- = SIBuiltin !GBuiltin -- | SICustom !PGColType -- deriving (Show, Eq) -- data GBuiltin -- = GInt -- | GFloat -- | GBoolean -- | GString -- deriving (Show, Eq) data TypedOperation = TypedOperation { _toType :: !G.OperationType , _toName :: !(Maybe G.Name) , _toSelectionSet :: ![Field] } deriving (Show, Eq) type ArgsMap = Map.HashMap G.Name AnnGValue type SelSet = Seq.Seq Field data Field = Field { _fAlias :: !G.Alias , _fName :: !G.Name , _fType :: !G.NamedType , _fArguments :: !ArgsMap , _fSelSet :: !SelSet } deriving (Eq, Show) $(J.deriveToJSON (J.aesonDrop 2 J.camelCase){J.omitNothingFields=True} ''Field ) -- newtype FieldMapAlias -- = FieldMapAlias -- { unFieldMapAlias :: Map.HashMap G.Alias (FieldG FieldMapAlias) -- } deriving (Show, Eq) -- newtype FieldMapName -- = FieldMapName -- { unFieldMapName :: Map.HashMap G.Name (NE.NonEmpty (FieldG FieldMapName)) -- } deriving (Show, Eq) -- type Field = FieldG FieldMapAlias -- type FieldGrouped = FieldG FieldMapName -- toFieldGrouped :: Field -> FieldGrouped -- toFieldGrouped = -- fmap groupFields -- where -- groupFields m = -- FieldMapName $ groupTuples $ -- flip map (Map.elems $ unFieldMapAlias m) $ \fld -> -- (_fName fld, toFieldGrouped fld) data FieldGroupSrc = FGSFragSprd !G.Name | FGSInlnFrag deriving (Show, Eq) data FieldGroup = FieldGroup { _fgSource :: !FieldGroupSrc , _fgFields :: !(Seq.Seq Field) } deriving (Show, Eq) -- data GLoc -- = GLoc -- { _glLine :: !Int -- , _glColumn :: !Int -- } deriving (Show, Eq) -- data GErr -- = GErr -- { _geMessage :: !Text -- , _geLocations :: ![GLoc] -- } deriving (Show, Eq) -- throwGE :: (MonadError QErr m) => Text -> m a -- throwGE msg = throwError $ QErr msg [] withDirectives :: ( MonadReader ValidationCtx m , MonadError QErr m) => [G.Directive] -> m a -> m (Maybe a) withDirectives dirs act = -- TODO, use the directives Just <$> act denormSel :: ( MonadReader ValidationCtx m , MonadError QErr m) => [G.Name] -- visited fragments -> ObjTyInfo -- parent type info -> G.Selection -> m (Maybe (Either Field FieldGroup)) denormSel visFrags parObjTyInfo sel = case sel of G.SelectionField fld -> do fldInfo <- getFieldInfo parObjTyInfo $ G._fName fld fmap Left <$> denormFld visFrags fldInfo fld G.SelectionFragmentSpread fragSprd -> fmap Right <$> denormFrag visFrags parTy fragSprd G.SelectionInlineFragment inlnFrag -> fmap Right <$> denormInlnFrag visFrags parObjTyInfo inlnFrag where parTy = _otiName parObjTyInfo processArgs :: ( MonadReader ValidationCtx m , MonadError QErr m) => ObjFldInfo -> [G.Argument] -> m (Map.HashMap G.Name AnnGValue) processArgs (ObjFldInfo _ fldName fldParams fldTy) argsL = do args <- onLeft (mkMapWith G._aName argsL) $ \dups -> throwVE $ "the following arguments are defined more than once: " <> showNames dups let requiredParams = Map.filter (G.isNotNull . _iviType) fldParams inpArgs <- forM args $ \(G.Argument argName argVal) -> do argTy <- getArgTy argName validateInputValue valueParser argTy argVal forM_ requiredParams $ \argDef -> do let param = _iviName argDef onNothing (Map.lookup param inpArgs) $ throwVE $ mconcat [ "the required argument ", showName param , " is missing on field ", showName fldName ] return inpArgs where getArgTy argName = onNothing (_iviType <$> Map.lookup argName fldParams) $ throwVE $ "no such argument " <> showName argName <> " defined on " <> "field " <> showName fldName denormFld :: ( MonadReader ValidationCtx m , MonadError QErr m) => [G.Name] -- visited fragments -> ObjFldInfo -> G.Field -> m (Maybe Field) denormFld visFrags fldInfo (G.Field aliasM name args dirs selSet) = do let fldTy = _fiTy fldInfo fldBaseTy = getBaseTy fldTy fldTyInfo <- getTyInfo fldBaseTy argMap <- processArgs fldInfo args fields <- case (fldTyInfo, selSet) of (TIObj _, []) -> throwVE $ "field " <> showName name <> " of type " <> G.showGT fldTy <> " must have a selection of subfields" (TIObj fldObjTyInfo, _) -> denormSelSet visFrags fldObjTyInfo selSet (TIScalar _, []) -> return Seq.empty (TIEnum _, []) -> return Seq.empty (TIInpObj _, _) -> throwVE $ "internal error: unexpected input type for field: " <> showName name -- when scalar/enum and no empty set (_, _) -> throwVE $ "field " <> showName name <> " must not have a " <> "selection since type " <> G.showGT fldTy <> " has no subfields" withDirectives dirs $ return $ Field (fromMaybe (G.Alias name) aliasM) name fldBaseTy argMap fields denormInlnFrag :: ( MonadReader ValidationCtx m , MonadError QErr m) => [G.Name] -- visited fragments -> ObjTyInfo -- type information of the field -> G.InlineFragment -> m (Maybe FieldGroup) denormInlnFrag visFrags fldTyInfo inlnFrag = do let fldTy = _otiName fldTyInfo let fragTy = fromMaybe fldTy tyM when (fldTy /= fragTy) $ throwVE $ "inline fragment is expected on type " <> showNamedTy fldTy <> " but found " <> showNamedTy fragTy withDirectives directives $ fmap (FieldGroup FGSInlnFrag) $ denormSelSet visFrags fldTyInfo selSet where G.InlineFragment tyM directives selSet = inlnFrag denormSelSet :: ( MonadReader ValidationCtx m , MonadError QErr m) => [G.Name] -- visited fragments -> ObjTyInfo -> G.SelectionSet -> m (Seq.Seq Field) denormSelSet visFrags fldTyInfo selSet = do resFlds <- catMaybes <$> mapM (denormSel visFrags fldTyInfo) selSet mergeFields $ foldl' flatten Seq.empty resFlds where flatten s (Left fld) = s Seq.|> fld flatten s (Right (FieldGroup _ flds)) = s Seq.>< flds mergeFields :: ( MonadReader ValidationCtx m , MonadError QErr m) => Seq.Seq Field -> m (Seq.Seq Field) mergeFields flds = fmap Seq.fromList $ forM fldGroups $ \fieldGroup -> do newFld <- checkMergeability fieldGroup childFields <- mergeFields $ foldl' (\l f -> l Seq.>< _fSelSet f) Seq.empty $ NE.toSeq fieldGroup return $ newFld {_fSelSet = childFields} where fldGroups = OMap.elems $ OMap.groupListWith _fAlias flds -- can a group be merged? checkMergeability fldGroup = do let groupedFlds = toList $ NE.toSeq fldGroup fldNames = L.nub $ map _fName groupedFlds args = L.nub $ map _fArguments groupedFlds fld = NE.head fldGroup fldAl = _fAlias fld when (length fldNames > 1) $ throwVE $ "cannot merge different fields under the same alias (" <> showName (G.unAlias fldAl) <> "): " <> showNames fldNames when (length args > 1) $ throwVE $ "cannot merge fields with different arguments" <> " under the same alias: " <> showName (G.unAlias fldAl) return fld onJust :: (Monad m) => Maybe a -> (a -> m ()) -> m () onJust m act = maybe (return ()) act m denormFrag :: ( MonadReader ValidationCtx m , MonadError QErr m) => [G.Name] -- visited fragments -> G.NamedType -- parent type -> G.FragmentSpread -> m (Maybe FieldGroup) denormFrag visFrags parTy (G.FragmentSpread name directives) = do -- check for cycles when (name `elem` visFrags) $ throwVE $ "cannot spread fragment " <> showName name <> " within itself via " <> T.intercalate "," (map G.unName visFrags) (FragDef _ fragTyInfo selSet) <- getFragInfo let fragTy = _otiName fragTyInfo -- we don't have unions or interfaces so we can get away with equality when (fragTy /= parTy) $ throwVE $ "cannot spread fragment " <> showName name <> " defined on " <> showNamedTy fragTy <> " when selecting fields of type " <> showNamedTy parTy resFlds <- withPathK "selset" $ denormSelSet (name:visFrags) fragTyInfo selSet withPathK "directives" $ withDirectives directives $ return $ FieldGroup (FGSFragSprd name) resFlds where getFragInfo = do dctx <- ask onNothing (Map.lookup name $ _vcFragDefMap dctx) $ throwVE $ "fragment '" <> G.unName name <> "' not found"