graphql-engine/server/src-lib/Hasura/NativeQuery/Metadata.hs
2023-02-03 16:29:22 +00:00

281 lines
9.8 KiB
Haskell

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE UndecidableInstances #-}
-- | This module houses the types and functions associated with the default
-- implementation of the metadata of native queries.
module Hasura.NativeQuery.Metadata
( NativeQueryNameImpl (..),
NativeQueryInfoImpl (..),
NativeQueryArgumentName (..),
TrackNativeQueryImpl (..),
InterpolatedItem (..),
InterpolatedQuery (..),
parseInterpolatedQuery,
defaultNativeQueryTrackToInfo,
module Hasura.NativeQuery.Types,
)
where
import Autodocodec
import Autodocodec qualified as AC
import Data.Aeson
import Data.Bifunctor (first)
import Data.Text qualified as T
import Data.Text.Extended (ToTxt)
import Hasura.Metadata.DTO.Utils (codecNamePrefix)
import Hasura.NativeQuery.Types
import Hasura.Prelude hiding (first)
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Common
import Hasura.SQL.Backend
-- The name of a native query. This appears as a root field name in the graphql schema.
newtype NativeQueryNameImpl = NativeQueryNameImpl {getNativeQueryNameImpl :: Text}
deriving newtype (Eq, Ord, Show, Hashable, NFData, ToJSON, FromJSON, ToTxt)
deriving stock (Generic)
instance HasCodec NativeQueryNameImpl where
codec = dimapCodec NativeQueryNameImpl getNativeQueryNameImpl codec
instance FromJSONKey NativeQueryNameImpl
instance ToJSONKey NativeQueryNameImpl
---------------------------------------
newtype RawQuery = RawQuery {getRawQuery :: Text}
deriving newtype (Eq, Ord, Show, FromJSON, ToJSON)
instance HasCodec RawQuery where
codec = AC.dimapCodec RawQuery getRawQuery codec
---------------------------------------
-- | A component of an interpolated query
data InterpolatedItem variable
= -- | normal text
IIText Text
| -- | a captured variable
IIVariable variable
deriving stock (Eq, Ord, Show, Functor, Foldable, Generic, Traversable)
-- | Converting an interpolated query back to text.
-- Should roundtrip with the 'parseInterpolatedQuery'.
ppInterpolatedItem :: InterpolatedItem NativeQueryArgumentName -> Text
ppInterpolatedItem (IIText t) = t
ppInterpolatedItem (IIVariable v) = "{{" <> getNativeQueryArgumentName v <> "}}"
deriving instance (Hashable variable) => Hashable (InterpolatedItem variable)
deriving instance (NFData variable) => NFData (InterpolatedItem variable)
---------------------------------------
-- | A list of native query components representing a single native query,
-- separating the variables from the text.
newtype InterpolatedQuery variable = InterpolatedQuery
{ getInterpolatedQuery :: [InterpolatedItem variable]
}
deriving newtype (Eq, Ord, Show, Generic)
deriving stock (Functor, Foldable, Traversable)
deriving newtype instance (Hashable variable) => Hashable (InterpolatedQuery variable)
deriving newtype instance (NFData variable) => NFData (InterpolatedQuery variable)
ppInterpolatedQuery :: InterpolatedQuery NativeQueryArgumentName -> Text
ppInterpolatedQuery (InterpolatedQuery parts) = foldMap ppInterpolatedItem parts
-- | We store the interpolated query as the user text and parse it back
-- when converting back to Haskell code.
instance HasCodec (InterpolatedQuery NativeQueryArgumentName) where
codec =
CommentCodec
("An interpolated query expressed in native code (SQL)")
$ bimapCodec
(first T.unpack . parseInterpolatedQuery)
ppInterpolatedQuery
textCodec
---------------------------------------
newtype NativeQueryArgumentName = NativeQueryArgumentName
{ getNativeQueryArgumentName :: Text
}
deriving newtype (Eq, Ord, Show, Hashable)
deriving stock (Generic)
instance HasCodec NativeQueryArgumentName where
codec = dimapCodec NativeQueryArgumentName getNativeQueryArgumentName codec
deriving newtype instance ToJSON NativeQueryArgumentName
deriving newtype instance FromJSON NativeQueryArgumentName
deriving newtype instance ToJSONKey NativeQueryArgumentName
deriving newtype instance FromJSONKey NativeQueryArgumentName
instance NFData (NativeQueryArgumentName)
---------------------------------------
-- | Default implementation of the Native Query metadata info object.
data NativeQueryInfoImpl (b :: BackendType) = NativeQueryInfoImpl
{ nqiiRootFieldName :: NativeQueryNameImpl,
nqiiCode :: InterpolatedQuery NativeQueryArgumentName,
nqiiReturns :: TableName b,
nqiiArguments :: HashMap NativeQueryArgumentName (ScalarType b),
nqiiDescription :: Maybe Text
}
deriving (Generic)
deriving instance Backend b => Eq (NativeQueryInfoImpl b)
deriving instance Backend b => Show (NativeQueryInfoImpl b)
instance Backend b => Hashable (NativeQueryInfoImpl b)
instance Backend b => NFData (NativeQueryInfoImpl b)
instance (Backend b, HasCodec (ScalarType b)) => HasCodec (NativeQueryInfoImpl b) where
codec =
CommentCodec
("A query in expressed in native code (SQL) to add to the GraphQL schema with configuration.")
$ AC.object (codecNamePrefix @b <> "NativeQueryInfo")
$ NativeQueryInfoImpl
<$> requiredField "root_field_name" fieldNameDoc
AC..= nqiiRootFieldName
<*> requiredField "code" sqlDoc
AC..= nqiiCode
<*> requiredField "returns" returnsDoc
AC..= nqiiReturns
<*> optionalFieldWithDefault "arguments" mempty argumentDoc
AC..= nqiiArguments
<*> optionalField "description" descriptionDoc
AC..= nqiiDescription
where
fieldNameDoc = "Root field name for the native query"
sqlDoc = "Native code expression (SQL) to run"
argumentDoc = "Free variables in the expression and their types"
returnsDoc = "Return type (table) of the expression"
descriptionDoc = "A description of the query which appears in the graphql schema"
deriving via
(Autodocodec (NativeQueryInfoImpl b))
instance
(Backend b, HasCodec (ScalarType b)) => (FromJSON (NativeQueryInfoImpl b))
deriving via
(Autodocodec (NativeQueryInfoImpl b))
instance
(Backend b, HasCodec (ScalarType b)) => (ToJSON (NativeQueryInfoImpl b))
-- | Default implementation of the 'track_native_query' request payload.
data TrackNativeQueryImpl (b :: BackendType) = TrackNativeQueryImpl
{ tnqSource :: SourceName,
tnqRootFieldName :: NativeQueryNameImpl,
tnqCode :: Text,
tnqArguments :: HashMap NativeQueryArgumentName (ScalarType b),
tnqDescription :: Maybe Text,
tnqReturns :: TableName b
}
-- | Default implementation of the method 'nativeQueryTrackToInfo'.
defaultNativeQueryTrackToInfo ::
forall b m.
( MonadIO m,
MonadError NativeQueryError m,
NativeQueryMetadata b,
NativeQueryInfo b ~ NativeQueryInfoImpl b
) =>
SourceConnConfiguration b ->
TrackNativeQueryImpl b ->
m (NativeQueryInfoImpl b)
defaultNativeQueryTrackToInfo sourceConnConfig TrackNativeQueryImpl {..} = do
nqiiCode <- liftEither $ mapLeft NativeQueryParseError (parseInterpolatedQuery tnqCode)
let nqiiRootFieldName = tnqRootFieldName
nqiiReturns = tnqReturns
nqiiArguments = tnqArguments
nqiiDescription = tnqDescription
nqInfoImpl = NativeQueryInfoImpl {..}
validateNativeQueryAgainstSource @b sourceConnConfig nqInfoImpl
pure nqInfoImpl
instance (Backend b, HasCodec (ScalarType b)) => HasCodec (TrackNativeQueryImpl b) where
codec =
CommentCodec
("A request to track a native query")
$ AC.object (codecNamePrefix @b <> "TrackNativeQuery")
$ TrackNativeQueryImpl
<$> requiredField "source" sourceDoc
AC..= tnqSource
<*> requiredField "root_field_name" rootFieldDoc
AC..= tnqRootFieldName
<*> requiredField "code" codeDoc
AC..= tnqCode
<*> optionalFieldWithDefault "arguments" mempty argumentsDoc
AC..= tnqArguments
<*> optionalField "description" descriptionDoc
AC..= tnqDescription
<*> requiredField "returns" returnsDoc
AC..= tnqReturns
where
sourceDoc = "The source in whic this native query should be tracked"
rootFieldDoc = "Root field name for the native query"
codeDoc = "Native code expression (SQL) to run"
argumentsDoc = "Free variables in the expression and their types"
returnsDoc = "Return type (table) of the expression"
descriptionDoc = "A description of the query which appears in the graphql schema"
deriving via
(Autodocodec (TrackNativeQueryImpl b))
instance
(Backend b, HasCodec (ScalarType b)) => FromJSON (TrackNativeQueryImpl b)
deriving via
(Autodocodec (TrackNativeQueryImpl b))
instance
(Backend b, HasCodec (ScalarType b)) => ToJSON (TrackNativeQueryImpl b)
-- | extract all of the `{{ variable }}` inside our query string
parseInterpolatedQuery ::
Text ->
Either Text (InterpolatedQuery NativeQueryArgumentName)
parseInterpolatedQuery =
fmap
( InterpolatedQuery
. mergeAdjacent
. trashEmpties
)
. consumeString
. T.unpack
where
trashEmpties = filter (/= IIText "")
mergeAdjacent = \case
(IIText a : IIText b : rest) ->
mergeAdjacent (IIText (a <> b) : rest)
(a : rest) -> a : mergeAdjacent rest
[] -> []
consumeString :: String -> Either Text [InterpolatedItem NativeQueryArgumentName]
consumeString str =
let (beforeCurly, fromCurly) = break (== '{') str
in case fromCurly of
('{' : '{' : rest) ->
(IIText (T.pack beforeCurly) :) <$> consumeVar rest
('{' : other) ->
(IIText (T.pack (beforeCurly <> "{")) :) <$> consumeString other
_other -> pure [IIText (T.pack beforeCurly)]
consumeVar :: String -> Either Text [InterpolatedItem NativeQueryArgumentName]
consumeVar str =
let (beforeCloseCurly, fromClosedCurly) = break (== '}') str
in case fromClosedCurly of
('}' : '}' : rest) ->
(IIVariable (NativeQueryArgumentName $ T.pack beforeCloseCurly) :) <$> consumeString rest
_ -> Left "Found '{{' without a matching closing '}}'"