event_sourcery 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +3 -1
  4. data/lib/event_sourcery/aggregate_root.rb +80 -1
  5. data/lib/event_sourcery/config.rb +32 -5
  6. data/lib/event_sourcery/errors.rb +8 -3
  7. data/lib/event_sourcery/event.rb +61 -0
  8. data/lib/event_sourcery/event_body_serializer.rb +16 -0
  9. data/lib/event_sourcery/event_processing/error_handlers/constant_retry.rb +9 -2
  10. data/lib/event_sourcery/event_processing/error_handlers/error_handler.rb +4 -1
  11. data/lib/event_sourcery/event_processing/error_handlers/exponential_backoff_retry.rb +15 -6
  12. data/lib/event_sourcery/event_processing/error_handlers/no_retry.rb +1 -0
  13. data/lib/event_sourcery/event_processing/esp_process.rb +6 -1
  14. data/lib/event_sourcery/event_processing/esp_runner.rb +2 -0
  15. data/lib/event_sourcery/event_processing/event_stream_processor.rb +42 -1
  16. data/lib/event_sourcery/event_processing/event_stream_processor_registry.rb +16 -0
  17. data/lib/event_sourcery/event_store/poll_waiter.rb +10 -0
  18. data/lib/event_sourcery/event_store/signal_handling_subscription_master.rb +7 -0
  19. data/lib/event_sourcery/event_store/subscription.rb +15 -0
  20. data/lib/event_sourcery/memory/config.rb +36 -0
  21. data/lib/event_sourcery/memory/event_store.rb +59 -0
  22. data/lib/event_sourcery/memory/projector.rb +25 -0
  23. data/lib/event_sourcery/memory/tracker.rb +19 -0
  24. data/lib/event_sourcery/repository.rb +28 -0
  25. data/lib/event_sourcery/rspec/event_store_shared_examples.rb +59 -74
  26. data/lib/event_sourcery/version.rb +2 -1
  27. data/lib/event_sourcery.rb +28 -0
  28. 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
- let(:aggregate_id) { SecureRandom.uuid }
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
- def new_event(aggregate_id: SecureRandom.uuid, type: 'test_event', body: {},
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(new_event)
21
- event_store.sink(new_event)
22
- event_store.sink(new_event)
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(new_event(uuid: uuid))
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(new_event)).to eq true
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 = new_event(body: { 'time' => time })
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 = new_event(causation_id: causation_id)
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 = new_event(correlation_id: correlation_id)
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([new_event(aggregate_id: aggregate_id, body: {e: 1}),
62
- new_event(aggregate_id: aggregate_id, body: {e: 2}),
63
- new_event(aggregate_id: aggregate_id, body: {e: 3})])
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([new_event(aggregate_id: aggregate_id, body: {e: 1}),
73
- new_event(aggregate_id: aggregate_id, body: {e: 2})])
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([new_event(aggregate_id: aggregate_id, body: {e: 1}),
76
- new_event(aggregate_id: aggregate_id, body: {e: 2})])
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 = new_event(aggregate_id: aggregate_id,
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 'test_event_2'
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(new_event(aggregate_id: aggregate_id))
90
+ event_store.sink(ItemAdded.new(aggregate_id: aggregate_id))
100
91
  end
101
92
 
102
93
  it 'saves an event' do
103
- event = new_event(aggregate_id: aggregate_id,
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 :test_event_2.to_s # shouldn't you get back what you put in, a symbol?
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([new_event(aggregate_id: aggregate_id, created_at: nil), new_event(aggregate_id: aggregate_id, created_at: time)])
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([new_event(aggregate_id: aggregate_id), new_event(aggregate_id: SecureRandom.uuid)])
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(new_event(aggregate_id: aggregate_id))
134
- event_store.sink(new_event(aggregate_id: aggregate_id))
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(new_event(aggregate_id: aggregate_id, type: 'item_added', body: { 'my' => 'data' }))
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(new_event(aggregate_id: aggregate_id, type: 'user_signed_up'))
151
- event_store.sink(new_event(aggregate_id: aggregate_id, type: 'item_added'))
152
- event_store.sink(new_event(aggregate_id: aggregate_id, type: 'item_added'))
153
- event_store.sink(new_event(aggregate_id: aggregate_id, type: 'item_rejected'))
154
- event_store.sink(new_event(aggregate_id: aggregate_id, type: 'user_signed_up'))
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(new_event(aggregate_id: aggregate_id))
164
- event_store.sink(new_event(aggregate_id: aggregate_id))
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(new_event(aggregate_id: aggregate_id, type: 'type_1'))
177
- event_store.sink(new_event(aggregate_id: aggregate_id, type: 'type_1'))
178
- event_store.sink(new_event(aggregate_id: aggregate_id, type: 'type_2'))
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: ['type_1'])).to eq 2
181
- expect(event_store.latest_event_id(event_types: ['type_2'])).to eq 3
182
- expect(event_store.latest_event_id(event_types: ['type_1', 'type_2'])).to eq 3
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(new_event(aggregate_id: aggregate_id, type: 'item_added', body: { 'my' => 'body' }))
191
- event_store.sink(new_event(aggregate_id: double(to_str: aggregate_id)))
192
- event_store.sink(new_event(aggregate_id: SecureRandom.uuid))
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(new_event(aggregate_id: aggregate_id,
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(new_event(aggregate_id: aggregate_id,
270
- type: :billing_details_provided,
271
- body: { my_event: 'data' }),
272
- expected_version: expected_version)
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(new_event(aggregate_id: aggregate_id))
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(new_event(aggregate_id: aggregate_id,
339
- type: :billing_details_provided,
340
- body: { my_event: 'data' },
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(new_event(aggregate_id: aggregate_id,
347
- type: :billing_details_provided,
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