{-# LANGUAGE RecordWildCards #-} module Eventful.Projection ( Projection (..) , latestProjection , allProjections , StreamProjection (..) , streamProjection , getLatestProjection , GloballyOrderedProjection (..) , globallyOrderedProjection , globallyOrderedProjectionEventHandler , getLatestGlobalProjection , serializedProjection , projectionMapMaybe ) where import Data.Foldable (foldl') import Data.Functor.Contravariant import Data.List (scanl') import Eventful.Serializer import Eventful.Store.Class import Eventful.UUID -- | A 'Projection' is a piece of @state@ that is constructed only from -- @event@s. A Projection is how you reconstruct event sourced state from the -- ordered stream of events that constitute that state. The "seed" of a -- Projection is the initial state before any events are applied. The event -- handler for a projection is the function that actually modifies state based -- on the given event. data Projection state event = Projection { projectionSeed :: state -- ^ Initial state of a projection , projectionEventHandler :: state -> event -> state -- ^ The function that applies and event to the current state, producing a -- new state. } instance Contravariant (Projection state) where contramap f (Projection seed handler) = Projection seed handler' where handler' state event = handler state (f event) -- | Computes the latest state of a 'Projection' from some events. latestProjection :: (Foldable t) => Projection state event -> t event -> state latestProjection (Projection seed handler) = foldl' handler seed -- | Given a list of events, produce all the Projections that were ever -- produced. Just a 'scanl' using 'projectionEventHandler'. This function is -- useful for testing 'Projection's; you can easily assert that all the states -- of a Projection are valid given a list of events. allProjections :: Projection state event -> [event] -> [state] allProjections (Projection seed handler) = scanl' handler seed -- | A 'StreamProjection' is a 'Projection' that has been constructed from -- events from a particular event stream. This is mostly useful so we can -- associate an 'EventVersion' with some state. data StreamProjection state event = StreamProjection { streamProjectionProjection :: Projection state event , streamProjectionUuid :: !UUID , streamProjectionVersion :: EventVersion , streamProjectionState :: !state } -- | Initialize a 'StreamProjection' with a 'Projection'. streamProjection :: Projection state event -> UUID -> StreamProjection state event streamProjection projection@Projection{..} uuid = StreamProjection projection uuid (-1) projectionSeed -- | Gets the latest projection from a store by using 'getEvents' and then -- applying the events using the Projection's event handler. getLatestProjection :: (Monad m) => EventStore event m -> StreamProjection state event -> m (StreamProjection state event) getLatestProjection store projection@StreamProjection{..} = do events <- getEvents store streamProjectionUuid (eventsStartingAt $ streamProjectionVersion + 1) let latestVersion = newEventVersion events latestState = foldl' (projectionEventHandler streamProjectionProjection) streamProjectionState $ storedEventEvent <$> events return $ projection { streamProjectionVersion = latestVersion , streamProjectionState = latestState } where newEventVersion [] = streamProjectionVersion newEventVersion es = maximum $ storedEventVersion <$> es -- | This is a combination of a 'Projection' and the latest projection state -- with respect to some 'SequenceNumber'. This is useful for in-memory read -- models, and for querying the latest state starting from some previous state -- at a lower 'SequenceNumber'. data GloballyOrderedProjection state serialized = GloballyOrderedProjection { globallyOrderedProjectionProjection :: !(Projection state (GloballyOrderedEvent serialized)) , globallyOrderedProjectionSequenceNumber :: !SequenceNumber , globallyOrderedProjectionState :: !state } -- | Initialize a 'GloballyOrderedProjection' at 'SequenceNumber' 0 and with -- the projection's seed value. globallyOrderedProjection :: Projection state (GloballyOrderedEvent serialized) -> GloballyOrderedProjection state serialized globallyOrderedProjection projection@Projection{..} = GloballyOrderedProjection projection 0 projectionSeed -- | This applies an event to a 'GloballyOrderedProjection'. NOTE: There is no -- guarantee that the 'SequenceNumber' for the event is the previous -- 'SequenceNumber' plus one (in fact, that isn't even a guarantee that some -- stores can provide). This function will update the -- 'GloballyOrderedProjetion' to use the sequence number of the event. globallyOrderedProjectionEventHandler :: GloballyOrderedProjection state serialized -> GloballyOrderedEvent serialized -> GloballyOrderedProjection state serialized globallyOrderedProjectionEventHandler GloballyOrderedProjection{..} event@GloballyOrderedEvent{..} = let Projection{..} = globallyOrderedProjectionProjection seqNum = globallyOrderedEventSequenceNumber state' = projectionEventHandler globallyOrderedProjectionState event in GloballyOrderedProjection globallyOrderedProjectionProjection seqNum state' -- | Gets globally ordered events from the event store and builds a -- 'Projection' based on 'ProjectionEvent'. Optionally accepts the current -- projection state as an argument. getLatestGlobalProjection :: (Monad m) => GloballyOrderedEventStore serialized m -> GloballyOrderedProjection state serialized -> m (GloballyOrderedProjection state serialized) getLatestGlobalProjection store globalProjection@GloballyOrderedProjection{..} = do events <- getSequencedEvents store (eventsStartingAt $ globallyOrderedProjectionSequenceNumber + 1) return $ foldl' globallyOrderedProjectionEventHandler globalProjection events -- | Use a 'Serializer' to wrap a 'Projection' with event type @event@ so it -- uses the @serialized@ type. serializedProjection :: Projection state event -> Serializer event serialized -> Projection state serialized serializedProjection proj Serializer{..} = projectionMapMaybe deserialize proj -- | Transform a 'Projection' when you only have a partial relationship between -- the source event type and the target event type. projectionMapMaybe :: (eventB -> Maybe eventA) -> Projection state eventA -> Projection state eventB projectionMapMaybe f (Projection seed handler) = Projection seed handler' where handler' state = maybe state (handler state) . f