diff --git a/CHANGELOG.md b/CHANGELOG.md index 15ce30691f2..c065a0b23a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -199,7 +199,7 @@ Please submit any feedback you may have for this feature at https://github.com/h ### Bug fixes and improvements - 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: allow all argument types for BigQuery routines - server: fix prefix/suffix behaviour for `graphql-default` naming convention (fixes #8544) diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 124d68f6e62..24a58c254c8 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -395,6 +395,7 @@ library , Control.Monad.Unique , Data.Aeson.Extended , Data.Aeson.KeyMap.Extended + , Data.Aeson.Kriti.Functions , Data.Environment , Data.HashMap.Strict.Extended , Data.HashMap.Strict.Multi diff --git a/server/src-lib/Data/Aeson/Kriti/Functions.hs b/server/src-lib/Data/Aeson/Kriti/Functions.hs new file mode 100644 index 00000000000..fccd7a5ba8f --- /dev/null +++ b/server/src-lib/Data/Aeson/Kriti/Functions.hs @@ -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" diff --git a/server/src-lib/Hasura/Backends/DataConnector/Adapter/ConfigTransform.hs b/server/src-lib/Hasura/Backends/DataConnector/Adapter/ConfigTransform.hs index f426ae6e39a..54a3be7d11d 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Adapter/ConfigTransform.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Adapter/ConfigTransform.hs @@ -7,6 +7,7 @@ where -------------------------------------------------------------------------------- import Data.Aeson qualified as J +import Data.Aeson.Kriti.Functions qualified as KFunc import Data.Environment qualified as Env import Data.HashMap.Strict qualified as M 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.Base.Error (Code (NotSupported), QErr, throw400) import Hasura.Prelude -import Kriti qualified -import Kriti.CustomFunctions 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 @@ -23,8 +22,8 @@ transformConfig config maybeTemplate scope env = do case maybeTemplate of Nothing -> pure config (Just t) -> - case Kriti.runKritiWith t (("$config", J.toJSON config) : scope) (additionalFunctions env) of - Left e -> throw400 NotSupported $ "transformConfig: Kriti template transform failed - " <> tshow (Kriti.serialize e) + case KFunc.runKritiWith t (("$config", J.toJSON config) : scope) (additionalFunctions env) of + Left e -> throw400 NotSupported $ "transformConfig: Kriti template transform failed - " <> tshow e Right (J.Object r) -> pure $ API.Config r 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 additionalFunctions :: Env.Environment -> M.HashMap T.Text (J.Value -> Either Kriti.CustomFunctionError J.Value) -additionalFunctions env = M.singleton "env" getEnv <> Kriti.basicFuncMap - 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" +additionalFunctions env = KFunc.environmentFunctions env diff --git a/server/src-lib/Hasura/RQL/DDL/Webhook/Transform.hs b/server/src-lib/Hasura/RQL/DDL/Webhook/Transform.hs index df1ee1ce4c3..b076cc0a83a 100644 --- a/server/src-lib/Hasura/RQL/DDL/Webhook/Transform.hs +++ b/server/src-lib/Hasura/RQL/DDL/Webhook/Transform.hs @@ -55,13 +55,13 @@ where import Control.Lens (Lens', lens, set, traverseOf, view) import Data.Aeson (FromJSON, ToJSON) import Data.Aeson.Extended qualified as J +import Data.Aeson.Kriti.Functions qualified as KFunc import Data.Bifunctor (first) import Data.ByteString.Lazy qualified as BL import Data.CaseInsensitive qualified as CI import Data.Coerce (Coercible) import Data.Functor.Barbie (AllBF, ApplicativeB, ConstraintsB, FunctorB, TraversableB) import Data.Functor.Barbie qualified as B -import Data.HashMap.Strict qualified as M import Data.Text.Encoding qualified as TE import Data.Validation qualified as V 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.QueryParams import Hasura.RQL.DDL.Webhook.Transform.Url -import Hasura.Session (SessionVariables, getSessionVariableValue, mkSessionVariable) -import Kriti qualified (runKriti) -import Kriti.Error qualified as Kriti (CustomFunctionError (CustomFunctionError), serialize) +import Hasura.Session (SessionVariables) 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, responseTransformReqCtx = J.toJSON reqCtx, 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 -- @@ -403,7 +391,7 @@ mkRespTemplateTransform _ Body.Remove _ = pure J.Null mkRespTemplateTransform engine (Body.ModifyAsJSON (Template template)) ResponseTransformCtx {..} = let context = [("$body", responseTransformBody), ("$request", responseTransformReqCtx)] 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 = case engine of Kriti -> do diff --git a/server/src-lib/Hasura/RQL/DDL/Webhook/Transform/Class.hs b/server/src-lib/Hasura/RQL/DDL/Webhook/Transform/Class.hs index 40044a8009f..23ed2ead189 100644 --- a/server/src-lib/Hasura/RQL/DDL/Webhook/Transform/Class.hs +++ b/server/src-lib/Hasura/RQL/DDL/Webhook/Transform/Class.hs @@ -43,6 +43,7 @@ import Control.Arrow (left) import Control.Lens (bimap, view) import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey) import Data.Aeson qualified as J +import Data.Aeson.Kriti.Functions as KFunc import Data.Binary.Builder (toLazyByteString) import Data.ByteString (ByteString) import Data.ByteString.Builder.Scientific (scientificBuilder) @@ -54,9 +55,7 @@ import Data.Text.Encoding qualified as TE import Data.Validation (Validation, fromEither) import Hasura.Incremental (Cacheable) import Hasura.Prelude -import Hasura.Session (SessionVariables, getSessionVariableValue, mkSessionVariable) -import Kriti (runKritiWith) -import Kriti.CustomFunctions qualified as Kriti (basicFuncMap) +import Hasura.Session (SessionVariables) import Kriti.Error qualified as Kriti (CustomFunctionError (..), serialize) import Kriti.Parser qualified as Kriti (parser) import Network.HTTP.Client.Transformable qualified as HTTP @@ -166,25 +165,14 @@ mkReqTransformCtx url sessionVars rtcEngine reqData = view HTTP.queryParams reqData & fmap \(key, val) -> (TE.decodeUtf8 key, fmap TE.decodeUtf8 val) in Just $ J.toJSON queryParams - rtcFunctions = M.singleton "getSessionVariable" getSessionVar in RequestTransformCtx { rtcBaseUrl, rtcBody, rtcSessionVariables, rtcQueryParams, 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. data ResponseTransformCtx = ResponseTransformCtx @@ -257,9 +245,9 @@ runRequestTemplateTransform template RequestTransformCtx {rtcEngine = Kriti, ..} [ ("$query_params",) <$> rtcQueryParams, ("$base_url",) <$> rtcBaseUrl ] - eResult = runKritiWith (unTemplate $ template) context rtcFunctions + eResult = KFunc.runKritiWith (unTemplate $ template) context rtcFunctions in eResult & left \kritiErr -> - let renderedErr = J.toJSON $ Kriti.serialize kritiErr + let renderedErr = J.toJSON kritiErr in TransformErrorBundle [renderedErr] -- TODO: Should this live in 'Hasura.RQL.DDL.Webhook.Transform.Validation'? @@ -291,9 +279,9 @@ runResponseTemplateTransform :: Either TransformErrorBundle J.Value runResponseTemplateTransform template ResponseTransformCtx {responseTransformEngine = Kriti, ..} = let context = [("$body", responseTransformBody), ("$request", responseTransformReqCtx)] - eResult = runKritiWith (unTemplate $ template) context responseTransformFunctions + eResult = KFunc.runKritiWith (unTemplate $ template) context responseTransformFunctions in eResult & left \kritiErr -> - let renderedErr = J.toJSON $ Kriti.serialize kritiErr + let renderedErr = J.toJSON kritiErr in TransformErrorBundle [renderedErr] -------------------------------------------------------------------------------