mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 04:51:35 +03:00
316 lines
9.1 KiB
Haskell
316 lines
9.1 KiB
Haskell
|
{-# 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"
|