event_sourcery 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +3 -1
- data/lib/event_sourcery/aggregate_root.rb +80 -1
- data/lib/event_sourcery/config.rb +32 -5
- data/lib/event_sourcery/errors.rb +8 -3
- data/lib/event_sourcery/event.rb +61 -0
- data/lib/event_sourcery/event_body_serializer.rb +16 -0
- data/lib/event_sourcery/event_processing/error_handlers/constant_retry.rb +9 -2
- data/lib/event_sourcery/event_processing/error_handlers/error_handler.rb +4 -1
- data/lib/event_sourcery/event_processing/error_handlers/exponential_backoff_retry.rb +15 -6
- data/lib/event_sourcery/event_processing/error_handlers/no_retry.rb +1 -0
- data/lib/event_sourcery/event_processing/esp_process.rb +6 -1
- data/lib/event_sourcery/event_processing/esp_runner.rb +2 -0
- data/lib/event_sourcery/event_processing/event_stream_processor.rb +42 -1
- data/lib/event_sourcery/event_processing/event_stream_processor_registry.rb +16 -0
- data/lib/event_sourcery/event_store/poll_waiter.rb +10 -0
- data/lib/event_sourcery/event_store/signal_handling_subscription_master.rb +7 -0
- data/lib/event_sourcery/event_store/subscription.rb +15 -0
- data/lib/event_sourcery/memory/config.rb +36 -0
- data/lib/event_sourcery/memory/event_store.rb +59 -0
- data/lib/event_sourcery/memory/projector.rb +25 -0
- data/lib/event_sourcery/memory/tracker.rb +19 -0
- data/lib/event_sourcery/repository.rb +28 -0
- data/lib/event_sourcery/rspec/event_store_shared_examples.rb +59 -74
- data/lib/event_sourcery/version.rb +2 -1
- data/lib/event_sourcery.rb +28 -0
- metadata +5 -3
@@ -5,22 +5,38 @@ module EventSourcery
|
|
5
5
|
@processors = []
|
6
6
|
end
|
7
7
|
|
8
|
+
# Register the class of the Event Stream Processor.
|
9
|
+
#
|
10
|
+
# @param klass [Class] the class to register
|
8
11
|
def register(klass)
|
9
12
|
@processors << klass
|
10
13
|
end
|
11
14
|
|
15
|
+
# Find a registered processor by its name.
|
16
|
+
#
|
17
|
+
# @param processor_name [String] name of the processor you're looking for
|
18
|
+
#
|
19
|
+
# @return [ESProcess, nil] the found processor object or nil
|
12
20
|
def find(processor_name)
|
13
21
|
@processors.find do |processor|
|
14
22
|
processor.processor_name == processor_name
|
15
23
|
end
|
16
24
|
end
|
17
25
|
|
26
|
+
# Find a registered processor by its type.
|
27
|
+
#
|
28
|
+
# @param constant [String] name of the constant the processor has included
|
29
|
+
#
|
30
|
+
# @return [ESProcess, nil] the found processor object or nil
|
18
31
|
def by_type(constant)
|
19
32
|
@processors.select do |processor|
|
20
33
|
processor.included_modules.include?(constant)
|
21
34
|
end
|
22
35
|
end
|
23
36
|
|
37
|
+
# Returns an array of all the registered processors.
|
38
|
+
#
|
39
|
+
# @return [Array] of all the processors that are registered
|
24
40
|
def all
|
25
41
|
@processors
|
26
42
|
end
|
@@ -1,10 +1,20 @@
|
|
1
1
|
module EventSourcery
|
2
2
|
module EventStore
|
3
|
+
|
4
|
+
# This class provides a basic poll waiter implementation that calls the provided block and sleeps for the specified interval, to be used by a {Subscription}.
|
3
5
|
class PollWaiter
|
6
|
+
#
|
7
|
+
# @param interval [Float] Optional. Will default to `0.5`
|
4
8
|
def initialize(interval: 0.5)
|
5
9
|
@interval = interval
|
6
10
|
end
|
7
11
|
|
12
|
+
# Start polling. Call the provided block and sleep. Repeat until `:stop` is thrown (usually via a subscription master).
|
13
|
+
#
|
14
|
+
# @param block [Proc] code block to be called when polling
|
15
|
+
#
|
16
|
+
# @see SignalHandlingSubscriptionMaster
|
17
|
+
# @see Subscription
|
8
18
|
def poll(&block)
|
9
19
|
catch(:stop) do
|
10
20
|
loop do
|
@@ -1,11 +1,18 @@
|
|
1
1
|
module EventSourcery
|
2
2
|
module EventStore
|
3
|
+
# Manages shutdown signals and facilitate graceful shutdowns of subscriptions.
|
4
|
+
#
|
5
|
+
# @see Subscription
|
3
6
|
class SignalHandlingSubscriptionMaster
|
4
7
|
def initialize
|
5
8
|
@shutdown_requested = false
|
6
9
|
setup_graceful_shutdown
|
7
10
|
end
|
8
11
|
|
12
|
+
# If a shutdown has been requested through a `TERM` or `INT` signal, this will throw a `:stop`
|
13
|
+
# (generally) causing a Subscription to stop listening for events.
|
14
|
+
#
|
15
|
+
# @see Subscription#start
|
9
16
|
def shutdown_if_requested
|
10
17
|
throw :stop if @shutdown_requested
|
11
18
|
end
|
@@ -1,6 +1,17 @@
|
|
1
1
|
module EventSourcery
|
2
2
|
module EventStore
|
3
|
+
|
4
|
+
# This allows Event Stream Processors (ESPs) to subscribe to an event store, and be notified when new events are
|
5
|
+
# added.
|
3
6
|
class Subscription
|
7
|
+
#
|
8
|
+
# @param event_store Event store to source events from
|
9
|
+
# @param poll_waiter Poll waiter instance used (such as {EventStore::PollWaiter}) for polling the event store
|
10
|
+
# @param from_event_id [Integer] Start reading events from this event ID
|
11
|
+
# @param event_types [Array] Optional. If specified, only subscribe to given event types.
|
12
|
+
# @param on_new_events [Proc] Code block to be executed when new events are received
|
13
|
+
# @param subscription_master A subscription master instance (such as {EventStore::SignalHandlingSubscriptionMaster}) which orchestrates a graceful shutdown of the subscription, if one is requested.
|
14
|
+
# @param events_table_name [Symbol] Optional. Defaults to `:events`
|
4
15
|
def initialize(event_store:,
|
5
16
|
poll_waiter:,
|
6
17
|
from_event_id:,
|
@@ -17,6 +28,10 @@ module EventSourcery
|
|
17
28
|
@current_event_id = from_event_id - 1
|
18
29
|
end
|
19
30
|
|
31
|
+
# Start listening for new events. This method will continue to listen for new events until a shutdown is requested
|
32
|
+
# through the subscription_master provided.
|
33
|
+
#
|
34
|
+
# @see EventStore::SignalHandlingSubscriptionMaster
|
20
35
|
def start
|
21
36
|
catch(:stop) do
|
22
37
|
@poll_waiter.poll do
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module Memory
|
3
|
+
class Config
|
4
|
+
attr_accessor :event_tracker,
|
5
|
+
:event_store,
|
6
|
+
:event_source,
|
7
|
+
:event_sink
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@event_tracker = Memory::Tracker.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def event_store
|
14
|
+
@event_store ||= EventStore.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def event_source
|
18
|
+
@event_source ||= ::EventSourcery::EventStore::EventSource.new(event_store)
|
19
|
+
end
|
20
|
+
|
21
|
+
def event_sink
|
22
|
+
@event_sink ||= ::EventSourcery::EventStore::EventSink.new(event_store)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.configure
|
28
|
+
yield config
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.config
|
32
|
+
@config ||= Config.new
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -1,13 +1,26 @@
|
|
1
1
|
module EventSourcery
|
2
2
|
module Memory
|
3
|
+
# In-memory event store.
|
4
|
+
#
|
5
|
+
# Note: This is not persisted and is generally used for testing.
|
3
6
|
class EventStore
|
4
7
|
include EventSourcery::EventStore::EachByRange
|
5
8
|
|
9
|
+
#
|
10
|
+
# @param events [Array] Optional. Collection of events
|
11
|
+
# @param event_builder Optional. Event builder instance. Will default to {Config#event_builder}
|
6
12
|
def initialize(events = [], event_builder: EventSourcery.config.event_builder)
|
7
13
|
@events = events
|
8
14
|
@event_builder = event_builder
|
15
|
+
@listeners = []
|
9
16
|
end
|
10
17
|
|
18
|
+
# Store given events to the in-memory store
|
19
|
+
#
|
20
|
+
# @param event_or_events Event(s) to be stored
|
21
|
+
# @param expected_version [Optional] Expected version for the aggregate. This is the version the caller of this method expect the aggregate to be in. If it's different from the expected version a {EventSourcery::ConcurrencyError} will be raised. Defaults to nil.
|
22
|
+
# @raise EventSourcery::ConcurrencyError
|
23
|
+
# @return Boolean
|
11
24
|
def sink(event_or_events, expected_version: nil)
|
12
25
|
events = Array(event_or_events)
|
13
26
|
ensure_one_aggregate(events)
|
@@ -30,9 +43,17 @@ module EventSourcery
|
|
30
43
|
)
|
31
44
|
end
|
32
45
|
|
46
|
+
project_events(events)
|
47
|
+
|
33
48
|
true
|
34
49
|
end
|
35
50
|
|
51
|
+
# Retrieve a subset of events
|
52
|
+
#
|
53
|
+
# @param id Starting from event ID
|
54
|
+
# @param event_types [Array] Optional. If supplied, only retrieve events of given type(s).
|
55
|
+
# @param limit [Integer] Optional. Number of events to retrieve (starting from the given event ID).
|
56
|
+
# @return Array
|
36
57
|
def get_next_from(id, event_types: nil, limit: 1000)
|
37
58
|
events = if event_types.nil?
|
38
59
|
@events
|
@@ -43,6 +64,10 @@ module EventSourcery
|
|
43
64
|
events.select { |event| event.id >= id }.first(limit)
|
44
65
|
end
|
45
66
|
|
67
|
+
# Retrieve the latest event ID
|
68
|
+
#
|
69
|
+
# @param event_types [Array] Optional. If supplied, only retrieve events of given type(s).
|
70
|
+
# @return Integer
|
46
71
|
def latest_event_id(event_types: nil)
|
47
72
|
events = if event_types.nil?
|
48
73
|
@events
|
@@ -53,24 +78,58 @@ module EventSourcery
|
|
53
78
|
events.empty? ? 0 : events.last.id
|
54
79
|
end
|
55
80
|
|
81
|
+
# Get all events for the given aggregate
|
82
|
+
#
|
83
|
+
# @param id [String] Aggregate ID (UUID as String)
|
84
|
+
# @return Array
|
56
85
|
def get_events_for_aggregate_id(id)
|
57
86
|
stringified_id = id.to_str
|
58
87
|
@events.select { |event| event.aggregate_id == stringified_id }
|
59
88
|
end
|
60
89
|
|
90
|
+
# Next version for the aggregate
|
91
|
+
#
|
92
|
+
# @param aggregate_id [String] Aggregate ID (UUID as String)
|
93
|
+
# @return Integer
|
61
94
|
def next_version(aggregate_id)
|
62
95
|
version_for(aggregate_id) + 1
|
63
96
|
end
|
64
97
|
|
98
|
+
# Current version for the aggregate
|
99
|
+
#
|
100
|
+
# @param aggregate_id [String] Aggregate ID (UUID as String)
|
101
|
+
# @return Integer
|
65
102
|
def version_for(aggregate_id)
|
66
103
|
get_events_for_aggregate_id(aggregate_id).count
|
67
104
|
end
|
68
105
|
|
106
|
+
# Ensure all events have the same aggregate
|
107
|
+
#
|
108
|
+
# @param events [Array] Collection of events
|
109
|
+
# @raise AtomicWriteToMultipleAggregatesNotSupported
|
69
110
|
def ensure_one_aggregate(events)
|
70
111
|
unless events.map(&:aggregate_id).uniq.one?
|
71
112
|
raise AtomicWriteToMultipleAggregatesNotSupported
|
72
113
|
end
|
73
114
|
end
|
115
|
+
|
116
|
+
# Adds a listener or listeners to the memory store.
|
117
|
+
# the #process(event) method will execute whenever an event is emitted
|
118
|
+
#
|
119
|
+
# @param listener A single listener or an array of listeners
|
120
|
+
def add_listeners(listeners)
|
121
|
+
@listeners.concat(Array(listeners))
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def project_events(events)
|
127
|
+
events.each do |event|
|
128
|
+
@listeners.each do |listener|
|
129
|
+
listener.process(event)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
74
133
|
end
|
75
134
|
end
|
76
135
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module Memory
|
3
|
+
module Projector
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.include(EventSourcery::EventProcessing::EventStreamProcessor)
|
7
|
+
base.include(InstanceMethods)
|
8
|
+
base.class_eval do
|
9
|
+
alias_method :project, :process
|
10
|
+
class << self
|
11
|
+
alias_method :project, :process
|
12
|
+
alias_method :projects_events, :processes_events
|
13
|
+
alias_method :projector_name, :processor_name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
def initialize(tracker: EventSourcery::Memory.config.event_tracker)
|
20
|
+
@tracker = tracker
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,24 +1,43 @@
|
|
1
1
|
module EventSourcery
|
2
2
|
module Memory
|
3
|
+
# Being able to know where you're at when reading an event stream
|
4
|
+
# is important. In here are mechanisms to do so.
|
3
5
|
class Tracker
|
6
|
+
# Tracking where you're in an event stream at via an in memory hash.
|
7
|
+
# Note: This is not persisted and is generally used for testing.
|
4
8
|
def initialize
|
5
9
|
@state = Hash.new(0)
|
6
10
|
end
|
7
11
|
|
12
|
+
# Register a new processor to track or
|
13
|
+
# reset an existing tracker's last processed event id.
|
14
|
+
# Will start from 0.
|
15
|
+
#
|
16
|
+
# @param processor_name [String] the name of the processor to track
|
8
17
|
def setup(processor_name)
|
9
18
|
@state[processor_name.to_s] = 0
|
10
19
|
end
|
11
20
|
|
21
|
+
# Update the given processor name to the given event id number.
|
22
|
+
#
|
23
|
+
# @param processor_name [String] the name of the processor to update
|
24
|
+
# @param event_id [Int] the number of the event to update
|
12
25
|
def processed_event(processor_name, event_id)
|
13
26
|
@state[processor_name.to_s] = event_id
|
14
27
|
end
|
15
28
|
|
16
29
|
alias :reset_last_processed_event_id :setup
|
17
30
|
|
31
|
+
# Find the last processed event id for a given processor name.
|
32
|
+
#
|
33
|
+
# @return [Int] the last event id that the given processor has processed
|
18
34
|
def last_processed_event_id(processor_name)
|
19
35
|
@state[processor_name.to_s]
|
20
36
|
end
|
21
37
|
|
38
|
+
# Returns an array of all the processors that are being tracked.
|
39
|
+
#
|
40
|
+
# @return [Array] an array of names of the tracked processors
|
22
41
|
def tracked_processors
|
23
42
|
@state.keys
|
24
43
|
end
|
@@ -1,20 +1,48 @@
|
|
1
1
|
module EventSourcery
|
2
|
+
# This class provides a set of methods to help load and save aggregate instances.
|
3
|
+
#
|
4
|
+
# Refer to {https://github.com/envato/event_sourcery_todo_app/blob/31e200f4a2a65be5d847a66a20e23a334d43086b/app/commands/todo/amend.rb#L26 EventSourceryTodoApp}
|
5
|
+
# for a more complete example.
|
6
|
+
# @example
|
7
|
+
# repository = EventSourcery::Repository.new(
|
8
|
+
# event_source: EventSourceryTodoApp.event_source,
|
9
|
+
# event_sink: EventSourceryTodoApp.event_sink,
|
10
|
+
# )
|
11
|
+
#
|
12
|
+
# aggregate = repository.load(Aggregates::Todo, command.aggregate_id)
|
13
|
+
# aggregate.amend(command.payload)
|
14
|
+
# repository.save(aggregate)
|
2
15
|
class Repository
|
16
|
+
# Create a new instance of the repository and load an aggregate instance
|
17
|
+
#
|
18
|
+
# @param aggregate_class Aggregate type
|
19
|
+
# @param aggregate_id [Integer] ID of the aggregate instance to be loaded
|
20
|
+
# @param event_source event source to be used for loading the events for the aggregate
|
21
|
+
# @param event_sink event sink to be used for saving any new events for the aggregate
|
3
22
|
def self.load(aggregate_class, aggregate_id, event_source:, event_sink:)
|
4
23
|
new(event_source: event_source, event_sink: event_sink)
|
5
24
|
.load(aggregate_class, aggregate_id)
|
6
25
|
end
|
7
26
|
|
27
|
+
# @param event_source event source to be used for loading the events for the aggregate
|
28
|
+
# @param event_sink event sink to be used for saving any new events for the aggregate
|
8
29
|
def initialize(event_source:, event_sink:)
|
9
30
|
@event_source = event_source
|
10
31
|
@event_sink = event_sink
|
11
32
|
end
|
12
33
|
|
34
|
+
# Load an aggregate instance
|
35
|
+
#
|
36
|
+
# @param aggregate_class Aggregate type
|
37
|
+
# @param aggregate_id [Integer] ID of the aggregate instance to be loaded
|
13
38
|
def load(aggregate_class, aggregate_id)
|
14
39
|
events = event_source.get_events_for_aggregate_id(aggregate_id)
|
15
40
|
aggregate_class.new(aggregate_id, events)
|
16
41
|
end
|
17
42
|
|
43
|
+
# Save any new events/changes in the provided aggregate to the event sink
|
44
|
+
#
|
45
|
+
# @param aggregate An aggregate instance to be saved
|
18
46
|
def save(aggregate)
|
19
47
|
new_events = aggregate.changes
|
20
48
|
if new_events.any?
|
@@ -1,25 +1,18 @@
|
|
1
1
|
RSpec.shared_examples 'an event store' do
|
2
|
-
|
2
|
+
TestEvent2 = Class.new(EventSourcery::Event)
|
3
|
+
UserSignedUp = Class.new(EventSourcery::Event)
|
4
|
+
ItemRejected = Class.new(EventSourcery::Event)
|
5
|
+
Type1 = Class.new(EventSourcery::Event)
|
6
|
+
Type2 = Class.new(EventSourcery::Event)
|
7
|
+
BillingDetailsProvided = Class.new(EventSourcery::Event)
|
3
8
|
|
4
|
-
|
5
|
-
id: nil, version: 1, created_at: nil, uuid: SecureRandom.uuid,
|
6
|
-
correlation_id: SecureRandom.uuid, causation_id: SecureRandom.uuid)
|
7
|
-
EventSourcery::Event.new(id: id,
|
8
|
-
aggregate_id: aggregate_id,
|
9
|
-
type: type,
|
10
|
-
body: body,
|
11
|
-
version: version,
|
12
|
-
created_at: created_at,
|
13
|
-
uuid: uuid,
|
14
|
-
correlation_id: correlation_id,
|
15
|
-
causation_id: causation_id)
|
16
|
-
end
|
9
|
+
let(:aggregate_id) { SecureRandom.uuid }
|
17
10
|
|
18
11
|
describe '#sink' do
|
19
12
|
it 'assigns auto incrementing event IDs' do
|
20
|
-
event_store.sink(
|
21
|
-
event_store.sink(
|
22
|
-
event_store.sink(
|
13
|
+
event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid))
|
14
|
+
event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid))
|
15
|
+
event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid))
|
23
16
|
events = event_store.get_next_from(1)
|
24
17
|
expect(events.count).to eq 3
|
25
18
|
expect(events.map(&:id)).to eq [1, 2, 3]
|
@@ -27,40 +20,40 @@ RSpec.shared_examples 'an event store' do
|
|
27
20
|
|
28
21
|
it 'assigns UUIDs' do
|
29
22
|
uuid = SecureRandom.uuid
|
30
|
-
event_store.sink(
|
23
|
+
event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid, uuid: uuid))
|
31
24
|
event = event_store.get_next_from(1).first
|
32
25
|
expect(event.uuid).to eq uuid
|
33
26
|
end
|
34
27
|
|
35
28
|
it 'returns true' do
|
36
|
-
expect(event_store.sink(
|
29
|
+
expect(event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid))).to eq true
|
37
30
|
end
|
38
31
|
|
39
32
|
it 'serializes the event body' do
|
40
33
|
time = Time.now
|
41
|
-
event =
|
34
|
+
event = ItemAdded.new(aggregate_id: SecureRandom.uuid, body: { 'time' => time })
|
42
35
|
expect(event_store.sink(event)).to eq true
|
43
36
|
expect(event_store.get_next_from(1, limit: 1).first.body).to eq('time' => time.iso8601)
|
44
37
|
end
|
45
38
|
|
46
39
|
it 'saves the causation_id' do
|
47
40
|
causation_id = SecureRandom.uuid
|
48
|
-
event =
|
41
|
+
event = ItemAdded.new(aggregate_id: SecureRandom.uuid, causation_id: causation_id)
|
49
42
|
event_store.sink(event)
|
50
43
|
expect(event_store.get_next_from(1, limit: 1).first.causation_id).to eq(causation_id)
|
51
44
|
end
|
52
45
|
|
53
46
|
it 'saves the correlation_id' do
|
54
47
|
correlation_id = SecureRandom.uuid
|
55
|
-
event =
|
48
|
+
event = ItemAdded.new(aggregate_id: SecureRandom.uuid, correlation_id: correlation_id)
|
56
49
|
event_store.sink(event)
|
57
50
|
expect(event_store.get_next_from(1, limit: 1).first.correlation_id).to eq(correlation_id)
|
58
51
|
end
|
59
52
|
|
60
53
|
it 'writes multiple events' do
|
61
|
-
event_store.sink([
|
62
|
-
|
63
|
-
|
54
|
+
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: {e: 1}),
|
55
|
+
ItemAdded.new(aggregate_id: aggregate_id, body: {e: 2}),
|
56
|
+
ItemAdded.new(aggregate_id: aggregate_id, body: {e: 3})])
|
64
57
|
events = event_store.get_next_from(1)
|
65
58
|
expect(events.count).to eq 3
|
66
59
|
expect(events.map(&:id)).to eq [1, 2, 3]
|
@@ -69,11 +62,11 @@ RSpec.shared_examples 'an event store' do
|
|
69
62
|
end
|
70
63
|
|
71
64
|
it 'sets the correct aggregates version' do
|
72
|
-
event_store.sink([
|
73
|
-
|
65
|
+
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: {e: 1}),
|
66
|
+
ItemAdded.new(aggregate_id: aggregate_id, body: {e: 2})])
|
74
67
|
# this will throw a unique constrain error if the aggregate version was not set correctly ^
|
75
|
-
event_store.sink([
|
76
|
-
|
68
|
+
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: {e: 1}),
|
69
|
+
ItemAdded.new(aggregate_id: aggregate_id, body: {e: 2})])
|
77
70
|
events = event_store.get_next_from(1)
|
78
71
|
expect(events.count).to eq 4
|
79
72
|
expect(events.map(&:id)).to eq [1, 2, 3, 4]
|
@@ -81,41 +74,37 @@ RSpec.shared_examples 'an event store' do
|
|
81
74
|
|
82
75
|
context 'with no existing aggregate stream' do
|
83
76
|
it 'saves an event' do
|
84
|
-
event =
|
85
|
-
type: :test_event_2,
|
86
|
-
body: { 'my' => 'data' })
|
77
|
+
event = TestEvent2.new(aggregate_id: aggregate_id, body: { 'my' => 'data' })
|
87
78
|
event_store.sink(event)
|
88
79
|
events = event_store.get_next_from(1)
|
89
80
|
expect(events.count).to eq 1
|
90
81
|
expect(events.first.id).to eq 1
|
91
82
|
expect(events.first.aggregate_id).to eq aggregate_id
|
92
|
-
expect(events.first.type).to eq '
|
83
|
+
expect(events.first.type).to eq 'test_event2'
|
93
84
|
expect(events.first.body).to eq({ 'my' => 'data' }) # should we symbolize keys when hydrating events?
|
94
85
|
end
|
95
86
|
end
|
96
87
|
|
97
88
|
context 'with an existing aggregate stream' do
|
98
89
|
before do
|
99
|
-
event_store.sink(
|
90
|
+
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
|
100
91
|
end
|
101
92
|
|
102
93
|
it 'saves an event' do
|
103
|
-
event =
|
104
|
-
type: :test_event_2,
|
105
|
-
body: { 'my' => 'data' })
|
94
|
+
event = TestEvent2.new(aggregate_id: aggregate_id, body: { 'my' => 'data' })
|
106
95
|
event_store.sink(event)
|
107
96
|
events = event_store.get_next_from(1)
|
108
97
|
expect(events.count).to eq 2
|
109
98
|
expect(events.last.id).to eq 2
|
110
99
|
expect(events.last.aggregate_id).to eq aggregate_id
|
111
|
-
expect(events.last.type).to eq :
|
100
|
+
expect(events.last.type).to eq :test_event2.to_s # shouldn't you get back what you put in, a symbol?
|
112
101
|
expect(events.last.body).to eq({ 'my' => 'data' }) # should we symbolize keys when hydrating events?
|
113
102
|
end
|
114
103
|
end
|
115
104
|
|
116
105
|
it 'correctly inserts created at times when inserting multiple events atomically' do
|
117
106
|
time = Time.parse('2016-10-14T00:00:00.646191Z')
|
118
|
-
event_store.sink([
|
107
|
+
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, created_at: nil), ItemAdded.new(aggregate_id: aggregate_id, created_at: time)])
|
119
108
|
created_ats = event_store.get_next_from(0).map(&:created_at)
|
120
109
|
expect(created_ats.map(&:class)).to eq [Time, Time]
|
121
110
|
expect(created_ats.last).to eq time
|
@@ -123,22 +112,22 @@ RSpec.shared_examples 'an event store' do
|
|
123
112
|
|
124
113
|
it 'raises an error if the events given are for more than one aggregate' do
|
125
114
|
expect {
|
126
|
-
event_store.sink([
|
115
|
+
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id), ItemAdded.new(aggregate_id: SecureRandom.uuid)])
|
127
116
|
}.to raise_error(EventSourcery::AtomicWriteToMultipleAggregatesNotSupported)
|
128
117
|
end
|
129
118
|
end
|
130
119
|
|
131
120
|
describe '#get_next_from' do
|
132
121
|
it 'gets a subset of events' do
|
133
|
-
event_store.sink(
|
134
|
-
event_store.sink(
|
122
|
+
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
|
123
|
+
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
|
135
124
|
expect(event_store.get_next_from(1, limit: 1).map(&:id)).to eq [1]
|
136
125
|
expect(event_store.get_next_from(2, limit: 1).map(&:id)).to eq [2]
|
137
126
|
expect(event_store.get_next_from(1, limit: 2).map(&:id)).to eq [1, 2]
|
138
127
|
end
|
139
128
|
|
140
129
|
it 'returns the event as expected' do
|
141
|
-
event_store.sink(
|
130
|
+
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id, body: { 'my' => 'data' }))
|
142
131
|
event = event_store.get_next_from(1, limit: 1).first
|
143
132
|
expect(event.aggregate_id).to eq aggregate_id
|
144
133
|
expect(event.type).to eq 'item_added'
|
@@ -147,11 +136,11 @@ RSpec.shared_examples 'an event store' do
|
|
147
136
|
end
|
148
137
|
|
149
138
|
it 'filters by event type' do
|
150
|
-
event_store.sink(
|
151
|
-
event_store.sink(
|
152
|
-
event_store.sink(
|
153
|
-
event_store.sink(
|
154
|
-
event_store.sink(
|
139
|
+
event_store.sink(UserSignedUp.new(aggregate_id: aggregate_id))
|
140
|
+
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
|
141
|
+
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
|
142
|
+
event_store.sink(ItemRejected.new(aggregate_id: aggregate_id))
|
143
|
+
event_store.sink(UserSignedUp.new(aggregate_id: aggregate_id))
|
155
144
|
events = event_store.get_next_from(1, event_types: ['user_signed_up'])
|
156
145
|
expect(events.count).to eq 2
|
157
146
|
expect(events.map(&:id)).to eq [1, 5]
|
@@ -160,8 +149,8 @@ RSpec.shared_examples 'an event store' do
|
|
160
149
|
|
161
150
|
describe '#latest_event_id' do
|
162
151
|
it 'returns the latest event id' do
|
163
|
-
event_store.sink(
|
164
|
-
event_store.sink(
|
152
|
+
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
|
153
|
+
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
|
165
154
|
expect(event_store.latest_event_id).to eq 2
|
166
155
|
end
|
167
156
|
|
@@ -173,13 +162,13 @@ RSpec.shared_examples 'an event store' do
|
|
173
162
|
|
174
163
|
context 'with event type filtering' do
|
175
164
|
it 'gets the latest event ID for a set of event types' do
|
176
|
-
event_store.sink(
|
177
|
-
event_store.sink(
|
178
|
-
event_store.sink(
|
165
|
+
event_store.sink(Type1.new(aggregate_id: aggregate_id))
|
166
|
+
event_store.sink(Type1.new(aggregate_id: aggregate_id))
|
167
|
+
event_store.sink(Type2.new(aggregate_id: aggregate_id))
|
179
168
|
|
180
|
-
expect(event_store.latest_event_id(event_types: ['
|
181
|
-
expect(event_store.latest_event_id(event_types: ['
|
182
|
-
expect(event_store.latest_event_id(event_types: ['
|
169
|
+
expect(event_store.latest_event_id(event_types: ['type1'])).to eq 2
|
170
|
+
expect(event_store.latest_event_id(event_types: ['type2'])).to eq 3
|
171
|
+
expect(event_store.latest_event_id(event_types: ['type1', 'type2'])).to eq 3
|
183
172
|
end
|
184
173
|
end
|
185
174
|
end
|
@@ -187,9 +176,9 @@ RSpec.shared_examples 'an event store' do
|
|
187
176
|
describe '#get_events_for_aggregate_id' do
|
188
177
|
RSpec.shared_examples 'gets events for a specific aggregate id' do
|
189
178
|
before do
|
190
|
-
event_store.sink(
|
191
|
-
event_store.sink(
|
192
|
-
event_store.sink(
|
179
|
+
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id, body: { 'my' => 'body' }))
|
180
|
+
event_store.sink(ItemAdded.new(aggregate_id: double(to_str: aggregate_id)))
|
181
|
+
event_store.sink(ItemAdded.new(aggregate_id: SecureRandom.uuid))
|
193
182
|
end
|
194
183
|
|
195
184
|
subject(:events) { event_store.get_events_for_aggregate_id(uuid) }
|
@@ -219,9 +208,7 @@ RSpec.shared_examples 'an event store' do
|
|
219
208
|
describe '#each_by_range' do
|
220
209
|
before do
|
221
210
|
(1..21).each do |i|
|
222
|
-
event_store.sink(
|
223
|
-
type: 'item_added',
|
224
|
-
body: {}))
|
211
|
+
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id, body: {}))
|
225
212
|
end
|
226
213
|
end
|
227
214
|
|
@@ -266,14 +253,14 @@ RSpec.shared_examples 'an event store' do
|
|
266
253
|
end
|
267
254
|
|
268
255
|
def save_event(expected_version: nil)
|
269
|
-
event_store.sink(
|
270
|
-
|
271
|
-
|
272
|
-
|
256
|
+
event_store.sink(
|
257
|
+
BillingDetailsProvided.new(aggregate_id: aggregate_id, body: { my_event: 'data' }),
|
258
|
+
expected_version: expected_version,
|
259
|
+
)
|
273
260
|
end
|
274
261
|
|
275
262
|
def add_event
|
276
|
-
event_store.sink(
|
263
|
+
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
|
277
264
|
end
|
278
265
|
|
279
266
|
def last_event
|
@@ -335,17 +322,15 @@ RSpec.shared_examples 'an event store' do
|
|
335
322
|
|
336
323
|
it 'allows overriding the created_at timestamp for events' do
|
337
324
|
time = Time.parse('2016-10-14T00:00:00.646191Z')
|
338
|
-
event_store.sink(
|
339
|
-
|
340
|
-
|
341
|
-
created_at: time))
|
325
|
+
event_store.sink(BillingDetailsProvided.new(aggregate_id: aggregate_id,
|
326
|
+
body: { my_event: 'data' },
|
327
|
+
created_at: time))
|
342
328
|
expect(last_event.created_at).to eq time
|
343
329
|
end
|
344
330
|
|
345
331
|
it "sets a created_at time when one isn't provided in the event" do
|
346
|
-
event_store.sink(
|
347
|
-
|
348
|
-
body: { my_event: 'data' }))
|
332
|
+
event_store.sink(BillingDetailsProvided.new(aggregate_id: aggregate_id,
|
333
|
+
body: { my_event: 'data' }))
|
349
334
|
expect(last_event.created_at).to be_instance_of(Time)
|
350
335
|
end
|
351
336
|
end
|