refactor: simplify control flow in App.postgrest

Currently, authentication and response execution each unwrap ExceptT with separate runExceptT calls, which split the main request flow across nested pattern matching and Either handling. Control flow is complex and difficult to understand.

The goal of this change is to make request execution as sequential
monadic code with clear error handling.

To implement that, request handling is now run in ExceptT over WriterT (Last ByteString) IO monad stack. Auth role is written after authentication succeeds and further returned along the response. Thanks to it response observation generation is centralized at the end of request handling.

It was necessary to abstract monad stack in getAuthResult, lookupJwtCache, postgrestResponse, and withTiming to enable introduction of WriterT.
This commit is contained in:
Michał Kłeczek
2026-06-02 11:28:19 +05:00
committed by Taimoor Zaeem
parent 1d6e0bd35f
commit 13c0e7061e
3 changed files with 32 additions and 37 deletions
+6 -5
View File
@@ -5,6 +5,7 @@ Description : PostgREST JWT validation results Cache.
This module provides functions to deal with the JWT cache.
-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
@@ -42,7 +43,7 @@ import Protolude
data JwtCacheState = JwtCacheState ObservationHandler (IORef JwtCache)
class CacheVariant m v where
cached :: SC.Cache m ByteString v -> ByteString -> ExceptT Error IO JSON.Object
cached :: (MonadError Error n, MonadIO n) => SC.Cache m ByteString v -> ByteString -> n JSON.Object
{-|
Jwt caching can have three different configurations:
@@ -60,12 +61,12 @@ data JwtCache =
forall m v. CacheVariant m v => JwtCache JwkSet (TVar Int) (SC.Cache m ByteString v)
instance CacheVariant IO (Either Error JSON.Object) where
cached c = lift . SC.cached c >=> liftEither
cached c = liftIO . SC.cached c >=> liftEither
instance CacheVariant (ExceptT Error IO) JSON.Object where
cached = SC.cached
cached c = liftIO . runExceptT . SC.cached c >=> liftEither
decode :: JwtCache -> ByteString -> ExceptT Error IO JSON.Object
decode :: (MonadError Error m, MonadIO m) => JwtCache -> ByteString -> m JSON.Object
decode JwtNoJwks = const $ throwError (JwtErr JwtSecretMissing)
decode (JwtNoCache key) = parseAndDecodeClaims key
decode (JwtCache _ _ c) = cached c
@@ -110,5 +111,5 @@ newJwtCache AppConfig{configJWKS, configJwtCacheMaxEntries} observationHandler =
(const . const $ lift $ observationHandler JwtCacheEviction) -- evictions metrics
alwaysValid) -- no invalidation for now
lookupJwtCache :: JwtCacheState -> Maybe ByteString -> ExceptT Error IO JSON.Object
lookupJwtCache :: (MonadError Error m, MonadIO m) => JwtCacheState -> Maybe ByteString -> m JSON.Object
lookupJwtCache (JwtCacheState _ cacheState) k = liftIO (readIORef cacheState) >>= flip (maybe (pure KM.empty)) k . decode