eventium-core 0.5.0 → 0.5.2
raw patch · 7 files changed
+311/−197 lines, 7 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Eventium.SchemaEvolution: addFieldIfAbsent :: Key -> Value -> Upcaster
- Eventium.SchemaEvolution: atKey :: Key -> Upcaster -> Upcaster
- Eventium.SchemaEvolution: currentVersion :: SchemaRegistry -> EventTypeName -> SchemaVersion
- Eventium.SchemaEvolution: data SchemaRegistry
- Eventium.SchemaEvolution: emptyRegistry :: SchemaRegistry
- Eventium.SchemaEvolution: registerUpcasters :: EventTypeName -> [Upcaster] -> SchemaRegistry -> SchemaRegistry
- Eventium.SchemaEvolution: removeField :: Key -> Upcaster
- Eventium.SchemaEvolution: renameField :: Key -> Key -> Upcaster
- Eventium.SchemaEvolution: type EventTypeName = Text
- Eventium.SchemaEvolution: type SchemaVersion = Int
- Eventium.SchemaEvolution: type Upcaster = Value -> Value
- Eventium.SchemaEvolution: unwrapEnvelope :: Value -> (SchemaVersion, Value)
- Eventium.SchemaEvolution: upcastingValueCodec :: (ToJSON a, FromJSON a) => (Value -> Maybe EventTypeName) -> SchemaRegistry -> Codec a Value
- Eventium.SchemaEvolution: wrapEnvelope :: SchemaVersion -> Value -> Value
+ Eventium.SchemaEvolution.Json: addFieldIfAbsent :: Key -> Value -> JsonUpcaster
+ Eventium.SchemaEvolution.Json: atKey :: Key -> JsonUpcaster -> JsonUpcaster
+ Eventium.SchemaEvolution.Json: removeField :: Key -> JsonUpcaster
+ Eventium.SchemaEvolution.Json: renameField :: Key -> Key -> JsonUpcaster
+ Eventium.SchemaEvolution.Json: type JsonUpcaster = Upcaster Value
+ Eventium.SchemaEvolution.Json: unwrapEnvelope :: Value -> (SchemaVersion, Value)
+ Eventium.SchemaEvolution.Json: upcastingValueCodec :: (ToJSON a, FromJSON a) => (Value -> Maybe EventTypeName) -> SchemaRegistry Value -> Codec a Value
+ Eventium.SchemaEvolution.Json: wrapEnvelope :: SchemaVersion -> Value -> Value
+ Eventium.SchemaEvolution.Types: currentVersion :: SchemaRegistry d -> EventTypeName -> SchemaVersion
+ Eventium.SchemaEvolution.Types: data SchemaRegistry d
+ Eventium.SchemaEvolution.Types: emptyRegistry :: SchemaRegistry d
+ Eventium.SchemaEvolution.Types: registerUpcasters :: EventTypeName -> [Upcaster d] -> SchemaRegistry d -> SchemaRegistry d
+ Eventium.SchemaEvolution.Types: type EventTypeName = Text
+ Eventium.SchemaEvolution.Types: type SchemaVersion = Int
+ Eventium.SchemaEvolution.Types: type Upcaster d = d -> d
+ Eventium.SchemaEvolution.Types: upcast :: SchemaRegistry d -> EventTypeName -> SchemaVersion -> d -> d
+ Eventium.Store.Types: eventTypeName :: Typeable a => EventTypeName
+ Eventium.Store.Types: eventTypeNameOf :: Typeable a => a -> EventTypeName
Files
- eventium-core.cabal +5/−1
- src/Eventium/SchemaEvolution.hs +34/−190
- src/Eventium/SchemaEvolution/Json.hs +173/−0
- src/Eventium/SchemaEvolution/Types.hs +76/−0
- src/Eventium/Store/Class.hs +3/−4
- src/Eventium/Store/Types.hs +19/−1
- tests/Eventium/SchemaEvolutionSpec.hs +1/−1
eventium-core.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: eventium-core-version: 0.5.0+version: 0.5.2 synopsis: Core module for eventium description: Eventium-core provides the core abstractions and utilities for building event sourcing systems in Haskell. It includes event store interfaces, command handlers, projections, event handlers, event publishers,@@ -49,6 +49,8 @@ Eventium.ProjectionCache.Types Eventium.ReadModel Eventium.SchemaEvolution+ Eventium.SchemaEvolution.Json+ Eventium.SchemaEvolution.Types Eventium.Store.Class Eventium.Store.Queries Eventium.Store.Types@@ -109,6 +111,8 @@ Eventium.ProjectionCache.Types Eventium.ReadModel Eventium.SchemaEvolution+ Eventium.SchemaEvolution.Json+ Eventium.SchemaEvolution.Types Eventium.Store.Class Eventium.Store.Queries Eventium.Store.Types
src/Eventium/SchemaEvolution.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE OverloadedStrings #-}- -- | Event schema evolution via upcast-on-read against an immutable log. -- -- Stored events are never mutated. Instead, when an event type's shape changes@@ -9,204 +7,50 @@ -- version-independent, and supports version-skipping (restoring an -- N-releases-old backup) simply by running more hops. ----- == Envelope------ Serialized payloads are wrapped in a versioned envelope:+-- This module is the entry point; it re-exports: ----- > { "schemaVersion": N, "payload": <event-json> }+-- * "Eventium.SchemaEvolution.Types" — the representation-agnostic __core__:+-- 'SchemaRegistry', 'registerUpcasters', 'currentVersion', and 'upcast',+-- parametric in a document representation @d@ ('Upcaster' @d = d -> d@).+-- The chaining, versioning, and version-skipping logic live here.+-- * "Eventium.SchemaEvolution.Json" — the JSON __instance__ over Aeson @Value@:+-- the versioned envelope, the field combinators ('atKey', 'addFieldIfAbsent',+-- …), and 'upcastingValueCodec'. ----- Events written before this envelope existed lack the top-level--- @schemaVersion@ key and are defined to be @schemaVersion = 1@ by convention;--- their whole stored value is the payload. Existing rows are never rewritten.+-- Import a submodule directly to limit scope; import this module for both. -- -- == Registry ----- A 'SchemaRegistry' maps an event-type name to an ordered list of single-hop+-- A 'SchemaRegistry' maps an 'EventTypeName' to an ordered list of single-hop -- upcasters @[v1->v2, v2->v3, ...]@. The current version of a type is -- @1 + number of hops@, so the two can never drift. A type with no registered--- upcasters is at version 1 and decodes with no transformation.+-- upcasters is at version 1 and 'upcast's to itself. ----- == Codec+-- == Envelope ----- 'upcastingValueCodec' is a drop-in 'Codec' between a domain event type and--- its stored 'Value'. It is parameterized by an @eventTypeOf@ function that--- reads the type name from a payload (the caller's tagging convention), which--- keeps this module agnostic to how any particular app tags its events. Because--- it is a plain 'Codec', it serves both synchronous (in-transaction) and--- asynchronous (polling) consumers, and every event type and backend, with no--- reader-level plumbing.+-- Serialized JSON payloads are wrapped in a versioned envelope+-- @{ "schemaVersion": N, "payload": <event-json> }@. Events written before the+-- envelope existed lack the key and are read as @schemaVersion = 1@; existing+-- rows are never rewritten.+--+-- == Representation scope+--+-- The core is parametric in @d@ so the machinery is stable across serializations,+-- but only JSON is shipped: every persistent store serializes to JSON+-- (@eventium-postgresql@ / @eventium-sqlite@ store @JSONString@); the in-memory+-- store keeps native values and never serializes, so schema evolution does not+-- apply there. Upcast-on-read is only meaningful against a __self-describing__+-- representation (you must rewrite fields of old data whose Haskell type may be+-- gone). A future self-describing format (e.g. CBOR @Term@) instantiates @d@ with+-- its own envelope + combinators, core untouched. Protobuf is deliberately out of+-- this path: non-self-describing wire + native field evolution (no proto3+-- required fields), so proto handles add\/remove itself and would use the+-- registry only for the /semantic/ hops it can't express. module Eventium.SchemaEvolution- ( -- * Versions and upcasters- EventTypeName,- SchemaVersion,- Upcaster,-- -- * Registry- SchemaRegistry,- emptyRegistry,- registerUpcasters,- currentVersion,-- -- * Upcaster combinators- atKey,- addFieldIfAbsent,- renameField,- removeField,-- -- * Envelope- wrapEnvelope,- unwrapEnvelope,-- -- * Codec- upcastingValueCodec,+ ( module Eventium.SchemaEvolution.Types,+ module Eventium.SchemaEvolution.Json, ) where -import Data.Aeson- ( FromJSON,- Result (..),- ToJSON,- Value (..),- fromJSON,- object,- toJSON,- (.=),- )-import Data.Aeson.Key (Key)-import qualified Data.Aeson.KeyMap as KeyMap-import Data.Map.Strict (Map)-import qualified Data.Map.Strict as Map-import Eventium.Codec (Codec (..))-import Eventium.Store.Types (EventTypeName)---- | The schema version of an event type. Version numbering starts at 1.-type SchemaVersion = Int---- | A pure, single-hop transform from one schema version's JSON to the next.-type Upcaster = Value -> Value---- -------------------------------------------------------------------------------- Upcaster combinators--- --------------------------------------------------------------------------------- These build the common field-level transforms without hand-rolled 'KeyMap'--- manipulation. Each is layout-agnostic and a no-op on non-object values, so--- they compose freely (e.g. focus into a nested object with 'atKey', then edit--- a field there).---- | Apply an upcaster to the value at @key@ within an object, leaving the rest--- untouched. A no-op if @key@ is absent, or if the value is not an object.--- Use this to focus a field-level transform into a nested object (e.g. an--- Aeson tagged constructor's @contents@).-atKey :: Key -> Upcaster -> Upcaster-atKey key f (Object o) = case KeyMap.lookup key o of- Just v -> Object (KeyMap.insert key (f v) o)- Nothing -> Object o-atKey _ _ v = v---- | Insert @key = value@ only if @key@ is not already present. Idempotent and--- never clobbers an existing value — the safe default for "a new field was--- added with default @value@".-addFieldIfAbsent :: Key -> Value -> Upcaster-addFieldIfAbsent key value (Object o) = Object (KeyMap.insertWith (\_new old -> old) key value o)-addFieldIfAbsent _ _ v = v---- | Move the value at @from@ to @to@ (overwriting @to@ if present), dropping--- @from@. A no-op if @from@ is absent.-renameField :: Key -> Key -> Upcaster-renameField from to (Object o) = case KeyMap.lookup from o of- Just v -> Object (KeyMap.insert to v (KeyMap.delete from o))- Nothing -> Object o-renameField _ _ v = v---- | Remove @key@ from an object. A no-op if @key@ is absent.-removeField :: Key -> Upcaster-removeField key (Object o) = Object (KeyMap.delete key o)-removeField _ v = v---- | A registry of upcaster chains, keyed by event-type name. The list for each--- type is ordered @[v1->v2, v2->v3, ...]@.-newtype SchemaRegistry = SchemaRegistry (Map EventTypeName [Upcaster])---- | A registry with no upcasters: every event type is at version 1.-emptyRegistry :: SchemaRegistry-emptyRegistry = SchemaRegistry Map.empty---- | Register the ordered upcaster chain for an event type, replacing any--- existing entry. The list must be ordered @[v1->v2, v2->v3, ...]@; its length--- determines the type's current version.-registerUpcasters :: EventTypeName -> [Upcaster] -> SchemaRegistry -> SchemaRegistry-registerUpcasters eventType hops (SchemaRegistry m) =- SchemaRegistry (Map.insert eventType hops m)---- | The current schema version of an event type: @1 + number of hops@. An--- unregistered type is at version 1.-currentVersion :: SchemaRegistry -> EventTypeName -> SchemaVersion-currentVersion reg eventType = 1 + length (hopsOf reg eventType)---- | All registered hops for an event type (@[]@ if unregistered).-hopsOf :: SchemaRegistry -> EventTypeName -> [Upcaster]-hopsOf (SchemaRegistry m) eventType = Map.findWithDefault [] eventType m---- | The hops needed to bring an event stored at @version@ up to current: drop--- the @version - 1@ hops already baked into the stored shape.-hopsFrom :: SchemaRegistry -> EventTypeName -> SchemaVersion -> [Upcaster]-hopsFrom reg eventType version = drop (max 0 (version - 1)) (hopsOf reg eventType)--envelopeVersionKey :: KeyMap.Key-envelopeVersionKey = "schemaVersion"--envelopePayloadKey :: KeyMap.Key-envelopePayloadKey = "payload"---- | Wrap a payload in a versioned envelope.-wrapEnvelope :: SchemaVersion -> Value -> Value-wrapEnvelope version payload =- object [envelopeVersionKey .= version, envelopePayloadKey .= payload]---- | Peel an envelope into @(version, payload)@. A value that is not a--- well-formed envelope — anything without both a numeric top-level--- @schemaVersion@ and a @payload@ sibling — is legacy pre-envelope data and is--- read as @(1, wholeValue)@.-unwrapEnvelope :: Value -> (SchemaVersion, Value)-unwrapEnvelope (Object o)- | Just version <- KeyMap.lookup envelopeVersionKey o >>= valueToInt,- Just payload <- KeyMap.lookup envelopePayloadKey o =- (version, payload)-unwrapEnvelope v = (1, v)--valueToInt :: Value -> Maybe SchemaVersion-valueToInt v = case fromJSON v of- Success n -> Just n- Error _ -> Nothing---- | A drop-in 'Codec' between a domain event type and its stored 'Value',--- applying schema evolution transparently.------ On 'encode', the current-version envelope is written. On 'decode', the--- envelope is peeled (or defaulted to version 1 for legacy data), the event--- type is resolved via @eventTypeOf@, the remaining upcaster hops are run, and--- the result is decoded into the current type. Decoding yields 'Nothing' if the--- upcasted value still does not match the current type.-upcastingValueCodec ::- (ToJSON a, FromJSON a) =>- -- | Read the event-type name from a payload (the app's tagging convention).- (Value -> Maybe EventTypeName) ->- SchemaRegistry ->- Codec a Value-upcastingValueCodec eventTypeOf reg =- Codec- { encode = \a ->- let payload = toJSON a- version = maybe 1 (currentVersion reg) (eventTypeOf payload)- in wrapEnvelope version payload,- decode = \stored ->- let (version, payload) = unwrapEnvelope stored- upcasted = case eventTypeOf payload of- Just eventType -> foldl (\acc hop -> hop acc) payload (hopsFrom reg eventType version)- Nothing -> payload- in resultToMaybe (fromJSON upcasted)- }- where- resultToMaybe (Success a) = Just a- resultToMaybe (Error _) = Nothing+import Eventium.SchemaEvolution.Json+import Eventium.SchemaEvolution.Types
+ src/Eventium/SchemaEvolution/Json.hs view
@@ -0,0 +1,173 @@+{-# LANGUAGE OverloadedStrings #-}++-- | The JSON instance of event schema evolution (see "Eventium.SchemaEvolution"+-- for the representation-agnostic core): the versioned envelope, field-level+-- upcaster combinators, and the upcasting 'Codec' — all over Aeson 'Value'.+--+-- == Envelope+--+-- Serialized payloads are wrapped in a versioned envelope:+--+-- > { "schemaVersion": N, "payload": <event-json> }+--+-- Events written before this envelope existed lack the top-level+-- @schemaVersion@ key and are defined to be @schemaVersion = 1@ by convention;+-- their whole stored value is the payload. Existing rows are never rewritten.+--+-- == Codec+--+-- 'upcastingValueCodec' is a drop-in 'Codec' between a domain event type and its+-- stored 'Value'. It is parameterized by an @eventTypeOf@ function that reads the+-- type name from a payload (the caller's tagging convention), which keeps it+-- agnostic to how any particular app tags its events. Because it is a plain+-- 'Codec', it serves both synchronous (in-transaction) and asynchronous+-- (polling) consumers, and every event type and backend, with no reader-level+-- plumbing.+module Eventium.SchemaEvolution.Json+ ( -- * JSON upcasters+ JsonUpcaster,++ -- * Field combinators+ atKey,+ addFieldIfAbsent,+ renameField,+ removeField,++ -- * Envelope+ wrapEnvelope,+ unwrapEnvelope,++ -- * Codec+ upcastingValueCodec,+ )+where++import Data.Aeson+ ( FromJSON,+ Result (..),+ ToJSON,+ Value (..),+ fromJSON,+ object,+ toJSON,+ (.=),+ )+import Data.Aeson.Key (Key)+import qualified Data.Aeson.KeyMap as KeyMap+import Eventium.Codec (Codec (..))+import Eventium.SchemaEvolution.Types+ ( EventTypeName,+ SchemaRegistry,+ SchemaVersion,+ Upcaster,+ currentVersion,+ upcast,+ )++-- | An 'Upcaster' over the JSON document model (Aeson 'Value') — the one+-- representation eventium ships.+type JsonUpcaster = Upcaster Value++-- -----------------------------------------------------------------------------+-- Field combinators+-- -----------------------------------------------------------------------------++-- These build the common field-level transforms without hand-rolled 'KeyMap'+-- manipulation. Each is a no-op on non-object values, so they compose freely+-- (e.g. focus into a nested object with 'atKey', then edit a field there).++-- | Apply an upcaster to the value at @key@ within an object, leaving the rest+-- untouched. A no-op if @key@ is absent, or if the value is not an object.+-- Use this to focus a field-level transform into a nested object (e.g. an+-- Aeson tagged constructor's @contents@).+atKey :: Key -> JsonUpcaster -> JsonUpcaster+atKey key f (Object o) = case KeyMap.lookup key o of+ Just v -> Object (KeyMap.insert key (f v) o)+ Nothing -> Object o+atKey _ _ v = v++-- | Insert @key = value@ only if @key@ is not already present. Idempotent and+-- never clobbers an existing value — the safe default for "a new field was+-- added with default @value@".+addFieldIfAbsent :: Key -> Value -> JsonUpcaster+addFieldIfAbsent key value (Object o) = Object (KeyMap.insertWith (\_new old -> old) key value o)+addFieldIfAbsent _ _ v = v++-- | Move the value at @from@ to @to@ (overwriting @to@ if present), dropping+-- @from@. A no-op if @from@ is absent.+renameField :: Key -> Key -> JsonUpcaster+renameField from to (Object o) = case KeyMap.lookup from o of+ Just v -> Object (KeyMap.insert to v (KeyMap.delete from o))+ Nothing -> Object o+renameField _ _ v = v++-- | Remove @key@ from an object. A no-op if @key@ is absent.+removeField :: Key -> JsonUpcaster+removeField key (Object o) = Object (KeyMap.delete key o)+removeField _ v = v++-- -----------------------------------------------------------------------------+-- Envelope+-- -----------------------------------------------------------------------------++envelopeVersionKey :: Key+envelopeVersionKey = "schemaVersion"++envelopePayloadKey :: Key+envelopePayloadKey = "payload"++-- | Wrap a payload in a versioned envelope.+wrapEnvelope :: SchemaVersion -> Value -> Value+wrapEnvelope version payload =+ object [envelopeVersionKey .= version, envelopePayloadKey .= payload]++-- | Peel an envelope into @(version, payload)@. A value that is not a+-- well-formed envelope — anything without both a numeric top-level+-- @schemaVersion@ and a @payload@ sibling — is legacy pre-envelope data and is+-- read as @(1, wholeValue)@.+unwrapEnvelope :: Value -> (SchemaVersion, Value)+unwrapEnvelope (Object o)+ | Just version <- KeyMap.lookup envelopeVersionKey o >>= valueToInt,+ Just payload <- KeyMap.lookup envelopePayloadKey o =+ (version, payload)+unwrapEnvelope v = (1, v)++valueToInt :: Value -> Maybe SchemaVersion+valueToInt v = case fromJSON v of+ Success n -> Just n+ Error _ -> Nothing++-- -----------------------------------------------------------------------------+-- Codec+-- -----------------------------------------------------------------------------++-- | A drop-in 'Codec' between a domain event type and its stored 'Value',+-- applying schema evolution transparently.+--+-- On 'encode', the current-version envelope is written. On 'decode', the+-- envelope is peeled (or defaulted to version 1 for legacy data), the event+-- type is resolved via @eventTypeOf@, the remaining upcaster hops are run via+-- 'upcast', and the result is decoded into the current type. Decoding yields+-- 'Nothing' if the upcasted value still does not match the current type.+upcastingValueCodec ::+ (ToJSON a, FromJSON a) =>+ -- | Read the event-type name from a payload (the app's tagging convention).+ (Value -> Maybe EventTypeName) ->+ SchemaRegistry Value ->+ Codec a Value+upcastingValueCodec eventTypeOf reg =+ Codec+ { encode = \a ->+ let payload = toJSON a+ version = maybe 1 (currentVersion reg) (eventTypeOf payload)+ in wrapEnvelope version payload,+ decode = \stored ->+ let (version, payload) = unwrapEnvelope stored+ upcasted = case eventTypeOf payload of+ Just eventType -> upcast reg eventType version payload+ Nothing -> payload+ in resultToMaybe (fromJSON upcasted)+ }+ where+ resultToMaybe (Success a) = Just a+ resultToMaybe (Error _) = Nothing
+ src/Eventium/SchemaEvolution/Types.hs view
@@ -0,0 +1,76 @@+-- | Representation-agnostic core of event schema evolution (see+-- "Eventium.SchemaEvolution" for the overview and the JSON instance).+--+-- 'SchemaRegistry', 'registerUpcasters', 'currentVersion', and 'upcast' are+-- parametric in a document representation @d@ ('Upcaster' @d = d -> d@) and never+-- inspect it. The chaining, versioning, and version-skipping logic all live here+-- and are shared by every representation; only the envelope and field+-- combinators are representation-specific (e.g. "Eventium.SchemaEvolution.Json").+module Eventium.SchemaEvolution.Types+ ( -- * Versions and upcasters+ EventTypeName,+ SchemaVersion,+ Upcaster,++ -- * Registry+ SchemaRegistry,+ emptyRegistry,+ registerUpcasters,+ currentVersion,++ -- * Applying the chain+ upcast,+ )+where++import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Eventium.Store.Types (EventTypeName)++-- | The schema version of an event type. Version numbering starts at 1.+type SchemaVersion = Int++-- | A pure, single-hop transform from one schema version to the next, over a+-- serialization's document representation @d@. The registry and chaining are+-- parametric in @d@ and never inspect it; only the envelope and field+-- combinators (per representation, e.g. "Eventium.SchemaEvolution.Json") are+-- representation-specific.+type Upcaster d = d -> d++-- | A registry of upcaster chains, keyed by event-type name, over a document+-- representation @d@. The list for each type is ordered @[v1->v2, v2->v3, ...]@.+-- The registry is parametric in @d@ and never inspects it.+newtype SchemaRegistry d = SchemaRegistry (Map EventTypeName [Upcaster d])++-- | A registry with no upcasters: every event type is at version 1.+emptyRegistry :: SchemaRegistry d+emptyRegistry = SchemaRegistry Map.empty++-- | Register the ordered upcaster chain for an event type, replacing any+-- existing entry. The list must be ordered @[v1->v2, v2->v3, ...]@; its length+-- determines the type's current version.+registerUpcasters :: EventTypeName -> [Upcaster d] -> SchemaRegistry d -> SchemaRegistry d+registerUpcasters eventType hops (SchemaRegistry m) =+ SchemaRegistry (Map.insert eventType hops m)++-- | The current schema version of an event type: @1 + number of hops@. An+-- unregistered type is at version 1.+currentVersion :: SchemaRegistry d -> EventTypeName -> SchemaVersion+currentVersion reg eventType = 1 + length (hopsOf reg eventType)++-- | Apply the upcaster chain that brings an event of @eventType@ stored at+-- @version@ up to the current shape: run the hops not yet baked into the stored+-- version, in order. Representation-agnostic — it only composes the registered+-- @d -> d@ hops.+upcast :: SchemaRegistry d -> EventTypeName -> SchemaVersion -> d -> d+upcast reg eventType version x =+ foldl (\acc hop -> hop acc) x (hopsFrom reg eventType version)++-- | All registered hops for an event type (@[]@ if unregistered).+hopsOf :: SchemaRegistry d -> EventTypeName -> [Upcaster d]+hopsOf (SchemaRegistry m) eventType = Map.findWithDefault [] eventType m++-- | The hops needed to bring an event stored at @version@ up to current: drop+-- the @version - 1@ hops already baked into the stored shape.+hopsFrom :: SchemaRegistry d -> EventTypeName -> SchemaVersion -> [Upcaster d]+hopsFrom reg eventType version = drop (max 0 (version - 1)) (hopsOf reg eventType)
src/Eventium/Store/Class.hs view
@@ -35,9 +35,8 @@ 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 Data.Typeable (Typeable) import Eventium.Codec import Eventium.Store.Queries import Eventium.Store.Types@@ -200,7 +199,7 @@ map ( \e -> TaggedEvent- (enricher (EventMetadata (T.pack . show $ typeOf e) Nothing Nothing (Just now)))+ (enricher (EventMetadata (eventTypeNameOf e) Nothing Nothing (Just now))) (codec.encode e) ) events@@ -217,7 +216,7 @@ tagEvents codec now = map $ \e -> TaggedEvent- (EventMetadata (T.pack . show $ typeOf e) Nothing Nothing (Just now))+ (EventMetadata (eventTypeNameOf e) Nothing Nothing (Just now)) (codec.encode e) -- | Like 'codecEventStoreWriter' but uses a 'TypeEmbedding' instead of
src/Eventium/Store/Types.hs view
@@ -1,6 +1,9 @@+{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-} -- | Pure data types for the event store. These types have no dependency on -- any monad or effect system. They are extracted from "Eventium.Store.Class"@@ -14,6 +17,8 @@ -- * Event metadata EventTypeName,+ eventTypeName,+ eventTypeNameOf, EventMetadata (..), emptyMetadata, MetadataEnricher,@@ -39,10 +44,11 @@ import Data.Aeson import qualified Data.List.NonEmpty as NE-import Data.Text (Text)+import Data.Text (Text, pack) import Data.Time (UTCTime) import Eventium.UUID import GHC.Generics (Generic)+import Type.Reflection (Typeable, typeRep) import Web.HttpApiData import Web.PathPieces @@ -50,6 +56,18 @@ -- 'EventMetadata', or the discriminator tag an app uses to key its schema -- evolution registry (see "Eventium.SchemaEvolution"). type EventTypeName = Text++-- | The 'EventTypeName' of an event type: its unqualified type name, derived+-- from 'Typeable'. Use when you have the type but no value — e.g. keying a+-- schema-evolution registry: @eventTypeName \@MyEvent@.+eventTypeName :: forall a. (Typeable a) => EventTypeName+eventTypeName = pack (show (typeRep @a))++-- | 'eventTypeName' for the type of a value. Handy at write time, where an+-- event value is in hand (e.g. deriving 'EventMetadata.eventType'). The value+-- is inspected only for its type.+eventTypeNameOf :: forall a. (Typeable a) => a -> EventTypeName+eventTypeNameOf _ = eventTypeName @a -- | Metadata carried alongside every stored event. data EventMetadata = EventMetadata
tests/Eventium/SchemaEvolutionSpec.hs view
@@ -70,7 +70,7 @@ setStep n (Object o) = Object (KeyMap.insert "step" (toJSON n) o) setStep _ v = v -registry :: SchemaRegistry+registry :: SchemaRegistry Value registry = registerUpcasters "Gizmo" [injectEnabled] $ registerUpcasters "Chain" [setStep 2, setStep 3] emptyRegistry