mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
e01008413e
This fix is a little ugly, but it’s the only simple solution without a significant refactoring that restructures the relationship between GraphQL/Validate and GraphQL/Resolve. The ugliness should go away if we implement something like #2801.
200 lines
7.9 KiB
Haskell
200 lines
7.9 KiB
Haskell
module Hasura.GraphQL.Resolve.InputValue
|
||
( withNotNull
|
||
, tyMismatch
|
||
|
||
, OpaqueValue
|
||
, OpaquePGValue
|
||
, mkParameterizablePGValue
|
||
, openOpaqueValue
|
||
, asPGColumnTypeAndValueM
|
||
, asPGColumnValueM
|
||
, asPGColumnValue
|
||
|
||
, asEnumVal
|
||
, asEnumValM
|
||
, withObject
|
||
, asObject
|
||
, withObjectM
|
||
, asObjectM
|
||
, withArray
|
||
, asArray
|
||
, withArrayM
|
||
, parseMany
|
||
, asPGColText
|
||
, asPGColTextM
|
||
) where
|
||
|
||
import Hasura.Prelude
|
||
|
||
import qualified Language.GraphQL.Draft.Syntax as G
|
||
|
||
import qualified Hasura.RQL.Types as RQL
|
||
|
||
import Hasura.GraphQL.Resolve.Context
|
||
import Hasura.GraphQL.Validate.Types
|
||
import Hasura.RQL.Types
|
||
import Hasura.SQL.Types
|
||
import Hasura.SQL.Value
|
||
|
||
withNotNull
|
||
:: (MonadError QErr m)
|
||
=> G.NamedType -> Maybe a -> m a
|
||
withNotNull nt v =
|
||
onNothing v $ throw500 $
|
||
"unexpected null for a value of type " <> showNamedTy nt
|
||
|
||
tyMismatch
|
||
:: (MonadError QErr m) => Text -> AnnInpVal -> m a
|
||
tyMismatch expectedTy v =
|
||
throw500 $ "expected " <> expectedTy <> ", found " <>
|
||
getAnnInpValKind (_aivValue v) <> " for value of type " <>
|
||
G.showGT (_aivType v)
|
||
|
||
-- | As part of query reusability tracking (see 'QueryReusability'), functions that parse input
|
||
-- values call 'markNotReusable' when the value comes from a variable. However, always calling
|
||
-- 'markNotReusable' when parsing column values (using 'asPGColumnValue' and its variants) would be
|
||
-- much too conservative: often the value is simply validated and wrapped immediately in 'UVPG',
|
||
-- which allows it to be parameterized over.
|
||
--
|
||
-- Always omitting the check would be incorrect, as some callers inspect the column values and use
|
||
-- them to generate different SQL, which is where 'OpaqueValue' comes in. Functions like
|
||
-- 'asPGColumnValue' return an 'OpaquePGValue', which can be safely converted to an 'UnresolvedVal'
|
||
-- via 'mkParameterizablePGValue' without marking the query as non-reusable. Other callers that wish
|
||
-- to inspect the value can instead call 'openOpaqueValue' to get the value out, and /that/ will
|
||
-- mark the query non-reusable, instead.
|
||
--
|
||
-- In other words, 'OpaqueValue' is a mechanism of delaying the 'markNotReusable' call until we’re
|
||
-- confident its value will actually affect the generated SQL.
|
||
data OpaqueValue a
|
||
= OpaqueValue
|
||
{ _opgvValue :: !a
|
||
, _opgvIsVariable :: !Bool
|
||
} deriving (Show)
|
||
type OpaquePGValue = OpaqueValue AnnPGVal
|
||
|
||
mkParameterizablePGValue :: OpaquePGValue -> UnresolvedVal
|
||
mkParameterizablePGValue (OpaqueValue v _) = UVPG v
|
||
|
||
openOpaqueValue :: (MonadReusability m) => OpaqueValue a -> m a
|
||
openOpaqueValue (OpaqueValue v isVariable) = when isVariable markNotReusable $> v
|
||
|
||
asPGColumnTypeAndValueM
|
||
:: (MonadReusability m, MonadError QErr m)
|
||
=> AnnInpVal
|
||
-> m (PGColumnType, WithScalarType (Maybe (OpaqueValue PGScalarValue)))
|
||
asPGColumnTypeAndValueM v = do
|
||
(columnType, scalarValueM) <- case _aivValue v of
|
||
AGScalar colTy val -> pure (PGColumnScalar colTy, WithScalarType colTy val)
|
||
AGEnum _ (AGEReference reference maybeValue) -> do
|
||
let maybeScalarValue = PGValText . RQL.getEnumValue <$> maybeValue
|
||
pure (PGColumnEnumReference reference, WithScalarType PGText maybeScalarValue)
|
||
_ -> tyMismatch "pgvalue" v
|
||
|
||
for_ (_aivVariable v) $ \variableName -> if
|
||
-- If the value is a nullable variable, then the caller might make a different decision based on
|
||
-- whether the result is 'Nothing' or 'Just', which would change the generated query, so we have
|
||
-- to unconditionally mark the query non-reusable.
|
||
| G.isNullable (_aivType v) -> markNotReusable
|
||
| otherwise -> recordVariableUse variableName columnType
|
||
|
||
let isVariable = isJust $ _aivVariable v
|
||
pure (columnType, fmap (flip OpaqueValue isVariable) <$> scalarValueM)
|
||
|
||
asPGColumnTypeAndAnnValueM
|
||
:: (MonadReusability m, MonadError QErr m) => AnnInpVal -> m (PGColumnType, Maybe OpaquePGValue)
|
||
asPGColumnTypeAndAnnValueM v = do
|
||
(columnType, scalarValueM) <- asPGColumnTypeAndValueM v
|
||
let mkAnnPGColVal = AnnPGVal (_aivVariable v) (G.isNullable (_aivType v))
|
||
replaceOpaqueValue (WithScalarType scalarType (OpaqueValue scalarValue isVariable)) =
|
||
OpaqueValue (mkAnnPGColVal (WithScalarType scalarType scalarValue)) isVariable
|
||
pure (columnType, replaceOpaqueValue <$> sequence scalarValueM)
|
||
|
||
asPGColumnValueM :: (MonadReusability m, MonadError QErr m) => AnnInpVal -> m (Maybe OpaquePGValue)
|
||
asPGColumnValueM = fmap snd . asPGColumnTypeAndAnnValueM
|
||
|
||
asPGColumnValue :: (MonadReusability m, MonadError QErr m) => AnnInpVal -> m OpaquePGValue
|
||
asPGColumnValue v = do
|
||
(columnType, annPGValM) <- asPGColumnTypeAndAnnValueM v
|
||
onNothing annPGValM $ throw500 ("unexpected null for type " <>> columnType)
|
||
|
||
openInputValue :: (MonadReusability m) => AnnInpVal -> m AnnGValue
|
||
openInputValue v = when (isJust $ _aivVariable v) markNotReusable $> _aivValue v
|
||
|
||
-- | Note: only handles “synthetic” enums (see 'EnumValuesInfo'). Enum table references are handled
|
||
-- by 'asPGColumnType' and its variants.
|
||
asEnumVal :: (MonadReusability m, MonadError QErr m) => AnnInpVal -> m (G.NamedType, G.EnumValue)
|
||
asEnumVal = asEnumValM >=> \case
|
||
(ty, Just val) -> pure (ty, val)
|
||
(ty, Nothing) -> throw500 $ "unexpected null for ty " <> showNamedTy ty
|
||
|
||
-- | Like 'asEnumVal', only handles “synthetic” enums.
|
||
asEnumValM :: (MonadReusability m, MonadError QErr m) => AnnInpVal -> m (G.NamedType, Maybe G.EnumValue)
|
||
asEnumValM v = openInputValue v >>= \case
|
||
AGEnum ty (AGESynthetic valM) -> return (ty, valM)
|
||
_ -> tyMismatch "enum" v
|
||
|
||
withObject
|
||
:: (MonadReusability m, MonadError QErr m) => (G.NamedType -> AnnGObject -> m a) -> AnnInpVal -> m a
|
||
withObject fn v = openInputValue v >>= \case
|
||
AGObject nt (Just obj) -> fn nt obj
|
||
AGObject _ Nothing ->
|
||
throw500 $ "unexpected null for ty"
|
||
<> G.showGT (_aivType v)
|
||
_ -> tyMismatch "object" v
|
||
|
||
asObject :: (MonadReusability m, MonadError QErr m) => AnnInpVal -> m AnnGObject
|
||
asObject = withObject (\_ o -> return o)
|
||
|
||
withObjectM
|
||
:: (MonadReusability m, MonadError QErr m)
|
||
=> (G.NamedType -> Maybe AnnGObject -> m a) -> AnnInpVal -> m a
|
||
withObjectM fn v = openInputValue v >>= \case
|
||
AGObject nt objM -> fn nt objM
|
||
_ -> tyMismatch "object" v
|
||
|
||
asObjectM :: (MonadReusability m, MonadError QErr m) => AnnInpVal -> m (Maybe AnnGObject)
|
||
asObjectM = withObjectM (\_ o -> return o)
|
||
|
||
withArrayM
|
||
:: (MonadReusability m, MonadError QErr m)
|
||
=> (G.ListType -> Maybe [AnnInpVal] -> m a) -> AnnInpVal -> m a
|
||
withArrayM fn v = openInputValue v >>= \case
|
||
AGArray lt listM -> fn lt listM
|
||
_ -> tyMismatch "array" v
|
||
|
||
withArray
|
||
:: (MonadReusability m, MonadError QErr m)
|
||
=> (G.ListType -> [AnnInpVal] -> m a) -> AnnInpVal -> m a
|
||
withArray fn v = openInputValue v >>= \case
|
||
AGArray lt (Just l) -> fn lt l
|
||
AGArray _ Nothing -> throw500 $ "unexpected null for ty"
|
||
<> G.showGT (_aivType v)
|
||
_ -> tyMismatch "array" v
|
||
|
||
asArray :: (MonadReusability m, MonadError QErr m) => AnnInpVal -> m [AnnInpVal]
|
||
asArray = withArray (\_ vals -> return vals)
|
||
|
||
parseMany
|
||
:: (MonadReusability m, MonadError QErr m) => (AnnInpVal -> m a) -> AnnInpVal -> m (Maybe [a])
|
||
parseMany fn v = openInputValue v >>= \case
|
||
AGArray _ arrM -> mapM (mapM fn) arrM
|
||
_ -> tyMismatch "array" v
|
||
|
||
onlyText
|
||
:: (MonadError QErr m)
|
||
=> PGScalarValue -> m Text
|
||
onlyText = \case
|
||
PGValText t -> return t
|
||
PGValVarchar t -> return t
|
||
_ -> throw500 "expecting text for asPGColText"
|
||
|
||
asPGColText :: (MonadReusability m, MonadError QErr m) => AnnInpVal -> m Text
|
||
asPGColText val = do
|
||
pgColVal <- openOpaqueValue =<< asPGColumnValue val
|
||
onlyText (pstValue $ _apvValue pgColVal)
|
||
|
||
asPGColTextM :: (MonadReusability m, MonadError QErr m) => AnnInpVal -> m (Maybe Text)
|
||
asPGColTextM val = do
|
||
pgColValM <- traverse openOpaqueValue =<< asPGColumnValueM val
|
||
traverse onlyText (pstValue . _apvValue <$> pgColValM)
|