mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 04:51:35 +03:00
e93d3d2735
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8214 GitOrigin-RevId: 1ecf4c9c362aad9eabad2134a70266ff1170577b
241 lines
8.5 KiB
Haskell
241 lines
8.5 KiB
Haskell
{-# LANGUAGE DeriveAnyClass #-}
|
|
{-# LANGUAGE TemplateHaskell #-}
|
|
{-# LANGUAGE UndecidableInstances #-}
|
|
|
|
-- | Metadata representation of a logical model in the metadata,
|
|
-- as well as a parser and prettyprinter for the query code.
|
|
module Hasura.LogicalModel.Metadata
|
|
( LogicalModelName (..),
|
|
LogicalModelMetadata (..),
|
|
lmmArguments,
|
|
lmmCode,
|
|
lmmDescription,
|
|
lmmReturns,
|
|
lmmRootFieldName,
|
|
lmmSelectPermissions,
|
|
LogicalModelArgumentName (..),
|
|
InterpolatedItem (..),
|
|
InterpolatedQuery (..),
|
|
WithLogicalModel (..),
|
|
parseInterpolatedQuery,
|
|
module Hasura.LogicalModel.Types,
|
|
)
|
|
where
|
|
|
|
import Autodocodec
|
|
import Autodocodec qualified as AC
|
|
import Control.Lens (makeLenses)
|
|
import Data.Aeson (FromJSON (parseJSON), FromJSONKey, ToJSON, ToJSONKey, (.!=), (.:), (.:?))
|
|
import Data.Aeson qualified as Aeson
|
|
import Data.Bifunctor (first)
|
|
import Data.HashMap.Strict.InsOrd.Autodocodec (sortedElemsCodec)
|
|
import Data.Text qualified as T
|
|
import Hasura.CustomReturnType (CustomReturnType)
|
|
import Hasura.LogicalModel.Types (LogicalModelName (..), NullableScalarType)
|
|
import Hasura.Metadata.DTO.Utils (codecNamePrefix)
|
|
import Hasura.Prelude hiding (first)
|
|
import Hasura.RQL.Types.Backend
|
|
import Hasura.RQL.Types.Common (SourceName, ToAesonPairs (toAesonPairs), defaultSource)
|
|
import Hasura.RQL.Types.Permission (SelPermDef, _pdRole)
|
|
import Hasura.SQL.Backend
|
|
import Hasura.Session (RoleName)
|
|
|
|
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, Data, Generic, Traversable)
|
|
|
|
-- | Converting an interpolated query back to text.
|
|
-- Should roundtrip with the 'parseInterpolatedQuery'.
|
|
ppInterpolatedItem :: InterpolatedItem LogicalModelArgumentName -> Text
|
|
ppInterpolatedItem (IIText t) = t
|
|
ppInterpolatedItem (IIVariable v) = "{{" <> getLogicalModelArgumentName v <> "}}"
|
|
|
|
deriving instance (Hashable variable) => Hashable (InterpolatedItem variable)
|
|
|
|
deriving instance (NFData variable) => NFData (InterpolatedItem variable)
|
|
|
|
---------------------------------------
|
|
|
|
-- | A list of logical model components representing a single logical model,
|
|
-- separating the variables from the text.
|
|
newtype InterpolatedQuery variable = InterpolatedQuery
|
|
{ getInterpolatedQuery :: [InterpolatedItem variable]
|
|
}
|
|
deriving newtype (Eq, Ord, Show, Generic)
|
|
deriving stock (Data, Functor, Foldable, Traversable)
|
|
|
|
deriving newtype instance (Hashable variable) => Hashable (InterpolatedQuery variable)
|
|
|
|
deriving newtype instance (NFData variable) => NFData (InterpolatedQuery variable)
|
|
|
|
ppInterpolatedQuery :: InterpolatedQuery LogicalModelArgumentName -> 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 v ~ LogicalModelArgumentName => HasCodec (InterpolatedQuery v) where
|
|
codec =
|
|
CommentCodec
|
|
("An interpolated query expressed in native code (SQL)")
|
|
$ bimapCodec
|
|
(first T.unpack . parseInterpolatedQuery)
|
|
ppInterpolatedQuery
|
|
textCodec
|
|
|
|
deriving via
|
|
(Autodocodec (InterpolatedQuery LogicalModelArgumentName))
|
|
instance
|
|
v ~ LogicalModelArgumentName =>
|
|
ToJSON (InterpolatedQuery v)
|
|
|
|
---------------------------------------
|
|
|
|
newtype LogicalModelArgumentName = LogicalModelArgumentName
|
|
{ getLogicalModelArgumentName :: Text
|
|
}
|
|
deriving newtype (Eq, Ord, Show, Hashable)
|
|
deriving stock (Generic)
|
|
|
|
instance HasCodec LogicalModelArgumentName where
|
|
codec = dimapCodec LogicalModelArgumentName getLogicalModelArgumentName codec
|
|
|
|
deriving newtype instance ToJSON LogicalModelArgumentName
|
|
|
|
deriving newtype instance FromJSON LogicalModelArgumentName
|
|
|
|
deriving newtype instance ToJSONKey LogicalModelArgumentName
|
|
|
|
deriving newtype instance FromJSONKey LogicalModelArgumentName
|
|
|
|
instance NFData LogicalModelArgumentName
|
|
|
|
---------------------------------------
|
|
|
|
-- | The representation of logical models within the metadata structure.
|
|
data LogicalModelMetadata (b :: BackendType) = LogicalModelMetadata
|
|
{ _lmmRootFieldName :: LogicalModelName,
|
|
_lmmCode :: InterpolatedQuery LogicalModelArgumentName,
|
|
_lmmReturns :: CustomReturnType b,
|
|
_lmmArguments :: HashMap LogicalModelArgumentName (NullableScalarType b),
|
|
_lmmSelectPermissions :: InsOrdHashMap RoleName (SelPermDef b),
|
|
_lmmDescription :: Maybe Text
|
|
}
|
|
deriving (Generic)
|
|
|
|
deriving instance Backend b => Eq (LogicalModelMetadata b)
|
|
|
|
deriving instance Backend b => Show (LogicalModelMetadata b)
|
|
|
|
instance (Backend b) => HasCodec (LogicalModelMetadata b) where
|
|
codec =
|
|
CommentCodec
|
|
("A logical model as represented in metadata.")
|
|
$ AC.object (codecNamePrefix @b <> "LogicalModelMetadata")
|
|
$ LogicalModelMetadata
|
|
<$> requiredField "root_field_name" fieldNameDoc
|
|
AC..= _lmmRootFieldName
|
|
<*> requiredField "code" sqlDoc
|
|
AC..= _lmmCode
|
|
<*> requiredField "returns" returnsDoc
|
|
AC..= _lmmReturns
|
|
<*> optionalFieldWithDefault "arguments" mempty argumentDoc
|
|
AC..= _lmmArguments
|
|
<*> optSortedList "select_permissions" _pdRole
|
|
AC..= _lmmSelectPermissions
|
|
<*> optionalField "description" descriptionDoc
|
|
AC..= _lmmDescription
|
|
where
|
|
fieldNameDoc = "Root field name for the logical model"
|
|
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 logical model which appears in the graphql schema"
|
|
|
|
optSortedList name keyForElem =
|
|
AC.optionalFieldWithOmittedDefaultWith' name (sortedElemsCodec keyForElem) mempty
|
|
|
|
deriving via
|
|
(Autodocodec (LogicalModelMetadata b))
|
|
instance
|
|
(Backend b) => (FromJSON (LogicalModelMetadata b))
|
|
|
|
deriving via
|
|
(Autodocodec (LogicalModelMetadata b))
|
|
instance
|
|
(Backend b) => (ToJSON (LogicalModelMetadata b))
|
|
|
|
-- | A wrapper to tie something to a particular logical model. Specifically, it
|
|
-- assumes the underlying '_wlmInfo' is represented as an object, and adds two
|
|
-- keys to that object: @source@ and @root_field_name@.
|
|
data WithLogicalModel a = WithLogicalModel
|
|
{ _wlmSource :: SourceName,
|
|
_wlmName :: LogicalModelName,
|
|
_wlmInfo :: a
|
|
}
|
|
deriving stock (Eq, Show)
|
|
|
|
instance (FromJSON a) => FromJSON (WithLogicalModel a) where
|
|
parseJSON = Aeson.withObject "LogicalModel" \obj -> do
|
|
_wlmSource <- obj .:? "source" .!= defaultSource
|
|
_wlmName <- obj .: "root_field_name"
|
|
_wlmInfo <- parseJSON (Aeson.Object obj)
|
|
|
|
pure WithLogicalModel {..}
|
|
|
|
instance (ToAesonPairs a) => ToJSON (WithLogicalModel a) where
|
|
toJSON (WithLogicalModel source name info) =
|
|
Aeson.object $ ("source", Aeson.toJSON source) : ("root_field_name", Aeson.toJSON name) : toAesonPairs info
|
|
|
|
-- | extract all of the `{{ variable }}` inside our query string
|
|
parseInterpolatedQuery ::
|
|
Text ->
|
|
Either Text (InterpolatedQuery LogicalModelArgumentName)
|
|
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 LogicalModelArgumentName]
|
|
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 LogicalModelArgumentName]
|
|
consumeVar str =
|
|
let (beforeCloseCurly, fromClosedCurly) = break (== '}') str
|
|
in case fromClosedCurly of
|
|
('}' : '}' : rest) ->
|
|
(IIVariable (LogicalModelArgumentName $ T.pack beforeCloseCurly) :) <$> consumeString rest
|
|
_ -> Left "Found '{{' without a matching closing '}}'"
|
|
|
|
makeLenses ''LogicalModelMetadata
|