packages feed

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

{-# LANGUAGE RankNTypes #-}

module Eventium.Store.Class
  ( -- * EventStore
    EventStoreReader (..),
    EventStoreWriter (..),
    VersionedEventStoreReader,
    GlobalEventStoreReader,
    VersionedEventStoreWriter,
    runEventStoreReaderUsing,
    runEventStoreWriterUsing,
    module Eventium.Store.Queries,
    module Eventium.Store.Types,

    -- * Codec
    codecEventStoreReader,
    lenientCodecEventStoreReader,
    codecVersionedEventStoreReader,
    codecGlobalEventStoreReader,
    codecEventStoreWriter,
    metadataEnrichingEventStoreWriter,
    metadataEnrichingEventStoreWriterWithEnricher,
    tagEvents,

    -- * Type embedding
    embeddedEventStoreWriter,

    -- * Utility functions
    transactionalExpectedWriteHelper,
  )
where

import Control.Exception (throw)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Functor ((<&>))
import Data.Functor.Contravariant
import Data.Maybe (mapMaybe)
import qualified Data.Text as T
import Data.Time (UTCTime, getCurrentTime)
import Data.Typeable (Typeable, typeOf)
import Eventium.Codec
import Eventium.Store.Queries
import Eventium.Store.Types
import Eventium.TypeEmbedding
import Eventium.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 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 $ \key expectedPos -> writer key expectedPos . fmap f

type VersionedEventStoreWriter = EventStoreWriter UUID EventVersion

-- | 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 ::
  (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 ::
  (forall a. mstore a -> m a) ->
  EventStoreWriter key position mstore event ->
  EventStoreWriter key position m event
runEventStoreWriterUsing runStore (EventStoreWriter f) =
  EventStoreWriter $ \key expectedPos events -> runStore $ f key expectedPos events

-- | Wraps an 'EventStoreReader' and transparently decodes events.
-- Throws 'DecodeError' if any event fails to decode.
-- Use 'lenientCodecEventStoreReader' to silently skip failures.
codecEventStoreReader ::
  (Monad m) =>
  Codec event encoded ->
  EventStoreReader key position m encoded ->
  EventStoreReader key position m event
codecEventStoreReader codec (EventStoreReader reader) =
  EventStoreReader $ fmap (map strictDecode) . reader
  where
    strictDecode s = case codec.decode s of
      Just a -> a
      Nothing -> throw $ DecodeError "codecEventStoreReader" "Failed to decode event"

-- | Like 'codecEventStoreReader' but silently drops events that fail
-- to decode.
--
-- Recommended for production use when using sum-type codecs, as it
-- allows readers to gracefully handle events they don't recognize.
lenientCodecEventStoreReader ::
  (Monad m) =>
  Codec event encoded ->
  EventStoreReader key position m encoded ->
  EventStoreReader key position m event
lenientCodecEventStoreReader codec (EventStoreReader reader) =
  EventStoreReader $ fmap (mapMaybe codec.decode) . reader

-- | Convenience wrapper around 'codecEventStoreReader' for
-- 'VersionedEventStoreReader'.
codecVersionedEventStoreReader ::
  (Monad m) =>
  Codec event encoded ->
  VersionedEventStoreReader m encoded ->
  VersionedEventStoreReader m event
codecVersionedEventStoreReader codec = codecEventStoreReader (traverseCodec codec)

-- | Convenience wrapper around 'codecEventStoreReader' for
-- 'GlobalEventStoreReader'.
codecGlobalEventStoreReader ::
  (Monad m) =>
  Codec event encoded ->
  GlobalEventStoreReader m encoded ->
  GlobalEventStoreReader m event
codecGlobalEventStoreReader codec = codecEventStoreReader (traverseCodec (traverseCodec codec))

-- | Like 'codecEventStoreReader' but for an 'EventStoreWriter'. Note that
-- 'EventStoreWriter' is an instance of 'Contravariant', so you can just use
-- @contramap (encode codec)@ instead of this function.
codecEventStoreWriter ::
  Codec event encoded ->
  EventStoreWriter key position m encoded ->
  EventStoreWriter key position m event
codecEventStoreWriter codec = contramap codec.encode

-- | Like 'metadataEnrichingEventStoreWriterWithEnricher' with 'id' as the
-- enricher — no custom metadata modifications are applied.
metadataEnrichingEventStoreWriter ::
  (MonadIO m, Typeable event) =>
  Codec event encoded ->
  EventStoreWriter key position m (TaggedEvent encoded) ->
  EventStoreWriter key position m event
metadataEnrichingEventStoreWriter = metadataEnrichingEventStoreWriterWithEnricher id

-- | Wraps an 'EventStoreWriter' that accepts 'TaggedEvent's, producing a
-- writer that accepts domain events. Each event is encoded and tagged
-- with metadata (event type name derived from 'Typeable', current
-- UTC timestamp). The supplied 'MetadataEnricher' is applied to the
-- generated metadata before writing — use this to inject application-level
-- fields such as @occurredAt@.
metadataEnrichingEventStoreWriterWithEnricher ::
  (MonadIO m, Typeable event) =>
  MetadataEnricher ->
  Codec event encoded ->
  EventStoreWriter key position m (TaggedEvent encoded) ->
  EventStoreWriter key position m event
metadataEnrichingEventStoreWriterWithEnricher enricher codec (EventStoreWriter write) =
  EventStoreWriter $ \key pos events -> do
    now <- liftIO getCurrentTime
    let tagged =
          map
            ( \e ->
                TaggedEvent
                  (enricher (EventMetadata (T.pack . show $ typeOf e) Nothing Nothing (Just now) Nothing))
                  (codec.encode e)
            )
            events
    write key pos tagged

-- | Tag events with metadata in a pure context. The caller supplies
-- the current time. Useful when 'MonadIO' is not available.
tagEvents ::
  (Typeable event) =>
  Codec event encoded ->
  UTCTime ->
  [event] ->
  [TaggedEvent encoded]
tagEvents codec now =
  map $ \e ->
    TaggedEvent
      (EventMetadata (T.pack . show $ typeOf e) Nothing Nothing (Just now) Nothing)
      (codec.encode e)

-- | Like 'codecEventStoreWriter' but uses a 'TypeEmbedding' instead of
-- a 'Codec'. Intended for embedding aggregate-specific event types into
-- an application-wide event sum type before writing to the store.
embeddedEventStoreWriter ::
  TypeEmbedding event adapted ->
  EventStoreWriter key position m adapted ->
  EventStoreWriter key position m event
embeddedEventStoreWriter emb = contramap emb.embed