mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
e6ee9db169
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9515 GitOrigin-RevId: 95e93d017ac9757a8088aea3015836b5d8236d66
152 lines
5.1 KiB
Haskell
152 lines
5.1 KiB
Haskell
{-# LANGUAGE DeriveAnyClass #-}
|
|
{-# LANGUAGE UndecidableInstances #-}
|
|
|
|
-- | Parser and prettyprinter for native query code.
|
|
module Hasura.NativeQuery.InterpolatedQuery
|
|
( ArgumentName (..),
|
|
InterpolatedItem (..),
|
|
InterpolatedQuery (..),
|
|
parseInterpolatedQuery,
|
|
getUniqueVariables,
|
|
trimQueryEnd,
|
|
module Hasura.LogicalModel.NullableScalarType,
|
|
)
|
|
where
|
|
|
|
import Autodocodec
|
|
import Autodocodec qualified as AC
|
|
import Control.Lens (over, _last)
|
|
import Data.Aeson (FromJSON, ToJSON)
|
|
import Data.Bifunctor (first)
|
|
import Data.Set (Set)
|
|
import Data.Set qualified as Set
|
|
import Data.Text qualified as T
|
|
import Hasura.LogicalModel.NullableScalarType (NullableScalarType (..), nullableScalarTypeMapCodec)
|
|
import Hasura.LogicalModelResolver.Types (ArgumentName (..))
|
|
import Hasura.Prelude hiding (first)
|
|
import Language.Haskell.TH.Syntax (Lift)
|
|
|
|
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, Lift, Traversable)
|
|
|
|
-- | Converting an interpolated query back to text.
|
|
-- Should roundtrip with the 'parseInterpolatedQuery'.
|
|
ppInterpolatedItem :: InterpolatedItem ArgumentName -> Text
|
|
ppInterpolatedItem (IIText t) = t
|
|
ppInterpolatedItem (IIVariable v) = "{{" <> getArgumentName v <> "}}"
|
|
|
|
deriving instance (Hashable variable) => Hashable (InterpolatedItem variable)
|
|
|
|
deriving instance (NFData variable) => NFData (InterpolatedItem variable)
|
|
|
|
---------------------------------------
|
|
|
|
-- | A list of stored procedure components representing a single stored procedure,
|
|
-- separating the variables from the text.
|
|
newtype InterpolatedQuery variable = InterpolatedQuery
|
|
{ getInterpolatedQuery :: [InterpolatedItem variable]
|
|
}
|
|
deriving newtype (Eq, Ord, Show, Generic)
|
|
deriving stock (Data, Functor, Foldable, Lift, Traversable)
|
|
|
|
deriving newtype instance (Hashable variable) => Hashable (InterpolatedQuery variable)
|
|
|
|
deriving newtype instance (NFData variable) => NFData (InterpolatedQuery variable)
|
|
|
|
ppInterpolatedQuery :: InterpolatedQuery ArgumentName -> 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 ~ ArgumentName) => 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 ArgumentName))
|
|
instance
|
|
(v ~ ArgumentName) =>
|
|
ToJSON (InterpolatedQuery v)
|
|
|
|
---------------------------------------
|
|
|
|
-- | extract all of the `{{ variable }}` inside our query string
|
|
parseInterpolatedQuery ::
|
|
Text ->
|
|
Either Text (InterpolatedQuery ArgumentName)
|
|
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 ArgumentName]
|
|
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 ArgumentName]
|
|
consumeVar str =
|
|
let (beforeCloseCurly, fromClosedCurly) = break (== '}') str
|
|
in case fromClosedCurly of
|
|
('}' : '}' : rest) ->
|
|
(IIVariable (ArgumentName $ T.pack beforeCloseCurly) :) <$> consumeString rest
|
|
_ -> Left "Found '{{' without a matching closing '}}'"
|
|
|
|
-- | Get a set of all arguments used in an interpolated query.
|
|
getUniqueVariables :: (Ord var) => InterpolatedQuery var -> Set var
|
|
getUniqueVariables (InterpolatedQuery items) =
|
|
flip foldMap items \case
|
|
IIText _ -> mempty
|
|
IIVariable variable -> Set.singleton variable
|
|
|
|
-- | Remove spaces and semicolon from the end of a query and add a newline, for sql backends.
|
|
trimQueryEnd :: InterpolatedQuery var -> InterpolatedQuery var
|
|
trimQueryEnd (InterpolatedQuery parts) =
|
|
InterpolatedQuery
|
|
$ over _last dropIt parts
|
|
-- if the user has a comment on the last line, this will make sure it doesn't interrupt the rest of the query
|
|
<> [IIText "\n"]
|
|
where
|
|
dropIt = \case
|
|
IIText txt ->
|
|
IIText
|
|
. T.dropWhileEnd (== ';')
|
|
. T.dropWhileEnd (\c -> c == ' ' || c == '\t' || c == '\n')
|
|
$ txt
|
|
IIVariable v -> IIVariable v
|