- how to communicate tenant and user performing the action to the domain APi. Explicit arguments or reader monad
- how to represent an action/event initiated by the system, eg via a cron job?
Domain API refers to the Haskell library that implements actions, like createTenant, createProduct, etc. Functions in the domain API may need the current user and tenant for the following:
- Implementing an audit log
- Implementing authorization (as opposed to authentication)
There are three possible ways to implement this:
Option 1: Explicit arguments
createTenant :: NewTenant -> AppM (Tenant)
createUser :: Tenant -> NewUser -> AppM(Tenant)
createProduct :: Tenant -> User -> NewProduct -> AppM(Product)
editProduct :: Tenant -> User -> EditProduct -> AppM(Product)
Simplest to implement. However, tedious to pass around a tenant and user to every single function in the domain API.
Option 2a: Reader monad with the entire "request context" as the reader env
data RequestContext = ReqestContext{user :: Maybe User, tenant :: Maybe Tenant, dbPool :: ConnectionPool}
type AppM = ReaderT ReqestContext
createTenant :: NewTenant -> AppM(Tenant)
createUser :: NewUser -> AppM(User)
createProduct :: NewProduct -> AppM(Product)
createProduct newproduction = do
ctx@RequestContext{user=user, tenant=tenant, dbPool=dbPool} <- ask
-- do whatever we need to do with `user`, `tenant` or `dbPool`
-- we can introduce some helper functions to get user, tenant, dbPool easily
askUser :: Monad m => ReaderT RequestContext m (Maybe User)
askUser = ask >>= (\ctx -> return $ ctx ^. user) -- shorter way to write this?
askTenant :: Monad m => ReaderT RequestContext m (Maybe Tenant)
askTenant = ask >>= (\ctx -> return $ ctx ^. tenant) -- shorter way to write this?
-- helper function to run domain API functions in the RequestContext
withRequestContext user tenant dbPool action = runReaderT action RequestContext{user=user, tenant=tenant, dbPool=dbPool}
The obvious advantage is that one doesn't have to pass around a user and tenant value to every domain function. Our domain API will anyways be in a ReaderT transformer stack (to be able to access things like the dbpool or the logger) and this integrates nicely with it. However, due to two edge cases we need to have a Maybe User and Maybe Tenant, (instead of a regular User and `Tenant):
createTenant which logically can not have a user and tenant value
cretaeUser which logically may have a user value only if it's NOT the first user of the tenant being created.
Option 2b: Monad with type-class contraints
class (Monad m) => HasUser m where
askUser :: m User
class (Monad m) => HasTenant m where
askUser :: m Tenant
data RequestContext t u = RequestContext {dbPool :: ConnectionPool, tenant :: t, user :: u}
type FullRequestContext = RequestContext Tenant User
type NoTenantRequestContext = RequestContext () ()
type NoUserRequestContext = RequestContext Tenant ()
newtype BaseAppM = ReaderT RequestContext
newtype AppM = ReaderT FullRequestContext
newtype NoTenantAppM = ReaderT NoTenantRequestContext
newtype NoUserAppM = ReaderT NoUserRequestContext
instance HasUser (BaseAppM t u) where
askUser = ask >>= (\ctx -> ctx ^. user)
instance HasTenant (BaseAppM t u) where
askUser = ask >>= (\ctx -> ctx ^. user)
createProduct :: (HasUser m, HasTenant m) => NewProduct -> m Product
createTenant :: (Monad m) => NewTenant -> m Product
createFirstUser :: (HasTenant m) => NewUser -> m User
createUser :: (HasTenant m, HasUser m) => NewUser -> m User
Reference for this idea: https://lexi-lambda.github.io/blog/2016/06/12/four-months-with-haskell/
Actually, I'm not sure if using type-class constraings for HasTenant and HasUser is bringing any advantages to the table here. The one idea that can be merged with Option 2a is to parameterize the RequestContext with tenant and user so that we can run the createTenant and createFirstUser functions in a monad where user & tenant are unit types () easily, thus avoiding Maybe User and Maybe Tenant easily.
Option 3: Implicit parameters
createProduct :: (?user :: User, ?tenant :: Tenant) => NewProduct -> AppM Product
createTenant :: NewTenant -> AppM Tenant
createFirstUser :: (?tenant :: Tenant) => NewUser -> AppM User
createUser :: (?tenant :: Tenant, ?user :: User) => NewUser -> AppM User
-- Possible usage
randomServantHandler param1 param2 = do
session <- getSessionIdFromServantCookie -- don't know what the actual function is called
(tenant, user) <- getTenantAndUserFromSession session
createProduct newProduct
More about implicit paramaters extenstion: https://ocharles.org.uk/blog/posts/2014-12-11-implicit-params.html Does this really give any advantages over a ReaderT monad?
Domain API refers to the Haskell library that implements actions, like
createTenant,createProduct, etc. Functions in the domain API may need the current user and tenant for the following:There are three possible ways to implement this:
Option 1: Explicit arguments
Simplest to implement. However, tedious to pass around a
tenantanduserto every single function in the domain API.Option 2a: Reader monad with the entire "request context" as the reader env
The obvious advantage is that one doesn't have to pass around a
userandtenantvalue to every domain function. Our domain API will anyways be in aReaderTtransformer stack (to be able to access things like the dbpool or the logger) and this integrates nicely with it. However, due to two edge cases we need to have aMaybe UserandMaybe Tenant, (instead of a regularUserand `Tenant):createTenantwhich logically can not have auserandtenantvaluecretaeUserwhich logically may have auservalue only if it's NOT the first user of the tenant being created.Option 2b: Monad with type-class contraints
Reference for this idea: https://lexi-lambda.github.io/blog/2016/06/12/four-months-with-haskell/
Actually, I'm not sure if using type-class constraings for
HasTenantandHasUseris bringing any advantages to the table here. The one idea that can be merged with Option 2a is to parameterize theRequestContextwith tenant and user so that we can run thecreateTenantandcreateFirstUserfunctions in a monad where user & tenant are unit types()easily, thus avoidingMaybe UserandMaybe Tenanteasily.Option 3: Implicit parameters
More about implicit paramaters extenstion: https://ocharles.org.uk/blog/posts/2014-12-11-implicit-params.html Does this really give any advantages over a
ReaderTmonad?