packages feed

morpheus-graphql-app 0.25.0 → 0.26.0

raw patch · 21 files changed

+435/−266 lines, 21 filesdep ~morpheus-graphql-coredep ~morpheus-graphql-testsPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: morpheus-graphql-core, morpheus-graphql-tests

API changes (from Hackage documentation)

+ Data.Morpheus.App.NamedResolvers: nullRes :: (LiftOperation o, Monad m) => Resolver o e m (ResultBuilder o e m)

Files

morpheus-graphql-app.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           morpheus-graphql-app-version:        0.25.0+version:        0.26.0 synopsis:       Morpheus GraphQL App description:    Build GraphQL APIs with your favourite functional language! category:       web, graphql@@ -42,8 +42,10 @@     test/api/validation/input-coercion/list/list-single/query.gql     test/api/validation/input-coercion/list/schema.gql     test/api/validation/input-coercion/list/signle-value/query.gql-    test/batching/deities.gql-    test/batching/deities/query.gql+    test/batching/object-lists/query.gql+    test/batching/objects-fields/query.gql+    test/batching/objects-lists-fields/query.gql+    test/batching/schema.gql     test/merge/schema/query-subscription-mutation/app-1.gql     test/merge/schema/query-subscription-mutation/app-2.gql     test/merge/schema/query-subscription-mutation/mutation/query.gql@@ -88,7 +90,12 @@     test/api/validation/input-coercion/list/list-single/response.json     test/api/validation/input-coercion/list/resolvers.json     test/api/validation/input-coercion/list/signle-value/response.json-    test/batching/deities/response.json+    test/batching/object-lists/batching.json+    test/batching/object-lists/response.json+    test/batching/objects-fields/batching.json+    test/batching/objects-fields/response.json+    test/batching/objects-lists-fields/batching.json+    test/batching/objects-lists-fields/response.json     test/merge/schema/query-subscription-mutation/app-1.json     test/merge/schema/query-subscription-mutation/app-2.json     test/merge/schema/query-subscription-mutation/mutation/response.json@@ -120,6 +127,7 @@       Data.Morpheus.App.NamedResolvers       Data.Morpheus.Types.GQLWrapper   other-modules:+      Data.Morpheus.App.Internal.Resolving.Batching       Data.Morpheus.App.Internal.Resolving.Event       Data.Morpheus.App.Internal.Resolving.Resolver       Data.Morpheus.App.Internal.Resolving.ResolverState@@ -142,7 +150,7 @@     , containers >=0.4.2.1 && <0.7.0     , hashable >=1.0.0 && <2.0.0     , megaparsec >=7.0.0 && <10.0.0-    , morpheus-graphql-core >=0.25.0 && <0.26.0+    , morpheus-graphql-core >=0.26.0 && <0.27.0     , mtl >=2.0.0 && <3.0.0     , relude >=0.3.0 && <2.0.0     , scientific >=0.3.6.2 && <0.4.0@@ -174,8 +182,8 @@     , hashable >=1.0.0 && <2.0.0     , megaparsec >=7.0.0 && <10.0.0     , morpheus-graphql-app-    , morpheus-graphql-core >=0.25.0 && <0.26.0-    , morpheus-graphql-tests >=0.25.0 && <0.26.0+    , morpheus-graphql-core >=0.26.0 && <0.27.0+    , morpheus-graphql-tests >=0.26.0 && <0.27.0     , mtl >=2.0.0 && <3.0.0     , relude >=0.3.0 && <2.0.0     , scientific >=0.3.6.2 && <0.4.0
+ src/Data/Morpheus/App/Internal/Resolving/Batching.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE NoImplicitPrelude #-}++module Data.Morpheus.App.Internal.Resolving.Batching+  ( CacheKey (..),+    LocalCache,+    useCached,+    buildCacheWith,+    ResolverMapContext (..),+    ResolverMapT (..),+    runResMapT,+  )+where++import Control.Monad.Except (MonadError (throwError))+import Data.ByteString.Lazy.Char8 (unpack)+import qualified Data.HashMap.Lazy as HM+import Data.Morpheus.App.Internal.Resolving.Types (NamedResolverRef (..), ResolverMap)+import Data.Morpheus.Core (RenderGQL, render)+import Data.Morpheus.Types.Internal.AST+  ( GQLError,+    Msg (..),+    SelectionContent,+    TypeName,+    VALID,+    ValidValue,+    internal,+  )+import GHC.Show (Show (show))+import Relude hiding (show)++type LocalCache = HashMap CacheKey ValidValue++useCached :: (Eq k, Show k, Hashable k, MonadError GQLError f) => HashMap k a -> k -> f a+useCached mp v = case HM.lookup v mp of+  Just x -> pure x+  Nothing -> throwError (internal $ "cache value could not found for key" <> msg (show v :: String))++dumpCache :: Bool -> LocalCache -> a -> a+dumpCache enabled xs a+  | null xs || not enabled = a+  | otherwise = trace ("\nCACHE:\n" <> intercalate "\n" (map printKeyValue $ HM.toList xs) <> "\n") a+  where+    printKeyValue (key, v) = " " <> show key <> ": " <> unpack (render v)++printSel :: RenderGQL a => a -> [Char]+printSel sel = map replace $ filter ignoreSpaces $ unpack (render sel)+  where+    ignoreSpaces x = x /= ' '+    replace '\n' = ' '+    replace x = x++data BatchEntry = BatchEntry+  { batchedSelection :: SelectionContent VALID,+    batchedType :: TypeName,+    batchedArguments :: [ValidValue]+  }++instance Show BatchEntry where+  show BatchEntry {..} = printSel batchedSelection <> ":" <> toString batchedType <> ":" <> show (map (unpack . render) batchedArguments)++data CacheKey = CacheKey+  { cachedSel :: SelectionContent VALID,+    cachedTypeName :: TypeName,+    cachedArg :: ValidValue+  }+  deriving (Eq, Generic)++instance Show CacheKey where+  show (CacheKey sel typename dep) = printSel sel <> ":" <> toString typename <> ":" <> unpack (render dep)++instance Hashable CacheKey where+  hashWithSalt s (CacheKey sel tyName arg) = hashWithSalt s (sel, tyName, render arg)++uniq :: (Eq a, Hashable a) => [a] -> [a]+uniq = HM.keys . HM.fromList . map (,True)++buildBatches :: [(SelectionContent VALID, NamedResolverRef)] -> [BatchEntry]+buildBatches inputs =+  let entityTypes = uniq $ map (second resolverTypeName) inputs+   in mapMaybe (selectByEntity inputs) entityTypes++selectByEntity :: [(SelectionContent VALID, NamedResolverRef)] -> (SelectionContent VALID, TypeName) -> Maybe BatchEntry+selectByEntity inputs (tSel, tName) = case filter areEq inputs of+  [] -> Nothing+  xs -> Just $ BatchEntry tSel tName (uniq $ concatMap (resolverArgument . snd) xs)+  where+    areEq (sel, v) = sel == tSel && tName == resolverTypeName v++type ResolverFun m = NamedResolverRef -> SelectionContent VALID -> m [ValidValue]++resolveBatched :: Monad m => ResolverFun m -> BatchEntry -> m LocalCache+resolveBatched f (BatchEntry sel name deps) = do+  res <- f (NamedResolverRef name deps) sel+  let keys = map (CacheKey sel name) deps+  let entries = zip keys res+  pure $ HM.fromList entries++updateCache :: (Monad m, Traversable t) => ResolverFun m -> LocalCache -> t BatchEntry -> m LocalCache+updateCache f cache entries = do+  caches <- traverse (resolveBatched f) entries+  let newCache = foldr (<>) cache caches+  pure $ dumpCache False newCache newCache++buildCacheWith :: Monad m => ResolverFun m -> LocalCache -> [(SelectionContent VALID, NamedResolverRef)] -> m LocalCache+buildCacheWith f cache entries = updateCache f cache (buildBatches entries)++data ResolverMapContext m = ResolverMapContext+  { localCache :: LocalCache,+    resolverMap :: ResolverMap m+  }++newtype ResolverMapT m a = ResolverMapT+  { _runResMapT :: ReaderT (ResolverMapContext m) m a+  }+  deriving+    ( Functor,+      Applicative,+      Monad,+      MonadReader (ResolverMapContext m)+    )++instance MonadTrans ResolverMapT where+  lift = ResolverMapT . lift++deriving instance MonadError GQLError m => MonadError GQLError (ResolverMapT m)++runResMapT :: ResolverMapT m a -> ResolverMapContext m -> m a+runResMapT (ResolverMapT x) = runReaderT x
src/Data/Morpheus/App/Internal/Resolving/ResolveValue.hs view
@@ -9,31 +9,34 @@ module Data.Morpheus.App.Internal.Resolving.ResolveValue   ( resolveRef,     resolveObject,+    ResolverMapContext (..),   ) where  import Control.Monad.Except (MonadError (throwError)) import qualified Data.HashMap.Lazy as HM+import Data.Morpheus.App.Internal.Resolving.Batching+  ( CacheKey (..),+    ResolverMapContext (..),+    ResolverMapT,+    buildCacheWith,+    runResMapT,+    useCached,+  ) import Data.Morpheus.App.Internal.Resolving.ResolverState   ( ResolverContext (..),     askFieldTypeName,     updateCurrentType,   ) import Data.Morpheus.App.Internal.Resolving.Types-  ( BatchEntry (..),-    CacheKey (..),-    LocalCache,-    NamedResolver (..),+  ( NamedResolver (..),     NamedResolverRef (..),     NamedResolverResult (..),     ObjectTypeResolver (..),     ResolverMap,     ResolverValue (..),-    buildBatches,-    dumpCache,     mkEnum,     mkUnion,-    useCached,   ) import Data.Morpheus.Error (subfieldsNotSelected) import Data.Morpheus.Internal.Utils@@ -64,8 +67,6 @@   ) import Relude hiding (empty) -type ResolverMapContext m = (LocalCache, ResolverMap m)- scanRefs :: (MonadError GQLError m, MonadReader ResolverContext m) => SelectionContent VALID -> ResolverValue m -> m [(SelectionContent VALID, NamedResolverRef)] scanRefs sel (ResList xs) = concat <$> traverse (scanRefs sel) xs scanRefs sel (ResLazy x) = x >>= scanRefs sel@@ -104,53 +105,39 @@     MonadReader ResolverContext m,     MonadError GQLError m   ) =>-  ResolverMapContext m ->   ResolverValue m ->   SelectionContent VALID ->-  m ValidValue-resolveSelection rmap res selection = do-  newRmap <- scanRefs selection res >>= buildCache rmap . buildBatches-  __resolveSelection newRmap res selection--buildCache :: (MonadError GQLError m, MonadReader ResolverContext m) => ResolverMapContext m -> [BatchEntry] -> m (LocalCache, ResolverMap m)-buildCache (cache, rmap) entries = do-  caches <- traverse (resolveCacheEntry (cache, rmap)) entries-  let newCache = foldr (<>) cache caches-  pure $ dumpCache False (newCache, rmap)+  ResolverMapT m ValidValue+resolveSelection res selection = do+  ctx <- ask+  newRmap <- lift (scanRefs selection res >>= buildCache ctx)+  local (const newRmap) (__resolveSelection res selection) -resolveCacheEntry :: (MonadError GQLError m, MonadReader ResolverContext m) => ResolverMapContext m -> BatchEntry -> m LocalCache-resolveCacheEntry rmap (BatchEntry sel name deps) = do-  res <- resolveRefsCached rmap (NamedResolverRef name deps) sel-  let keys = map (CacheKey sel name) deps-  let entries = zip keys res-  pure $ HM.fromList entries+buildCache :: (MonadError GQLError m, MonadReader ResolverContext m) => ResolverMapContext m -> [(SelectionContent VALID, NamedResolverRef)] -> m (ResolverMapContext m)+buildCache ctx@(ResolverMapContext cache rmap) entries = (`ResolverMapContext` rmap) <$> buildCacheWith (resolveRefsCached ctx) cache entries  __resolveSelection ::   ( Monad m,     MonadReader ResolverContext m,     MonadError GQLError m   ) =>-  ResolverMapContext m ->   ResolverValue m ->   SelectionContent VALID ->-  m ValidValue-__resolveSelection rmap (ResLazy x) selection =-  x >>= flip (resolveSelection rmap) selection-__resolveSelection rmap (ResList xs) selection =-  List <$> traverse (flip (resolveSelection rmap) selection) xs--- Object ------------------__resolveSelection rmap (ResObject tyName obj) sel = withObject tyName (resolveObject rmap obj) sel--- ENUM-__resolveSelection _ (ResEnum name) SelectionField = pure $ Scalar $ String $ unpackName name-__resolveSelection rmap (ResEnum name) unionSel@UnionSelection {} =-  resolveSelection rmap (mkUnion name [(unitFieldName, pure $ mkEnum unitTypeName)]) unionSel-__resolveSelection _ ResEnum {} _ = throwError (internal "wrong selection on enum value")--- SCALARS-__resolveSelection _rmap ResNull _ = pure Null-__resolveSelection _rmap (ResScalar x) SelectionField = pure $ Scalar x-__resolveSelection _rmap ResScalar {} _ =-  throwError (internal "scalar Resolver should only receive SelectionField")-__resolveSelection rmap (ResRef ref) sel = ref >>= flip (resolveRef rmap) sel+  ResolverMapT m ValidValue+__resolveSelection (ResLazy x) selection = lift x >>= (`resolveSelection` selection)+__resolveSelection (ResList xs) selection = List <$> traverse (`resolveSelection` selection) xs+__resolveSelection (ResObject tyName obj) sel = do+  ctx <- ask+  lift $ withObject tyName (resolveObject ctx obj) sel+__resolveSelection (ResEnum name) SelectionField = pure $ Scalar $ String $ unpackName name+__resolveSelection (ResEnum name) unionSel@UnionSelection {} = resolveSelection (mkUnion name [(unitFieldName, pure $ mkEnum unitTypeName)]) unionSel+__resolveSelection ResEnum {} _ = throwError (internal "wrong selection on enum value")+__resolveSelection ResNull _ = pure Null+__resolveSelection (ResScalar x) SelectionField = pure $ Scalar x+__resolveSelection ResScalar {} _ = throwError (internal "scalar Resolver should only receive SelectionField")+__resolveSelection (ResRef ref) sel = do+  ctx <- ask+  lift (ref >>= flip (resolveRef ctx) sel)  withObject ::   ( MonadError GQLError m,@@ -170,7 +157,7 @@       where         fx (Just x) y = Just <$> (x <:> unionTagSelection y)         fx Nothing y = pure $ Just $ unionTagSelection y-    checkContent _ = noEmptySelection+    checkContent SelectionField = noEmptySelection  noEmptySelection :: (MonadError GQLError m, MonadReader ResolverContext m) => m value noEmptySelection = do@@ -181,15 +168,15 @@   ( MonadError GQLError m,     MonadReader ResolverContext m   ) =>-  (LocalCache, ResolverMap m) ->+  ResolverMapContext m ->   NamedResolverRef ->   SelectionContent VALID ->   m ValidValue resolveRef rmap ref selection = resolveRefsCached rmap ref selection >>= toOne -toOne :: (MonadError GQLError f) => [a] -> f a+toOne :: (MonadError GQLError f, Show a) => [a] -> f a toOne [x] = pure x-toOne _ = throwError (internal "TODO:")+toOne x = throwError (internal ("expected only one resolved value for " <> msg (show x :: String)))  resolveRefsCached ::   ( MonadError GQLError m,@@ -199,41 +186,42 @@   NamedResolverRef ->   SelectionContent VALID ->   m [ValidValue]-resolveRefsCached (cache, rmap) (NamedResolverRef name args) selection = do+resolveRefsCached ctx (NamedResolverRef name args) selection = do   let keys = map (CacheKey selection name) args   let cached = map resolveCached keys   let cachedMap = HM.fromList (mapMaybe unp cached)-  notCachedMap <- resolveUncached (cache, rmap) name selection $ map fst $ filter (isNothing . snd) cached+  notCachedMap <- runResMapT (resolveUncached name selection $ map fst $ filter (isNothing . snd) cached) ctx   traverse (useCached (cachedMap <> notCachedMap)) args   where     unp (_, Nothing) = Nothing     unp (x, Just y) = Just (x, y)-    resolveCached key = (cachedArg key, HM.lookup key cache)+    resolveCached key = (cachedArg key, HM.lookup key $ localCache ctx)  processResult ::   (MonadError GQLError m, MonadReader ResolverContext m) =>-  ResolverMapContext m ->   TypeName ->   SelectionContent VALID ->   NamedResolverResult m ->-  m ValidValue-processResult rmap typename selection (NamedObjectResolver res) = withObject (Just typename) (resolveObject rmap res) selection-processResult rmap _ selection (NamedUnionResolver unionRef) = resolveSelection rmap (ResRef $ pure unionRef) selection-processResult rmap _ selection (NamedEnumResolver value) = resolveSelection rmap (ResEnum value) selection-processResult rmap _ selection NamedNullResolver = resolveSelection rmap ResNull selection+  ResolverMapT m ValidValue+processResult typename selection (NamedObjectResolver res) = do+  ctx <- ask+  lift $ withObject (Just typename) (resolveObject ctx res) selection+processResult _ selection (NamedUnionResolver unionRef) = resolveSelection (ResRef $ pure unionRef) selection+processResult _ selection (NamedEnumResolver value) = resolveSelection (ResEnum value) selection+processResult _ selection NamedNullResolver = resolveSelection ResNull selection  resolveUncached ::   ( MonadError GQLError m,     MonadReader ResolverContext m   ) =>-  (LocalCache, ResolverMap m) ->   TypeName ->   SelectionContent VALID ->   [ValidValue] ->-  m (HashMap ValidValue ValidValue)-resolveUncached _ _ _ [] = pure empty-resolveUncached rmap typename selection xs = do-  vs <- getNamedResolverBy (NamedResolverRef typename xs) (snd rmap) >>= traverse (processResult rmap typename selection)+  ResolverMapT m (HashMap ValidValue ValidValue)+resolveUncached _ _ [] = pure empty+resolveUncached typename selection xs = do+  rmap <- asks resolverMap+  vs <- lift (getNamedResolverBy (NamedResolverRef typename xs) rmap) >>= traverse (processResult typename selection)   pure $ HM.fromList (zip xs vs)  getNamedResolverBy ::@@ -249,34 +237,33 @@   ( MonadReader ResolverContext m,     MonadError GQLError m   ) =>-  (LocalCache, ResolverMap m) ->+  ResolverMapContext m ->   ObjectTypeResolver m ->   Maybe (SelectionSet VALID) ->   m ValidValue resolveObject rmap drv sel = do-  newCache <- objectRefs drv sel >>= buildCache rmap . buildBatches+  newCache <- objectRefs drv sel >>= buildCache rmap   Object <$> maybe (pure empty) (traverseCollection (resolver newCache)) sel   where-    resolver newCache currentSelection = do+    resolver cacheCTX currentSelection = do       t <- askFieldTypeName (selectionName currentSelection)       updateCurrentType t $         local (\ctx -> ctx {currentSelection}) $           ObjectEntry (keyOf currentSelection)-            <$> runFieldResolver newCache currentSelection drv+            <$> runResMapT (runFieldResolver currentSelection drv) cacheCTX  runFieldResolver ::   ( Monad m,     MonadReader ResolverContext m,     MonadError GQLError m   ) =>-  (LocalCache, ResolverMap m) ->   Selection VALID ->   ObjectTypeResolver m ->-  m ValidValue-runFieldResolver rmap Selection {selectionName, selectionContent}+  ResolverMapT m ValidValue+runFieldResolver Selection {selectionName, selectionContent}   | selectionName == "__typename" =-      const (Scalar . String . unpackName <$> asks (typeName . currentType))+      const (Scalar . String . unpackName <$> lift (asks (typeName . currentType)))   | otherwise =-      maybe (pure Null) (>>= \x -> resolveSelection rmap x selectionContent)+      maybe (pure Null) (lift >=> (`resolveSelection` selectionContent))         . HM.lookup selectionName         . objectFields
src/Data/Morpheus/App/Internal/Resolving/RootResolverValue.hs view
@@ -92,7 +92,7 @@   selection =     do       root <- runResolverStateT (toResolverStateT res) ctx-      runResolver channels (resolveObject mempty root (Just selection)) ctx+      runResolver channels (resolveObject (ResolverMapContext mempty mempty) root (Just selection)) ctx  runRootResolverValue :: Monad m => RootResolverValue e m -> ResolverContext -> ResponseStream e m (Value VALID) runRootResolverValue@@ -118,7 +118,7 @@     where       selectByOperation Query = withIntrospection (\sel -> runResolver Nothing (resolvedValue sel) ctx) ctx         where-          resolvedValue selection = resolveRef (empty, queryResolverMap) (NamedResolverRef "Query" ["ROOT"]) (SelectionSet selection)+          resolvedValue selection = resolveRef (ResolverMapContext empty queryResolverMap) (NamedResolverRef "Query" ["ROOT"]) (SelectionSet selection)       selectByOperation _ = throwError "mutation and subscription is not supported for namedResolvers"  withIntrospection :: Monad m => (SelectionSet VALID -> ResponseStream event m ValidValue) -> ResolverContext -> ResponseStream event m ValidValue@@ -131,7 +131,7 @@     mergeRoot y x  introspection :: Monad m => SelectionSet VALID -> ResolverContext -> ResponseStream event m ValidValue-introspection selection ctx@ResolverContext {schema} = runResolver Nothing (resolveObject mempty (schemaAPI schema) (Just selection)) ctx+introspection selection ctx@ResolverContext {schema} = runResolver Nothing (resolveObject (ResolverMapContext mempty mempty) (schemaAPI schema) (Just selection)) ctx  mergeRoot :: MonadError GQLError m => ValidValue -> ValidValue -> m ValidValue mergeRoot (Object x) (Object y) = Object <$> merge x y
src/Data/Morpheus/App/Internal/Resolving/Types.hs view
@@ -1,6 +1,5 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-}@@ -9,7 +8,6 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE NoImplicitPrelude #-}@@ -33,80 +31,24 @@     mkObjectMaybe,     mkUnion,     NamedResolverFun,-    buildBatches,-    Cache,-    CacheKey (..),-    BatchEntry (..),-    LocalCache,-    dumpCache,-    useCached,   ) where  import Control.Monad.Except (MonadError (throwError))-import Data.ByteString.Lazy.Char8 (unpack) import qualified Data.HashMap.Lazy as HM-import Data.Morpheus.Core (RenderGQL, render) import Data.Morpheus.Internal.Ext (Merge (..)) import Data.Morpheus.Internal.Utils (KeyOf (keyOf)) import Data.Morpheus.Types.Internal.AST   ( FieldName,     GQLError,     ScalarValue (..),-    SelectionContent,     TypeName,-    VALID,     ValidValue,     internal,   ) import GHC.Show (Show (show)) import Relude hiding (show) -type LocalCache = HashMap CacheKey ValidValue--useCached :: (Eq k, Hashable k, MonadError GQLError f) => HashMap k a -> k -> f a-useCached mp v = case HM.lookup v mp of-  Just x -> pure x-  Nothing -> throwError (internal "TODO:")--dumpCache :: Bool -> (LocalCache, ResolverMap m) -> (LocalCache, ResolverMap m)-dumpCache enabled xs-  | null (fst xs) || not enabled = xs-  | otherwise = trace ("\nCACHE:\n" <> intercalate "\n" (map printKeyValue $ HM.toList $ fst xs) <> "\n") xs-  where-    printKeyValue (key, v) = " " <> show key <> ": " <> unpack (render v)--printSel :: RenderGQL a => a -> [Char]-printSel sel = map replace $ filter ignoreSpaces $ unpack (render sel)-  where-    ignoreSpaces x = x /= ' '-    replace '\n' = ' '-    replace x = x--data BatchEntry = BatchEntry-  { batchedSelection :: SelectionContent VALID,-    batchedType :: TypeName,-    batchedArguments :: [ValidValue]-  }--instance Show BatchEntry where-  show (BatchEntry sel typename dep) = printSel sel <> ":" <> toString typename <> ":" <> show (map (unpack . render) dep)--data CacheKey = CacheKey-  { cachedSel :: SelectionContent VALID,-    cachedTypeName :: TypeName,-    cachedArg :: ValidValue-  }-  deriving (Eq, Generic)--instance Show CacheKey where-  show (CacheKey sel typename dep) = printSel sel <> ":" <> toString typename <> ":" <> unpack (render dep)--instance Hashable CacheKey where-  hashWithSalt s (CacheKey sel tyName arg) = hashWithSalt s (sel, tyName, render arg)--type Cache m = HashMap CacheKey (NamedResolverResult m)- type ResolverMap (m :: Type -> Type) = HashMap TypeName (NamedResolver m)  type NamedResolverArg = [ValidValue]@@ -134,21 +76,6 @@     resolverArgument :: NamedResolverArg   }   deriving (Show)--uniq :: (Eq a, Hashable a) => [a] -> [a]-uniq = HM.keys . HM.fromList . map (,True)--buildBatches :: [(SelectionContent VALID, NamedResolverRef)] -> [BatchEntry]-buildBatches inputs =-  let entityTypes = uniq $ map (second resolverTypeName) inputs-   in mapMaybe (selectByEntity inputs) entityTypes--selectByEntity :: [(SelectionContent VALID, NamedResolverRef)] -> (SelectionContent VALID, TypeName) -> Maybe BatchEntry-selectByEntity inputs (tSel, tName) = case filter areEq inputs of-  [] -> Nothing-  xs -> Just $ BatchEntry tSel tName (uniq $ concatMap (resolverArgument . snd) xs)-  where-    areEq (sel, v) = sel == tSel && tName == resolverTypeName v  data NamedResolverResult (m :: Type -> Type)   = NamedObjectResolver (ObjectTypeResolver m)
src/Data/Morpheus/App/Internal/Resolving/Utils.hs view
@@ -22,6 +22,8 @@ import Control.Monad.Except (MonadError (throwError)) import qualified Data.Aeson as A import Data.Morpheus.App.Internal.Resolving.ResolverState+  ( ResolverContext (..),+  ) import Data.Morpheus.App.Internal.Resolving.Types   ( NamedResolverRef (..),     ObjectTypeResolver (..),
src/Data/Morpheus/App/NamedResolvers.hs view
@@ -10,6 +10,7 @@     NamedResolverFunction,     RootResolverValue,     ResultBuilder,+    nullRes,   ) where @@ -57,6 +58,9 @@ variant :: (LiftOperation o, Monad m) => TypeName -> ValidValue -> Resolver o e m (ResultBuilder o e m) variant tName = pure . Union tName +nullRes :: (LiftOperation o, Monad m) => Resolver o e m (ResultBuilder o e m)+nullRes = pure Null+ queryResolvers :: Monad m => [(TypeName, NamedResolverFunction QUERY e m)] -> RootResolverValue e m queryResolvers = NamedResolversValue . mkResolverMap @@ -64,6 +68,7 @@ data ResultBuilder o e m   = Object [(FieldName, Resolver o e m (ResolverValue (Resolver o e m)))]   | Union TypeName ValidValue+  | Null  mkResolverMap :: (LiftOperation o, Monad m) => [(TypeName, NamedResolverFunction o e m)] -> ResolverMap (Resolver o e m) mkResolverMap = HM.fromList . map packRes@@ -73,3 +78,4 @@       where         mapValue (Object x) = NamedObjectResolver (ObjectTypeResolver $ HM.fromList x)         mapValue (Union name x) = NamedUnionResolver (NamedResolverRef name [x])+        mapValue Null = NamedNullResolver
test/Batching.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-} {-# LANGUAGE NoImplicitPrelude #-}  module Batching@@ -6,20 +7,24 @@   ) where -import Data.ByteString.Lazy.Char8 (unpack)+import Control.Monad.Except (MonadError (throwError))+import Data.Aeson (eitherDecode) import qualified Data.ByteString.Lazy.Char8 as LBS+import Data.HashSet (fromList)+import Data.Map (lookup) import Data.Morpheus.App   ( App (..),     mkApp,     runApp,   )-import Data.Morpheus.App.Internal.Resolving (resultOr)+import Data.Morpheus.App.Internal.Resolving (ResolverValue, resultOr) import Data.Morpheus.App.NamedResolvers   ( NamedResolverFunction,     RootResolverValue,     enum,     getArgument,     list,+    nullRes,     object,     queryResolvers,     ref,@@ -27,67 +32,99 @@   ) import Data.Morpheus.Core   ( parseSchema,-    render,   ) import Data.Morpheus.Types.IO   ( GQLRequest (..),     GQLResponse,   )-import Data.Morpheus.Types.Internal.AST (QUERY, Schema, VALID)-import Relude hiding (ByteString)+import Data.Morpheus.Types.Internal.AST+  ( QUERY,+    Schema,+    TypeName,+    VALID,+    ValidValue,+    unpackName,+  )+import Relude hiding (ByteString, fromList) import Test.Morpheus   ( FileUrl,+    file,     testApi,   ) import Test.Tasty   ( TestTree,   ) --- DEITIES+type BatchedValues = (HashSet ValidValue) -debugArgs :: String -> NamedResolverFunction QUERY e m -> NamedResolverFunction QUERY e m-debugArgs name f args = trace (name <> ":: " <> intercalate ", " (map (unpack . render) args)) (f args)+type BatchingConstraints = Map Text BatchedValues +typeConstraint :: Monad m => BatchingConstraints -> (TypeName, NamedResolverFunction QUERY e m) -> (TypeName, NamedResolverFunction QUERY e m)+typeConstraint cons (name, f) = (name,) $ maybe f (require f) (lookup (unpackName name) cons)++require :: Monad m => NamedResolverFunction QUERY e m -> BatchedValues -> NamedResolverFunction QUERY e m+require f req args+  | fromList args == req = f args+  | otherwise = throwError ("was not batched! expected: " <> show req <> "got: " <> show args)++gods :: [ValidValue]+gods = ["poseidon", "morpheus", "zeus"]++getName :: ValidValue -> ResolverValue m+getName "zeus" = "Zeus"+getName "morpheus" = "Morpheus"+getName "poseidon" = "Zeus"+getName "cronos" = "Cronos"+getName _ = ""++getPowers :: ValidValue -> [ResolverValue m]+getPowers "zeus" = [enum "Thunderbolt"]+getPowers "morpheus" = [enum "Shapeshifting"]+getPowers _ = []+ deityResolver :: Monad m => NamedResolverFunction QUERY e m-deityResolver = debugArgs "DEITY" (traverse getDeity)+deityResolver = traverse getDeity   where-    getDeity "zeus" =-      object-        [ ("name", pure "Zeus"),-          ("power", pure $ list [])-        ]-    getDeity _ =-      object-        [ ("name", pure "Morpheus"),-          ("power", pure $ list [enum "Shapeshifting"])-        ]+    getDeity name+      | name `elem` gods =+          object+            [ ("name", pure $ getName name),+              ("power", pure $ list $ getPowers name)+            ]+      | otherwise = nullRes  resolveQuery :: Monad m => NamedResolverFunction QUERY e m-resolveQuery = debugArgs "QUERY" (traverse _resolveQuery)+resolveQuery = traverse getQuery   where-    _resolveQuery _ =+    getQuery _ =       object         [ ("deity", ref "Deity" <$> getArgument "id"),           ("deities", pure $ refs "Deity" ["zeus", "morpheus"])         ] -resolvers :: Monad m => RootResolverValue e m-resolvers =-  queryResolvers-    [ ("Query", resolveQuery),-      ("Deity", deityResolver)-    ]+resolvers :: Monad m => BatchingConstraints -> RootResolverValue e m+resolvers cons =+  queryResolvers $+    typeConstraint cons+      <$> [ ("Query", resolveQuery),+            ("Deity", deityResolver)+          ] -getSchema :: String -> IO (Schema VALID)-getSchema url = LBS.readFile url >>= resultOr (fail . show) pure . parseSchema+getSchema :: FileUrl -> IO (Schema VALID)+getSchema url = LBS.readFile (toString url) >>= resultOr (fail . show) pure . parseSchema -getApps :: FileUrl -> IO (App e IO)-getApps _ = do-  schemaDeities <- getSchema "test/named-resolvers/deities.gql"-  pure $ mkApp schemaDeities resolvers+getBatchingConstraint :: FileUrl -> IO BatchingConstraints+getBatchingConstraint url = LBS.readFile (toString (file url "batching.json")) >>= either (fail . show) pure . eitherDecode +getApps :: BatchingConstraints -> FileUrl -> IO (App e IO)+getApps con x = do+  schemaDeities <- getSchema (file x "schema.gql")+  pure $ mkApp schemaDeities (resolvers con)+ runBatchingTest :: FileUrl -> FileUrl -> TestTree-runBatchingTest url = testApi api+runBatchingTest url fileUrl = testApi api fileUrl   where     api :: GQLRequest -> IO GQLResponse-    api req = getApps url >>= (`runApp` req)+    api req = do+      constraints <- getBatchingConstraint fileUrl+      getApps constraints url >>= (`runApp` req)
− test/batching/deities.gql
@@ -1,14 +0,0 @@-enum Power {-  Shapeshifting-  Thunderbolt-}--type Deity {-  name: String!-  power: [String!]-}--type Query {-  deities: [Deity!]!-  deity(id: ID): Deity-}
− test/batching/deities/query.gql
@@ -1,26 +0,0 @@-query {-  deities {-    name-    power-  }--  heroes: deities {-    name-    power-  }--  cronos: deity(id: "cronos") {-    name-    power-  }--  poseidon: deity(id: "poseidon") {-    name-    power-  }--  morpheus: deity(id: "morpheus") {-    name-    power-  }-}
− test/batching/deities/response.json
@@ -1,36 +0,0 @@-{-  "data": {-    "poseidon": {-      "name": "Morpheus",-      "power": ["Shapeshifting"]-    },-    "morpheus": {-      "name": "Morpheus",-      "power": ["Shapeshifting"]-    },-    "heroes": [-      {-        "name": "Zeus",-        "power": []-      },-      {-        "name": "Morpheus",-        "power": ["Shapeshifting"]-      }-    ],-    "cronos": {-      "name": "Morpheus",-      "power": ["Shapeshifting"]-    },-    "deities": [-      {-        "name": "Zeus",-        "power": []-      },-      {-        "name": "Morpheus",-        "power": ["Shapeshifting"]-      }-    ]-  }-}
+ test/batching/object-lists/batching.json view
@@ -0,0 +1,4 @@+{+  "Deity": ["morpheus", "zeus"],+  "Query": ["ROOT"]+}
+ test/batching/object-lists/query.gql view
@@ -0,0 +1,6 @@+query {+  deities {+    name+    power+  }+}
+ test/batching/object-lists/response.json view
@@ -0,0 +1,14 @@+{+  "data": {+    "deities": [+      {+        "name": "Zeus",+        "power": ["Thunderbolt"]+      },+      {+        "name": "Morpheus",+        "power": ["Shapeshifting"]+      }+    ]+  }+}
+ test/batching/objects-fields/batching.json view
@@ -0,0 +1,4 @@+{+  "Deity": ["cronos", "poseidon", "morpheus"],+  "Query": ["ROOT"]+}
+ test/batching/objects-fields/query.gql view
@@ -0,0 +1,16 @@+query {+  cronos: deity(id: "cronos") {+    name+    power+  }++  poseidon: deity(id: "poseidon") {+    name+    power+  }++  morpheus: deity(id: "morpheus") {+    name+    power+  }+}
+ test/batching/objects-fields/response.json view
@@ -0,0 +1,13 @@+{+  "data": {+    "poseidon": {+      "name": "Zeus",+      "power": []+    },+    "morpheus": {+      "name": "Morpheus",+      "power": ["Shapeshifting"]+    },+    "cronos": null+  }+}
+ test/batching/objects-lists-fields/batching.json view
@@ -0,0 +1,4 @@+{+  "Deity": ["cronos", "poseidon", "morpheus", "zeus"],+  "Query": ["ROOT"]+}
+ test/batching/objects-lists-fields/query.gql view
@@ -0,0 +1,26 @@+query {+  deities {+    name+    power+  }++  heroes: deities {+    name+    power+  }++  cronos: deity(id: "cronos") {+    name+    power+  }++  poseidon: deity(id: "poseidon") {+    name+    power+  }++  morpheus: deity(id: "morpheus") {+    name+    power+  }+}
+ test/batching/objects-lists-fields/response.json view
@@ -0,0 +1,33 @@+{+  "data": {+    "poseidon": {+      "name": "Zeus",+      "power": []+    },+    "morpheus": {+      "name": "Morpheus",+      "power": ["Shapeshifting"]+    },+    "heroes": [+      {+        "name": "Zeus",+        "power": ["Thunderbolt"]+      },+      {+        "name": "Morpheus",+        "power": ["Shapeshifting"]+      }+    ],+    "cronos": null,+    "deities": [+      {+        "name": "Zeus",+        "power": ["Thunderbolt"]+      },+      {+        "name": "Morpheus",+        "power": ["Shapeshifting"]+      }+    ]+  }+}
+ test/batching/schema.gql view
@@ -0,0 +1,14 @@+enum Power {+  Shapeshifting+  Thunderbolt+}++type Deity {+  name: String!+  power: [String!]+}++type Query {+  deities: [Deity!]!+  deity(id: ID): Deity+}