eventful-core-0.2.0: src/Eventful/Store/Class.hs
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
module Eventful.Store.Class
( -- * EventStore
EventStoreReader (..)
, EventStoreWriter (..)
, VersionedEventStoreReader
, GlobalEventStoreReader
, StreamEvent (..)
, VersionedStreamEvent
, GlobalStreamEvent
, ExpectedVersion (..)
, EventWriteError (..)
, runEventStoreReaderUsing
, runEventStoreWriterUsing
, module Eventful.Store.Queries
-- * Serialization
, serializedEventStoreReader
, serializedVersionedEventStoreReader
, serializedGlobalEventStoreReader
, serializedEventStoreWriter
-- * Utility types
, EventVersion (..)
, SequenceNumber (..)
-- * Utility functions
, transactionalExpectedWriteHelper
) where
import Data.Aeson
import Data.Functor.Contravariant
import Data.Maybe (mapMaybe)
import Web.HttpApiData
import Web.PathPieces
import Eventful.Serializer
import Eventful.Store.Queries
import Eventful.UUID
-- | An 'EventStoreReader' is a function to query a stream from an event store.
-- It operates in some monad @m@ and returns events of type @event@ from a
-- stream at @key@ ordered by @position@.
newtype EventStoreReader key position m event = EventStoreReader { getEvents :: QueryRange key position -> m [event] }
instance (Functor m) => Functor (EventStoreReader key position m) where
fmap f (EventStoreReader reader) = EventStoreReader $ fmap (fmap f) <$> reader
type VersionedEventStoreReader m event = EventStoreReader UUID EventVersion m (VersionedStreamEvent event)
type GlobalEventStoreReader m event = EventStoreReader () SequenceNumber m (GlobalStreamEvent event)
-- | An 'EventStoreWriter' is a function to write some events of type @event@
-- to an event store in some monad @m@.
newtype EventStoreWriter m event = EventStoreWriter { storeEvents :: ExpectedVersion -> UUID -> [event] -> m (Maybe EventWriteError) }
instance Contravariant (EventStoreWriter m) where
contramap f (EventStoreWriter writer) = EventStoreWriter $ \vers uuid -> writer vers uuid . fmap f
-- | An event along with the @key@ for the event stream it is from and its
-- @position@ in that event stream.
data StreamEvent key position event
= StreamEvent
{ streamEventKey :: !key
, streamEventPosition :: !position
, streamEventEvent :: !event
} deriving (Show, Eq, Functor, Foldable, Traversable)
type VersionedStreamEvent event = StreamEvent UUID EventVersion event
type GlobalStreamEvent event = StreamEvent () SequenceNumber (VersionedStreamEvent event)
-- | ExpectedVersion is used to assert the event stream is at a certain version
-- number. This is used when multiple writers are concurrently writing to the
-- event store. If the expected version is incorrect, then storing fails.
data ExpectedVersion
= AnyVersion
-- ^ Used when the writer doesn't care what version the stream is at.
| NoStream
-- ^ The stream shouldn't exist yet.
| StreamExists
-- ^ The stream should already exist.
| ExactVersion EventVersion
-- ^ Used to assert the stream is at a particular version.
deriving (Show, Eq)
data EventWriteError
= EventStreamNotAtExpectedVersion EventVersion
deriving (Show, Eq)
-- | Helper to create 'storeEventsRaw' given a function to get the latest
-- stream version and a function to write to the event store. **NOTE**: This
-- only works if the monad @m@ is transactional.
transactionalExpectedWriteHelper
:: (Monad m)
=> (UUID -> m EventVersion)
-> (UUID -> [event] -> m ())
-> ExpectedVersion -> UUID -> [event] -> m (Maybe EventWriteError)
transactionalExpectedWriteHelper getLatestVersion' storeEvents' expected =
go expected getLatestVersion' storeEvents'
where
go AnyVersion = transactionalExpectedWriteHelper' Nothing
go NoStream = transactionalExpectedWriteHelper' (Just $ (==) (-1))
go StreamExists = transactionalExpectedWriteHelper' (Just (> (-1)))
go (ExactVersion vers) = transactionalExpectedWriteHelper' (Just $ (==) vers)
transactionalExpectedWriteHelper'
:: (Monad m)
=> Maybe (EventVersion -> Bool)
-> (UUID -> m EventVersion)
-> (UUID -> [event] -> m ())
-> UUID -> [event] -> m (Maybe EventWriteError)
transactionalExpectedWriteHelper' Nothing _ storeEvents' uuid events =
storeEvents' uuid events >> return Nothing
transactionalExpectedWriteHelper' (Just f) getLatestVersion' storeEvents' uuid events = do
latestVersion <- getLatestVersion' uuid
if f latestVersion
then storeEvents' uuid events >> return Nothing
else return $ Just $ EventStreamNotAtExpectedVersion latestVersion
-- | Changes the monad an 'EventStoreReader' runs in. This is useful to run
-- event stores in another 'Monad' while forgetting the original 'Monad'.
runEventStoreReaderUsing
:: (Monad m, Monad mstore)
=> (forall a. mstore a -> m a)
-> EventStoreReader key position mstore event
-> EventStoreReader key position m event
runEventStoreReaderUsing runStore (EventStoreReader f) = EventStoreReader (runStore . f)
-- | Analog of 'runEventStoreReaderUsing' for a 'EventStoreWriter'.
runEventStoreWriterUsing
:: (Monad m, Monad mstore)
=> (forall a. mstore a -> m a)
-> EventStoreWriter mstore event
-> EventStoreWriter m event
runEventStoreWriterUsing runStore (EventStoreWriter f) =
EventStoreWriter $ \vers uuid events -> runStore $ f vers uuid events
-- | Wraps an 'EventStoreReader' and transparently serializes/deserializes
-- events for you. Note that in this implementation deserialization errors are
-- simply ignored (the event is not returned).
serializedEventStoreReader
:: (Monad m)
=> Serializer event serialized
-> EventStoreReader key position m serialized
-> EventStoreReader key position m event
serializedEventStoreReader Serializer{..} (EventStoreReader reader) =
EventStoreReader $ fmap (mapMaybe deserialize) . reader
-- | Convenience wrapper around 'serializedEventStoreReader' for
-- 'VersionedEventStoreReader'.
serializedVersionedEventStoreReader
:: (Monad m)
=> Serializer event serialized
-> VersionedEventStoreReader m serialized
-> VersionedEventStoreReader m event
serializedVersionedEventStoreReader serializer = serializedEventStoreReader (traverseSerializer serializer)
-- | Convenience wrapper around 'serializedEventStoreReader' for
-- 'GlobalEventStoreReader'.
serializedGlobalEventStoreReader
:: (Monad m)
=> Serializer event serialized
-> GlobalEventStoreReader m serialized
-> GlobalEventStoreReader m event
serializedGlobalEventStoreReader serializer = serializedEventStoreReader (traverseSerializer (traverseSerializer serializer))
-- | Like 'serializedEventStoreReader' but for an 'EventStoreWriter'. Note that
-- 'EventStoreWriter' is an instance of 'Contravariant', so you can just use
-- @contramap serialize@ instead of this function.
serializedEventStoreWriter
:: (Monad m)
=> Serializer event serialized
-> EventStoreWriter m serialized
-> EventStoreWriter m event
serializedEventStoreWriter Serializer{..} = contramap serialize
-- | Event versions are a strictly increasing series of integers for each
-- projection. They allow us to order the events when they are replayed, and
-- they also help as a concurrency check in a multi-threaded environment so
-- services modifying the projection can be sure the projection didn't change
-- during their execution.
newtype EventVersion = EventVersion { unEventVersion :: Int }
deriving (Show, Read, Ord, Eq, Enum, Num, FromJSON, ToJSON)
-- | The sequence number gives us a global ordering of events in a particular
-- event store. Using sequence numbers is not strictly necessary for an event
-- sourcing and CQRS system, but it makes it way easier to replay events
-- consistently without having to use distributed transactions in an event bus.
-- In SQL-based event stores, they are also very cheap to create.
newtype SequenceNumber = SequenceNumber { unSequenceNumber :: Int }
deriving (Show, Read, Ord, Eq, Enum, Num, FromJSON, ToJSON,
PathPiece, ToHttpApiData, FromHttpApiData)