graphql-engine/server/src-lib/Hasura/RQL/DDL/Schema/Function.hs
Vladimir Ciobanu da8f6981d4 server: reduce the number of backend dispatches
Fixes https://github.com/hasura/graphql-engine-mono/issues/712

Main point of interest: the `Hasura.SQL.Backend` module.

This PR creates an `Exists` type indexed by indexed type and packed constraint while hiding all of its complexity by not exporting the constructor.

Existential constructors/types which are no longer (directly) existential:
- [X] BackendSourceInfo :: BackendSourceInfo
- [x] BackendSourceMetadata :: BackendSourceMetadata
- [x] MOSourceObjId :: MetadatObjId
- [x] SOSourceObj :: SchemaObjId
- [x] RFDB :: RootField
- [x] LQP :: LiveQueryPlan
- [x] ExecutionStep :: ExecStepDB

This PR also removes ALL usages of `Typeable.cast` from our codebase. We still need to derive `Typeable` in a few places in order to be able to derive `Data` in one place. I have not dug deeper to see why this is needed.

GitOrigin-RevId: bb47e957192e4bb0af4c4116aee7bb92f7983445
2021-03-15 13:03:55 +00:00

225 lines
8.7 KiB
Haskell

{- |
Description: Create/delete SQL functions to/from Hasura metadata.
-}
module Hasura.RQL.DDL.Schema.Function where
import Hasura.Prelude
import qualified Data.HashMap.Strict as Map
import qualified Data.HashMap.Strict.InsOrd as OMap
import Data.Aeson
import Data.Text.Extended
import qualified Hasura.SQL.AnyBackend as AB
import Hasura.EncJSON
import Hasura.RQL.Types
import Hasura.Session
newtype TrackFunction b
= TrackFunction
{ tfName :: FunctionName b }
deriving instance (Backend b) => Show (TrackFunction b)
deriving instance (Backend b) => Eq (TrackFunction b)
deriving instance (Backend b) => FromJSON (TrackFunction b)
deriving instance (Backend b) => ToJSON (TrackFunction b)
-- | Track function, Phase 1:
-- Validate function tracking operation. Fails if function is already being
-- tracked, or if a table with the same name is being tracked.
trackFunctionP1
:: forall m b
. (CacheRM m, QErrM m, Backend b)
=> SourceName -> FunctionName b -> m ()
trackFunctionP1 sourceName qf = do
rawSchemaCache <- askSchemaCache
when (isJust $ unsafeFunctionInfo @b sourceName qf $ scSources rawSchemaCache) $
throw400 AlreadyTracked $ "function already tracked : " <>> qf
let qt = functionToTable qf
when (isJust $ unsafeTableInfo @b sourceName qt $ scSources rawSchemaCache) $
throw400 NotSupported $ "table with name " <> qf <<> " already exists"
trackFunctionP2
:: forall b m
. (MonadError QErr m, CacheRWM m, MetadataM m, BackendMetadata b)
=> SourceName -> FunctionName b -> FunctionConfig -> m EncJSON
trackFunctionP2 sourceName qf config = do
buildSchemaCacheFor
(MOSourceObjId sourceName $ AB.mkAnyBackend $ SMOFunction qf)
$ MetadataModifier
$ metaSources.ix sourceName.toSourceMetadata.smFunctions
%~ OMap.insert qf (FunctionMetadata qf config mempty)
pure successMsg
handleMultipleFunctions :: (QErrM m, Backend b) => FunctionName b -> [a] -> m a
handleMultipleFunctions qf = \case
[] ->
throw400 NotExists $ "no such function exists in postgres : " <>> qf
[fi] -> return fi
_ ->
throw400 NotSupported $
"function " <> qf <<> " is overloaded. Overloaded functions are not supported"
runTrackFunc
:: (MonadError QErr m, CacheRWM m, MetadataM m, BackendMetadata b)
=> TrackFunction b -> m EncJSON
runTrackFunc (TrackFunction qf)= do
-- v1 track_function lacks a means to take extra arguments
trackFunctionP1 defaultSource qf
trackFunctionP2 defaultSource qf emptyFunctionConfig
runTrackFunctionV2
:: (QErrM m, CacheRWM m, MetadataM m)
=> TrackFunctionV2 -> m EncJSON
runTrackFunctionV2 (TrackFunctionV2 source qf config) = do
trackFunctionP1 source qf
trackFunctionP2 source qf config
-- | JSON API payload for 'untrack_function':
--
-- https://hasura.io/docs/latest/graphql/core/api-reference/schema-metadata-api/custom-functions.html#untrack-function
data UnTrackFunction b
= UnTrackFunction
{ _utfFunction :: !(FunctionName b)
, _utfSource :: !SourceName
} deriving (Generic)
deriving instance (Backend b) => Show (UnTrackFunction b)
deriving instance (Backend b) => Eq (UnTrackFunction b)
instance (Backend b) => ToJSON (UnTrackFunction b) where
toJSON = genericToJSON hasuraJSON
instance (Backend b) => FromJSON (UnTrackFunction b) where
parseJSON v = withSource <|> withoutSource
where
withoutSource = UnTrackFunction <$> parseJSON v <*> pure defaultSource
withSource = flip (withObject "UnTrackFunction") v \o ->
UnTrackFunction <$> o .: "function"
<*> o .:? "source" .!= defaultSource
askFunctionInfo
:: forall m b
. (CacheRM m, MonadError QErr m, Backend b)
=> SourceName -> FunctionName b -> m (FunctionInfo b)
askFunctionInfo source functionName = do
sourceCache <- scSources <$> askSchemaCache
unsafeFunctionInfo @b source functionName sourceCache
`onNothing` throw400 NotExists ("function " <> functionName <<> " not found in the cache")
runUntrackFunc
:: (CacheRWM m, MonadError QErr m, MetadataM m, BackendMetadata b)
=> UnTrackFunction b -> m EncJSON
runUntrackFunc (UnTrackFunction functionName sourceName) = do
void $ askFunctionInfo sourceName functionName
withNewInconsistentObjsCheck
$ buildSchemaCache
$ dropFunctionInMetadata defaultSource functionName
pure successMsg
dropFunctionInMetadata :: (BackendMetadata b) => SourceName -> FunctionName b -> MetadataModifier
dropFunctionInMetadata source function = MetadataModifier $
metaSources.ix source.toSourceMetadata.smFunctions %~ OMap.delete function
{- Note [Function Permissions]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before we started supporting tracking volatile functions, permissions
for a function was inferred from the target table of the function.
The rationale behind this is that a stable/immutable function does not
modify the database and the data returned by the function is filtered using
the permissions that are specified precisely for that data.
Now consider mutable/volatile functions, we can't automatically infer whether or
not these functions should be exposed for the sole reason that they can modify
the database. This necessitates a permission system for functions.
So, we introduce a new API `pg_create_function_permission` which will
explicitly grant permission to a function to a role. For creating a
function permission, the role must have select permissions configured
for the target table.
Since, this is a breaking change, we enable it only when the graphql-engine
is started with
`--infer-function-permissions`/HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS set
to false (by default, it's set to true).
-}
data CreateFunctionPermission b
= CreateFunctionPermission
{ _afpFunction :: !(FunctionName b)
, _afpSource :: !SourceName
, _afpRole :: !RoleName
} deriving (Generic)
deriving instance (Backend b) => Show (CreateFunctionPermission b)
deriving instance (Backend b) => Eq (CreateFunctionPermission b)
instance (Backend b) => ToJSON (CreateFunctionPermission b) where
toJSON = genericToJSON hasuraJSON
instance (Backend b) => FromJSON (CreateFunctionPermission b) where
parseJSON v =
flip (withObject "CreateFunctionPermission") v $ \o ->
CreateFunctionPermission
<$> o .: "function"
<*> o .:? "source" .!= defaultSource
<*> o .: "role"
runCreateFunctionPermission
:: forall m b
. ( CacheRWM m
, MonadError QErr m
, MetadataM m
, BackendMetadata b
)
=> CreateFunctionPermission b
-> m EncJSON
runCreateFunctionPermission (CreateFunctionPermission functionName source role) = do
sourceCache <- scSources <$> askSchemaCache
functionInfo <- askFunctionInfo source functionName
when (role `elem` _fiPermissions functionInfo) $
throw400 AlreadyExists $
"permission of role "
<> role <<> " already exists for function " <> functionName <<> " in source: " <>> source
functionTableInfo <-
unsafeTableInfo @b source (_fiReturnType functionInfo) sourceCache
`onNothing` throw400 NotExists ("function's return table " <> _fiReturnType functionInfo <<> " not found in the cache")
unless (role `Map.member` _tiRolePermInfoMap functionTableInfo) $
throw400 NotSupported $
"function permission can only be added when the function's return table "
<> _fiReturnType functionInfo <<> " has select permission configured for role: " <>> role
buildSchemaCacheFor
(MOSourceObjId source
$ AB.mkAnyBackend (SMOFunctionPermission functionName role))
$ MetadataModifier
$ metaSources.ix
source.toSourceMetadata.smFunctions.ix
functionName.fmPermissions
%~ (:) (FunctionPermissionMetadata role)
pure successMsg
dropFunctionPermissionInMetadata
:: (BackendMetadata b)
=> SourceName -> FunctionName b -> RoleName -> MetadataModifier
dropFunctionPermissionInMetadata source function role = MetadataModifier $
metaSources.ix source.toSourceMetadata.smFunctions.ix function.fmPermissions %~ filter ((/=) role . _fpmRole)
type DropFunctionPermission = CreateFunctionPermission
runDropFunctionPermission
:: forall m b
. ( CacheRWM m
, MonadError QErr m
, MetadataM m
, BackendMetadata b
)
=> DropFunctionPermission b
-> m EncJSON
runDropFunctionPermission (CreateFunctionPermission functionName source role) = do
functionInfo <- askFunctionInfo source functionName
unless (role `elem` _fiPermissions functionInfo) $
throw400 NotExists $
"permission of role "
<> role <<> " does not exist for function " <> functionName <<> " in source: " <>> source
buildSchemaCacheFor
(MOSourceObjId source
$ AB.mkAnyBackend
$ SMOFunctionPermission functionName role)
$ dropFunctionPermissionInMetadata source functionName role
pure successMsg