packages feed

eventium-core-0.1.0: src/Eventium/Store/Class.hs

{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}

module Eventium.Store.Class
  ( -- * EventStore
    EventStoreReader (..),
    EventStoreWriter (..),
    VersionedEventStoreReader,
    GlobalEventStoreReader,
    VersionedEventStoreWriter,
    StreamEvent (..),
    VersionedStreamEvent,
    GlobalStreamEvent,
    ExpectedPosition (..),
    EventWriteError (..),
    runEventStoreReaderUsing,
    runEventStoreWriterUsing,
    module Eventium.Store.Queries,

    -- * Serialization
    serializedEventStoreReader,
    serializedVersionedEventStoreReader,
    serializedGlobalEventStoreReader,
    serializedEventStoreWriter,

    -- * Utility types
    EventVersion (..),
    SequenceNumber (..),

    -- * Utility functions
    transactionalExpectedWriteHelper,
  )
where

import Data.Aeson
import Data.Functor ((<&>))
import Data.Functor.Contravariant
import Data.Maybe (mapMaybe)
import Eventium.Serializer
import Eventium.Store.Queries
import Eventium.UUID
import Web.HttpApiData
import Web.PathPieces

-- | 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 key position m event
  = EventStoreWriter {storeEvents :: key -> ExpectedPosition position -> [event] -> m (Either (EventWriteError position) EventVersion)}

instance Contravariant (EventStoreWriter key position m) where
  contramap f (EventStoreWriter writer) = EventStoreWriter $ \vers uuid -> writer vers uuid . fmap f

type VersionedEventStoreWriter = EventStoreWriter UUID EventVersion

-- | 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)

-- | ExpectedPosition is used to assert the event stream is at a certain
-- position. This is used when multiple writers are concurrently writing to the
-- event store. If the expected position is incorrect, then storing fails.
data ExpectedPosition position
  = -- | Used when the writer doesn't care what position the stream is at.
    AnyPosition
  | -- | The stream shouldn't exist yet.
    NoStream
  | -- | The stream should already exist.
    StreamExists
  | -- | Used to assert the stream is at a particular position.
    ExactPosition position
  deriving (Show, Eq)

newtype EventWriteError position
  = EventStreamNotAtExpectedVersion position
  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, Ord position, Num position) =>
  (key -> m position) ->
  (key -> [event] -> m EventVersion) ->
  key ->
  ExpectedPosition position ->
  [event] ->
  m (Either (EventWriteError position) EventVersion)
transactionalExpectedWriteHelper getLatestVersion' storeEvents' key expected =
  go expected getLatestVersion' storeEvents' key
  where
    go AnyPosition = transactionalExpectedWriteHelper' Nothing
    go NoStream = transactionalExpectedWriteHelper' (Just $ (==) (-1))
    go StreamExists = transactionalExpectedWriteHelper' (Just (> (-1)))
    go (ExactPosition pos) = transactionalExpectedWriteHelper' (Just $ (==) pos)

transactionalExpectedWriteHelper' ::
  (Monad m) =>
  Maybe (position -> Bool) ->
  (key -> m position) ->
  (key -> [event] -> m EventVersion) ->
  key ->
  [event] ->
  m (Either (EventWriteError position) EventVersion)
transactionalExpectedWriteHelper' Nothing _ storeEvents' uuid events =
  storeEvents' uuid events <&> Right
transactionalExpectedWriteHelper' (Just f) getLatestVersion' storeEvents' uuid events = do
  latestVersion <- getLatestVersion' uuid
  if f latestVersion
    then storeEvents' uuid events <&> Right
    else return $ Left $ 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 key posirion mstore event ->
  EventStoreWriter key posirion 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 key position m serialized ->
  EventStoreWriter key position 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
    )