mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
975b022b29
This reduces the usage of "utils" modules in the parsers code, especially those that are simply re-exported from elsewhere, to facilitate extracting the parsers code into its own library. It mostly inlines the imports that are re-exported from `Hasura.Prelude` and `Data.Parser.JSONPath`. It also removes references to `Data.*.Extended` modules. When necessary, it re-implements the functionality (which is typically trivial). It does not tackle all external dependencies. I observed the following that will take more work: - `Data.GADT.Compare.Extended` - `Data.Text.Extended` - `Hasura.Base.Error` - `Hasura.RQL.Types.Common` - `Hasura.Server.Utils` PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4964 GitOrigin-RevId: 54ad3c1b7a31f13e34340ebe9fcc36d0ad57b8bd
291 lines
11 KiB
Haskell
291 lines
11 KiB
Haskell
-- | Definition of all supported GraphQL directives.
|
|
module Hasura.GraphQL.Parser.Directives
|
|
( -- list of directives, for the schema
|
|
directivesInfo,
|
|
inclusionDirectives,
|
|
customDirectives,
|
|
-- Custom Directive Types
|
|
CachedDirective (..),
|
|
DirectiveMap,
|
|
-- lookup keys for directives
|
|
include,
|
|
skip,
|
|
cached,
|
|
multipleRootFields,
|
|
-- parsing utilities
|
|
parseDirectives,
|
|
withDirective,
|
|
-- exposed for tests
|
|
Directive (..),
|
|
DirectiveKey (..),
|
|
skipDirective,
|
|
includeDirective,
|
|
cachedDirective,
|
|
multipleRootFieldsDirective,
|
|
)
|
|
where
|
|
|
|
import Control.Monad (guard, unless)
|
|
import Control.Monad.Identity (Identity (..))
|
|
import Data.Aeson.Key qualified as K
|
|
import Data.Aeson.Types (JSONPathElement (Key))
|
|
import Data.Dependent.Map qualified as DM
|
|
import Data.Dependent.Sum (DSum (..))
|
|
import Data.Foldable (for_)
|
|
import Data.GADT.Compare.Extended
|
|
import Data.HashMap.Strict qualified as M
|
|
import Data.HashSet qualified as S
|
|
import Data.List qualified as L
|
|
import Data.Maybe (catMaybes)
|
|
import Data.Text.Extended
|
|
import Data.Traversable (for)
|
|
import Data.Typeable (eqT)
|
|
import Hasura.GraphQL.Parser.Class
|
|
import Hasura.GraphQL.Parser.DirectiveName qualified as Name
|
|
import Hasura.GraphQL.Parser.Internal.Input
|
|
import Hasura.GraphQL.Parser.Internal.Scalars
|
|
import Hasura.GraphQL.Parser.Schema
|
|
import Hasura.GraphQL.Parser.Variable
|
|
import Language.GraphQL.Draft.Syntax qualified as G
|
|
import Type.Reflection (Typeable, typeRep, (:~:) (..))
|
|
import Prelude
|
|
|
|
-- Disable custom prelude warnings in preparation for extracting this module into a separate package.
|
|
{-# ANN module ("HLint: ignore Use onNothing" :: String) #-}
|
|
|
|
-- | Returns the schema information for all supported directives, for each of
|
|
-- which a @DirectiveDefinition@ will be inserted into the schema.
|
|
--
|
|
-- To add a new custom directives to the schema, add it to @customDirectives@.
|
|
-- Inclusion directives are treated separately, as they are not concerned with
|
|
-- altering the result of the parsers, but about whether a given field should be
|
|
-- parsed at all (see @collectFields@ for more information).
|
|
--
|
|
-- Alternatively, you could manually add a @DirectiveInfo@ to this list;
|
|
-- however, if you do so, and do not provide a corresponding definition in
|
|
-- @customDirectives@, calls to @parseDirectives@ will NOT attempt to parse that
|
|
-- new directive: they will only validate that it is used correctly. You can use
|
|
-- this option to implement directives that do not match our simple inclusion /
|
|
-- custom dichotomy.
|
|
--
|
|
-- To create a directive definition, you need to create a @Directive@ using
|
|
-- @mkDirective@, and export from this module its corresponding @DirectiveKey@
|
|
-- (see @skip@ below for an example). Make sure the type in the key matches the
|
|
-- type of the parser!
|
|
--
|
|
-- Directives may be "hidden", in which case they won't advertised in the
|
|
-- schema, but silently accepted. This is un-advisable and should only be used
|
|
-- when there's no other way around it.
|
|
directivesInfo :: forall m origin. MonadParse m => [DirectiveInfo origin]
|
|
directivesInfo = do
|
|
dir <- inclusionDirectives @m <> customDirectives @m
|
|
guard $ dAdvertised dir
|
|
pure $ dDefinition dir
|
|
|
|
-- | Not exported, only used internally; identical to 'directivesInfo', but also
|
|
-- contains hidden directives.
|
|
allDirectives :: forall m origin. MonadParse m => [DirectiveInfo origin]
|
|
allDirectives = map dDefinition $ inclusionDirectives @m <> customDirectives @m
|
|
|
|
inclusionDirectives :: forall m origin. MonadParse m => [Directive origin m]
|
|
inclusionDirectives = [includeDirective @m, skipDirective @m]
|
|
|
|
customDirectives :: forall m origin. MonadParse m => [Directive origin m]
|
|
customDirectives = [cachedDirective @m, multipleRootFieldsDirective @m]
|
|
|
|
-- | Parses directives, given a location. Ensures that all directives are known
|
|
-- and match the location; subsequently builds a dependent map of the results,
|
|
-- that can be then introspected with @withDirective@. The list of parsers that
|
|
-- should be applied is given as an argument: if a valid directive is found, but
|
|
-- for which no parser is provided, it will be ignored.
|
|
--
|
|
-- Example use:
|
|
--
|
|
-- dMap <- parseDirectives customDirectives (DLExecutable EDLQUERY) directives
|
|
-- withDirective dMap cached $ onJust \_ -> tagAsCached
|
|
parseDirectives ::
|
|
forall origin m.
|
|
MonadParse m =>
|
|
[Directive origin m] ->
|
|
G.DirectiveLocation ->
|
|
[G.Directive Variable] ->
|
|
m DirectiveMap
|
|
parseDirectives directiveParsers location givenDirectives = do
|
|
result <-
|
|
catMaybes <$> for givenDirectives \directive -> do
|
|
let name = G._dName directive
|
|
-- check the directive has a matching definition
|
|
DirectiveInfo {diLocations} <-
|
|
maybe (parseError ("directive " <> name <<> " is not defined in the schema")) pure $
|
|
L.find (\di -> diName di == name) (allDirectives @m)
|
|
-- check that it is allowed at the current location
|
|
unless (location `elem` diLocations) $
|
|
parseError $ "directive " <> name <<> " is not allowed on " <> humanReadable location
|
|
-- if we are expecting to parse it now, create a dmap entry
|
|
case L.find (\d -> diName (dDefinition d) == name) directiveParsers of
|
|
Nothing -> pure Nothing
|
|
Just (Directive {dParser}) -> do
|
|
result <- dParser directive
|
|
pure $ Just (name, DirectiveKey name :=> pure result)
|
|
-- check that the result does not contain duplicates
|
|
let dups = duplicates $ fst <$> result
|
|
unless (null dups) $
|
|
parseError $ "the following directives are used more than once: " <> commaSeparated dups
|
|
pure $ DM.fromList $ snd <$> result
|
|
where
|
|
humanReadable = \case
|
|
G.DLExecutable G.EDLQUERY -> "a query"
|
|
G.DLExecutable G.EDLMUTATION -> "a mutation"
|
|
G.DLExecutable G.EDLSUBSCRIPTION -> "a subscription"
|
|
G.DLExecutable G.EDLFIELD -> "a field"
|
|
G.DLExecutable G.EDLFRAGMENT_DEFINITION -> "a fragment definition"
|
|
G.DLExecutable G.EDLFRAGMENT_SPREAD -> "a fragment spread"
|
|
G.DLExecutable G.EDLINLINE_FRAGMENT -> "an inline fragment"
|
|
G.DLTypeSystem G.TSDLSCHEMA -> "the schema"
|
|
G.DLTypeSystem G.TSDLSCALAR -> "a scalar definition"
|
|
G.DLTypeSystem G.TSDLOBJECT -> "an object definition"
|
|
G.DLTypeSystem G.TSDLFIELD_DEFINITION -> "a field definition"
|
|
G.DLTypeSystem G.TSDLARGUMENT_DEFINITION -> "an argument definition"
|
|
G.DLTypeSystem G.TSDLINTERFACE -> "an interface definition"
|
|
G.DLTypeSystem G.TSDLUNION -> "an union definition"
|
|
G.DLTypeSystem G.TSDLENUM -> "an enum definition"
|
|
G.DLTypeSystem G.TSDLENUM_VALUE -> "an enum value definition"
|
|
G.DLTypeSystem G.TSDLINPUT_OBJECT -> "an input object definition"
|
|
G.DLTypeSystem G.TSDLINPUT_FIELD_DEFINITION -> "an input field definition"
|
|
duplicates = S.fromList . M.keys . M.filter (> 1) . M.fromListWith (+) . map (,1 :: Int)
|
|
|
|
withDirective ::
|
|
DirectiveMap ->
|
|
DirectiveKey a ->
|
|
(Maybe a -> m b) ->
|
|
m b
|
|
withDirective dmap key callback = callback $ runIdentity <$> DM.lookup key dmap
|
|
|
|
-- Cached custom directive.
|
|
|
|
cachedDirective :: forall m origin. MonadParse m => Directive origin m
|
|
cachedDirective =
|
|
mkDirective
|
|
Name._cached
|
|
(Just "whether this query should be cached (Hasura Cloud only)")
|
|
True
|
|
[G.DLExecutable G.EDLQUERY]
|
|
(CachedDirective <$> ttlArgument <*> forcedArgument)
|
|
where
|
|
-- Optionally set the cache entry time to live
|
|
ttlArgument :: InputFieldsParser origin m Int
|
|
ttlArgument = fieldWithDefault Name._ttl (Just "measured in seconds") (G.VInt 60) $ fromIntegral <$> int
|
|
|
|
-- Optionally Force a refresh of the cache entry
|
|
forcedArgument :: InputFieldsParser origin m Bool
|
|
forcedArgument = fieldWithDefault Name._refresh (Just "refresh the cache entry") (G.VBoolean False) boolean
|
|
|
|
data CachedDirective = CachedDirective {cdTtl :: Int, cdRefresh :: Bool}
|
|
|
|
cached :: DirectiveKey CachedDirective
|
|
cached = DirectiveKey Name._cached
|
|
|
|
-- Subscription tests custom directive.
|
|
|
|
multipleRootFieldsDirective :: MonadParse m => Directive origin m
|
|
multipleRootFieldsDirective =
|
|
mkDirective
|
|
Name.__multiple_top_level_fields
|
|
(Just "INTERNAL TESTING TOOL DO NOT USE")
|
|
False -- not advertised in the schema
|
|
[G.DLExecutable G.EDLSUBSCRIPTION]
|
|
(pure ())
|
|
|
|
multipleRootFields :: DirectiveKey ()
|
|
multipleRootFields = DirectiveKey Name.__multiple_top_level_fields
|
|
|
|
-- Built-in inclusion directives
|
|
|
|
skipDirective :: MonadParse m => Directive origin m
|
|
skipDirective =
|
|
mkDirective
|
|
Name._skip
|
|
(Just "whether this query should be skipped")
|
|
True
|
|
[ G.DLExecutable G.EDLFIELD,
|
|
G.DLExecutable G.EDLFRAGMENT_SPREAD,
|
|
G.DLExecutable G.EDLINLINE_FRAGMENT
|
|
]
|
|
ifArgument
|
|
|
|
includeDirective :: MonadParse m => Directive origin m
|
|
includeDirective =
|
|
mkDirective
|
|
Name._include
|
|
(Just "whether this query should be included")
|
|
True
|
|
[ G.DLExecutable G.EDLFIELD,
|
|
G.DLExecutable G.EDLFRAGMENT_SPREAD,
|
|
G.DLExecutable G.EDLINLINE_FRAGMENT
|
|
]
|
|
ifArgument
|
|
|
|
skip :: DirectiveKey Bool
|
|
skip = DirectiveKey Name._skip
|
|
|
|
include :: DirectiveKey Bool
|
|
include = DirectiveKey Name._include
|
|
|
|
ifArgument :: MonadParse m => InputFieldsParser origin m Bool
|
|
ifArgument = field Name._if Nothing boolean
|
|
|
|
-- Parser type for directives.
|
|
|
|
data Directive origin m where
|
|
Directive ::
|
|
forall origin m a.
|
|
(MonadParse m, Typeable a) =>
|
|
{ dDefinition :: DirectiveInfo origin,
|
|
dAdvertised :: Bool,
|
|
dParser :: G.Directive Variable -> m a
|
|
} ->
|
|
Directive origin m
|
|
|
|
data DirectiveKey a where
|
|
DirectiveKey :: Typeable a => G.Name -> DirectiveKey a
|
|
|
|
instance GEq DirectiveKey where
|
|
geq
|
|
(DirectiveKey name1 :: DirectiveKey a1)
|
|
(DirectiveKey name2 :: DirectiveKey a2)
|
|
| name1 == name2,
|
|
Just Refl <- eqT @a1 @a2 =
|
|
Just Refl
|
|
| otherwise = Nothing
|
|
|
|
instance GCompare DirectiveKey where
|
|
gcompare
|
|
(DirectiveKey name1 :: DirectiveKey a1)
|
|
(DirectiveKey name2 :: DirectiveKey a2) =
|
|
strengthenOrdering (compare name1 name2)
|
|
`extendGOrdering` gcompare (typeRep @a1) (typeRep @a2)
|
|
`extendGOrdering` GEQ
|
|
|
|
type DirectiveMap = DM.DMap DirectiveKey Identity
|
|
|
|
mkDirective ::
|
|
(MonadParse m, Typeable a) =>
|
|
G.Name ->
|
|
Maybe G.Description ->
|
|
Bool ->
|
|
[G.DirectiveLocation] ->
|
|
InputFieldsParser origin m a ->
|
|
Directive origin m
|
|
mkDirective name description advertised location argsParser =
|
|
Directive
|
|
{ dDefinition = DirectiveInfo name description (ifDefinitions argsParser) location,
|
|
dAdvertised = advertised,
|
|
dParser = \(G.Directive _name arguments) -> withKey (Key $ K.fromText $ G.unName name) $ do
|
|
for_ (M.keys arguments) \argumentName ->
|
|
unless (argumentName `S.member` argumentNames) $
|
|
parseError $ name <<> " has no argument named " <>> argumentName
|
|
withKey (Key $ K.fromText "args") $ ifParser argsParser $ GraphQLValue <$> arguments
|
|
}
|
|
where
|
|
argumentNames = S.fromList (dName <$> ifDefinitions argsParser)
|