Moving kriti function references into a single module for coordination of availability

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5095
GitOrigin-RevId: 8394c33f0182baad53306c6efc3ab720957b7339
This commit is contained in:
Lyndon Maydwell 2022-07-21 17:05:46 +10:00 committed by hasura-bot
parent b326fd978a
commit 6821b90910
6 changed files with 82 additions and 47 deletions

View File

@ -199,7 +199,7 @@ Please submit any feedback you may have for this feature at https://github.com/h
### Bug fixes and improvements ### Bug fixes and improvements
- server: fix bug where hasura SQL trigger was not dropped when MSSQL source is dropped - server: fix bug where hasura SQL trigger was not dropped when MSSQL source is dropped
- server: Kriti `basicFunctions` now available for REST Connectors - server: Kriti `basicFunctions` now available for REST Connectors and Webhook Transforms
- server: use `root_field_namespace` as prefix for remote schema (fixes #8438) - server: use `root_field_namespace` as prefix for remote schema (fixes #8438)
- server: allow all argument types for BigQuery routines - server: allow all argument types for BigQuery routines
- server: fix prefix/suffix behaviour for `graphql-default` naming convention (fixes #8544) - server: fix prefix/suffix behaviour for `graphql-default` naming convention (fixes #8544)

View File

@ -395,6 +395,7 @@ library
, Control.Monad.Unique , Control.Monad.Unique
, Data.Aeson.Extended , Data.Aeson.Extended
, Data.Aeson.KeyMap.Extended , Data.Aeson.KeyMap.Extended
, Data.Aeson.Kriti.Functions
, Data.Environment , Data.Environment
, Data.HashMap.Strict.Extended , Data.HashMap.Strict.Extended
, Data.HashMap.Strict.Multi , Data.HashMap.Strict.Multi

View File

@ -0,0 +1,65 @@
-- | Module of reusable functions for Kriti transforms.
--
-- NOTE: This defines an alternative `runKritiWith` that includes the basicFunctions by default.
-- You should probably invoke Kriti through this module rather than directly in order to
-- make updating the functions available only require touching this module.
--
-- TODO: This should be added to the documentation and referenced in (for-example) REST Connectors once
-- the documentation refactor project is complete.
module Data.Aeson.Kriti.Functions (runKriti, runKritiWith, basicFunctions, environmentFunctions, sessionFunctions) where
import Control.Arrow (left)
import Data.Aeson qualified as J
import Data.Environment qualified as Env
import Data.HashMap.Strict qualified as M
import Data.Text qualified as T
import Hasura.Prelude
import Hasura.Session (SessionVariables, getSessionVariableValue, mkSessionVariable)
import Kriti qualified
import Kriti.CustomFunctions qualified as Kriti
import Kriti.Error (SerializeError (serialize), SerializedError)
import Kriti.Error qualified as Kriti
type KritiFunc = J.Value -> Either Kriti.CustomFunctionError J.Value
-- | `Data.Aeson.Kriti.Functions.runKriti` attaches the basicFunctions by default
-- NOTE: The error type is SerializedError due to KritiError not currently being exported
runKriti :: Text -> [(Text, J.Value)] -> Either SerializedError J.Value
runKriti t m = left serialize $ Kriti.runKritiWith t m basicFunctions
-- | `Data.Aeson.Kriti.Functions.runKritiWith` attaches the basicFunctions by default.
runKritiWith :: Text -> [(Text, J.Value)] -> HashMap Text KritiFunc -> Either SerializedError J.Value
runKritiWith t m f = left serialize $ Kriti.runKritiWith t m (basicFunctions <> f)
-- | Re-Export of the Kriti 'stdlib'
basicFunctions :: M.HashMap Text KritiFunc
basicFunctions = Kriti.basicFuncMap
-- | Functions that interact with environment variables
environmentFunctions :: Env.Environment -> M.HashMap Text KritiFunc
environmentFunctions env =
M.fromList
[ ("getEnvironmentVariable", getEnvVar)
]
where
getEnvVar :: J.Value -> Either Kriti.CustomFunctionError J.Value
getEnvVar = \case
J.Null -> Right $ J.Null
J.String k -> Right $ J.toJSON $ Env.lookupEnv env (T.unpack k)
_ -> Left $ Kriti.CustomFunctionError "Environment variable name should be a string"
-- | Functions that interact with HGE session during requests
sessionFunctions :: Maybe SessionVariables -> M.HashMap Text KritiFunc
sessionFunctions sessionVars = M.singleton "getSessionVariable" getSessionVar
where
-- Returns Null if session-variables aren't passed in
-- Throws an error if session variable isn't found. Perhaps a version that returns null would also be useful.
-- Lookups are case-insensitive
getSessionVar :: J.Value -> Either Kriti.CustomFunctionError J.Value
getSessionVar = \case
J.Null -> Right $ J.Null
J.String txt ->
case sessionVars >>= getSessionVariableValue (mkSessionVariable txt) of
Just x -> Right $ J.String x
Nothing -> Left . Kriti.CustomFunctionError $ "Session variable \"" <> txt <> "\" not found"
_ -> Left $ Kriti.CustomFunctionError "Session variable name should be a string"

View File

@ -7,6 +7,7 @@ where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
import Data.Aeson qualified as J import Data.Aeson qualified as J
import Data.Aeson.Kriti.Functions qualified as KFunc
import Data.Environment qualified as Env import Data.Environment qualified as Env
import Data.HashMap.Strict qualified as M import Data.HashMap.Strict qualified as M
import Data.Text qualified as T import Data.Text qualified as T
@ -14,8 +15,6 @@ import Hasura.Backends.DataConnector.API qualified as API
import Hasura.Backends.DataConnector.Adapter.Types (ConnSourceConfig (ConnSourceConfig, template, value), SourceConfig (..)) import Hasura.Backends.DataConnector.Adapter.Types (ConnSourceConfig (ConnSourceConfig, template, value), SourceConfig (..))
import Hasura.Base.Error (Code (NotSupported), QErr, throw400) import Hasura.Base.Error (Code (NotSupported), QErr, throw400)
import Hasura.Prelude import Hasura.Prelude
import Kriti qualified
import Kriti.CustomFunctions qualified as Kriti
import Kriti.Error qualified as Kriti import Kriti.Error qualified as Kriti
transformConfig :: (MonadError QErr m) => API.Config -> Maybe Text -> [(T.Text, J.Value)] -> Env.Environment -> m API.Config transformConfig :: (MonadError QErr m) => API.Config -> Maybe Text -> [(T.Text, J.Value)] -> Env.Environment -> m API.Config
@ -23,8 +22,8 @@ transformConfig config maybeTemplate scope env = do
case maybeTemplate of case maybeTemplate of
Nothing -> pure config Nothing -> pure config
(Just t) -> (Just t) ->
case Kriti.runKritiWith t (("$config", J.toJSON config) : scope) (additionalFunctions env) of case KFunc.runKritiWith t (("$config", J.toJSON config) : scope) (additionalFunctions env) of
Left e -> throw400 NotSupported $ "transformConfig: Kriti template transform failed - " <> tshow (Kriti.serialize e) Left e -> throw400 NotSupported $ "transformConfig: Kriti template transform failed - " <> tshow e
Right (J.Object r) -> pure $ API.Config r Right (J.Object r) -> pure $ API.Config r
Right o -> throw400 NotSupported $ "transformConfig: Kriti did not decode into Object - " <> tshow o Right o -> throw400 NotSupported $ "transformConfig: Kriti did not decode into Object - " <> tshow o
@ -37,10 +36,4 @@ transformConnSourceConfig :: (MonadError QErr m) => ConnSourceConfig -> [(T.Text
transformConnSourceConfig ConnSourceConfig {value, template} scope env = transformConfig value template scope env transformConnSourceConfig ConnSourceConfig {value, template} scope env = transformConfig value template scope env
additionalFunctions :: Env.Environment -> M.HashMap T.Text (J.Value -> Either Kriti.CustomFunctionError J.Value) additionalFunctions :: Env.Environment -> M.HashMap T.Text (J.Value -> Either Kriti.CustomFunctionError J.Value)
additionalFunctions env = M.singleton "env" getEnv <> Kriti.basicFuncMap additionalFunctions env = KFunc.environmentFunctions env
where
getEnv :: J.Value -> Either Kriti.CustomFunctionError J.Value
getEnv x = case x of
J.Null -> Right $ J.Null
J.String k -> Right $ J.toJSON $ Env.lookupEnv env (T.unpack k)
_ -> Left $ Kriti.CustomFunctionError "Environment variable name should be a string"

View File

@ -55,13 +55,13 @@ where
import Control.Lens (Lens', lens, set, traverseOf, view) import Control.Lens (Lens', lens, set, traverseOf, view)
import Data.Aeson (FromJSON, ToJSON) import Data.Aeson (FromJSON, ToJSON)
import Data.Aeson.Extended qualified as J import Data.Aeson.Extended qualified as J
import Data.Aeson.Kriti.Functions qualified as KFunc
import Data.Bifunctor (first) import Data.Bifunctor (first)
import Data.ByteString.Lazy qualified as BL import Data.ByteString.Lazy qualified as BL
import Data.CaseInsensitive qualified as CI import Data.CaseInsensitive qualified as CI
import Data.Coerce (Coercible) import Data.Coerce (Coercible)
import Data.Functor.Barbie (AllBF, ApplicativeB, ConstraintsB, FunctorB, TraversableB) import Data.Functor.Barbie (AllBF, ApplicativeB, ConstraintsB, FunctorB, TraversableB)
import Data.Functor.Barbie qualified as B import Data.Functor.Barbie qualified as B
import Data.HashMap.Strict qualified as M
import Data.Text.Encoding qualified as TE import Data.Text.Encoding qualified as TE
import Data.Validation qualified as V import Data.Validation qualified as V
import Hasura.Incremental (Cacheable) import Hasura.Incremental (Cacheable)
@ -73,9 +73,7 @@ import Hasura.RQL.DDL.Webhook.Transform.Headers (Headers (..), HeadersTransformF
import Hasura.RQL.DDL.Webhook.Transform.Method import Hasura.RQL.DDL.Webhook.Transform.Method
import Hasura.RQL.DDL.Webhook.Transform.QueryParams import Hasura.RQL.DDL.Webhook.Transform.QueryParams
import Hasura.RQL.DDL.Webhook.Transform.Url import Hasura.RQL.DDL.Webhook.Transform.Url
import Hasura.Session (SessionVariables, getSessionVariableValue, mkSessionVariable) import Hasura.Session (SessionVariables)
import Kriti qualified (runKriti)
import Kriti.Error qualified as Kriti (CustomFunctionError (CustomFunctionError), serialize)
import Network.HTTP.Client.Transformable qualified as HTTP import Network.HTTP.Client.Transformable qualified as HTTP
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
@ -376,18 +374,8 @@ buildRespTransformCtx reqCtx sessionVars engine respBody =
{ responseTransformBody = fromMaybe J.Null $ J.decode @J.Value respBody, { responseTransformBody = fromMaybe J.Null $ J.decode @J.Value respBody,
responseTransformReqCtx = J.toJSON reqCtx, responseTransformReqCtx = J.toJSON reqCtx,
responseTransformEngine = engine, responseTransformEngine = engine,
responseTransformFunctions = M.singleton "getSessionVariable" getSessionVar responseTransformFunctions = KFunc.sessionFunctions sessionVars
} }
where
getSessionVar :: J.Value -> Either Kriti.CustomFunctionError J.Value
getSessionVar inp = case inp of
J.String txt ->
case sessionVarValue of
Just x -> Right $ J.String x
Nothing -> Left . Kriti.CustomFunctionError $ "Session variable \"" <> txt <> "\" not found"
where
sessionVarValue = sessionVars >>= getSessionVariableValue (mkSessionVariable txt)
_ -> Left $ Kriti.CustomFunctionError "Session variable name should be a string"
-- | Construct a Template Transformation function for Responses -- | Construct a Template Transformation function for Responses
-- --
@ -403,7 +391,7 @@ mkRespTemplateTransform _ Body.Remove _ = pure J.Null
mkRespTemplateTransform engine (Body.ModifyAsJSON (Template template)) ResponseTransformCtx {..} = mkRespTemplateTransform engine (Body.ModifyAsJSON (Template template)) ResponseTransformCtx {..} =
let context = [("$body", responseTransformBody), ("$request", responseTransformReqCtx)] let context = [("$body", responseTransformBody), ("$request", responseTransformReqCtx)]
in case engine of in case engine of
Kriti -> first (TransformErrorBundle . pure . J.toJSON . Kriti.serialize) $ Kriti.runKriti template context Kriti -> first (TransformErrorBundle . pure . J.toJSON) $ KFunc.runKriti template context
mkRespTemplateTransform engine (Body.ModifyAsFormURLEncoded formTemplates) context = mkRespTemplateTransform engine (Body.ModifyAsFormURLEncoded formTemplates) context =
case engine of case engine of
Kriti -> do Kriti -> do

View File

@ -43,6 +43,7 @@ import Control.Arrow (left)
import Control.Lens (bimap, view) import Control.Lens (bimap, view)
import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey) import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey)
import Data.Aeson qualified as J import Data.Aeson qualified as J
import Data.Aeson.Kriti.Functions as KFunc
import Data.Binary.Builder (toLazyByteString) import Data.Binary.Builder (toLazyByteString)
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.ByteString.Builder.Scientific (scientificBuilder) import Data.ByteString.Builder.Scientific (scientificBuilder)
@ -54,9 +55,7 @@ import Data.Text.Encoding qualified as TE
import Data.Validation (Validation, fromEither) import Data.Validation (Validation, fromEither)
import Hasura.Incremental (Cacheable) import Hasura.Incremental (Cacheable)
import Hasura.Prelude import Hasura.Prelude
import Hasura.Session (SessionVariables, getSessionVariableValue, mkSessionVariable) import Hasura.Session (SessionVariables)
import Kriti (runKritiWith)
import Kriti.CustomFunctions qualified as Kriti (basicFuncMap)
import Kriti.Error qualified as Kriti (CustomFunctionError (..), serialize) import Kriti.Error qualified as Kriti (CustomFunctionError (..), serialize)
import Kriti.Parser qualified as Kriti (parser) import Kriti.Parser qualified as Kriti (parser)
import Network.HTTP.Client.Transformable qualified as HTTP import Network.HTTP.Client.Transformable qualified as HTTP
@ -166,25 +165,14 @@ mkReqTransformCtx url sessionVars rtcEngine reqData =
view HTTP.queryParams reqData & fmap \(key, val) -> view HTTP.queryParams reqData & fmap \(key, val) ->
(TE.decodeUtf8 key, fmap TE.decodeUtf8 val) (TE.decodeUtf8 key, fmap TE.decodeUtf8 val)
in Just $ J.toJSON queryParams in Just $ J.toJSON queryParams
rtcFunctions = M.singleton "getSessionVariable" getSessionVar
in RequestTransformCtx in RequestTransformCtx
{ rtcBaseUrl, { rtcBaseUrl,
rtcBody, rtcBody,
rtcSessionVariables, rtcSessionVariables,
rtcQueryParams, rtcQueryParams,
rtcEngine, rtcEngine,
rtcFunctions = rtcFunctions <> Kriti.basicFuncMap rtcFunctions = KFunc.sessionFunctions sessionVars
} }
where
getSessionVar :: J.Value -> Either Kriti.CustomFunctionError J.Value
getSessionVar inp = case inp of
J.String txt ->
case sessionVarValue of
Just x -> Right $ J.String x
Nothing -> Left . Kriti.CustomFunctionError $ "Session variable \"" <> txt <> "\" not found"
where
sessionVarValue = sessionVars >>= getSessionVariableValue (mkSessionVariable txt)
_ -> Left $ Kriti.CustomFunctionError "Session variable name should be a string"
-- | Common context that is made available to all response transformations. -- | Common context that is made available to all response transformations.
data ResponseTransformCtx = ResponseTransformCtx data ResponseTransformCtx = ResponseTransformCtx
@ -257,9 +245,9 @@ runRequestTemplateTransform template RequestTransformCtx {rtcEngine = Kriti, ..}
[ ("$query_params",) <$> rtcQueryParams, [ ("$query_params",) <$> rtcQueryParams,
("$base_url",) <$> rtcBaseUrl ("$base_url",) <$> rtcBaseUrl
] ]
eResult = runKritiWith (unTemplate $ template) context rtcFunctions eResult = KFunc.runKritiWith (unTemplate $ template) context rtcFunctions
in eResult & left \kritiErr -> in eResult & left \kritiErr ->
let renderedErr = J.toJSON $ Kriti.serialize kritiErr let renderedErr = J.toJSON kritiErr
in TransformErrorBundle [renderedErr] in TransformErrorBundle [renderedErr]
-- TODO: Should this live in 'Hasura.RQL.DDL.Webhook.Transform.Validation'? -- TODO: Should this live in 'Hasura.RQL.DDL.Webhook.Transform.Validation'?
@ -291,9 +279,9 @@ runResponseTemplateTransform ::
Either TransformErrorBundle J.Value Either TransformErrorBundle J.Value
runResponseTemplateTransform template ResponseTransformCtx {responseTransformEngine = Kriti, ..} = runResponseTemplateTransform template ResponseTransformCtx {responseTransformEngine = Kriti, ..} =
let context = [("$body", responseTransformBody), ("$request", responseTransformReqCtx)] let context = [("$body", responseTransformBody), ("$request", responseTransformReqCtx)]
eResult = runKritiWith (unTemplate $ template) context responseTransformFunctions eResult = KFunc.runKritiWith (unTemplate $ template) context responseTransformFunctions
in eResult & left \kritiErr -> in eResult & left \kritiErr ->
let renderedErr = J.toJSON $ Kriti.serialize kritiErr let renderedErr = J.toJSON kritiErr
in TransformErrorBundle [renderedErr] in TransformErrorBundle [renderedErr]
------------------------------------------------------------------------------- -------------------------------------------------------------------------------