packages feed

cqrs-example-0.10.0: src/CQRSExample/Query.hs

{-# LANGUAGE DeriveGeneric #-}
module CQRSExample.Query
       ( QueryM
       , QueryState
       , QTaskState(..)
       , newQueryState
       , runQuery
       , qCompletedTaskIdList
       , qTaskList
       , reactToEvents
       ) where

import           Control.Concurrent.STM (atomically)
import           Control.Concurrent.STM.TVar (TVar, readTVar, writeTVar)
import           Control.DeepSeq (NFData(..), ($!!))
import           Control.Monad (forM_, when)
import           Control.Monad.Trans.Class (lift)
import           Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)
import           Data.CQRS.Query (PersistedEvent(..))
import qualified Data.List as L
import           Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import           Data.Text (Text)
import           Data.Typeable (Typeable)
import           Data.UUID.Types (UUID)
import           GHC.Generics (Generic)

import           CQRSExample.Aggregates ()
import           CQRSExample.Events (Event(..), TaskEvent(..))

-- Tasks.
data QTask =
    QTask { qTaskId :: UUID
          , qTaskTitle :: Text
          , qTaskState :: QTaskState }
    deriving (Typeable, Ord, Eq, Show, Generic)

data QTaskState = QTaskOpen
                | QTaskCompleted
                | QTaskArchived
                  deriving (Typeable, Ord, Eq, Show, Generic)

-- Queryable state.
data QueryState = QueryState
    { qTasks :: Map UUID QTask
    , qAggregateVersions :: Map UUID Int
    } deriving (Show, Generic)

-- Need NFData instances for deep evaluation.
instance NFData QTaskState
instance NFData QTask
instance NFData QueryState

-- Query monad.
type QueryM a = ReaderT (TVar QueryState) IO a

-- Create query state.
newQueryState :: QueryState
newQueryState = QueryState { qTasks = M.empty
                           , qAggregateVersions = M.empty
                           }

-- runQuery
runQuery :: TVar QueryState -> QueryM a -> IO a
runQuery qs q = runReaderT q qs

-- Run query against the current state and return the result.
qRunQ :: QueryM QueryState
qRunQ = do
  qsr <- ask
  lift $ atomically $ readTVar qsr

-- Query the task list
qTaskList :: QueryM [(UUID, Text, QTaskState)]
qTaskList = do
  tasks <- fmap qTasks qRunQ
  return $ map f $ filter p $ L.sortBy sf $ M.elems tasks
  where
    f t = (qTaskId t, qTaskTitle t, qTaskState t)
    sf (QTask _ title1 _) (QTask _ title2 _) = compare title1 title2
    p (QTask _ _ QTaskOpen) = True
    p (QTask _ _ QTaskCompleted) = True
    p (QTask _ _ QTaskArchived) = False

qCompletedTaskIdList :: QueryM [UUID]
qCompletedTaskIdList = do
  tasks <- fmap qTasks qRunQ
  return $ map f $ filter p $ M.elems tasks
    where
      f (QTask i _ _) = i
      p (QTask _ _ QTaskCompleted) = True
      p (QTask _ _ QTaskOpen) = False
      p (QTask _ _ QTaskArchived) = False

reactToEvents :: UUID -> [PersistedEvent Event] -> QueryM ()
reactToEvents aggregateId evs = do
  -- Just handle each event separately; in real production code
  -- updates should probably be batched to avoid excessive IO
  -- operations.
  forM_ evs $ \pev -> do
    -- Log event data.
    lift $ putStrLn $ "aggregateId: " ++ show aggregateId ++ " event: " ++ show pev
    -- Update the query state
    qsVar <- ask
    lift $ atomically $ do
      qs <- readTVar qsVar
      when (isApplicable qs pev) $ do
        let qs' = react (peEvent pev) $!! qs
        let qs'' = updateAggregateVersion qs' pev
        writeTVar qsVar $!! qs''

  where
    -- Check if the event is actually next in the sequence. If it
    -- isn't then we have a race between an event which arrived via
    -- repository publishing and an event which was read from the
    -- persistent event store archives. In this case we just ignore
    -- the event since either a) we've already processed it, or b)
    -- it's a future event which we cannot quite process yet. We'll
    -- eventually re-receive the event via archive traversal.
    isApplicable :: QueryState -> PersistedEvent a -> Bool
    isApplicable qs pev =
        peSequenceNumber pev == 1 + M.findWithDefault (-1) aggregateId (qAggregateVersions qs)

    updateAggregateVersion :: QueryState -> PersistedEvent a -> QueryState
    updateAggregateVersion qs pev =
        qs { qAggregateVersions = M.insert aggregateId (peSequenceNumber pev) $ qAggregateVersions qs }

    reactTask (TaskAdded title) =
        M.insert aggregateId (QTask aggregateId title QTaskOpen)
    reactTask TaskCompleted =
        M.adjust (\task -> task { qTaskState = QTaskCompleted }) aggregateId
    reactTask TaskReopened =
        M.adjust (\task -> task { qTaskState = QTaskOpen }) aggregateId
    reactTask TaskArchived =
        M.adjust (\task -> task { qTaskState = QTaskArchived }) aggregateId

    react (TaskEvent e) qs =
        qs { qTasks = reactTask e $ qTasks qs }