2021-02-14 09:07:52 +03:00
|
|
|
module Hasura.Backends.Postgres.DDL.Function
|
|
|
|
( buildFunctionInfo
|
|
|
|
, mkFunctionArgs
|
|
|
|
)
|
|
|
|
where
|
|
|
|
|
2021-03-15 16:02:58 +03:00
|
|
|
import Hasura.Prelude
|
|
|
|
|
2021-02-14 09:07:52 +03:00
|
|
|
import qualified Control.Monad.Validate as MV
|
|
|
|
import qualified Data.HashSet as S
|
|
|
|
import qualified Data.Sequence as Seq
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
|
|
|
|
|
|
|
import Control.Lens hiding (from, index, op, (.=))
|
|
|
|
import Data.Text.Extended
|
|
|
|
|
2021-03-15 16:02:58 +03:00
|
|
|
import qualified Hasura.SQL.AnyBackend as AB
|
|
|
|
|
2021-02-14 09:07:52 +03:00
|
|
|
import Hasura.Backends.Postgres.SQL.Types
|
2021-05-11 18:18:31 +03:00
|
|
|
import Hasura.Base.Error
|
2021-04-22 00:44:37 +03:00
|
|
|
import Hasura.RQL.Types.Backend
|
2021-02-14 09:07:52 +03:00
|
|
|
import Hasura.RQL.Types.Common
|
|
|
|
import Hasura.RQL.Types.Function
|
|
|
|
import Hasura.RQL.Types.SchemaCache
|
|
|
|
import Hasura.RQL.Types.SchemaCacheTypes
|
|
|
|
import Hasura.SQL.Backend
|
|
|
|
import Hasura.Server.Utils
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
|
|
|
|
mkFunctionArgs :: Int -> [QualifiedPGType] -> [FunctionArgName] -> [FunctionArg ('Postgres pgKind)]
|
2021-02-14 09:07:52 +03:00
|
|
|
mkFunctionArgs defArgsNo tys argNames =
|
|
|
|
bool withNames withNoNames $ null argNames
|
|
|
|
where
|
|
|
|
hasDefaultBoolSeq = replicate (length tys - defArgsNo) (HasDefault False)
|
|
|
|
-- only last arguments can have default expression
|
|
|
|
<> replicate defArgsNo (HasDefault True)
|
|
|
|
|
|
|
|
tysWithHasDefault = zip tys hasDefaultBoolSeq
|
|
|
|
|
|
|
|
withNoNames = flip map tysWithHasDefault $ uncurry $ FunctionArg Nothing
|
|
|
|
withNames = zipWith mkArg argNames tysWithHasDefault
|
|
|
|
|
|
|
|
mkArg "" (ty, hasDef) = FunctionArg Nothing ty hasDef
|
|
|
|
mkArg n (ty, hasDef) = FunctionArg (Just n) ty hasDef
|
|
|
|
|
|
|
|
data FunctionIntegrityError
|
|
|
|
= FunctionNameNotGQLCompliant
|
|
|
|
| FunctionVariadic
|
|
|
|
| FunctionReturnNotCompositeType
|
|
|
|
| FunctionReturnNotTable
|
|
|
|
| NonVolatileFunctionAsMutation
|
|
|
|
| FunctionSessionArgumentNotJSON !FunctionArgName
|
|
|
|
| FunctionInvalidSessionArgument !FunctionArgName
|
|
|
|
| FunctionInvalidArgumentNames [FunctionArgName]
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
|
|
|
buildFunctionInfo
|
2021-04-22 00:44:37 +03:00
|
|
|
:: forall pgKind m
|
|
|
|
. (Backend ('Postgres pgKind), QErrM m)
|
2021-02-14 09:07:52 +03:00
|
|
|
=> SourceName
|
|
|
|
-> QualifiedFunction
|
|
|
|
-> SystemDefined
|
|
|
|
-> FunctionConfig
|
|
|
|
-> [FunctionPermissionMetadata]
|
2021-07-19 18:35:16 +03:00
|
|
|
-> RawFunctionInfo ('Postgres pgKind)
|
2021-04-22 00:44:37 +03:00
|
|
|
-> m (FunctionInfo ('Postgres pgKind), SchemaDependency)
|
2021-02-14 09:07:52 +03:00
|
|
|
buildFunctionInfo source qf systemDefined FunctionConfig{..} permissions rawFuncInfo =
|
|
|
|
either (throw400 NotSupported . showErrors) pure
|
|
|
|
=<< MV.runValidateT validateFunction
|
|
|
|
where
|
|
|
|
functionArgs = mkFunctionArgs defArgsNo inpArgTyps inpArgNames
|
2021-07-19 18:35:16 +03:00
|
|
|
PGRawFunctionInfo _ hasVariadic funVol retSn retN retTyTyp retSet
|
2021-02-14 09:07:52 +03:00
|
|
|
inpArgTyps inpArgNames defArgsNo returnsTab descM
|
|
|
|
= rawFuncInfo
|
|
|
|
returnType = QualifiedPGType retSn retN retTyTyp
|
|
|
|
|
|
|
|
throwValidateError = MV.dispute . pure
|
|
|
|
|
|
|
|
validateFunction = do
|
|
|
|
unless (has _Right $ qualifiedObjectToName qf) $
|
|
|
|
throwValidateError FunctionNameNotGQLCompliant
|
|
|
|
when hasVariadic $ throwValidateError FunctionVariadic
|
|
|
|
when (retTyTyp /= PGKindComposite) $ throwValidateError FunctionReturnNotCompositeType
|
|
|
|
unless returnsTab $ throwValidateError FunctionReturnNotTable
|
|
|
|
-- We mostly take the user at their word here and will, e.g. expose a
|
|
|
|
-- function as a query if it is marked VOLATILE (since perhaps the user
|
|
|
|
-- is using the function to do some logging, say). But this is also a
|
|
|
|
-- footgun we'll need to try to document (since `VOLATILE` is default
|
|
|
|
-- when volatility is omitted). See the original approach here:
|
|
|
|
-- https://github.com/hasura/graphql-engine/pull/5858
|
|
|
|
--
|
|
|
|
-- This is the one exception where we do some validation. We're not
|
|
|
|
-- commited to this check, and it would be backwards compatible to remove
|
|
|
|
-- it, but this seemed like an obvious case:
|
|
|
|
when (funVol /= FTVOLATILE && _fcExposedAs == Just FEAMutation) $
|
2021-03-15 16:02:58 +03:00
|
|
|
throwValidateError NonVolatileFunctionAsMutation
|
2021-02-14 09:07:52 +03:00
|
|
|
-- If 'exposed_as' is omitted we'll infer it from the volatility:
|
|
|
|
let exposeAs = flip fromMaybe _fcExposedAs $ case funVol of
|
|
|
|
FTVOLATILE -> FEAMutation
|
|
|
|
_ -> FEAQuery
|
|
|
|
|
|
|
|
-- validate function argument names
|
|
|
|
validateFunctionArgNames
|
|
|
|
|
|
|
|
inputArguments <- makeInputArguments
|
|
|
|
|
|
|
|
let retTable = typeToTable returnType
|
|
|
|
retJsonAggSelect = bool JASSingleObject JASMultipleRows retSet
|
|
|
|
functionInfo =
|
|
|
|
FunctionInfo qf systemDefined funVol exposeAs inputArguments
|
2021-07-19 18:35:16 +03:00
|
|
|
retTable (getPGDescription <$> descM) (S.fromList $ _fpmRole <$> permissions)
|
2021-02-14 09:07:52 +03:00
|
|
|
retJsonAggSelect
|
|
|
|
|
|
|
|
pure ( functionInfo
|
2021-03-15 16:02:58 +03:00
|
|
|
, SchemaDependency
|
|
|
|
(SOSourceObj source
|
|
|
|
$ AB.mkAnyBackend
|
2021-04-22 00:44:37 +03:00
|
|
|
$ SOITable @('Postgres pgKind) retTable)
|
2021-03-15 16:02:58 +03:00
|
|
|
DRTable
|
2021-02-14 09:07:52 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
validateFunctionArgNames = do
|
|
|
|
let argNames = mapMaybe faName functionArgs
|
|
|
|
invalidArgs = filter (isNothing . G.mkName . getFuncArgNameTxt) argNames
|
|
|
|
unless (null invalidArgs) $
|
|
|
|
throwValidateError $ FunctionInvalidArgumentNames invalidArgs
|
|
|
|
|
|
|
|
makeInputArguments =
|
|
|
|
case _fcSessionArgument of
|
|
|
|
Nothing -> pure $ Seq.fromList $ map IAUserProvided functionArgs
|
|
|
|
Just sessionArgName -> do
|
|
|
|
unless (any (\arg -> Just sessionArgName == faName arg) functionArgs) $
|
|
|
|
throwValidateError $ FunctionInvalidSessionArgument sessionArgName
|
|
|
|
fmap Seq.fromList $ forM functionArgs $ \arg ->
|
|
|
|
if Just sessionArgName == faName arg then do
|
|
|
|
let argTy = _qptName $ faType arg
|
|
|
|
if argTy == PGJSON then pure $ IASessionVariables sessionArgName
|
|
|
|
else MV.refute $ pure $ FunctionSessionArgumentNotJSON sessionArgName
|
|
|
|
else pure $ IAUserProvided arg
|
|
|
|
|
|
|
|
showErrors allErrors =
|
|
|
|
"the function " <> qf <<> " cannot be tracked "
|
|
|
|
<> makeReasonMessage allErrors showOneError
|
|
|
|
|
|
|
|
showOneError = \case
|
|
|
|
FunctionNameNotGQLCompliant -> "function name is not a legal GraphQL identifier"
|
|
|
|
FunctionVariadic -> "function with \"VARIADIC\" parameters are not supported"
|
|
|
|
FunctionReturnNotCompositeType -> "the function does not return a \"COMPOSITE\" type"
|
|
|
|
FunctionReturnNotTable -> "the function does not return a table"
|
|
|
|
NonVolatileFunctionAsMutation ->
|
|
|
|
"the function was requested to be exposed as a mutation, but is not marked VOLATILE. " <>
|
|
|
|
"Maybe the function was given the wrong volatility when it was defined?"
|
|
|
|
FunctionSessionArgumentNotJSON argName ->
|
|
|
|
"given session argument " <> argName <<> " is not of type json"
|
|
|
|
FunctionInvalidSessionArgument argName ->
|
|
|
|
"given session argument " <> argName <<> " not the input argument of the function"
|
|
|
|
FunctionInvalidArgumentNames args ->
|
|
|
|
let argsText = T.intercalate "," $ map getFuncArgNameTxt args
|
|
|
|
in "the function arguments " <> argsText <> " are not in compliance with GraphQL spec"
|