packages feed

message-db-temp-0.0.1.0: src/MessageDb/Temp.hs

{-# LANGUAGE QuasiQuotes #-}

module MessageDb.Temp
  ( ConnectionStrings (..),
    withConnectionStrings,
  )
where

import Control.Exception (Exception, IOException)
import Control.Exception.Safe (bracket, throwIO)
import Control.Monad (void)
import Control.Monad.Catch (Handler (Handler))
import qualified Control.Retry as Retry
import Data.ByteString (ByteString)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Monoid (getLast)
import qualified Database.PostgreSQL.Simple as Postgres
import Database.PostgreSQL.Simple.Options (Options)
import qualified Database.PostgreSQL.Simple.Options as PostgresOptions
import Database.PostgreSQL.Simple.SqlQQ (sql)
import Database.Postgres.Temp (Accum (Merge))
import qualified Database.Postgres.Temp as PostgresTemp
import qualified Paths_message_db_temp
import System.Environment (getEnvironment)
import qualified System.Process.Typed as Process


-- | Connection strings used to connect to your temporary message-db.
data ConnectionStrings = ConnectionStrings
  { privilegedConnectionString :: ByteString
  -- ^ Connection string used to connect to the database as a privileged user.
  , normalConnectionString :: ByteString
  -- ^ Connection string used to connect to the database as a user that only has the 'message_store' role.
  }


migrate :: Options -> IO ()
migrate options = do
  hostEnv <- getEnvironment

  installScriptPath <-
    Paths_message_db_temp.getDataFileName "official-message-db-upstream/database/install.sh"

  let fromLast =
        fromMaybe "" . getLast

      processEnv =
        hostEnv
          <> [
               ( "PGHOST"
               , fromLast $ PostgresOptions.host options
               )
             ,
               ( "PGDATABASE"
               , fromLast $ PostgresOptions.dbname options
               )
             ,
               ( "PGPORT"
               , fromLast . fmap show $ PostgresOptions.port options
               )
             ,
               ( "PGUSER"
               , fromLast $ PostgresOptions.user options
               )
             ]
      command = Process.proc "bash" [installScriptPath]
   in void . Process.readProcess_ $ Process.setEnv processEnv command

  let url = PostgresOptions.toConnectionString options
   in bracket (Postgres.connectPostgreSQL url) Postgres.close $ \connection -> do
        let query =
              [sql|
                CREATE ROLE normal_user 
                WITH LOGIN PASSWORD 'password' 
                IN ROLE message_store;
              |]
         in void $ Postgres.execute_ connection query

        let query =
              [sql|
                ALTER ROLE normal_user 
                SET search_path TO message_store,public;
              |]
         in void $ Postgres.execute_ connection query

        let query =
              [sql|
                ALTER ROLE privileged_user
                SET search_path TO message_store,public;
              |]
         in void $ Postgres.execute_ connection query


-- | Create and use a temporary message-db for testing.
withConnectionStrings :: (ConnectionStrings -> IO a) -> IO a
withConnectionStrings use = do
  let tempConfig =
        mempty
          { PostgresTemp.connectionOptions =
              mempty
                { PostgresOptions.user = pure "privileged_user"
                }
          , PostgresTemp.initDbConfig =
              Merge $
                mempty
                  { PostgresTemp.commandLine =
                      mempty{PostgresTemp.keyBased = Map.fromList [("--username=", Just "privileged_user")]}
                  }
          , PostgresTemp.postgresConfigFile =
              [ ("message_store.sql_condition", "on")
              ]
          }

  let retryPolicy =
        Retry.limitRetries 10

      exceptionHandlers =
        let restartFor :: forall e a. Exception e => a -> Handler IO Bool
            restartFor _ = Handler @_ @_ @e $ \_ -> pure True
         in [ restartFor @PostgresTemp.StartError
            , restartFor @IOException
            ]

  Retry.recovering retryPolicy exceptionHandlers $ \_ -> do
    result <- PostgresTemp.withConfig tempConfig $ \db -> do
      let options =
            PostgresTemp.toConnectionOptions db

          tempMessageDb =
            let privilegedConnectionString =
                  PostgresOptions.toConnectionString $
                    options
                      { PostgresOptions.dbname = pure "message_store"
                      }

                normalConnectionString =
                  PostgresOptions.toConnectionString $
                    options
                      { PostgresOptions.user = pure "normal_user"
                      , PostgresOptions.password = pure "password"
                      , PostgresOptions.dbname = pure "message_store"
                      }
             in ConnectionStrings{..}
      migrate options *> use tempMessageDb

    case result of
      Left err -> throwIO err
      Right value -> pure value