Skip to content

Discussion: Current tenant and user for the domain API #28

@saurabhnanda

Description

@saurabhnanda
  • 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?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions