mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 04:24:35 +03:00
2a9bc2354f
* validation support for unions and interfaces * refactor SQL generation logic for improved readability * '/v1/relay' endpoint for relay schema * implement 'Node' interface and top level 'node' field resolver * add relay toggle on graphiql * fix explain api response & index plan id with query type * add hasura mutations to relay * add relay pytests * update CHANGELOG.md Co-authored-by: rakeshkky <12475069+rakeshkky@users.noreply.github.com> Co-authored-by: Rishichandra Wawhal <rishi@hasura.io> Co-authored-by: Rikin Kachhia <54616969+rikinsk@users.noreply.github.com>
332 lines
13 KiB
Haskell
332 lines
13 KiB
Haskell
module Hasura.GraphQL.Validate
|
|
( validateGQ
|
|
, showVars
|
|
, RootSelectionSet(..)
|
|
, SelectionSet(..)
|
|
, Field(..)
|
|
, getTypedOp
|
|
, QueryParts(..)
|
|
, getQueryParts
|
|
|
|
, ReusableVariableTypes(..)
|
|
, ReusableVariableValues
|
|
, validateVariablesForReuse
|
|
|
|
, isQueryInAllowlist
|
|
|
|
, unValidateArgsMap
|
|
, unValidateSelectionSet
|
|
, unValidateField
|
|
) where
|
|
|
|
import Hasura.Prelude
|
|
|
|
import Data.Has
|
|
import Data.Time
|
|
|
|
import qualified Data.Aeson as A
|
|
import qualified Data.HashMap.Strict as Map
|
|
import qualified Data.HashMap.Strict.InsOrd as OMap
|
|
import qualified Data.HashSet as HS
|
|
import qualified Data.Text as T
|
|
import qualified Data.UUID as UUID
|
|
import qualified Database.PG.Query as Q
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
|
|
|
import Hasura.GraphQL.NormalForm
|
|
import Hasura.GraphQL.Resolve.InputValue (annInpValueToJson)
|
|
import Hasura.GraphQL.Schema
|
|
import Hasura.GraphQL.Transport.HTTP.Protocol
|
|
import Hasura.GraphQL.Utils
|
|
import Hasura.GraphQL.Validate.Context
|
|
import Hasura.GraphQL.Validate.InputValue
|
|
import Hasura.GraphQL.Validate.SelectionSet
|
|
import Hasura.GraphQL.Validate.Types
|
|
import Hasura.RQL.DML.Select.Types
|
|
import Hasura.RQL.Types
|
|
import Hasura.SQL.Time
|
|
import Hasura.SQL.Value
|
|
|
|
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
|
|
validateVariables
|
|
:: (MonadReader r m, Has TypeMap r, MonadError QErr m)
|
|
=> [G.VariableDefinition] -> VariableValues -> m AnnVarVals
|
|
validateVariables 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
|
|
|
|
traverse validateVariable varDefs
|
|
where
|
|
validateVariable (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 $
|
|
"variables can only be defined on input types"
|
|
<> "(enums, scalars, input objects), but "
|
|
<> showNamedTy baseTy <> " is an object type"
|
|
|
|
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"
|
|
|
|
|
|
showVars :: (Functor f, Foldable f) => f G.Variable -> Text
|
|
showVars = showNames . fmap G.unVariable
|
|
|
|
-- | This is similar in spirit to 'validateVariables' but uses preexisting 'ReusableVariableTypes'
|
|
-- information to parse Postgres values directly for use with a reusable query plan. (Ideally, it
|
|
-- would be nice to be able to share more of the logic instead of duplicating it.)
|
|
validateVariablesForReuse
|
|
:: (MonadError QErr m)
|
|
=> ReusableVariableTypes -> Maybe VariableValues -> m ReusableVariableValues
|
|
validateVariablesForReuse (ReusableVariableTypes varTypes) varValsM =
|
|
withPathK "variableValues" $ do
|
|
let unexpectedVars = filter (not . (`Map.member` varTypes)) $ Map.keys varVals
|
|
unless (null unexpectedVars) $
|
|
throwVE $ "unexpected variables: " <> showVars unexpectedVars
|
|
|
|
flip Map.traverseWithKey varTypes $ \varName varType ->
|
|
withPathK (G.unName $ G.unVariable varName) $ do
|
|
varVal <- onNothing (Map.lookup varName varVals) $
|
|
throwVE "expected a value for non-nullable variable"
|
|
-- TODO: we don't have the graphql type
|
|
-- <> " of type: " <> T.pack (show varType)
|
|
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"
|
|
fragmentTypeInfo <- getFragmentTyInfo onTy
|
|
return $ FragDef n fragmentTypeInfo selSet
|
|
|
|
validateGQ
|
|
:: (MonadError QErr m, MonadReader GCtx m, MonadReusability m)
|
|
=> QueryParts -> m RootSelectionSet
|
|
validateGQ (QueryParts opDef opRoot fragDefsL varValsM) = do
|
|
ctx <- ask
|
|
|
|
-- annotate the variables of this operation
|
|
annVarVals <- validateVariables (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 $ parseObjectSelectionSet valCtx opRoot $
|
|
G._todSelectionSet opDef
|
|
|
|
case G._todType opDef of
|
|
G.OperationTypeQuery -> return $ RQuery selSet
|
|
G.OperationTypeMutation -> return $ RMutation selSet
|
|
G.OperationTypeSubscription ->
|
|
case OMap.toList $ unAliasedFields $ unObjectSelectionSet selSet of
|
|
[] -> throw500 "empty selset for subscription"
|
|
(_:rst) -> do
|
|
-- As an internal testing feature, we support subscribing to multiple
|
|
-- selection sets. First check if the corresponding directive is set.
|
|
let multipleAllowed = G.Directive "_multiple_top_level_fields" [] `elem` G._todDirectives opDef
|
|
unless (multipleAllowed || null rst) $
|
|
throwVE "subscriptions must select one top level field"
|
|
return $ RSubscription selSet
|
|
|
|
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
|
|
|
|
-- | Convert the validated arguments to GraphQL parser AST arguments
|
|
unValidateArgsMap :: ArgsMap -> [RemoteFieldArgument]
|
|
unValidateArgsMap argsMap =
|
|
map (\(n, inpVal) ->
|
|
let _rfaArgument = G.Argument n $ unValidateInpVal inpVal
|
|
_rfaVariable = unValidateInpVariable inpVal
|
|
in RemoteFieldArgument {..})
|
|
. Map.toList $ argsMap
|
|
|
|
-- | Convert the validated field to GraphQL parser AST field
|
|
unValidateField :: G.Alias -> Field -> G.Field
|
|
unValidateField alias (Field name _ argsMap selSet) =
|
|
let args = map (\(n, inpVal) -> G.Argument n $ unValidateInpVal inpVal) $
|
|
Map.toList argsMap
|
|
in G.Field (Just alias) name args [] $ unValidateSelectionSet selSet
|
|
|
|
-- | Convert the validated selection set to GraphQL parser AST selection set
|
|
unValidateSelectionSet :: SelectionSet -> G.SelectionSet
|
|
unValidateSelectionSet = \case
|
|
SelectionSetObject selectionSet -> fromSelectionSet selectionSet
|
|
SelectionSetInterface selectionSet -> fromScopedSelectionSet selectionSet
|
|
SelectionSetUnion selectionSet -> fromScopedSelectionSet selectionSet
|
|
SelectionSetNone -> mempty
|
|
where
|
|
fromAliasedFields :: (IsField f) => AliasedFields f -> G.SelectionSet
|
|
fromAliasedFields =
|
|
map (G.SelectionField . uncurry unValidateField) .
|
|
OMap.toList . fmap toField . unAliasedFields
|
|
fromSelectionSet =
|
|
fromAliasedFields . unObjectSelectionSet
|
|
toInlineSelection typeName =
|
|
G.SelectionInlineFragment . G.InlineFragment (Just typeName) mempty .
|
|
fromSelectionSet
|
|
fromScopedSelectionSet (ScopedSelectionSet base specific) =
|
|
map (uncurry toInlineSelection) (Map.toList specific) <> fromAliasedFields base
|
|
|
|
-- | Get the variable definition and it's value (if exists)
|
|
unValidateInpVariable :: AnnInpVal -> Maybe [(G.VariableDefinition,A.Value)]
|
|
unValidateInpVariable inputValue =
|
|
case (_aivValue inputValue) of
|
|
AGScalar _ _ -> mkVariableDefnValueTuple inputValue
|
|
AGEnum _ _ -> mkVariableDefnValueTuple inputValue
|
|
AGObject _ o ->
|
|
(\obj ->
|
|
let listObjects = OMap.toList obj
|
|
in concat $
|
|
mapMaybe (\(_, inpVal) -> unValidateInpVariable inpVal) listObjects)
|
|
<$> o
|
|
AGArray _ _ -> mkVariableDefnValueTuple inputValue
|
|
where
|
|
mkVariableDefnValueTuple val = maybe Nothing (\vars -> Just [vars]) $
|
|
variableDefnValueTuple val
|
|
|
|
variableDefnValueTuple :: AnnInpVal -> Maybe (G.VariableDefinition,A.Value)
|
|
variableDefnValueTuple inpVal@AnnInpVal {..} =
|
|
let varDefn = G.VariableDefinition <$> _aivVariable <*> Just _aivType <*> Just Nothing
|
|
in (,) <$> varDefn <*> Just (annInpValueToJson inpVal)
|
|
|
|
-- | Convert the validated input value to GraphQL value, if the input value
|
|
-- is a variable then it will be returned without resolving it, otherwise it
|
|
-- will be resolved
|
|
unValidateInpVal :: AnnInpVal -> G.Value
|
|
unValidateInpVal (AnnInpVal _ var val) = fromMaybe G.VNull $
|
|
-- if a variable is found, then directly return that, if not found then
|
|
-- convert it into a G.Value and return it
|
|
case var of
|
|
Just var' -> Just $ G.VVariable var'
|
|
Nothing ->
|
|
case val of
|
|
AGScalar _ v -> pgScalarToGValue <$> v
|
|
AGEnum _ v -> pgEnumToGEnum v
|
|
AGObject _ o ->
|
|
(G.VObject . G.ObjectValueG
|
|
. map (uncurry G.ObjectFieldG . (second unValidateInpVal))
|
|
. OMap.toList
|
|
) <$> o
|
|
AGArray _ vs -> (G.VList . G.ListValueG . map unValidateInpVal) <$> vs
|
|
|
|
where
|
|
pgEnumToGEnum :: AnnGEnumValue -> Maybe G.Value
|
|
pgEnumToGEnum = \case
|
|
AGESynthetic v -> G.VEnum <$> v
|
|
AGEReference _ v -> (G.VEnum . G.EnumValue . G.Name . getEnumValue) <$> v
|
|
|
|
pgScalarToGValue :: PGScalarValue -> G.Value
|
|
pgScalarToGValue = \case
|
|
PGValInteger i -> G.VInt $ fromIntegral i
|
|
PGValSmallInt i -> G.VInt $ fromIntegral i
|
|
PGValBigInt i -> G.VInt $ fromIntegral i
|
|
PGValFloat f -> G.VFloat $ realToFrac f
|
|
PGValDouble d -> G.VFloat $ realToFrac d
|
|
-- TODO: Scientific is a danger zone; use its safe conv function.
|
|
PGValNumeric sc -> G.VFloat $ realToFrac sc
|
|
PGValMoney m -> G.VFloat $ realToFrac m
|
|
PGValBoolean b -> G.VBoolean b
|
|
PGValChar t -> toStringValue $ T.singleton t
|
|
PGValVarchar t -> toStringValue t
|
|
PGValText t -> toStringValue t
|
|
PGValCitext t -> toStringValue t
|
|
PGValDate d -> toStringValue $ T.pack $ showGregorian d
|
|
PGValTimeStampTZ u -> toStringValue $ T.pack $
|
|
formatTime defaultTimeLocale "%FT%T%QZ" u
|
|
PGValTimeStamp u -> toStringValue $ T.pack $
|
|
formatTime defaultTimeLocale "%FT%T%QZ" u
|
|
PGValTimeTZ (ZonedTimeOfDay tod tz) ->
|
|
toStringValue $ T.pack (show tod ++ timeZoneOffsetString tz)
|
|
PGNull _ -> G.VNull
|
|
PGValJSON (Q.JSON v) -> jsonValueToGValue v
|
|
PGValJSONB (Q.JSONB v) -> jsonValueToGValue v
|
|
PGValGeo v -> jsonValueToGValue $ A.toJSON v
|
|
PGValRaster v -> jsonValueToGValue $ A.toJSON v
|
|
PGValUUID u -> toStringValue $ UUID.toText u
|
|
PGValUnknown t -> toStringValue t
|
|
where
|
|
toStringValue = G.VString . G.StringValue
|