mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
e0c0043e76
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9284 GitOrigin-RevId: 2f2cf2ad01900a54e4bdb970205ac0ef313c7e00
230 lines
8.2 KiB
Haskell
230 lines
8.2 KiB
Haskell
{-# LANGUAGE TemplateHaskell #-}
|
||
|
||
-- | This module implements /fragment inlining/, which converts all fragment
|
||
-- spreads in a GraphQL query to inline fragments. For example, given a query like
|
||
--
|
||
-- > query {
|
||
-- > users {
|
||
-- > id
|
||
-- > ...userFields
|
||
-- > }
|
||
-- > }
|
||
-- >
|
||
-- > fragment userFields on User {
|
||
-- > name
|
||
-- > favoriteColor
|
||
-- > }
|
||
--
|
||
-- the fragment inliner will convert it to this:
|
||
--
|
||
-- > query {
|
||
-- > users {
|
||
-- > id
|
||
-- > ... on User {
|
||
-- > name
|
||
-- > favoriteColor
|
||
-- > }
|
||
-- > }
|
||
-- > }
|
||
--
|
||
-- This is a straightforward and mechanical transformation, but it simplifies
|
||
-- further processing, since we catch unbound fragments and recursive fragment
|
||
-- definitions early in the pipeline, so parsing does not have to worry about it.
|
||
-- In that sense, fragment inlining is similar to the variable resolution pass
|
||
-- performed by "Hasura.GraphQL.Execute.Resolve", but for fragment definitions
|
||
-- rather than variables.
|
||
module Hasura.GraphQL.Execute.Inline
|
||
( InlineMT,
|
||
InlineM,
|
||
inlineSelectionSet,
|
||
inlineField,
|
||
runInlineMT,
|
||
runInlineM,
|
||
)
|
||
where
|
||
|
||
import Control.Lens
|
||
import Data.HashMap.Strict.Extended qualified as HashMap
|
||
import Data.HashSet qualified as Set
|
||
import Data.List qualified as L
|
||
import Data.Text qualified as T
|
||
import Data.Text.Extended
|
||
import Hasura.Base.Error
|
||
import Hasura.Prelude
|
||
import Hasura.Server.Utils
|
||
import Language.GraphQL.Draft.Syntax
|
||
|
||
-- | Internal bookkeeping used during inlining.
|
||
data InlineEnv = InlineEnv
|
||
{ -- | All known fragment definitions.
|
||
_ieFragmentDefinitions :: HashMap Name FragmentDefinition,
|
||
-- | Fragments we’re currently inlining higher up in the call stack, used to
|
||
-- detect fragment cycles.
|
||
_ieFragmentStack :: [Name]
|
||
}
|
||
|
||
-- | Internal bookkeeping used during inlining.
|
||
newtype InlineState = InlineState
|
||
{ -- | A cache of fragment definitions we’ve already inlined, so we don’t need
|
||
-- to inline them again.
|
||
_isFragmentCache :: HashMap Name (InlineFragment NoFragments Name)
|
||
}
|
||
|
||
$(makeLensesFor [("_ieFragmentStack", "ieFragmentStack")] ''InlineEnv)
|
||
$(makeLenses ''InlineState)
|
||
|
||
type MonadInline m =
|
||
( MonadError QErr m,
|
||
MonadReader InlineEnv m,
|
||
MonadState InlineState m
|
||
)
|
||
|
||
type InlineMT m a = (MonadError QErr m) => (StateT InlineState (ReaderT InlineEnv m)) a
|
||
|
||
type InlineM a = InlineMT (Except QErr) a
|
||
|
||
{-# INLINE runInlineMT #-}
|
||
runInlineMT ::
|
||
forall m a.
|
||
(MonadError QErr m) =>
|
||
HashMap Name FragmentDefinition ->
|
||
InlineMT m a ->
|
||
m a
|
||
runInlineMT uniqueFragmentDefinitions =
|
||
flip
|
||
runReaderT
|
||
InlineEnv
|
||
{ _ieFragmentDefinitions = uniqueFragmentDefinitions,
|
||
_ieFragmentStack = []
|
||
}
|
||
. flip evalStateT InlineState {_isFragmentCache = mempty}
|
||
|
||
{-# INLINE runInlineM #-}
|
||
runInlineM ::
|
||
forall a.
|
||
HashMap Name FragmentDefinition ->
|
||
InlineM a ->
|
||
Either QErr a
|
||
runInlineM fragments = runExcept . runInlineMT fragments
|
||
|
||
-- | Inlines all fragment spreads in a 'SelectionSet'; see the module
|
||
-- documentation for "Hasura.GraphQL.Execute.Inline" for details.
|
||
inlineSelectionSet ::
|
||
(MonadError QErr m, Foldable t) =>
|
||
t FragmentDefinition ->
|
||
SelectionSet FragmentSpread Name ->
|
||
m (SelectionSet NoFragments Name)
|
||
inlineSelectionSet fragmentDefinitions selectionSet = do
|
||
let fragmentDefinitionMap = HashMap.groupOnNE _fdName fragmentDefinitions
|
||
uniqueFragmentDefinitions <- flip
|
||
HashMap.traverseWithKey
|
||
fragmentDefinitionMap
|
||
\fragmentName fragmentDefinitions' ->
|
||
case fragmentDefinitions' of
|
||
a :| [] -> return a
|
||
_ -> throw400 ParseFailed $ "multiple definitions for fragment " <>> fragmentName
|
||
let usedFragmentNames = Set.fromList $ fragmentsInSelectionSet selectionSet
|
||
definedFragmentNames = Set.fromList $ HashMap.keys uniqueFragmentDefinitions
|
||
-- At the time of writing, this check is disabled using
|
||
-- a local binding because, the master branch doesn't implement this
|
||
-- check.
|
||
-- TODO: Do this check using a feature flag
|
||
isFragmentValidationEnabled = False
|
||
when (isFragmentValidationEnabled && (usedFragmentNames /= definedFragmentNames))
|
||
$ throw400 ValidationFailed
|
||
$ "following fragment(s) have been defined, but have not been used in the query - "
|
||
<> T.concat
|
||
( L.intersperse ", "
|
||
$ map unName
|
||
$ Set.toList
|
||
$ Set.difference definedFragmentNames usedFragmentNames
|
||
)
|
||
-- The below code is a manual inlining of 'runInlineMT', as appearently the
|
||
-- inlining optimization does not trigger, even with the INLINE pragma.
|
||
traverse inlineSelection selectionSet
|
||
& flip evalStateT InlineState {_isFragmentCache = mempty}
|
||
& flip
|
||
runReaderT
|
||
InlineEnv
|
||
{ _ieFragmentDefinitions = uniqueFragmentDefinitions,
|
||
_ieFragmentStack = []
|
||
}
|
||
where
|
||
fragmentsInSelectionSet :: SelectionSet FragmentSpread Name -> [Name]
|
||
fragmentsInSelectionSet selectionSet' = concatMap getFragFromSelection selectionSet'
|
||
|
||
getFragFromSelection :: Selection FragmentSpread Name -> [Name]
|
||
getFragFromSelection = \case
|
||
SelectionField fld -> fragmentsInSelectionSet $ _fSelectionSet fld
|
||
SelectionFragmentSpread fragmentSpread -> [_fsName fragmentSpread]
|
||
SelectionInlineFragment inlineFragment -> fragmentsInSelectionSet $ _ifSelectionSet inlineFragment
|
||
|
||
inlineSelection ::
|
||
(MonadInline m) =>
|
||
Selection FragmentSpread Name ->
|
||
m (Selection NoFragments Name)
|
||
inlineSelection (SelectionField field) =
|
||
withPathK "selectionSet" $ SelectionField <$> inlineField field
|
||
inlineSelection (SelectionFragmentSpread spread) =
|
||
withPathK "selectionSet"
|
||
$ SelectionInlineFragment
|
||
<$> inlineFragmentSpread spread
|
||
inlineSelection (SelectionInlineFragment fragment@InlineFragment {_ifSelectionSet}) = do
|
||
selectionSet <- traverse inlineSelection _ifSelectionSet
|
||
pure $! SelectionInlineFragment fragment {_ifSelectionSet = selectionSet}
|
||
|
||
{-# INLINE inlineField #-}
|
||
inlineField :: (MonadInline m) => Field FragmentSpread Name -> m (Field NoFragments Name)
|
||
inlineField field@(Field {_fSelectionSet}) = withPathK (unName $ _fName field) $ do
|
||
selectionSet <- traverse inlineSelection _fSelectionSet
|
||
pure $! field {_fSelectionSet = selectionSet}
|
||
|
||
inlineFragmentSpread ::
|
||
(MonadInline m) =>
|
||
FragmentSpread Name ->
|
||
m (InlineFragment NoFragments Name)
|
||
inlineFragmentSpread FragmentSpread {_fsName, _fsDirectives} = do
|
||
InlineEnv {_ieFragmentDefinitions, _ieFragmentStack} <- ask
|
||
InlineState {_isFragmentCache} <- get
|
||
|
||
if
|
||
-- If we’ve already inlined this fragment, no need to process it again.
|
||
| Just fragment <- HashMap.lookup _fsName _isFragmentCache ->
|
||
pure $! addSpreadDirectives fragment
|
||
-- Fragment cycles are always illegal; see
|
||
-- http://spec.graphql.org/June2018/#sec-Fragment-spreads-must-not-form-cycles
|
||
| (fragmentCycle, _ : _) <- break (== _fsName) _ieFragmentStack ->
|
||
throw400 ValidationFailed
|
||
$ "the fragment definition(s) "
|
||
<> englishList "and" (toTxt <$> (_fsName :| reverse fragmentCycle))
|
||
<> " form a cycle"
|
||
-- We didn’t hit the fragment cache, so look up the definition and convert
|
||
-- it to an inline fragment.
|
||
| Just FragmentDefinition {_fdTypeCondition, _fdSelectionSet} <-
|
||
HashMap.lookup _fsName _ieFragmentDefinitions -> withPathK (unName _fsName) $ do
|
||
selectionSet <-
|
||
locally ieFragmentStack (_fsName :)
|
||
$ traverse inlineSelection _fdSelectionSet
|
||
|
||
let fragment =
|
||
InlineFragment
|
||
{ _ifTypeCondition = Just _fdTypeCondition,
|
||
-- As far as I can tell, the GraphQL spec says that directives
|
||
-- on the fragment definition do NOT apply to the fields in its
|
||
-- selection set.
|
||
_ifDirectives = [],
|
||
_ifSelectionSet = selectionSet
|
||
}
|
||
modify' $ over isFragmentCache $ HashMap.insert _fsName fragment
|
||
pure $! addSpreadDirectives fragment
|
||
|
||
-- If we get here, the fragment name is unbound; raise an error.
|
||
-- http://spec.graphql.org/June2018/#sec-Fragment-spread-target-defined
|
||
| otherwise ->
|
||
throw400 ValidationFailed
|
||
$ "reference to undefined fragment "
|
||
<>> _fsName
|
||
where
|
||
addSpreadDirectives fragment =
|
||
fragment {_ifDirectives = _ifDirectives fragment ++ _fsDirectives}
|