Search code examples
haskellhaskell-snap-framework

Use subsnaplet during snaplet initialization?


I have some snaplet like this:

data DB b = DB
  {_pgsql :: Snaplet Postgresql
  ,dbCache :: Map Text Text
  }

And I wish to fill dbCache from postgresql database. Seems that it is natural to do this during snaplet initialization.

initDB :: SnapletInit b (DB b)
initDB = makeSnaplet "db" "cached database" Nothing $ do
  pgs <- nestSnaplet "pgsql" pgsql pgsInit
  cache <- getSomeDataPlease pgs 
  return $ DB pgs cache

So, the question is: How is it possible to use pgs :: Snaplet Postgres within Initializer monad to read data from db?


Solution

  • The DB access functions provided by snaplet-postgresql-simple run in any monad that is an instance of the HasPostgres type class. Typically, this will be the Handler monad for your application.

    You can't use Handler functions inside an Initializer. The whole point of the Initializer monad is to set up the initial state data type that is needed to run the web server and the Handler monad. So it's truly impossible to run handlers inside an initializer--unless of course you're running one web server from inside another web server...ick.

    So you have two possible options. You could create a HasPostgres instance for your Initializer. But that doesn't make much sense unless you're connecting to a static server. This might be acceptable if you're doing debugging. Sometimes I'll do that for IO to make it trivial to test my database functions:

    instance HasPostgres IO where
        getPostgresState = do
            pool <- createPool (connect $ ConnectInfo "127.0.0.1" ...) ...
            return $ Postgres pool
    

    But in general it won't make sense to make an instance like this for use in production code. This means that if you want to access the database in an Initializer you have to use the postgresql-simple functions directly rather than the wrappers provided by snaplet-postgresql-simple. That's why I exported the pgPool accessor function. It will look something like this:

    initDB :: SnapletInit b (DB b)
    initDB = makeSnaplet "db" "cached database" Nothing $ do
        pgs <- nestSnaplet "pgsql" pgsql pgsInit
        let pool = pgPool $ extract pgs
        results <- liftIO $ withResource pool (\conn -> query_ conn myQuery)
    

    You can see a real live example of this in snaplet-postgresql-simple's auth backend.

    Update:

    I just uploaded a new version of snaplet-postgresql-simple to hackage that provides a HasPostgres instance for ReaderT. This allows you to accomplish this more simply with runReaderT. There's a small code snippet of this in the documentation.