packages feed

freckle-app 1.8.1.0 → 1.9.0.0

raw patch · 16 files changed

+346/−126 lines, 16 filesdep +resourcetPVP ok

version bump matches the API change (PVP)

Dependencies added: resourcet

API changes (from Hackage documentation)

- Freckle.App.Database: class HasVaultData env
- Freckle.App.Database: getVaultData :: HasVaultData env => env -> Maybe XRayVaultData
- Freckle.App.Database: instance Freckle.App.Database.HasVaultData (Yesod.Core.Types.HandlerData child site)
- Freckle.App.Test: PoolConfig :: !IO a -> !a -> IO () -> !Double -> !Int -> PoolConfig a
- Freckle.App.Test: [createResource] :: PoolConfig a -> !IO a
- Freckle.App.Test: [freeResource] :: PoolConfig a -> !a -> IO ()
- Freckle.App.Test: [poolCacheTTL] :: PoolConfig a -> !Double
- Freckle.App.Test: [poolMaxResources] :: PoolConfig a -> !Int
- Freckle.App.Test: class HasVaultData env
- Freckle.App.Test: getVaultData :: HasVaultData env => env -> Maybe XRayVaultData
- Freckle.App.Test: instance Freckle.App.Database.HasVaultData app => Freckle.App.Database.HasVaultData (Freckle.App.Test.AppExample app a)
+ Freckle.App: AppT :: ReaderT app (LoggingT (ResourceT m)) a -> AppT app m a
+ Freckle.App: [unAppT] :: AppT app m a -> ReaderT app (LoggingT (ResourceT m)) a
+ Freckle.App: instance Control.Monad.Catch.MonadCatch m => Control.Monad.Catch.MonadCatch (Freckle.App.AppT app m)
+ Freckle.App: instance Control.Monad.Catch.MonadMask m => Control.Monad.Catch.MonadMask (Freckle.App.AppT app m)
+ Freckle.App: instance Control.Monad.Catch.MonadThrow m => Control.Monad.Catch.MonadThrow (Freckle.App.AppT app m)
+ Freckle.App: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Freckle.App.AppT app m)
+ Freckle.App: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.Logger.MonadLogger (Freckle.App.AppT app m)
+ Freckle.App: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.Logger.MonadLoggerIO (Freckle.App.AppT app m)
+ Freckle.App: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.Trans.Resource.Internal.MonadResource (Freckle.App.AppT app m)
+ Freckle.App: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Control.Monad.IO.Unlift.MonadUnliftIO (Freckle.App.AppT app m)
+ Freckle.App: instance Control.Monad.Primitive.PrimMonad m => Control.Monad.Primitive.PrimMonad (Freckle.App.AppT app m)
+ Freckle.App: instance GHC.Base.Applicative m => Freckle.App.OpenTelemetry.MonadTracer (Freckle.App.AppT app m)
+ Freckle.App: instance GHC.Base.Applicative m => GHC.Base.Applicative (Freckle.App.AppT app m)
+ Freckle.App: instance GHC.Base.Functor m => GHC.Base.Functor (Freckle.App.AppT app m)
+ Freckle.App: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader app (Freckle.App.AppT app m)
+ Freckle.App: instance GHC.Base.Monad m => GHC.Base.Monad (Freckle.App.AppT app m)
+ Freckle.App: newtype AppT app m a
+ Freckle.App: runAppT :: (MonadUnliftIO m, HasLogger app) => AppT app m a -> app -> m a
+ Freckle.App: setLineBuffering :: MonadIO m => m ()
+ Freckle.App.Database: instance Freckle.App.Database.HasSqlPool site => Freckle.App.Database.HasSqlPool (Yesod.Core.Types.HandlerData child site)
+ Freckle.App.OpenTelemetry: class MonadTracer m
+ Freckle.App.OpenTelemetry: getVaultData :: MonadTracer m => m (Maybe XRayVaultData)
+ Freckle.App.OpenTelemetry: instance Freckle.App.OpenTelemetry.MonadTracer (Yesod.Core.Types.HandlerFor app)
+ Freckle.App.Prelude: type HasCallStack = ?callStack :: CallStack
+ Freckle.App.Test: defaultPoolConfig :: IO a -> (a -> IO ()) -> Double -> Int -> PoolConfig a
+ Freckle.App.Test: instance Control.Monad.Catch.MonadMask (Freckle.App.Test.AppExample app)
+ Freckle.App.Test: instance Freckle.App.OpenTelemetry.MonadTracer (Freckle.App.Test.AppExample app)
+ Freckle.App.Test: setNumStripes :: Maybe Int -> PoolConfig a -> PoolConfig a
+ Freckle.App.Wai: addThreadContextFromRequest :: (Request -> [Pair]) -> Middleware
+ Freckle.App.Wai: addThreadContextFromStatsTags :: HasStatsClient env => env -> Middleware
+ Freckle.App.Wai: requestLogger :: HasLogger env => env -> Middleware
+ Freckle.App.Wai: requestStats :: HasStatsClient env => env -> (Request -> [(Text, Text)]) -> Middleware
+ Network.Wai.Middleware.Cors: corsMiddleware :: (ByteString -> Bool) -> [ByteString] -> Middleware
+ Network.Wai.Middleware.Stats: addThreadContextFromStatsTags :: HasStatsClient env => env -> Middleware
- Freckle.App: runApp :: HasLogger app => (forall b. (app -> IO b) -> IO b) -> ReaderT app (LoggingT IO) a -> IO a
+ Freckle.App: runApp :: HasLogger app => (forall b. (app -> IO b) -> IO b) -> AppT app IO a -> IO a
- Freckle.App.Database: runDB :: (HasSqlPool app, HasStatsClient app, HasVaultData app, MonadUnliftIO m, MonadReader app m) => SqlPersistT m a -> m a
+ Freckle.App.Database: runDB :: (MonadUnliftIO m, MonadTracer m, MonadReader app m, HasSqlPool app, HasStatsClient app) => SqlPersistT m a -> m a
- Freckle.App.Test: runDB :: (HasSqlPool app, HasStatsClient app, HasVaultData app, MonadUnliftIO m, MonadReader app m) => SqlPersistT m a -> m a
+ Freckle.App.Test: runDB :: (MonadUnliftIO m, MonadTracer m, MonadReader app m, HasSqlPool app, HasStatsClient app) => SqlPersistT m a -> m a

Files

CHANGELOG.md view
@@ -1,4 +1,11 @@-## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.8.1.0...main)+## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.9.0.0...main)++## [v1.9.0.0](https://github.com/freckle/freckle-app/compare/v1.8.1.0...v1.9.0.0)++- Refactor `Freckle.App.Wai` interface+- Add `addThreadContextFromStatsTags`+- Replace `HasXRayVaultData` with `MonadTracer`+- Introduce `AppT`  ## [v1.8.1.0](https://github.com/freckle/freckle-app/compare/v1.8.0.0...v1.8.1.0) 
README.md view
@@ -1,5 +1,8 @@ # freckle-app +[![Hackage](https://img.shields.io/hackage/v/freckle-app.svg?style=flat)](https://hackage.haskell.org/package/freckle-app)+[![CI](https://github.com/freckle/freckle-app/actions/workflows/ci.yml/badge.svg)](https://github.com/freckle/freckle-app/actions/workflows/ci.yml)+ Haskell application toolkit used at Freckle.  ---
freckle-app.cabal view
@@ -1,6 +1,6 @@ cabal-version:      1.18 name:               freckle-app-version:            1.8.1.0+version:            1.9.0.0 license:            MIT license-file:       LICENSE maintainer:         Freckle Education@@ -41,6 +41,7 @@         Freckle.App.Memcached.CacheTTL         Freckle.App.Memcached.Client         Freckle.App.Memcached.Servers+        Freckle.App.OpenTelemetry         Freckle.App.Prelude         Freckle.App.Scientist         Freckle.App.Stats@@ -54,6 +55,7 @@         Freckle.App.Yesod         Freckle.App.Yesod.Routes         Network.HTTP.Link.Compat+        Network.Wai.Middleware.Cors         Network.Wai.Middleware.Stats         Yesod.Core.Lens @@ -69,6 +71,12 @@         RecordWildCards ScopedTypeVariables StandaloneDeriving         TypeApplications TypeFamilies +    ghc-options:+        -fignore-optim-changes -Weverything -Wno-all-missed-specialisations+        -Wno-missing-exported-signatures -Wno-missing-import-lists+        -Wno-missing-local-signatures -Wno-monomorphism-restriction+        -Wno-safe -Wno-unsafe+     build-depends:         Blammo,         Glob,@@ -116,6 +124,7 @@         postgresql-simple,         primitive,         resource-pool,+        resourcet,         retry >=0.8.1.0,         safe,         scientist,@@ -135,6 +144,16 @@         yaml,         yesod-core +    if impl(ghc >=9.2)+        ghc-options: -Wno-missing-kind-signatures++    if impl(ghc >=8.10)+        ghc-options:+            -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module++    if impl(ghc >=8.8)+        ghc-options: -fwrite-ide-info+ test-suite doctest     type:               exitcode-stdio-1.0     main-is:            Main.hs@@ -150,10 +169,26 @@         RecordWildCards ScopedTypeVariables StandaloneDeriving         TypeApplications TypeFamilies +    ghc-options:+        -fignore-optim-changes -Weverything -Wno-all-missed-specialisations+        -Wno-missing-exported-signatures -Wno-missing-import-lists+        -Wno-missing-local-signatures -Wno-monomorphism-restriction+        -Wno-safe -Wno-unsafe+     build-depends:         base >=4.12.0.0 && <5,         freckle-app +    if impl(ghc >=9.2)+        ghc-options: -Wno-missing-kind-signatures++    if impl(ghc >=8.10)+        ghc-options:+            -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module++    if impl(ghc >=8.8)+        ghc-options: -fwrite-ide-info+ test-suite spec     type:               exitcode-stdio-1.0     main-is:            Main.hs@@ -181,7 +216,12 @@         RecordWildCards ScopedTypeVariables StandaloneDeriving         TypeApplications TypeFamilies -    ghc-options:        -threaded -rtsopts -with-rtsopts=-N+    ghc-options:+        -fignore-optim-changes -Weverything -Wno-all-missed-specialisations+        -Wno-missing-exported-signatures -Wno-missing-import-lists+        -Wno-missing-local-signatures -Wno-monomorphism-restriction+        -Wno-safe -Wno-unsafe -threaded -rtsopts -with-rtsopts=-N+     build-depends:         Blammo,         QuickCheck,@@ -205,3 +245,13 @@         vector,         wai,         wai-extra++    if impl(ghc >=9.2)+        ghc-options: -Wno-missing-kind-signatures++    if impl(ghc >=8.10)+        ghc-options:+            -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module++    if impl(ghc >=8.8)+        ghc-options: -fwrite-ide-info
library/Freckle/App.hs view
@@ -59,15 +59,25 @@ -- >     myAppAction -- >     myOtherAppAction --+-- == 'AppT'+--+-- Functions like @myAppAction@ will be run in the concrete stack 'AppT', but+-- you should prefer using using constraints (e.g. @'MonadReader' app@). See its+-- docs for all the constraints it satisfies.+-- -- == Database ----- Adding Database access requires an instance of @'HasSqlPool'@ on your @App@--- type. Most often, this will be easiest if you indeed separate a @Config@--- attribute:+-- Adding Database access requires a few more instances on your @App@ type: --+-- - @'HasSqlPool'@: so we can, you know, talk to a DB+-- - @'HasStatsClient'@: so we can manage connection count metrics+--+-- Most often, this will be easiest if you indeed separate a @Config@ attribute:+-- -- > data Config = Config -- >   { configDbPoolSize :: Int -- >   , configLogSettings :: LogSettings+-- >   , configStatsSettings :: StatsSettings -- >   } -- -- So you can isolate Env-related concerns@@ -76,6 +86,7 @@ -- > loadConfig = Env.parse id $ Config -- >   <$> Env.var Env.auto "PGPOOLSIZE" (Env.def 1) -- >   <*> LoggerEnv.parser+-- >   <*> envParseStatsSettings -- -- from the runtime application state: --@@ -83,6 +94,7 @@ -- >   { appConfig :: Config -- >   , appLogger :: Logger -- >   , appSqlPool :: SqlPool+-- >   , appStatsClient :: StatsClient -- >   } -- > -- > instance HasLogger App where@@ -90,6 +102,9 @@ -- > -- > instance HasSqlPool App where -- >   getSqlPool = appSqlPool+-- >+-- > instance HasStatsClient App where+-- >   statsClientL = lens appStatsClient $ \x y -> x { appStatsClient = y } -- -- The @"Freckle.App.Database"@ module provides @'makePostgresPool'@ for -- building a Pool given this (limited) config data:@@ -99,12 +114,17 @@ -- >   appConfig{..} <- loadConfig -- >   appLogger <- newLogger configLoggerSettings -- >   appSqlPool <- runLoggerLoggingT appLogger $ makePostgresPool configDbPoolSize--- >   f App{..}+-- >   withStatsClient configStatsSettings $ \appStatsClient -> do+-- >     f App{..} -- -- This unlocks @'runDB'@ for your application: -- -- > myAppAction--- >   :: (MonadIO m, MonadReader env m, HasSqlPool env)+-- >   :: ( MonadUnliftIO m+-- >      , MonadReader env m+-- >      , HasSqlPool env+-- >      , HasStatsClient env+-- >      ) -- >   => SqlPersistT m [Entity Something] -- > myAppAction = runDB $ selectList [] [] --@@ -124,7 +144,8 @@ -- >       result <- myAppAction :: AppExample App Text -- >       result `shouldBe` "as expected" ----- If your app type 'HasSqlPool', you can use 'runDB' in your specs too:+-- If your @App@ type has the required instances, you can use 'runDB' in your+-- specs too: -- -- > spec :: Spec -- > spec = aroundAll loadApp $ do@@ -135,23 +156,87 @@ -- module Freckle.App   ( runApp-  , module X+  , setLineBuffering++  -- * Concrete transformer stack+  , AppT(..)+  , runAppT++  -- * Re-exports+  , module Freckle.App.Database+  , module Freckle.App.OpenTelemetry+  , module Blammo.Logging+  , module Control.Monad.Reader   ) where -import Prelude+import Freckle.App.Prelude -import Blammo.Logging as X-import Control.Monad.Reader as X-import Freckle.App.Database as X+import Blammo.Logging+import Control.Monad.Catch (MonadCatch, MonadThrow)+import Control.Monad.IO.Unlift (MonadUnliftIO(..))+import Control.Monad.Primitive (PrimMonad(..))+import Control.Monad.Reader+import Control.Monad.Trans.Resource (MonadResource, ResourceT, runResourceT)+import Freckle.App.Database+import Freckle.App.OpenTelemetry import System.IO (BufferMode(..), hSetBuffering, stderr, stdout)  runApp   :: HasLogger app   => (forall b . (app -> IO b) -> IO b)-  -> ReaderT app (LoggingT IO) a+  -> AppT app IO a   -> IO a runApp loadApp action = do-  -- Ensure output is streamed if in a Docker container+  setLineBuffering+  loadApp $ runAppT action++-- | Ensure output is streamed if in a Docker container+--+-- 'runApp' calls this for you, but it may be useful if you're running the app+-- some other way.+--+setLineBuffering :: MonadIO m => m ()+setLineBuffering = liftIO $ do   hSetBuffering stdout LineBuffering   hSetBuffering stderr LineBuffering-  loadApp $ \app -> runLoggerLoggingT app $ runReaderT action app++newtype AppT app m a = AppT+  { unAppT :: ReaderT app (LoggingT (ResourceT m)) a+  }+  deriving newtype+    ( Functor+    , Applicative+    , Monad+    , MonadIO+    , MonadThrow+    , MonadCatch+    , MonadMask+    , MonadLogger+    , MonadLoggerIO+    , MonadResource+    , MonadReader app+    )++-- Just copies ReaderT's definition. This can be newtype-derived in GHC 8.10+,+-- but we do it by hand while we want to support older.+instance MonadUnliftIO m => MonadUnliftIO (AppT app m) where+  withRunInIO inner = AppT $ withRunInIO $ \run -> inner $ run . unAppT+  {-# INLINE withRunInIO #-}++instance PrimMonad m => PrimMonad (AppT app m) where+  type PrimState (AppT app m) = PrimState m++  -- This should really just be `lift . primitive`, but:+  --+  -- - We'd need `MonadTrans (AppT app)`, which meh+  -- - We'd need an orphan `Primitive LoggingT`, which no thanks+  --+  primitive = AppT . lift . lift . lift . primitive+  {-# INLINE primitive #-}++instance Applicative m => MonadTracer (AppT app m) where+  getVaultData = pure Nothing++runAppT :: (MonadUnliftIO m, HasLogger app) => AppT app m a -> app -> m a+runAppT action app =+  runResourceT $ runLoggerLoggingT app $ runReaderT (unAppT action) app
library/Freckle/App/Aeson.hs view
@@ -2,7 +2,9 @@  -- | Compatibility module for "Data.Aeson" 1.x vs 2.0 ----- TODO: An @aeson-compat@ package. Sadly, the name is taken.+-- This should be its own package, but the obvious name (@aeson-compat@) is+-- taken by something old and unrelated. I think that's why no one is doing it+-- yet, including us. -- module Freckle.App.Aeson   ( module X@@ -20,4 +22,6 @@ import Data.HashMap.Strict as X #endif +-- Ignored twice because HLint changes the name in some version+{-# ANN module ("HLint: ignore Avoid restricted alias" :: String) #-} {-# ANN module ("HLint: ignore Avoid restricted qualification" :: String) #-}
library/Freckle/App/Database.hs view
@@ -1,14 +1,10 @@ {-# LANGUAGE ApplicativeDo #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE StrictData #-} {-# LANGUAGE TypeOperators #-}  -- | Database access for your @App@ module Freckle.App.Database   ( HasSqlPool(..)-  , HasVaultData(..)   , SqlPool   , makePostgresPool   , makePostgresPoolWith@@ -27,6 +23,7 @@  import Blammo.Logging import qualified Control.Immortal as Immortal+import Control.Monad.IO.Unlift (MonadUnliftIO(..)) import Control.Monad.Reader import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as BS8@@ -47,6 +44,7 @@   (Connection, Only(..), connectPostgreSQL, execute) import Database.PostgreSQL.Simple.SqlQQ (sql) import qualified Freckle.App.Env as Env+import Freckle.App.OpenTelemetry (MonadTracer(..)) import Freckle.App.Stats (HasStatsClient) import qualified Freckle.App.Stats as Stats import Network.AWS.XRayClient.Persistent@@ -56,8 +54,7 @@ import UnliftIO.Concurrent (threadDelay) import UnliftIO.Exception (displayException) import UnliftIO.IORef-import Yesod.Core (MonadUnliftIO(withRunInIO), YesodRequest(reqWaiRequest))-import Yesod.Core.Types (HandlerData(handlerRequest))+import Yesod.Core.Types (HandlerData(..), RunHandlerEnv(..))  type SqlPool = Pool SqlBackend @@ -67,11 +64,8 @@ instance HasSqlPool SqlPool where   getSqlPool = id -class HasVaultData env where-  getVaultData :: env -> Maybe XRayVaultData--instance HasVaultData (HandlerData child site) where-  getVaultData = vaultDataFromRequest . reqWaiRequest . handlerRequest+instance HasSqlPool site => HasSqlPool (HandlerData child site) where+  getSqlPool = getSqlPool . rheSite . handlerEnv  makePostgresPool :: (MonadUnliftIO m, MonadLoggerIO m) => m SqlPool makePostgresPool = do@@ -80,31 +74,24 @@     Env.parse id $ Env.kept $ envParseDatabaseConf postgresPasswordSource   makePostgresPoolWith conf +-- | Run a Database action with connection stats and tracing runDB-  :: ( HasSqlPool app-     , HasStatsClient app-     , HasVaultData app-     , MonadUnliftIO m+  :: ( MonadUnliftIO m+     , MonadTracer m      , MonadReader app m+     , HasSqlPool app+     , HasStatsClient app      )   => SqlPersistT m a   -> m a runDB action = do   pool <- asks getSqlPool-  mVaultData <- asks getVaultData-  Stats.withGauge Stats.dbConnections $-    maybe-      runSqlPool-      (runSqlPoolXRay "runDB")-      mVaultData-      action-      pool+  mVaultData <- getVaultData+  Stats.withGauge Stats.dbConnections+    $ maybe runSqlPool (runSqlPoolXRay "runDB") mVaultData action pool  runDBSimple-  :: ( HasSqlPool app-     , MonadUnliftIO m-     , MonadReader app m-     )+  :: (HasSqlPool app, MonadUnliftIO m, MonadReader app m)   => SqlPersistT m a   -> m a runDBSimple action = do@@ -234,9 +221,6 @@  createAuroraIamToken :: MonadIO m => PostgresConnectionConf -> m AuroraIamToken createAuroraIamToken aitPostgresConnectionConf@PostgresConnectionConf {..} = do-  -- TODO: Consider recording how long creating an auth token takes-  -- somewhere, even if it is just in the logs, so we get an idea of how long-  -- it takes in prod.   aitToken <- T.strip . decodeUtf8 . BSL.toStrict <$> readProcessStdout_     (proc       "aws"
library/Freckle/App/Dotenv.hs view
@@ -43,10 +43,9 @@     let examplePath = takeDirectory path </> ".env.example"     exampleExists <- doesFileExist examplePath -    void $ Dotenv.loadFile $ Dotenv.Config+    void $ Dotenv.loadFile $ Dotenv.defaultConfig       { Dotenv.configPath = [path]       , Dotenv.configExamplePath = [ examplePath | exampleExists ]-      , Dotenv.configOverride = False       }  locateInParents :: FilePath -> IO (Maybe FilePath)
library/Freckle/App/Memcached/CacheKey.hs view
@@ -10,7 +10,6 @@ import Data.Char (isControl, isSpace) import qualified Data.Text as T import Database.Memcache.Types (Key)-import GHC.Stack (HasCallStack) import UnliftIO.Exception (throwString)  newtype CacheKey = CacheKey Text
+ library/Freckle/App/OpenTelemetry.hs view
@@ -0,0 +1,19 @@+module Freckle.App.OpenTelemetry+  ( MonadTracer(..)+  ) where++import Freckle.App.Prelude++import Network.AWS.XRayClient.WAI (XRayVaultData, vaultDataFromRequest)+import Yesod.Core (HandlerFor, waiRequest)++-- | Class for reading 'XRayVaultData'+--+-- This is named the same as the OpenTelemetry class we'll use once we move to+-- that tracing system+--+class MonadTracer m where+  getVaultData :: m (Maybe XRayVaultData)++instance MonadTracer (HandlerFor app) where+  getVaultData = vaultDataFromRequest <$> waiRequest
library/Freckle/App/Prelude.hs view
@@ -1,13 +1,12 @@ -- | Those functions and types we can't do without! module Freckle.App.Prelude-  (-  -- * 'Prelude' as the starting point, without common partial functions-    module Prelude+  ( module Prelude    -- * Commonly imported types   , Alternative   , Exception   , Generic+  , HasCallStack   , HashMap   , HashSet   , Hashable@@ -100,9 +99,9 @@ import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Primitive (PrimMonad) import Control.Monad.Reader (MonadReader, ReaderT)+import Data.Hashable (Hashable) import Data.HashMap.Strict (HashMap) import Data.HashSet (HashSet)-import Data.Hashable (Hashable) import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map)@@ -111,6 +110,7 @@ import Data.Time (NominalDiffTime, UTCTime, getCurrentTime) import Data.Vector (Vector) import GHC.Generics (Generic)+import GHC.Stack (HasCallStack) import UnliftIO (MonadUnliftIO) import UnliftIO.Exception (Exception) 
library/Freckle/App/Test.hs view
@@ -46,9 +46,11 @@ import Control.Monad.Trans.Control import Database.Persist.Sql (SqlPersistT, runSqlPool) import qualified Freckle.App.Dotenv as Dotenv+import Freckle.App.OpenTelemetry import qualified Test.Hspec as Hspec hiding (expectationFailure) import Test.Hspec.Core.Spec (Arg, Example, SpecWith, evaluateExample) import qualified Test.Hspec.Expectations.Lifted as Hspec (expectationFailure)+import qualified UnliftIO.Exception as UnliftIO  -- | An Hspec example over some @App@ value --@@ -85,6 +87,17 @@   withRunInIO inner =     AppExample $ withRunInIO $ \run -> inner (run . unAppExample) +instance MonadMask (AppExample app) where+  mask = UnliftIO.mask+  uninterruptibleMask = UnliftIO.uninterruptibleMask+  generalBracket acquire release use = mask $ \unmasked -> do+    resource <- acquire+    b <- unmasked (use resource) `UnliftIO.catch` \e -> do+      _ <- release resource (ExitCaseException e)+      throwM e+    c <- release resource (ExitCaseSuccess b)+    pure (b, c)+ instance MonadRandom (AppExample app) where   getRandomR = liftIO . getRandomR   getRandom = liftIO getRandom@@ -103,8 +116,8 @@     params     ($ ()) -instance HasVaultData app => HasVaultData (AppExample app a) where-  getVaultData = asks getVaultData+instance MonadTracer (AppExample app) where+  getVaultData = pure Nothing  -- | A type restricted version of id --
library/Freckle/App/Wai.hs view
@@ -1,77 +1,31 @@ -- | Integration of "Freckle.App" tooling with "Network.Wai" module Freckle.App.Wai   ( noCacheMiddleware-  , corsMiddleware   , denyFrameEmbeddingMiddleware-  ) where -import Freckle.App.Prelude+  -- * CORS+  , corsMiddleware -import Data.ByteString (ByteString)-import qualified Data.ByteString as BS-import qualified Data.CaseInsensitive as CI-import Network.HTTP.Types (ResponseHeaders)-import Network.HTTP.Types.Status (status200)+  -- * Logs+  , requestLogger+  , addThreadContextFromRequest++  -- * Metrics+  , addThreadContextFromStatsTags+  , requestStats+  ) where+ import Network.Wai-import Network.Wai.Middleware.AddHeaders (addHeaders)+import Network.Wai.Middleware.AddHeaders+import Network.Wai.Middleware.Cors+import Network.Wai.Middleware.Logging+import Network.Wai.Middleware.Stats +-- | Middleware that adds header to disable all caching noCacheMiddleware :: Middleware-noCacheMiddleware = addHeaders [cacheControlHeader]- where-  cacheControlHeader =-    ("Cache-Control", "no-cache, no-store, max-age=0, private")--corsMiddleware-  :: (ByteString -> Bool)-  -- ^ Predicate that returns 'True' for valid @Origin@ values-  -> [ByteString]-  -- ^ Extra headers to add to @Expose-Headers@-  -> Middleware-corsMiddleware validateOrigin extraExposedHeaders =-  handleOptions validateOrigin extraExposedHeaders-    . addCORSHeaders validateOrigin extraExposedHeaders+noCacheMiddleware =+  addHeaders [("Cache-Control", "no-cache, no-store, max-age=0, private")]  -- | Middleware that adds header to deny all frame embedding denyFrameEmbeddingMiddleware :: Middleware denyFrameEmbeddingMiddleware = addHeaders [("X-Frame-Options", "DENY")]--handleOptions :: (ByteString -> Bool) -> [ByteString] -> Middleware-handleOptions validateOrigin extraExposedHeaders app req sendResponse =-  case (requestMethod req, lookup "Origin" (requestHeaders req)) of-    ("OPTIONS", Just origin) -> sendResponse $ responseLBS-      status200-      (toHeaders $ corsResponseHeaders validateOrigin extraExposedHeaders origin-      )-      mempty-    _ -> app req sendResponse- where-  toHeaders :: [(ByteString, ByteString)] -> ResponseHeaders-  toHeaders = map (first CI.mk)--addCORSHeaders :: (ByteString -> Bool) -> [ByteString] -> Middleware-addCORSHeaders validateOrigin extraExposedHeaders app req sendResponse =-  case lookup "Origin" (requestHeaders req) of-    Nothing -> app req sendResponse-    Just origin -> addHeaders-      (corsResponseHeaders validateOrigin extraExposedHeaders origin)-      app-      req-      sendResponse--corsResponseHeaders-  :: (ByteString -> Bool)-  -> [ByteString]-  -> ByteString-  -> [(ByteString, ByteString)]-corsResponseHeaders validateOrigin extraExposedHeaders origin =-  [ ("Access-Control-Allow-Origin", validatedOrigin)-  , ("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, PATCH")-  , ("Access-Control-Allow-Credentials", "true")-  , ("Access-Control-Allow-Headers", "Content-Type, *")-  , ("Access-Control-Expose-Headers", BS.intercalate ", " exposedHeaders)-  ]- where-  validatedOrigin = if validateOrigin origin then origin else "BADORIGIN"--  exposedHeaders =-    ["Set-Cookie", "Content-Disposition", "Link"] <> extraExposedHeaders
+ library/Network/Wai/Middleware/Cors.hs view
@@ -0,0 +1,65 @@+-- | TODO: Can we use <https://hackage.haskell.org/package/wai-cors> instead?+module Network.Wai.Middleware.Cors+  ( corsMiddleware+  ) where++import Freckle.App.Prelude++import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.CaseInsensitive as CI+import Network.HTTP.Types (ResponseHeaders)+import Network.HTTP.Types.Status (status200)+import Network.Wai+import Network.Wai.Middleware.AddHeaders++corsMiddleware+  :: (ByteString -> Bool)+  -- ^ Predicate that returns 'True' for valid @Origin@ values+  -> [ByteString]+  -- ^ Extra headers to add to @Expose-Headers@+  -> Middleware+corsMiddleware validateOrigin extraExposedHeaders =+  handleOptions validateOrigin extraExposedHeaders+    . addCORSHeaders validateOrigin extraExposedHeaders++handleOptions :: (ByteString -> Bool) -> [ByteString] -> Middleware+handleOptions validateOrigin extraExposedHeaders app req sendResponse =+  case (requestMethod req, lookup "Origin" (requestHeaders req)) of+    ("OPTIONS", Just origin) -> sendResponse $ responseLBS+      status200+      (toHeaders $ corsResponseHeaders validateOrigin extraExposedHeaders origin+      )+      mempty+    _ -> app req sendResponse+ where+  toHeaders :: [(ByteString, ByteString)] -> ResponseHeaders+  toHeaders = map (first CI.mk)++addCORSHeaders :: (ByteString -> Bool) -> [ByteString] -> Middleware+addCORSHeaders validateOrigin extraExposedHeaders app req sendResponse =+  case lookup "Origin" (requestHeaders req) of+    Nothing -> app req sendResponse+    Just origin -> addHeaders+      (corsResponseHeaders validateOrigin extraExposedHeaders origin)+      app+      req+      sendResponse++corsResponseHeaders+  :: (ByteString -> Bool)+  -> [ByteString]+  -> ByteString+  -> [(ByteString, ByteString)]+corsResponseHeaders validateOrigin extraExposedHeaders origin =+  [ ("Access-Control-Allow-Origin", validatedOrigin)+  , ("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, PATCH")+  , ("Access-Control-Allow-Credentials", "true")+  , ("Access-Control-Allow-Headers", "Content-Type, *")+  , ("Access-Control-Expose-Headers", BS.intercalate ", " exposedHeaders)+  ]+ where+  validatedOrigin = if validateOrigin origin then origin else "BADORIGIN"++  exposedHeaders =+    ["Set-Cookie", "Content-Disposition", "Link"] <> extraExposedHeaders
library/Network/Wai/Middleware/Stats.hs view
@@ -1,15 +1,30 @@ module Network.Wai.Middleware.Stats-  ( requestStats+  ( addThreadContextFromStatsTags+  , requestStats   ) where  import Freckle.App.Prelude +import Blammo.Logging (Pair, withThreadContext)+import Control.Lens ((^.)) import Control.Monad.Reader (runReaderT)-import Freckle.App.Stats (HasStatsClient)+import Data.Aeson ((.=))+import qualified Freckle.App.Aeson as Key+import Freckle.App.Stats (HasStatsClient(..), tagsL) import qualified Freckle.App.Stats as Stats import Network.HTTP.Types.Status (Status(..)) import Network.Wai (Middleware, Request, requestMethod, responseStatus) +-- | Add any tags in the ambient 'StatsClient' to the logging context+addThreadContextFromStatsTags :: HasStatsClient env => env -> Middleware+addThreadContextFromStatsTags env app req respond = do+  let context = uncurry fromTag <$> env ^. statsClientL . tagsL+  withThreadContext context $ app req respond+ where+  fromTag :: Text -> Text -> Pair+  fromTag k v = Key.fromText k .= v++-- | Emit @requests@ and @response_time_ms@ metrics requestStats   :: HasStatsClient env => env -> (Request -> [(Text, Text)]) -> Middleware requestStats env getTags app req respond = do
package.yaml view
@@ -1,6 +1,5 @@---- name: freckle-app-version: 1.8.1.0+version: 1.9.0.0 maintainer: Freckle Education category: Utils github: freckle/freckle-app@@ -14,6 +13,29 @@ extra-source-files:   - package.yaml +ghc-options:+  - -fignore-optim-changes+  - -Weverything+  - -Wno-all-missed-specialisations+  - -Wno-missing-exported-signatures # re-enables missing-signatures+  - -Wno-missing-import-lists+  - -Wno-missing-local-signatures+  - -Wno-monomorphism-restriction+  - -Wno-safe+  - -Wno-unsafe++when:+  - condition: "impl(ghc >= 9.2)"+    ghc-options:+      - -Wno-missing-kind-signatures+  - condition: "impl(ghc >= 8.10)"+    ghc-options:+      - -Wno-missing-safe-haskell-mode+      - -Wno-prepositive-qualified-module+  - condition: "impl(ghc >= 8.8)"+    ghc-options:+      - -fwrite-ide-info+ dependencies:   - base < 5 @@ -90,6 +112,7 @@     - persistent-postgresql     - postgresql-simple     - primitive+    - resourcet     - resource-pool     - retry >= 0.8.1.0 # retryingDynamic     - safe
tests/Spec.hs view
@@ -1,1 +1,1 @@-{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec -Wno-missing-export-lists #-}