mirror of
https://github.com/zhigang1992/graphql-engine.git
synced 2026-05-25 18:32:23 +08:00
These changes also add a new type, PGColumnType, between PGColInfo and PGScalarType, and they process PGRawColumnType values into PGColumnType values during schema cache generation.
227 lines
7.6 KiB
Haskell
227 lines
7.6 KiB
Haskell
module Hasura.GraphQL.Validate
|
|
( validateGQ
|
|
, showVars
|
|
, RootSelSet(..)
|
|
, getTypedOp
|
|
, QueryParts (..)
|
|
, getQueryParts
|
|
, getAnnVarVals
|
|
|
|
, isQueryInAllowlist
|
|
|
|
, VarPGTypes
|
|
, AnnPGVarVals
|
|
, getAnnPGVarVals
|
|
, Field(..)
|
|
, SelSet
|
|
) where
|
|
|
|
import Data.Has
|
|
import Hasura.Prelude
|
|
|
|
import qualified Data.HashMap.Strict as Map
|
|
import qualified Data.HashSet as HS
|
|
import qualified Data.Sequence as Seq
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
|
|
|
import Hasura.GraphQL.Schema
|
|
import Hasura.GraphQL.Transport.HTTP.Protocol
|
|
import Hasura.GraphQL.Validate.Context
|
|
import Hasura.GraphQL.Validate.Field
|
|
import Hasura.GraphQL.Validate.InputValue
|
|
import Hasura.GraphQL.Validate.Types
|
|
import Hasura.RQL.Types
|
|
import Hasura.RQL.Types.QueryCollection
|
|
|
|
import Hasura.SQL.Types (PGScalarTyped)
|
|
import Hasura.SQL.Value (PGColValue)
|
|
|
|
data QueryParts
|
|
= QueryParts
|
|
{ qpOpDef :: !G.TypedOperationDefinition
|
|
, qpOpRoot :: !ObjTyInfo
|
|
, qpFragDefsL :: ![G.FragmentDefinition]
|
|
, qpVarValsM :: !(Maybe VariableValues)
|
|
} deriving (Show, Eq)
|
|
|
|
getTypedOp
|
|
:: (MonadError QErr m)
|
|
=> Maybe OperationName
|
|
-> [G.SelectionSet]
|
|
-> [G.TypedOperationDefinition]
|
|
-> m G.TypedOperationDefinition
|
|
getTypedOp opNameM selSets opDefs =
|
|
case (opNameM, selSets, opDefs) of
|
|
(Just opName, [], _) -> do
|
|
let n = _unOperationName opName
|
|
opDefM = find (\opDef -> G._todName opDef == Just n) opDefs
|
|
onNothing opDefM $ throwVE $
|
|
"no such operation found in the document: " <> showName n
|
|
(Just _, _, _) ->
|
|
throwVE $ "operationName cannot be used when " <>
|
|
"an anonymous operation exists in the document"
|
|
(Nothing, [selSet], []) ->
|
|
return $ G.TypedOperationDefinition
|
|
G.OperationTypeQuery Nothing [] [] selSet
|
|
(Nothing, [], [opDef]) ->
|
|
return opDef
|
|
(Nothing, _, _) ->
|
|
throwVE $ "exactly one operation has to be present " <>
|
|
"in the document when operationName is not specified"
|
|
|
|
-- For all the variables defined there will be a value in the final map
|
|
-- If no default, not in variables and nullable, then null value
|
|
getAnnVarVals
|
|
:: ( MonadReader r m, Has TypeMap r
|
|
, MonadError QErr m
|
|
)
|
|
=> [G.VariableDefinition]
|
|
-> VariableValues
|
|
-> m AnnVarVals
|
|
getAnnVarVals varDefsL inpVals = withPathK "variableValues" $ do
|
|
|
|
varDefs <- onLeft (mkMapWith G._vdVariable varDefsL) $ \dups ->
|
|
throwVE $ "the following variables are defined more than once: " <>
|
|
showVars dups
|
|
|
|
let unexpectedVars = filter (not . (`Map.member` varDefs)) $ Map.keys inpVals
|
|
|
|
unless (null unexpectedVars) $
|
|
throwVE $ "unexpected variables in variableValues: " <>
|
|
showVars unexpectedVars
|
|
|
|
forM varDefs $ \(G.VariableDefinition var ty defM) -> do
|
|
let baseTy = getBaseTy ty
|
|
baseTyInfo <- getTyInfoVE baseTy
|
|
-- check that the variable is defined on input types
|
|
when (isObjTy baseTyInfo) $ throwVE $ objTyErrMsg baseTy
|
|
|
|
let defM' = bool (defM <|> Just G.VCNull) defM $ G.isNotNull ty
|
|
annDefM <- withPathK "defaultValue" $
|
|
mapM (validateInputValue constValueParser ty) defM'
|
|
let inpValM = Map.lookup var inpVals
|
|
annInpValM <- withPathK (G.unName $ G.unVariable var) $
|
|
mapM (validateInputValue jsonParser ty) inpValM
|
|
let varValM = annInpValM <|> annDefM
|
|
onNothing varValM $ throwVE $
|
|
"expecting a value for non-nullable variable: " <>
|
|
showVars [var] <>
|
|
" of type: " <> G.showGT ty <>
|
|
" in variableValues"
|
|
where
|
|
objTyErrMsg namedTy =
|
|
"variables can only be defined on input types"
|
|
<> "(enums, scalars, input objects), but "
|
|
<> showNamedTy namedTy <> " is an object type"
|
|
|
|
showVars :: (Functor f, Foldable f) => f G.Variable -> Text
|
|
showVars = showNames . fmap G.unVariable
|
|
|
|
type VarPGTypes = Map.HashMap G.Variable PGColumnType
|
|
type AnnPGVarVals = Map.HashMap G.Variable (PGScalarTyped PGColValue)
|
|
|
|
-- this is in similar spirit to getAnnVarVals, however
|
|
-- here it is much simpler and can get rid of typemap requirement
|
|
-- combine the two if possible
|
|
getAnnPGVarVals
|
|
:: (MonadError QErr m)
|
|
=> VarPGTypes
|
|
-> Maybe VariableValues
|
|
-> m AnnPGVarVals
|
|
getAnnPGVarVals varTypes varValsM =
|
|
flip Map.traverseWithKey varTypes $ \varName varType -> do
|
|
let unexpectedVars = filter
|
|
(not . (`Map.member` varTypes)) $ Map.keys varVals
|
|
unless (null unexpectedVars) $
|
|
throwVE $ "unexpected variables in variableValues: " <>
|
|
showVars unexpectedVars
|
|
varVal <- onNothing (Map.lookup varName varVals) $
|
|
throwVE $ "expecting a value for non-nullable variable: " <>
|
|
showVars [varName] <>
|
|
-- TODO: we don't have the graphql type
|
|
-- " of type: " <> T.pack (show varType) <>
|
|
" in variableValues"
|
|
parsePGScalarValue varType varVal
|
|
where
|
|
varVals = fromMaybe Map.empty varValsM
|
|
|
|
validateFrag
|
|
:: (MonadError QErr m, MonadReader r m, Has TypeMap r)
|
|
=> G.FragmentDefinition -> m FragDef
|
|
validateFrag (G.FragmentDefinition n onTy dirs selSet) = do
|
|
unless (null dirs) $ throwVE
|
|
"unexpected directives at fragment definition"
|
|
tyInfo <- getTyInfoVE onTy
|
|
objTyInfo <- onNothing (getObjTyM tyInfo) $ throwVE
|
|
"fragments can only be defined on object types"
|
|
return $ FragDef n objTyInfo selSet
|
|
|
|
data RootSelSet
|
|
= RQuery !SelSet
|
|
| RMutation !SelSet
|
|
| RSubscription !Field
|
|
deriving (Show, Eq)
|
|
|
|
-- {-# SCC validateGQ #-}
|
|
validateGQ
|
|
:: (MonadError QErr m, MonadReader GCtx m)
|
|
-- => GraphQLRequest
|
|
=> QueryParts
|
|
-> m RootSelSet
|
|
validateGQ (QueryParts opDef opRoot fragDefsL varValsM) = do
|
|
|
|
ctx <- ask
|
|
|
|
-- annotate the variables of this operation
|
|
annVarVals <- getAnnVarVals (G._todVariableDefinitions opDef) $
|
|
fromMaybe Map.empty varValsM
|
|
|
|
-- annotate the fragments
|
|
fragDefs <- onLeft (mkMapWith G._fdName fragDefsL) $ \dups ->
|
|
throwVE $ "the following fragments are defined more than once: " <>
|
|
showNames dups
|
|
annFragDefs <- mapM validateFrag fragDefs
|
|
|
|
-- build a validation ctx
|
|
let valCtx = ValidationCtx (_gTypes ctx) annVarVals annFragDefs
|
|
|
|
selSet <- flip runReaderT valCtx $ denormSelSet [] opRoot $
|
|
G._todSelectionSet opDef
|
|
|
|
case G._todType opDef of
|
|
G.OperationTypeQuery -> return $ RQuery selSet
|
|
G.OperationTypeMutation -> return $ RMutation selSet
|
|
G.OperationTypeSubscription ->
|
|
case Seq.viewl selSet of
|
|
Seq.EmptyL -> throw500 "empty selset for subscription"
|
|
fld Seq.:< rst -> do
|
|
unless (null rst) $
|
|
throwVE "subscription must select only one top level field"
|
|
return $ RSubscription fld
|
|
|
|
isQueryInAllowlist :: GQLExecDoc -> HS.HashSet GQLQuery -> Bool
|
|
isQueryInAllowlist q = HS.member gqlQuery
|
|
where
|
|
gqlQuery = GQLQuery $ G.ExecutableDocument $ stripTypenames $
|
|
unGQLExecDoc q
|
|
|
|
getQueryParts
|
|
:: ( MonadError QErr m, MonadReader GCtx m)
|
|
=> GQLReqParsed
|
|
-> m QueryParts
|
|
getQueryParts (GQLReq opNameM q varValsM) = do
|
|
-- get the operation that needs to be evaluated
|
|
opDef <- getTypedOp opNameM selSets opDefs
|
|
ctx <- ask
|
|
|
|
-- get the operation root
|
|
opRoot <- case G._todType opDef of
|
|
G.OperationTypeQuery -> return $ _gQueryRoot ctx
|
|
G.OperationTypeMutation ->
|
|
onNothing (_gMutRoot ctx) $ throwVE "no mutations exist"
|
|
G.OperationTypeSubscription ->
|
|
onNothing (_gSubRoot ctx) $ throwVE "no subscriptions exist"
|
|
return $ QueryParts opDef opRoot fragDefsL varValsM
|
|
where
|
|
(selSets, opDefs, fragDefsL) = G.partitionExDefs $ unGQLExecDoc q
|