Add support for customising function root field names

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2468
Co-authored-by: Philip Lykke Carlsen <358550+plcplc@users.noreply.github.com>
GitOrigin-RevId: 5ff85bb02e4e651376a40914b7ae0aabc8524a05
This commit is contained in:
Gil Mizrahi 2021-10-07 16:02:19 +03:00 committed by hasura-bot
parent 826020d796
commit 42cd2e69c0
18 changed files with 704 additions and 60 deletions

View File

@ -2,8 +2,17 @@
## Next release
(Add entries below in the order of server, console, cli, docs, others)
- server: add support for openapi json of REST Endpoints
### Function field names customization (#7405)
It is now possible to specify the GraphQL names of tracked SQL functions in
Postgres sources, and different names may be given to the `_aggregate` and
suffix-less versions. Aliases may be set by both
`/v1/metadata/pg_track_function` and the new API endpoint
`/v1/metadata/pg_set_function_customization.`
### Bug fixes and improvements
- server: add support for openapi json of REST Endpoints
- server: enable inherited roles by default in the graphql-engine
- server: support MSSQL insert mutations
- console: fix v2 metadata imports

View File

@ -125,7 +125,7 @@ Args syntax
* - comment
- false
- String
- Comment for the function. This comment would replace the auto-generated
- Comment for the function. This comment would replace the auto-generated
comment for the function field in the GraphQL schema.
.. _pg_untrack_function:
@ -173,6 +173,62 @@ Args syntax
- :ref:`SourceName <SourceName>`
- Name of the source database of the function (default: ``default``)
.. _pg_set_function_customization:
pg_set_function_customization
-----------------------------
``pg_set_function_customization`` allows you to customize any given function with
a custom name and custom root fields of an already tracked
function. This will **replace** the already present customization.
Set the configuration for a function called ``search_articles``:
.. code-block:: http
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "pg_set_function_customization",
"args": {
"function": "search_articles",
"source": "default",
"configuration": {
"custom_root_fields": {
"function": "FindArticles",
"function_aggregate": "FindArticlesAgg"
}
}
}
}
.. _pg_set_function_customization_syntax:
Args syntax
^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - function
- true
- :ref:`FunctionName <FunctionName>`
- Name of the function
* - configuration
- false
- :ref:`Function Configuration <function_configuration>`
- Configuration for the function
* - source
- false
- :ref:`SourceName <SourceName>`
- Name of the source database of the function (default: ``default``)
.. _pg_create_function_permission:
pg_create_function_permission

View File

@ -137,6 +137,11 @@ The various types of queries are listed in the following table:
- 1
- Remove a Postgres SQL function
* - :ref:`pg_set_function_customization <pg_set_function_customization>`
- :ref:`pg_set_function_customization_args <pg_set_function_customization_syntax>`
- 1
- Set function customization of an already tracked Postgres function
* - :ref:`pg_create_function_permission`
- :ref:`pg_create_function_permission_args <pg_create_function_permission_syntax>`
- 1

View File

@ -558,6 +558,27 @@ Custom Root Fields
- ``String``
- Customise the ``delete_<table-name>_by_pk`` root field
.. _custom_function_root_fields:
Custom Function Root Fields
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - function
- false
- ``String``
- Customise the ``<function-name>`` root field
* - function_aggregate
- false
- ``String``
- Customise the ``<function-name>_aggregete`` root field
.. _InsertPermission:
InsertPermission
@ -1593,6 +1614,16 @@ Function Configuration
- Required
- Schema
- Description
* - custom_name
- false
- ``String``
- Customise the ``<function-name>`` with the provided custom name value.
The GraphQL nodes for the function will be generated according to the custom name.
* - custom_root_fields
- false
- :ref:`Custom Function Root Fields <custom_function_root_fields>`
- Customise the root fields
* - session_argument
- false
- `String`

View File

@ -61,7 +61,7 @@ buildFunctionInfo ::
RawFunctionInfo ('Postgres pgKind) ->
Maybe Text ->
m (FunctionInfo ('Postgres pgKind), SchemaDependency)
buildFunctionInfo source qf systemDefined FunctionConfig {..} permissions rawFuncInfo comment =
buildFunctionInfo source qf systemDefined fc@FunctionConfig {..} permissions rawFuncInfo comment =
either (throw400 NotSupported . showErrors) pure
=<< MV.runValidateT validateFunction
where
@ -112,11 +112,17 @@ buildFunctionInfo source qf systemDefined FunctionConfig {..} permissions rawFun
inputArguments <- makeInputArguments
funcGivenName <- functionGraphQLName @('Postgres pgKind) qf `onLeft` throwError
let retTable = typeToTable returnType
retJsonAggSelect = bool JASSingleObject JASMultipleRows retSet
functionInfo =
FunctionInfo
qf
(getFunctionGQLName funcGivenName fc)
(getFunctionArgsGQLName funcGivenName fc)
(getFunctionAggregateGQLName funcGivenName fc)
systemDefined
funVol
exposeAs

View File

@ -197,17 +197,15 @@ buildFunctionRelayQueryFields ::
SelPermInfo ('Postgres pgKind) ->
m [FieldParser n (QueryRootField UnpreparedValue)]
buildFunctionRelayQueryFields sourceName sourceInfo queryTagsConfig functionName functionInfo tableName pkeyColumns selPerms = do
funcName <- functionGraphQLName @('Postgres pgKind) functionName `onLeft` throwError
let mkRF =
RFDB sourceName
. AB.mkAnyBackend
. SourceConfigWith sourceInfo queryTagsConfig
. QDBR
fieldName = funcName <> $$(G.litName "_connection")
fieldDesc = Just $ G.Description $ "execute function " <> functionName <<> " which returns " <>> tableName
fmap afold $
optionalFieldParser (mkRF . QDBConnection) $
selectFunctionConnection sourceName functionInfo fieldName fieldDesc pkeyColumns selPerms
selectFunctionConnection sourceName functionInfo fieldDesc pkeyColumns selPerms
----------------------------------------------------------------
-- Individual components

View File

@ -177,27 +177,29 @@ buildFunctionQueryFields ::
SelPermInfo b ->
m [FieldParser n (QueryRootField UnpreparedValue)]
buildFunctionQueryFields sourceName sourceInfo queryTagsConfig functionName functionInfo tableName selPerms = do
funcName <- functionGraphQLName @b functionName `onLeft` throwError
let mkRF =
RFDB sourceName
. AB.mkAnyBackend
. SourceConfigWith sourceInfo queryTagsConfig
. QDBR
-- select function
funcDesc =
Just . G.Description $
flip fromMaybe (_fiComment functionInfo) $ "execute function " <> functionName <<> " which returns " <>> tableName
-- select function agg
funcAggName = funcName <> $$(G.litName "_aggregate")
funcAggDesc = Just $ G.Description $ "execute function " <> functionName <<> " and query aggregates on result of table type " <>> tableName
queryResultType =
case _fiJsonAggSelect functionInfo of
JASMultipleRows -> QDBMultipleRows
JASSingleObject -> QDBSingleRow
catMaybes
<$> sequenceA
[ requiredFieldParser (mkRF . queryResultType) $ selectFunction sourceName functionInfo funcName funcDesc selPerms,
optionalFieldParser (mkRF . QDBAggregation) $ selectFunctionAggregate sourceName functionInfo funcAggName funcAggDesc selPerms
[ requiredFieldParser (mkRF . queryResultType) $ selectFunction sourceName functionInfo funcDesc selPerms,
optionalFieldParser (mkRF . QDBAggregation) $ selectFunctionAggregate sourceName functionInfo funcAggDesc selPerms
]
buildFunctionMutationFields ::
@ -212,16 +214,17 @@ buildFunctionMutationFields ::
SelPermInfo b ->
m [FieldParser n (MutationRootField UnpreparedValue)]
buildFunctionMutationFields sourceName sourceInfo queryTagsConfig functionName functionInfo tableName selPerms = do
funcName <- functionGraphQLName @b functionName `onLeft` throwError
let mkRF =
RFDB sourceName
. AB.mkAnyBackend
. SourceConfigWith sourceInfo queryTagsConfig
. MDBR
funcDesc = Just $ G.Description $ "execute VOLATILE function " <> functionName <<> " which returns " <>> tableName
jsonAggSelect = _fiJsonAggSelect functionInfo
catMaybes
<$> sequenceA
[ requiredFieldParser (mkRF . MDBFunction jsonAggSelect) $ selectFunction sourceName functionInfo funcName funcDesc selPerms
[ requiredFieldParser (mkRF . MDBFunction jsonAggSelect) $ selectFunction sourceName functionInfo funcDesc selPerms
-- TODO: do we want aggregate mutation functions?
]

View File

@ -536,34 +536,31 @@ selectFunction ::
SourceName ->
-- | SQL function info
FunctionInfo b ->
-- | field display name
G.Name ->
-- | field description, if any
Maybe G.Description ->
-- | select permissions of the target table
SelPermInfo b ->
m (FieldParser n (SelectExp b))
selectFunction sourceName function fieldName description selectPermissions = do
selectFunction sourceName fi@FunctionInfo {..} description selectPermissions = do
stringifyNum <- asks $ qcStringifyNum . getter
let tableName = _fiReturnType function
tableInfo <- askTableInfo sourceName tableName
tableInfo <- askTableInfo sourceName _fiReturnType
tableArgsParser <- tableArguments sourceName tableInfo selectPermissions
functionArgsParser <- customSQLFunctionArgs function
selectionSetParser <- returnFunctionParser sourceName tableInfo selectPermissions
functionArgsParser <- customSQLFunctionArgs fi _fiGQLName _fiGQLArgsName
let argsParser = liftA2 (,) functionArgsParser tableArgsParser
pure $
P.subselection fieldName description argsParser selectionSetParser
P.subselection _fiGQLName description argsParser selectionSetParser
<&> \((funcArgs, tableArgs'), fields) ->
IR.AnnSelectG
{ IR._asnFields = fields,
IR._asnFrom = IR.FromFunction (_fiName function) funcArgs Nothing,
IR._asnFrom = IR.FromFunction _fiSQLName funcArgs Nothing,
IR._asnPerm = tablePermissionsInfo selectPermissions,
IR._asnArgs = tableArgs',
IR._asnStrfyNum = stringifyNum
}
where
returnFunctionParser =
case _fiJsonAggSelect function of
case _fiJsonAggSelect of
JASSingleObject -> tableSelectionSet
JASMultipleRows -> tableSelectionList
@ -574,22 +571,19 @@ selectFunctionAggregate ::
SourceName ->
-- | SQL function info
FunctionInfo b ->
-- | field display name
G.Name ->
-- | field description, if any
Maybe G.Description ->
-- | select permissions of the target table
SelPermInfo b ->
m (Maybe (FieldParser n (AggSelectExp b)))
selectFunctionAggregate sourceName function fieldName description selectPermissions = runMaybeT do
let tableName = _fiReturnType function
selectFunctionAggregate sourceName fi@FunctionInfo {..} description selectPermissions = runMaybeT do
guard $ spiAllowAgg selectPermissions
xNodesAgg <- hoistMaybe $ nodesAggExtension @b
tableInfo <- askTableInfo sourceName tableName
tableInfo <- askTableInfo sourceName _fiReturnType
stringifyNum <- asks $ qcStringifyNum . getter
tableGQLName <- getTableGQLName tableInfo
tableArgsParser <- lift $ tableArguments sourceName tableInfo selectPermissions
functionArgsParser <- lift $ customSQLFunctionArgs function
functionArgsParser <- lift $ customSQLFunctionArgs fi _fiGQLAggregateName _fiGQLArgsName
aggregateParser <- lift $ tableAggregationFields sourceName tableInfo selectPermissions
selectionName <- lift $ pure tableGQLName <&> (<> $$(G.litName "_aggregate"))
nodesParser <- lift $ tableSelectionList sourceName tableInfo selectPermissions
@ -604,11 +598,11 @@ selectFunctionAggregate sourceName function fieldName description selectPermissi
IR.TAFAgg <$> P.subselection_ $$(G.litName "aggregate") Nothing aggregateParser
]
pure $
P.subselection fieldName description argsParser aggregationParser
P.subselection _fiGQLAggregateName description argsParser aggregationParser
<&> \((funcArgs, tableArgs'), fields) ->
IR.AnnSelectG
{ IR._asnFields = fields,
IR._asnFrom = IR.FromFunction (_fiName function) funcArgs Nothing,
IR._asnFrom = IR.FromFunction _fiSQLName funcArgs Nothing,
IR._asnPerm = tablePermissionsInfo selectPermissions,
IR._asnArgs = tableArgs',
IR._asnStrfyNum = stringifyNum
@ -621,8 +615,6 @@ selectFunctionConnection ::
SourceName ->
-- | SQL function info
FunctionInfo ('Postgres pgKind) ->
-- | field display name
G.Name ->
-- | field description, if any
Maybe G.Description ->
-- | primary key columns of the target table
@ -630,13 +622,13 @@ selectFunctionConnection ::
-- | select permissions of the target table
SelPermInfo ('Postgres pgKind) ->
m (Maybe (FieldParser n (ConnectionSelectExp ('Postgres pgKind))))
selectFunctionConnection sourceName function fieldName description pkeyColumns selectPermissions =
selectFunctionConnection sourceName fi@FunctionInfo {..} description pkeyColumns selectPermissions = do
let fieldName = _fiGQLName <> $$(G.litName "_connection")
for (relayExtension @('Postgres pgKind)) \xRelayInfo -> do
stringifyNum <- asks $ qcStringifyNum . getter
let tableName = _fiReturnType function
tableInfo <- askTableInfo sourceName tableName
tableInfo <- askTableInfo sourceName _fiReturnType
tableConnectionArgsParser <- tableConnectionArgs pkeyColumns sourceName tableInfo selectPermissions
functionArgsParser <- customSQLFunctionArgs function
functionArgsParser <- customSQLFunctionArgs fi _fiGQLName _fiGQLArgsName
selectionSetParser <- tableConnectionSelectionSet sourceName tableInfo selectPermissions
let argsParser = liftA2 (,) functionArgsParser tableConnectionArgsParser
pure $
@ -650,7 +642,7 @@ selectFunctionConnection sourceName function fieldName description pkeyColumns s
IR._csSelect =
IR.AnnSelectG
{ IR._asnFields = fields,
IR._asnFrom = IR.FromFunction (_fiName function) funcArgs Nothing,
IR._asnFrom = IR.FromFunction _fiSQLName funcArgs Nothing,
IR._asnPerm = tablePermissionsInfo selectPermissions,
IR._asnArgs = args,
IR._asnStrfyNum = stringifyNum
@ -1412,8 +1404,18 @@ remoteRelationshipField remoteFieldInfo = runMaybeT do
customSQLFunctionArgs ::
(BackendSchema b, MonadSchema n m, MonadTableInfo r m) =>
FunctionInfo b ->
G.Name ->
G.Name ->
m (InputFieldsParser n (IR.FunctionArgsExpTableRow b (UnpreparedValue b)))
customSQLFunctionArgs FunctionInfo {..} = functionArgs (FTACustomFunction _fiName) _fiInputArgs
customSQLFunctionArgs FunctionInfo {..} functionName functionArgsName =
functionArgs
( FTACustomFunction $
CustomFunctionNames
{ cfnFunctionName = functionName,
cfnArgsName = functionArgsName
}
)
_fiInputArgs
-- | Parses the arguments to the underlying sql function of a computed field or
-- a custom function. All arguments to the underlying sql function are parsed
@ -1463,8 +1465,8 @@ functionArgs functionTrackedAs (toList -> inputArgs) = do
computedFieldGQLName <- textToName $ computedFieldNameToText computedFieldName
tableGQLName <- getTableGQLName @b tableInfo
pure $ computedFieldGQLName <> $$(G.litName "_") <> tableGQLName <> $$(G.litName "_args")
FTACustomFunction functionName ->
fmap (<> $$(G.litName "_args")) $ functionGraphQLName @b functionName `onLeft` throwError
FTACustomFunction (CustomFunctionNames {cfnArgsName}) ->
pure cfnArgsName
let fieldName = $$(G.litName "args")
fieldDesc =
case functionTrackedAs of
@ -1472,8 +1474,8 @@ functionArgs functionTrackedAs (toList -> inputArgs) = do
G.Description $
"input parameters for computed field "
<> computedFieldName <<> " defined on table " <>> tableName
FTACustomFunction functionName ->
G.Description $ "input parameters for function " <>> functionName
FTACustomFunction (CustomFunctionNames {cfnFunctionName}) ->
G.Description $ "input parameters for function " <>> cfnFunctionName
objectParser =
P.object objectName Nothing (sequenceA argumentParsers) `P.bind` \arguments -> do
-- After successfully parsing, we create a dictionary of the parsed fields

View File

@ -2,7 +2,7 @@
-- Description: Create/delete SQL functions to/from Hasura metadata.
module Hasura.RQL.DDL.Schema.Function where
import Control.Lens ((^.))
import Control.Lens ((.~), (^.))
import Data.Aeson
import Data.HashMap.Strict qualified as Map
import Data.HashMap.Strict.InsOrd qualified as OMap
@ -289,3 +289,37 @@ runDropFunctionPermission (FunctionPermissionArgument functionName source role)
)
$ dropFunctionPermissionInMetadata @b source functionName role
pure successMsg
-- | Represents the payload of the API command 'pg_set_function_customization'.
--
-- See the Hasura API reference for a detailed description.
data SetFunctionCustomization b = SetFunctionCustomization
{ _sfcSource :: SourceName,
_sfcFunction :: FunctionName b,
_sfcConfiguration :: FunctionConfig
}
deriving instance Backend b => Show (SetFunctionCustomization b)
deriving instance Backend b => Eq (SetFunctionCustomization b)
instance (Backend b) => FromJSON (SetFunctionCustomization b) where
parseJSON = withObject "set function customization" $ \o ->
SetFunctionCustomization
<$> o .:? "source" .!= defaultSource
<*> o .: "function"
<*> o .: "configuration"
-- | Changes the custom names of a function. Used in the API command 'pg_set_function_customization'.
runSetFunctionCustomization ::
forall b m.
(QErrM m, CacheRWM m, MetadataM m, Backend b, BackendMetadata b) =>
SetFunctionCustomization b ->
m EncJSON
runSetFunctionCustomization (SetFunctionCustomization source function config) = do
void $ askFunInfo @b source function
buildSchemaCacheFor
(MOSourceObjId source $ AB.mkAnyBackend $ SMOFunction @b function)
$ MetadataModifier $
((functionMetadataSetter @b source function) . fmConfiguration) .~ config
return successMsg

View File

@ -16,6 +16,7 @@ module Hasura.RQL.Types
askTabInfoSource,
askTableCoreInfo,
askTableCoreInfoSource,
askFunInfo,
askFieldInfoMap,
askFieldInfoMapSource,
assertColumnExists,
@ -117,6 +118,22 @@ askTabInfo sourceName tableName = do
where
errMsg = "table " <> tableName <<> " does not exist in source: " <> sourceNameToText sourceName
-- | Tries to extract the function information from the metadata.
--
-- If the function does not exist, will throw a 'not-exists' error.
askFunInfo ::
forall b m.
(QErrM m, CacheRM m, Backend b) =>
SourceName ->
FunctionName b ->
m (FunctionInfo b)
askFunInfo sourceName functionName = do
rawSchemaCache <- askSchemaCache
unsafeFunctionInfo sourceName functionName (scSources rawSchemaCache)
`onNothing` throw400 NotExists errMsg
where
errMsg = "function " <> functionName <<> " does not exist in source: " <> sourceNameToText sourceName
askTabInfoSource ::
forall b m.
(QErrM m, TableInfoRM b m, Backend b) =>

View File

@ -16,6 +16,7 @@ import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Function
import Hasura.SQL.Backend
import Language.GraphQL.Draft.Syntax (Name)
newtype ComputedFieldName = ComputedFieldName {unComputedFieldName :: NonEmptyText}
deriving (Show, Eq, Ord, NFData, FromJSON, ToJSON, ToJSONKey, Q.ToPrepArg, ToTxt, Hashable, Q.FromCol, Generic, Cacheable)
@ -90,10 +91,19 @@ instance ToJSON FunctionSessionArgument where
toJSON (FunctionSessionArgument argName _) = toJSON argName
data FunctionTrackedAs (b :: BackendType)
= FTAComputedField !ComputedFieldName !SourceName !(TableName b)
| FTACustomFunction !(FunctionName b)
= FTAComputedField ComputedFieldName SourceName (TableName b)
| FTACustomFunction CustomFunctionNames
deriving (Generic)
-- | The function name and input arguments name for the "args" field parser.
--
-- > function_name(args: args_name)
data CustomFunctionNames = CustomFunctionNames
{ cfnFunctionName :: Name,
cfnArgsName :: Name
}
deriving (Show, Eq, Generic)
deriving instance Backend b => Show (FunctionTrackedAs b)
deriving instance Backend b => Eq (FunctionTrackedAs b)

View File

@ -5,6 +5,7 @@ import Data.Aeson
import Data.Aeson.Casing
import Data.Aeson.TH
import Data.Char (toLower)
import Data.List.Extended as LE
import Data.Sequence qualified as Seq
import Data.Text qualified as T
import Data.Text.Extended
@ -14,6 +15,7 @@ import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Common
import Hasura.SQL.Backend
import Hasura.Session
import Language.GraphQL.Draft.Syntax qualified as G
-- | https://www.postgresql.org/docs/current/xfunc-volatility.html
data FunctionVolatility
@ -104,26 +106,70 @@ $(deriveJSON hasuraJSON ''FunctionPermissionInfo)
type FunctionPermissionsMap = HashMap RoleName FunctionPermissionInfo
-- | Custom root fields for functions. When set, will be the names exposed
-- to the user in the schema.
--
-- See rfcs/function-root-field-customisation.md for more information.
data FunctionCustomRootFields = FunctionCustomRootFields
{ _fcrfFunction :: Maybe G.Name,
_fcrfFunctionAggregate :: Maybe G.Name
}
deriving (Show, Eq, Generic)
instance NFData FunctionCustomRootFields
instance Cacheable FunctionCustomRootFields
$(deriveToJSON hasuraJSON {omitNothingFields = True} ''FunctionCustomRootFields)
instance FromJSON FunctionCustomRootFields where
parseJSON = withObject "Object" $ \obj -> do
function <- obj .:? "function"
functionAggregate <- obj .:? "function_aggregate"
case (function, functionAggregate) of
(Just f, Just fa)
| f == fa ->
fail $
T.unpack $
"the following custom root field names are duplicated: "
<> toTxt f <<> " and " <>> toTxt fa
_ ->
pure ()
pure $ FunctionCustomRootFields function functionAggregate
-- | A function custom root fields without custom names set. This is the default.
emptyFunctionCustomRootFields :: FunctionCustomRootFields
emptyFunctionCustomRootFields =
FunctionCustomRootFields
{ _fcrfFunction = Nothing,
_fcrfFunctionAggregate = Nothing
}
-- | Tracked SQL function metadata. See 'buildFunctionInfo'.
data FunctionInfo (b :: BackendType) = FunctionInfo
{ _fiName :: !(FunctionName b),
_fiSystemDefined :: !SystemDefined,
_fiVolatility :: !FunctionVolatility,
{ _fiSQLName :: FunctionName b,
_fiGQLName :: G.Name,
_fiGQLArgsName :: G.Name,
_fiGQLAggregateName :: G.Name,
_fiSystemDefined :: SystemDefined,
_fiVolatility :: FunctionVolatility,
-- | In which part of the schema should this function be exposed?
--
-- See 'mkFunctionInfo' and '_fcExposedAs'.
_fiExposedAs :: !FunctionExposedAs,
_fiInputArgs :: !(Seq.Seq (FunctionInputArgument b)),
_fiExposedAs :: FunctionExposedAs,
_fiInputArgs :: Seq.Seq (FunctionInputArgument b),
-- | NOTE: when a table is created, a new composite type of the same name is
-- automatically created; so strictly speaking this field means "the function
-- returns the composite type corresponding to this table".
_fiReturnType :: !(TableName b),
_fiReturnType :: TableName b,
-- | this field represents the description of the function as present on the database
_fiDescription :: !(Maybe Text),
_fiPermissions :: !FunctionPermissionsMap,
_fiDescription :: Maybe Text,
-- | Roles to which the function is accessible
_fiJsonAggSelect :: !JsonAggSelect,
_fiComment :: !(Maybe Text)
_fiPermissions :: FunctionPermissionsMap,
_fiJsonAggSelect :: JsonAggSelect,
_fiComment :: Maybe Text
}
deriving (Generic)
@ -136,6 +182,56 @@ instance (Backend b) => ToJSON (FunctionInfo b) where
$(makeLenses ''FunctionInfo)
-- | Apply function name customization to function arguments, as detailed in
-- 'rfcs/function-root-field-customisation.md'. We want the different
-- variations of a function (i.e. basic, aggregate) to share the same type name
-- for their arguments.
getFunctionArgsGQLName ::
-- | The GQL version of the DB name of the function
G.Name ->
FunctionConfig ->
G.Name
getFunctionArgsGQLName
funcGivenName
FunctionConfig {..} =
fromMaybe funcGivenName _fcCustomName <> $$(G.litName "_args")
-- | Apply function name customization to the basic function variation, as
-- detailed in 'rfcs/function-root-field-customisation.md'.
getFunctionGQLName ::
G.Name ->
FunctionConfig ->
G.Name
getFunctionGQLName
funcGivenName
FunctionConfig
{ _fcCustomRootFields = FunctionCustomRootFields {..},
..
} =
choice
[ _fcrfFunction,
_fcCustomName
]
& fromMaybe funcGivenName
-- | Apply function name customization to the aggregate function variation, as
-- detailed in 'rfcs/function-root-field-customisation.md'.
getFunctionAggregateGQLName ::
G.Name ->
FunctionConfig ->
G.Name
getFunctionAggregateGQLName
funcGivenName
FunctionConfig
{ _fcCustomRootFields = FunctionCustomRootFields {..},
..
} =
choice
[ _fcrfFunctionAggregate,
_fcCustomName <&> (<> $$(G.litName "_aggregate"))
]
& fromMaybe (funcGivenName <> $$(G.litName "_aggregate"))
getInputArgs :: FunctionInfo b -> Seq.Seq (FunctionArg b)
getInputArgs =
Seq.fromList . mapMaybe (^? _IAUserProvided) . toList . _fiInputArgs
@ -144,15 +240,18 @@ type FunctionCache b = HashMap (FunctionName b) (FunctionInfo b) -- info of all
-- Metadata requests related types
-- | Tracked function configuration, and payload of the 'track_function' API call.
-- | Tracked function configuration, and payload of the 'pg_track_function' and
-- 'pg_set_function_customization' API calls.
data FunctionConfig = FunctionConfig
{ _fcSessionArgument :: !(Maybe FunctionArgName),
{ _fcSessionArgument :: Maybe FunctionArgName,
-- | In which top-level field should we expose this function?
--
-- The user might omit this, in which case we'll infer the location from the
-- SQL functions volatility. See 'mkFunctionInfo' or the @track_function@ API
-- docs for details of validation, etc.
_fcExposedAs :: !(Maybe FunctionExposedAs)
_fcExposedAs :: Maybe FunctionExposedAs,
_fcCustomRootFields :: FunctionCustomRootFields,
_fcCustomName :: Maybe G.Name
}
deriving (Show, Eq, Generic)
@ -160,11 +259,19 @@ instance NFData FunctionConfig
instance Cacheable FunctionConfig
$(deriveJSON hasuraJSON {omitNothingFields = True} ''FunctionConfig)
instance FromJSON FunctionConfig where
parseJSON = withObject "FunctionConfig" $ \obj ->
FunctionConfig
<$> obj .:? "session_argument"
<*> obj .:? "exposed_as"
<*> obj .:? "custom_root_fields" .!= emptyFunctionCustomRootFields
<*> obj .:? "custom_name"
$(deriveToJSON hasuraJSON {omitNothingFields = True} ''FunctionConfig)
-- | The default function config; v1 of the API implies this.
emptyFunctionConfig :: FunctionConfig
emptyFunctionConfig = FunctionConfig Nothing Nothing
emptyFunctionConfig = FunctionConfig Nothing Nothing emptyFunctionCustomRootFields Nothing
-- Lists are used to model overloaded functions.
type DBFunctionsMetadata b = HashMap (FunctionName b) [RawFunctionInfo b]

View File

@ -489,6 +489,16 @@ tableMetadataSetter ::
tableMetadataSetter source table =
metaSources . ix source . toSourceMetadata . smTables . ix table
-- | A lens setter for the metadata of a specific function as identified by
-- the source name and function name.
functionMetadataSetter ::
(BackendMetadata b) =>
SourceName ->
FunctionName b ->
ASetter' Metadata (FunctionMetadata b)
functionMetadataSetter source function =
metaSources . ix source . toSourceMetadata . smFunctions . ix function
data MetadataNoSources = MetadataNoSources
{ _mnsTables :: !(Tables ('Postgres 'Vanilla)),
_mnsFunctions :: !(Functions ('Postgres 'Vanilla)),

View File

@ -86,7 +86,8 @@ tablePermissionsCommands =
]
functionCommands =
[ commandParser "track_function" $ RMTrackFunction . mkAnyBackend @b,
commandParser "untrack_function" $ RMUntrackFunction . mkAnyBackend @b
commandParser "untrack_function" $ RMUntrackFunction . mkAnyBackend @b,
commandParser "set_function_customization" $ RMSetFunctionCustomization . mkAnyBackend @b
]
functionPermissionsCommands =
[ commandParser "create_function_permission" $ RMCreateFunctionPermission . mkAnyBackend @b,

View File

@ -85,6 +85,7 @@ data RQLMetadataV1
| -- Functions
RMTrackFunction !(AnyBackend TrackFunctionV2)
| RMUntrackFunction !(AnyBackend UnTrackFunction)
| RMSetFunctionCustomization (AnyBackend SetFunctionCustomization)
| -- Functions permissions
RMCreateFunctionPermission !(AnyBackend FunctionPermissionArgument)
| RMDropFunctionPermission !(AnyBackend FunctionPermissionArgument)
@ -395,6 +396,7 @@ runMetadataQueryV1M env currentResourceVersion = \case
RMRenameSource q -> runRenameSource q
RMTrackTable q -> dispatchMetadata runTrackTableV2Q q
RMUntrackTable q -> dispatchMetadata runUntrackTableQ q
RMSetFunctionCustomization q -> dispatchMetadata runSetFunctionCustomization q
RMSetTableCustomization q -> dispatchMetadata runSetTableCustomization q
RMPgSetTableIsEnum q -> runSetExistingTableIsEnumQ q
RMCreateInsertPermission q -> dispatchMetadata runCreatePerm q

View File

@ -49,6 +49,7 @@ data RQLMetadataV1
| -- Functions
RMTrackFunction !(AnyBackend TrackFunctionV2)
| RMUntrackFunction !(AnyBackend UnTrackFunction)
| RMSetFunctionCustomization (AnyBackend SetFunctionCustomization)
| -- Functions permissions
RMCreateFunctionPermission !(AnyBackend FunctionPermissionArgument)
| RMDropFunctionPermission !(AnyBackend FunctionPermissionArgument)

View File

@ -0,0 +1,349 @@
# # Overview
# In this file, we are testing that:
#
# * We can supply custom names to `pg_track_function` and have them applied in the schema
# * Name aliases are applied correctly for all the possible specified cases, see https://github.com/hasura/graphql-engine-mono/pull/2419
# * We can omit custom names from `pg_track_function`
# * We can set and unset custom names with `set_function_customization`
# * We test that we don't introduce name clashes
- description: Define the SQL function the tests will be using
url: /v2/query
status: 200
query:
type: run_sql
args:
sql: |
create function unaliased_function(t test)
returns setof post as $$
select *
from post
where
title ilike ('%' || t.name || '%')
$$ language sql stable;
# Test: We can supply (all) aliases via `pg_track_function`, and the names are applied.
- description: Test that we supply all custom names to `pg_track_function`.
url: /v1/metadata
status: 200
response:
message: success
query:
type: pg_track_function
args:
function: unaliased_function
configuration:
custom_root_fields:
function: nameBase
function_aggregate: nameAggregate
- description: Execute the function without the alias, which should fail
url: /v1/graphql
status: 200
query:
query: |
query {
unaliased_function (args: {t: "(1, hasura,311cf381-71e7-449b-bac5-86cd6deafd5b)"}) {
title
content
}
}
response:
"errors": [
{
"message": "field \"unaliased_function\" not found in type: 'query_root'",
"extensions": {
"code": "validation-failed",
"path": "$.selectionSet.unaliased_function"
}
}
]
- description: Execute the function by its alias, which should succeed
url: /v1/graphql
status: 200
query:
query: |
query {
nameBase (args: {t: "(1, hasura,311cf381-71e7-449b-bac5-86cd6deafd5b)"}) {
title
content
}
}
response:
data:
nameBase:
- title: post by hasura
content: content for post
- description: Execute the function with aggregation by its aggregation-alias, which should succeed
url: /v1/graphql
status: 200
query:
query: |
query {
nameAggregate (args: {t: "(1, hasura,311cf381-71e7-449b-bac5-86cd6deafd5b)"}) {
aggregate {
count
}
}
}
response:
data:
nameAggregate:
aggregate:
count: 1
- description: Teardown the test of fully given aliases
url: /v1/metadata
status: 200
response:
message: success
query:
type: pg_untrack_function
args:
name: unaliased_function
# Test: We can supply just the base alias via `pg_track_function`, and the names are applied.
- description: Test that we supply all custom names to `pg_track_function`.
url: /v1/metadata
status: 200
response:
message: success
query:
type: pg_track_function
args:
function: unaliased_function
configuration:
custom_name: aliased
- description: Execute the function without the alias, which should fail
url: /v1/graphql
status: 200
query:
query: |
query {
unaliased_function (args: {t: "(1, hasura,311cf381-71e7-449b-bac5-86cd6deafd5b)"}) {
title
content
}
}
response:
"errors": [
{
"message": "field \"unaliased_function\" not found in type: 'query_root'",
"extensions": {
"code": "validation-failed",
"path": "$.selectionSet.unaliased_function"
}
}
]
- description: Execute the function by its alias, which should succeed
url: /v1/graphql
status: 200
query:
query: |
query {
aliased (args: {t: "(1, hasura,311cf381-71e7-449b-bac5-86cd6deafd5b)"}) {
title
content
}
}
response:
data:
aliased:
- title: post by hasura
content: content for post
- description: Execute the function with aggregation by its aggregation-alias, which should succeed
url: /v1/graphql
status: 200
query:
query: |
query {
aliased_aggregate (args: {t: "(1, hasura,311cf381-71e7-449b-bac5-86cd6deafd5b)"}) {
aggregate {
count
}
}
}
response:
data:
aliased_aggregate:
aggregate:
count: 1
# Test: Using set_function_customization
- description: Test that we fail on non-existing functions
url: /v1/metadata
status: 400
response:
code: not-exists
error: 'function "non_existing_function" does not exist in source: default'
path: $.args
query:
type: pg_set_function_customization
args:
function: non_existing_function
configuration: { }
- description: Test that we fail on duplicate custom root fields
url: /v1/metadata
status: 400
response:
code: parse-failed
error: "Error when parsing command set_function_customization.\n\
See our documentation at https://hasura.io/docs/latest/graphql/core/api-reference/metadata-api/index.html#metadata-apis.\n\
Internal error message: the following custom root field names are duplicated: \"duplicate_name\" and \"duplicate_name\""
path: $.args.configuration.custom_root_fields
query:
type: pg_set_function_customization
args:
function: unaliased_function
configuration:
custom_root_fields:
function: duplicate_name
function_aggregate: duplicate_name
- description: Test that we can clear the customized names
url: /v1/metadata
status: 200
response:
message: success
query:
type: pg_set_function_customization
args:
function: unaliased_function
configuration: { }
- descripion: Execute the function without the alias, which should now work
url: /v1/graphql
status: 200
query:
query: |
query {
unaliased_function (args: {t: "(1, hasura,311cf381-71e7-449b-bac5-86cd6deafd5b)"}) {
title
content
}
}
response:
data:
unaliased_function:
- title: post by hasura
content: content for post
- description: Test that we can set customized names using `set_function_customization`
url: /v1/metadata
status: 200
response:
message: success
query:
type: pg_set_function_customization
args:
function: unaliased_function
configuration:
custom_root_fields:
function: nameBaseOther
- description: Execute the function by its alias
url: /v1/graphql
status: 200
query:
query: |
query {
nameBaseOther (args: {t: "(1, hasura,311cf381-71e7-449b-bac5-86cd6deafd5b)"}) {
title
content
}
}
response:
data:
nameBaseOther:
- title: post by hasura
content: content for post
- description: Execute the function with aggregation (unaliased), which should succeed.
url: /v1/graphql
status: 200
query:
query: |
query {
unaliased_function_aggregate (args: {t: "(1, hasura,311cf381-71e7-449b-bac5-86cd6deafd5b)"}) {
aggregate {
count
}
}
}
response:
data:
unaliased_function_aggregate:
aggregate:
count: 1
# Test: We reject name clashes: A function is aliased to an existing (unaliased) function.
# Note: We are not going to go to great lengths to ensure all the different
# conceivable ways collisions could be introduced are avoided, because there is
# a general mechanism in the code that builds the schema cache which ensures no
# collisions appear in the final, generated schema.
# (issue https://github.com/hasura/graphql-engine-mono/issues/1868 has a bit
# more info on this subject)
- description: "Setup: Define a separate function to cause clashes"
url: /v2/query
status: 200
query:
type: run_sql
args:
sql: |
create function unaliased_function2(t test)
returns setof post as $$
select *
from post
where
title ilike ('%' || t.name || '%')
$$ language sql stable;
- description: "Setup: Track the new function"
url: /v1/metadata
status: 200
response:
message: success
query:
type: pg_track_function
args:
function: unaliased_function2
- description: "Setup: clear up aliases of unaliased_function"
url: /v1/metadata
status: 200
response:
message: success
query:
type: pg_set_function_customization
args:
function: unaliased_function
configuration: {}
- description: Test that we cannot alias `unaliased_function2` to `unaliased_function`
url: /v1/metadata
status: 500
response:
code: unexpected
error: 'found duplicate fields in selection set: unaliased_function_aggregate, unaliased_function'
path: $.args
query:
type: pg_set_function_customization
args:
function: unaliased_function2
configuration:
custom_name: unaliased_function

View File

@ -1019,6 +1019,9 @@ class TestGraphQLQueryFunctions:
def test_tracking_function_with_composite_type_argument(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/track_non_base_function_arg_type.yaml')
def test_tracking_function_with_customized_names(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/track_customised_names.yaml')
@classmethod
def dir(cls):
return 'queries/graphql_query/functions'