aggregates 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +83 -94
- data/lib/aggregates.rb +29 -18
- data/lib/aggregates/aggregate_repository.rb +32 -0
- data/lib/aggregates/aggregate_root.rb +11 -28
- data/lib/aggregates/auditor.rb +6 -6
- data/lib/aggregates/command.rb +14 -9
- data/lib/aggregates/command_dispatcher.rb +13 -28
- data/lib/aggregates/command_execution.rb +21 -0
- data/lib/aggregates/command_filter.rb +6 -5
- data/lib/aggregates/command_processor.rb +6 -3
- data/lib/aggregates/domain.rb +40 -0
- data/lib/aggregates/domain_executor.rb +39 -0
- data/lib/aggregates/domain_message.rb +7 -13
- data/lib/aggregates/domain_object.rb +37 -0
- data/lib/aggregates/event.rb +2 -1
- data/lib/aggregates/event_processor.rb +1 -4
- data/lib/aggregates/event_stream.rb +20 -7
- data/lib/aggregates/identity.rb +1 -1
- data/lib/aggregates/in_memory_storage_backend.rb +2 -4
- data/lib/aggregates/message_processor.rb +10 -4
- data/lib/aggregates/{types.rb → value_object.rb} +1 -4
- metadata +11 -37
- data/lib/aggregates/configuration.rb +0 -37
- data/lib/aggregates/dynamoid/dynamoid_storage_backend.rb +0 -73
- data/lib/aggregates/with_aggregate_helpers.rb +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a8ea9821a6f5fa681750a5d64885972c899285705e255c519336ceba98dc4afd
|
4
|
+
data.tar.gz: 875d8fa0127fd8b708e656c9761a5852baa54cccee2885f27a95f6cc40c3a5bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b42b9939913cf8854cd33c35b9fa56b85a84b287a5dea4fd402ec7263f20aec612bbe1b35ca49e1fe8b0ae39c4a7cd8e28d55afd01b0239acf5da3ccfebf8945
|
7
|
+
data.tar.gz: 74e3e52d34228e9aba39d161dc237a756d8f343281944653cd08020d1ce9f4b154302d2d0afb95374ea8d964fa9b751a2bf0f67ef0fa94a3e35a85197ec4071b
|
data/README.md
CHANGED
@@ -23,16 +23,14 @@ _Warning:_ This Gem is in active development and probably doesn't work correctly
|
|
23
23
|
- [Creating Commands](#creating-commands)
|
24
24
|
- [Creating Events](#creating-events)
|
25
25
|
- [Processing Commands](#processing-commands)
|
26
|
+
- [Value Objects](#value-objects)
|
26
27
|
- [Filtering Commands](#filtering-commands)
|
27
28
|
- [Processing Events](#processing-events)
|
28
|
-
- [
|
29
|
-
- [
|
30
|
-
- [Configuring](#configuring)
|
29
|
+
- [Building The Domain](#building-the-domain)
|
30
|
+
- [Executing Your Domain](#executing-your-domain)
|
31
31
|
- [Storage Backends](#storage-backends)
|
32
|
-
|
33
|
-
- [
|
34
|
-
- [Adding Event Processors](#adding-event-processors)
|
35
|
-
- [Adding Command Filters](#adding-command-filters)
|
32
|
+
- [Executing Commands](#executing-commands)
|
33
|
+
- [Auditing Aggregates](#auditing-aggregates)
|
36
34
|
- [Development](#development)
|
37
35
|
- [Tests](#tests)
|
38
36
|
- [Versioning](#versioning)
|
@@ -48,7 +46,7 @@ _Warning:_ This Gem is in active development and probably doesn't work correctly
|
|
48
46
|
|
49
47
|
- Pluggable Event / Command Storage Backends
|
50
48
|
- Tools for Command Validation, Filtering, and Execution.
|
51
|
-
-
|
49
|
+
- Opinionated structure for CQRS, Domain-Driven Design, and Event Sourcing.
|
52
50
|
|
53
51
|
## Requirements
|
54
52
|
|
@@ -71,7 +69,7 @@ Or Add the following to your Gemfile:
|
|
71
69
|
An AggregateRoot is a grouping of domain object(s) that work to encapsulate
|
72
70
|
a single part of your Domain or Business Logic. The general design of aggregate roots should be as follows:
|
73
71
|
|
74
|
-
- Create functions that encapsulate different operations on your Aggregate Roots. These functions should enforce
|
72
|
+
- Create functions that encapsulate different operations on your Aggregate Roots. These functions should enforce business logic constraints and then capture state changes by creating events.
|
75
73
|
- Create event handlers that actually perform the state changes captured by those events.
|
76
74
|
|
77
75
|
A simple example is below:
|
@@ -80,11 +78,11 @@ A simple example is below:
|
|
80
78
|
class Post < Aggregates::AggregateRoot
|
81
79
|
# Write functions that encapsulate business logic.
|
82
80
|
def publish(command)
|
83
|
-
apply
|
81
|
+
apply PostPublished, body: command.body, category: command.category
|
84
82
|
end
|
85
83
|
|
86
84
|
# Modify the state of the aggregate from the emitted events.
|
87
|
-
on
|
85
|
+
on PostPublished do |event|
|
88
86
|
@body = event.body
|
89
87
|
@category = event.category
|
90
88
|
end
|
@@ -96,23 +94,23 @@ as well. Every `on` block that applies to the event will be called in order from
|
|
96
94
|
|
97
95
|
### Creating Commands
|
98
96
|
|
99
|
-
Commands are a type of domain message that define the shape and contract of data needed to perform an action.
|
97
|
+
Commands are a type of domain message that define the shape and contract of data needed to perform an action.
|
98
|
+
Essentially, they provide the api for interacting with your domain.
|
99
|
+
Commands should have descriptive names capturing the change they are intended to make.
|
100
|
+
For instance, `ChangeUserEmail` or `AddComment`.
|
100
101
|
|
101
102
|
```ruby
|
102
103
|
class PublishPost < Aggregates::Command
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
class Contract < Contract
|
109
|
-
rule(:body) do
|
110
|
-
key.failure('Post not long enough') unless value.length > 10
|
111
|
-
end
|
112
|
-
end
|
104
|
+
interacts_with Post
|
105
|
+
|
106
|
+
attribute :body
|
107
|
+
attribute :category
|
108
|
+
validates_length_of :body, minimum: 10
|
113
109
|
end
|
114
110
|
```
|
115
111
|
|
112
|
+
You can specify them via attr accessors and use `ActiveModel::Validations` to enforce data constraints.
|
113
|
+
|
116
114
|
### Creating Events
|
117
115
|
|
118
116
|
An Event describes something that happened. They are named in passed tense.
|
@@ -120,9 +118,9 @@ For instance, if the user's email has changed, then you might create an event ty
|
|
120
118
|
`UserEmailChanged`.
|
121
119
|
|
122
120
|
```ruby
|
123
|
-
class
|
124
|
-
attribute :body
|
125
|
-
attribute :category
|
121
|
+
class PostPublished < Aggregates::Event
|
122
|
+
attribute :body
|
123
|
+
attribute :category
|
126
124
|
end
|
127
125
|
```
|
128
126
|
|
@@ -131,48 +129,60 @@ end
|
|
131
129
|
The goal of a `CommandProcessor` is to route commands that have passed validation and
|
132
130
|
filtering. They should invoke business logic on their respective aggregates. Doing so is accomplished by using the same message-handling DSL as in our `AggregateRoots`, this time for commands.
|
133
131
|
|
134
|
-
A helper function, `with_aggregate`, is provided to help retrieve the appropriate aggregate
|
135
|
-
for a given command.
|
136
|
-
|
137
132
|
```ruby
|
138
133
|
class PostCommandProcessor < Aggregates::CommandProcessor
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
end
|
134
|
+
# Instead of `process`, you may use `on`
|
135
|
+
process PublishPost do |command, post|
|
136
|
+
post.publish(command)
|
143
137
|
end
|
144
138
|
end
|
145
139
|
```
|
140
|
+
_Note:_ the message-handling DSL (`process`) supports passing a super class of any given event
|
141
|
+
as well. Every `process` block that applies to the event will be called in order from most specific to least specific.
|
142
|
+
|
143
|
+
### Value Objects
|
144
|
+
|
145
|
+
Often times you will find that you will have data clumps that are similar pieces of data that have the same rules, and schema. Typically these values represent a valid type in your domain and should be combined as a single value. That is where `ValueObject` comes in. The api is the same as commands and events.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
class Name < Aggregates::ValueObject
|
149
|
+
attribute :first_name
|
150
|
+
attribute :last_name
|
151
|
+
validates_presence_of :first_name, :last_name
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
When you have a command, validation logic will automatically include validating nested value objects to an arbitrary depth.
|
146
156
|
|
147
|
-
_Note:_ the message-handling DSL (`on`) supports passing a super class of any given event
|
148
|
-
as well. Every `on` block that applies to the event will be called in order from most specific to least specific.
|
149
157
|
|
150
158
|
### Filtering Commands
|
151
159
|
|
152
|
-
There are times where commands should not be executed by the domain logic. You can opt to include a condition in your command processor or aggregate. However, that is not always extensible if you have repeated logic between many commands. Additionally, it violates the single
|
160
|
+
There are times where commands should not be executed by the domain logic. You can opt to include a condition in your command processor or aggregate. However, that is not always extensible if you have repeated logic between many commands. Additionally, it violates the single responsibility principal.
|
153
161
|
|
154
|
-
Instead, it is best to support this kind of filtering logic using `CommandFilters`. A `CommandFilter` uses the same Message Handling message-handling DSL as the rest of the `Aggregates` gem.
|
162
|
+
Instead, it is best to support this kind of filtering logic using `CommandFilters`. A `CommandFilter` uses the same Message Handling message-handling DSL as the rest of the `Aggregates` gem.
|
163
|
+
This time, it needs to return a true/false back to the gem to determine whether or not (true/false) the command should be allowed. Many command filters can provide many blocks of the `filter` or `on` DSL.
|
164
|
+
If any one of the filters rejects the command then the command will not be processed.
|
155
165
|
|
156
166
|
```ruby
|
157
167
|
class UpdatePostCommand < Aggregates::Command
|
158
|
-
|
168
|
+
interacts_with Post
|
169
|
+
attribute :commanding_user_id
|
159
170
|
end
|
160
171
|
|
161
172
|
class UpdatePostBody < UpdatePostCommand
|
162
|
-
attribute :body
|
173
|
+
attribute :body
|
163
174
|
end
|
164
175
|
|
165
176
|
class PostCommandFilter < Aggregates::CommandFilter
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
end
|
177
|
+
# Instead of `filter`, you may use `on`
|
178
|
+
filter UpdatePostCommand do |command, post|
|
179
|
+
post.owner_id == command.commanding_user_id
|
170
180
|
end
|
171
181
|
end
|
172
182
|
```
|
173
183
|
|
174
184
|
In this example, we are using a super class of `UpdatePostBody`.
|
175
|
-
As with all MessageProcessors, calling `
|
185
|
+
As with all MessageProcessors, calling `filter` with a super class
|
176
186
|
will be called when any child class is being processed. In other words,
|
177
187
|
`on UpdatePostCommand` will be called when you call `Aggregates.execute_command`
|
178
188
|
with an instance of `UpdatePostBody`.
|
@@ -191,33 +201,55 @@ class RssUpdateProcessor < Aggregates::EventProcessor
|
|
191
201
|
# ...
|
192
202
|
end
|
193
203
|
|
194
|
-
on
|
204
|
+
on PostPublished do |event|
|
195
205
|
update_feed_for_new_post(event)
|
196
206
|
end
|
197
207
|
end
|
198
208
|
```
|
199
209
|
|
200
|
-
|
201
|
-
|
210
|
+
### Building The Domain
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
domain = Aggregates.create_domain do
|
214
|
+
# Adding Command Processors
|
215
|
+
process_commands_with PostCommandProcessor.new
|
216
|
+
# Adding Event Processors
|
217
|
+
process_events_with RssUpdateProcessor.new
|
218
|
+
# Adding Command Filters
|
219
|
+
filter_commands_with MyCommandFilter.new
|
220
|
+
end
|
221
|
+
```
|
202
222
|
|
203
|
-
### Executing
|
223
|
+
### Executing Your Domain
|
224
|
+
|
225
|
+
#### Storage Backends
|
226
|
+
|
227
|
+
Storage Backends are the method by which events and commands are stored in
|
228
|
+
the system. You need to specify one in order to execute your domain.
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
executor = domain.execute_with MyAwesomeStorageBackend.new
|
232
|
+
```
|
233
|
+
|
234
|
+
#### Executing Commands
|
204
235
|
|
205
236
|
```ruby
|
206
237
|
aggregate_id = Aggregates.new_aggregate_id
|
207
238
|
command = CreateThing.new(foo: 1, bar: false, aggregate_id: aggregate_id)
|
208
|
-
|
239
|
+
executor.execute_command(command)
|
209
240
|
|
210
241
|
increment = IncrementFooThing.new(aggregate_id: aggregate_id)
|
211
242
|
toggle = ToggleBarThing.new(aggregate_id: aggregate_id)
|
212
|
-
|
243
|
+
executor.execute_command(command)
|
244
|
+
executor.execute_command(command)
|
213
245
|
```
|
214
246
|
|
215
|
-
|
247
|
+
#### Auditing Aggregates
|
216
248
|
|
217
249
|
```ruby
|
218
250
|
aggregate_id = Aggregates.new_aggregate_id
|
219
251
|
# ... Commands and stuff happened.
|
220
|
-
auditor =
|
252
|
+
auditor = executor.audit MyAggregateType aggregate_id
|
221
253
|
|
222
254
|
# Each of these returns a list to investigate using.
|
223
255
|
events = auditor.events # Or events_processed_by(time) or events_processed_after(time)
|
@@ -228,49 +260,6 @@ commands = auditor.commands # Or commands_processed_by(time) or commands_process
|
|
228
260
|
aggregate_at_time = auditor.inspect_state_at(Time.now - 1.hour)
|
229
261
|
```
|
230
262
|
|
231
|
-
### Configuring
|
232
|
-
|
233
|
-
#### Storage Backends
|
234
|
-
|
235
|
-
Storage Backends at the method by which events and commands are stored in
|
236
|
-
the system.
|
237
|
-
|
238
|
-
```ruby
|
239
|
-
Aggregates.configure do |config|
|
240
|
-
config.store_with MyAwesomeStorageBackend.new
|
241
|
-
end
|
242
|
-
```
|
243
|
-
|
244
|
-
##### Dynamoid
|
245
|
-
|
246
|
-
If `Aggregates` can `require 'dynamoid'` then it will provide the `Aggregates::Dynamoid::DynamoidStorageBackend` that
|
247
|
-
stores using the [Dynmoid Gem](https://github.com/Dynamoid/dynamoid) for AWS DynamoDB.
|
248
|
-
|
249
|
-
#### Adding Command Processors
|
250
|
-
|
251
|
-
```ruby
|
252
|
-
Aggregates.configure do |config|
|
253
|
-
# May call this method many times with different processors.
|
254
|
-
config.process_commands_with PostCommandProcessor.new
|
255
|
-
end
|
256
|
-
```
|
257
|
-
|
258
|
-
#### Adding Event Processors
|
259
|
-
|
260
|
-
```ruby
|
261
|
-
Aggregates.configure do |config|
|
262
|
-
# May call this method many times with different processors.
|
263
|
-
config.process_events_with RssUpdateProcessor.new
|
264
|
-
end
|
265
|
-
```
|
266
|
-
|
267
|
-
#### Adding Command Filters
|
268
|
-
|
269
|
-
```ruby
|
270
|
-
Aggregates.configure do |config|
|
271
|
-
config.filter_commands_with MyCommandFilter.new
|
272
|
-
end
|
273
|
-
```
|
274
263
|
|
275
264
|
## Development
|
276
265
|
|
data/lib/aggregates.rb
CHANGED
@@ -1,33 +1,44 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
require_relative './aggregates/domain_object'
|
6
|
+
require_relative './aggregates/domain_message'
|
7
|
+
require_relative './aggregates/message_processor'
|
8
|
+
|
9
|
+
require_relative './aggregates/aggregate_root'
|
10
|
+
require_relative './aggregates/auditor'
|
11
|
+
require_relative './aggregates/command'
|
12
|
+
require_relative './aggregates/command_processor'
|
13
|
+
require_relative './aggregates/command_filter'
|
14
|
+
require_relative './aggregates/command_validation_error'
|
15
|
+
require_relative './aggregates/domain'
|
16
|
+
require_relative './aggregates/domain_executor'
|
17
|
+
require_relative './aggregates/event'
|
18
|
+
require_relative './aggregates/event_processor'
|
19
|
+
require_relative './aggregates/event_stream'
|
20
|
+
require_relative './aggregates/value_object'
|
21
|
+
|
22
|
+
require_relative './aggregates/storage_backend'
|
23
|
+
require_relative './aggregates/in_memory_storage_backend'
|
7
24
|
|
8
25
|
# A helpful library for building CQRS and Event Sourced Applications.
|
9
26
|
module Aggregates
|
10
|
-
def self.configure
|
11
|
-
yield Configuration.instance
|
12
|
-
end
|
13
|
-
|
14
27
|
def self.new_aggregate_id
|
15
|
-
|
28
|
+
new_uuid
|
16
29
|
end
|
17
30
|
|
18
31
|
def self.new_message_id
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.execute_command(command)
|
23
|
-
CommandDispatcher.instance.execute_command command
|
32
|
+
new_uuid
|
24
33
|
end
|
25
34
|
|
26
|
-
def self.
|
27
|
-
|
35
|
+
def self.create_domain(&block)
|
36
|
+
domain = Domain.new
|
37
|
+
domain.instance_exec(&block)
|
38
|
+
domain
|
28
39
|
end
|
29
40
|
|
30
|
-
def self.
|
31
|
-
|
41
|
+
def self.new_uuid
|
42
|
+
SecureRandom.uuid.to_s
|
32
43
|
end
|
33
44
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './event_stream'
|
4
|
+
|
5
|
+
module Aggregates
|
6
|
+
# Uses the storage backend to store load aggregates.
|
7
|
+
class AggregateRepository
|
8
|
+
def initialize(storage_backend)
|
9
|
+
@storage_backend = storage_backend
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_aggregate(type, id, at: nil)
|
13
|
+
event_stream = create_aggregate_event_stream(type, id)
|
14
|
+
aggregate = type.new(id, event_stream)
|
15
|
+
replay_events_on_aggregate(aggregate, event_stream, at)
|
16
|
+
aggregate
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def create_aggregate_event_stream(type, id)
|
22
|
+
EventStream.new(@storage_backend, type, id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def replay_events_on_aggregate(aggregate, event_stream, at)
|
26
|
+
events = event_stream.load_events ending_at: at
|
27
|
+
events.each do |event|
|
28
|
+
aggregate.process_event(event)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative './message_processor'
|
4
|
+
|
3
5
|
module Aggregates
|
4
6
|
# An AggregateRoot is a central grouping of domain object(s) that work to encapsulate
|
5
7
|
# parts of our Domain or Business Logic.
|
@@ -10,31 +12,23 @@ module Aggregates
|
|
10
12
|
#
|
11
13
|
# - Create event handlers that actually performed the state changes captured by the events
|
12
14
|
# made by processing commands using the above functions.
|
13
|
-
class AggregateRoot
|
14
|
-
|
15
|
+
class AggregateRoot
|
16
|
+
include MessageProcessor
|
15
17
|
|
16
|
-
|
17
|
-
def self.get_by_id(id)
|
18
|
-
instance = new id
|
19
|
-
instance.replay_history
|
20
|
-
instance
|
21
|
-
end
|
18
|
+
attr_reader :id
|
22
19
|
|
23
20
|
# Creates a new instance of an aggregate root. This should not be called directly. Instead, it should
|
24
21
|
# be called by calling AggregateRoot.get_by_id.
|
25
22
|
# :reek:BooleanParameter
|
26
|
-
def initialize(id,
|
27
|
-
super()
|
28
|
-
|
23
|
+
def initialize(id, event_stream)
|
29
24
|
@id = id
|
30
|
-
@mutable = mutable
|
31
25
|
@sequence_number = 1
|
32
|
-
@event_stream =
|
26
|
+
@event_stream = event_stream
|
33
27
|
end
|
34
28
|
|
35
29
|
def process_event(event)
|
36
|
-
super
|
37
30
|
@sequence_number += 1
|
31
|
+
handle_message event
|
38
32
|
end
|
39
33
|
|
40
34
|
# Takes an event type and some parameters with which to create it. Then performs the following actions
|
@@ -43,21 +37,10 @@ module Aggregates
|
|
43
37
|
# 3.) Produces the event on the event stream so that is saved by the storage backend and processed
|
44
38
|
# by the configured processors of the given type.
|
45
39
|
def apply(event, params = {})
|
46
|
-
raise FrozenError unless @mutable
|
47
|
-
|
48
40
|
event = build_event(event, params)
|
49
|
-
process_event
|
50
|
-
@event_stream.publish
|
51
|
-
|
52
|
-
|
53
|
-
# Loads all events from the event stream of this instance and reprocesses them to
|
54
|
-
# get the current state of the aggregate.
|
55
|
-
def replay_history(up_to: nil)
|
56
|
-
events = @event_stream.load_events
|
57
|
-
events = events.select { |event| event.created_at <= up_to } if up_to.present?
|
58
|
-
events.each do |event|
|
59
|
-
process_event event
|
60
|
-
end
|
41
|
+
results = process_event(event)
|
42
|
+
@event_stream.publish(event)
|
43
|
+
results
|
61
44
|
end
|
62
45
|
|
63
46
|
private
|
data/lib/aggregates/auditor.rb
CHANGED
@@ -6,7 +6,8 @@ module Aggregates
|
|
6
6
|
class Auditor
|
7
7
|
attr_reader :type, :aggregate_id
|
8
8
|
|
9
|
-
def initialize(type, aggregate_id)
|
9
|
+
def initialize(storage_backend, type, aggregate_id)
|
10
|
+
@storage_backend = storage_backend
|
10
11
|
@type = type
|
11
12
|
@aggregate_id = aggregate_id
|
12
13
|
end
|
@@ -15,19 +16,18 @@ module Aggregates
|
|
15
16
|
# on the aggregate alone. Only events that happened prior to the time specified are
|
16
17
|
# processed.
|
17
18
|
def inspect_state_at(time)
|
18
|
-
|
19
|
-
|
20
|
-
aggregate
|
19
|
+
aggregate_repository = AggregateRepository.new(@storage_backend)
|
20
|
+
aggregate_repository.load_aggregate(@type, @aggregate_id, at: time)
|
21
21
|
end
|
22
22
|
|
23
23
|
# Returns all stored events for a given aggregate.
|
24
24
|
def events
|
25
|
-
@events ||=
|
25
|
+
@events ||= @storage_backend.load_events_by_aggregate_id(@aggregate_id)
|
26
26
|
end
|
27
27
|
|
28
28
|
# Returns all commands for a given aggregate.
|
29
29
|
def commands
|
30
|
-
@commands ||=
|
30
|
+
@commands ||= @storage_backend.load_commands_by_aggregate_id(@aggregate_id)
|
31
31
|
end
|
32
32
|
|
33
33
|
def events_processed_by(time)
|
data/lib/aggregates/command.rb
CHANGED
@@ -1,25 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require 'dry-validation'
|
3
|
+
require 'active_model/errors'
|
5
4
|
|
6
5
|
module Aggregates
|
7
6
|
# Commands are a type of message that define the shape and contract data that is accepted for an attempt
|
8
7
|
# at performing a state change on a given aggregate. Essentially, they provide the api for interacting with
|
9
8
|
# your domain. Commands should have descriptive names capturing the change they are intended to make to the domain.
|
10
9
|
# For instance, `ChangeUserEmail` or `AddComment`.
|
10
|
+
# :reek:MissingSafeMethod { exclude: [ validate! ] }
|
11
11
|
class Command < DomainMessage
|
12
|
-
|
13
|
-
|
14
|
-
end
|
12
|
+
class << self
|
13
|
+
attr_reader :aggregate_type
|
15
14
|
|
16
|
-
|
17
|
-
|
15
|
+
def interacts_with(aggregate_type)
|
16
|
+
@aggregate_type = aggregate_type
|
17
|
+
end
|
18
18
|
end
|
19
19
|
|
20
20
|
def validate!
|
21
|
-
|
22
|
-
|
21
|
+
super
|
22
|
+
rescue ActiveModel::ValidationError
|
23
|
+
raise Aggregates::CommandValidationError, errors.as_json
|
24
|
+
end
|
25
|
+
|
26
|
+
def load_related_aggregate(aggregate_repo)
|
27
|
+
aggregate_repo.load_aggregate(self.class.aggregate_type, aggregate_id)
|
23
28
|
end
|
24
29
|
end
|
25
30
|
end
|
@@ -1,53 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'singleton'
|
4
|
-
|
5
3
|
module Aggregates
|
6
4
|
# The CommandDispatcher is effectively a router of incoming commands to CommandProcessors that are responsible
|
7
5
|
# for handling them appropriately. By convention, you likely will not need to interact with it directly, instead
|
8
6
|
# simply call Aggregates.process_command or Aggregates.process_commands.
|
9
7
|
class CommandDispatcher
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
@config = Configuration.instance
|
14
|
-
end
|
15
|
-
|
16
|
-
# Takes a sequence of commands and executes them one at a time.
|
17
|
-
def process_commands(*commands)
|
18
|
-
commands.each do |command|
|
19
|
-
process_command command
|
20
|
-
end
|
8
|
+
def initialize(command_processors, command_filters)
|
9
|
+
@command_processors = command_processors
|
10
|
+
@command_filters = command_filters
|
21
11
|
end
|
22
12
|
|
23
13
|
# Takes a single command and processes it. The command will be validated through it's contract, sent to command
|
24
14
|
# processors and finally stored with the configured StorageBackend used for messages.
|
25
|
-
def
|
26
|
-
|
27
|
-
return unless should_process? command
|
15
|
+
def execute_command(execution)
|
16
|
+
return false unless should_process? execution
|
28
17
|
|
29
|
-
send_to_processors
|
30
|
-
|
18
|
+
send_to_processors(execution)
|
19
|
+
true
|
31
20
|
end
|
32
21
|
|
33
22
|
private
|
34
23
|
|
35
|
-
def should_process?(
|
24
|
+
def should_process?(execution)
|
36
25
|
# Each command processor is going to give a true/false value for itself.
|
37
26
|
# So if they all allow it, then we can return true. Else false.
|
38
|
-
@
|
39
|
-
command_filter.allow?
|
27
|
+
@command_filters.all? do |command_filter|
|
28
|
+
command_filter.allow?(execution)
|
40
29
|
end
|
41
30
|
end
|
42
31
|
|
43
|
-
def send_to_processors(
|
44
|
-
@
|
45
|
-
command_processor.
|
32
|
+
def send_to_processors(execution)
|
33
|
+
@command_processors.each do |command_processor|
|
34
|
+
command_processor.process(execution)
|
46
35
|
end
|
47
36
|
end
|
48
|
-
|
49
|
-
def store(command)
|
50
|
-
@config.storage_backend.store_command command
|
51
|
-
end
|
52
37
|
end
|
53
38
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aggregates
|
4
|
+
# Captures the execution of a command with the aggregate at its current state.
|
5
|
+
class CommandExecution
|
6
|
+
attr_reader :command
|
7
|
+
|
8
|
+
def initialize(aggregate_repo, command)
|
9
|
+
@aggregate_repo = aggregate_repo
|
10
|
+
@command = command
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute_with(handler)
|
14
|
+
handler.invoke_handlers(command, aggregate)
|
15
|
+
end
|
16
|
+
|
17
|
+
def aggregate
|
18
|
+
command.load_related_aggregate(@aggregate_repo)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -2,12 +2,13 @@
|
|
2
2
|
|
3
3
|
module Aggregates
|
4
4
|
# Applies filters to commands to decouple filtering logic from the CommandProcessor.
|
5
|
-
class CommandFilter
|
6
|
-
|
7
|
-
|
5
|
+
class CommandFilter < CommandProcessor
|
6
|
+
class << self
|
7
|
+
alias filter on
|
8
|
+
end
|
8
9
|
|
9
|
-
def allow?(
|
10
|
-
|
10
|
+
def allow?(execution)
|
11
|
+
process(execution).all?
|
11
12
|
end
|
12
13
|
end
|
13
14
|
end
|
@@ -4,10 +4,13 @@ module Aggregates
|
|
4
4
|
# A command processor is a type that correlates commands to operations on an aggregate root.
|
5
5
|
class CommandProcessor
|
6
6
|
include MessageProcessor
|
7
|
-
include WithAggregateHelpers
|
8
7
|
|
9
|
-
|
10
|
-
|
8
|
+
class << self
|
9
|
+
alias process on
|
10
|
+
end
|
11
|
+
|
12
|
+
def process(execution)
|
13
|
+
execution.execute_with(self)
|
11
14
|
end
|
12
15
|
end
|
13
16
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './command_dispatcher'
|
4
|
+
require_relative './domain_executor'
|
5
|
+
|
6
|
+
module Aggregates
|
7
|
+
# Defines the collection of command processors, event processors, and command filters
|
8
|
+
# that are executed together.
|
9
|
+
class Domain
|
10
|
+
attr_reader :command_processors, :event_processors, :command_filters
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@command_processors = []
|
14
|
+
@event_processors = []
|
15
|
+
@command_filters = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def process_events_with(*event_processors)
|
19
|
+
event_processors.each do |event_processor|
|
20
|
+
@event_processors << event_processor
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def process_commands_with(*command_processors)
|
25
|
+
command_processors.each do |command_processor|
|
26
|
+
@command_processors << command_processor
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def filter_commands_with(*command_filters)
|
31
|
+
command_filters.each do |command_filter|
|
32
|
+
@command_filters << command_filter
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def execute_with(storage_backend)
|
37
|
+
DomainExecutor.new(storage_backend, self)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './aggregate_repository'
|
4
|
+
require_relative './command_execution'
|
5
|
+
|
6
|
+
module Aggregates
|
7
|
+
# Combines a storage backend and a domain in order to execute that domain.
|
8
|
+
class DomainExecutor
|
9
|
+
attr_reader :storage_backend, :domain
|
10
|
+
|
11
|
+
def initialize(storage_backend, domain)
|
12
|
+
@aggregate_repository = AggregateRepository.new(storage_backend)
|
13
|
+
@dispatcher = CommandDispatcher.new(domain.command_processors, domain.command_filters)
|
14
|
+
@storage_backend = storage_backend
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute_command(command)
|
18
|
+
command.validate!
|
19
|
+
command_execution = CommandExecution.new(@aggregate_repository, command)
|
20
|
+
dispatch(command_execution)
|
21
|
+
end
|
22
|
+
|
23
|
+
def audit(type, aggregate_id)
|
24
|
+
Auditor.new(@storage_backend, type, aggregate_id)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def store_command(command)
|
30
|
+
@storage_backend.store_command(command)
|
31
|
+
end
|
32
|
+
|
33
|
+
def dispatch(command_execution)
|
34
|
+
result = @dispatcher.execute_command(command_execution)
|
35
|
+
store_command(command_execution.command) if result
|
36
|
+
result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,23 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'dry-struct'
|
4
|
-
|
5
3
|
module Aggregates
|
6
4
|
# The DomainMessage is not a class that should generally be interacted with unless
|
7
5
|
# extending Aggregates itself. It provides some core functionality that message types
|
8
6
|
# (Event and Command) both require.
|
9
|
-
class DomainMessage <
|
10
|
-
|
11
|
-
|
12
|
-
attribute :created_at, Types::Strict::DateTime.default(proc { Time.now })
|
13
|
-
|
14
|
-
def to_json(*args)
|
15
|
-
json_data = attributes.merge({ JSON.create_id => self.class.name })
|
16
|
-
json_data.to_json(args)
|
7
|
+
class DomainMessage < DomainObject
|
8
|
+
def initialize(attributes = {})
|
9
|
+
super(attributes.merge({ message_id: Aggregates.new_message_id, created_at: Time.now }))
|
17
10
|
end
|
18
11
|
|
19
|
-
|
20
|
-
|
21
|
-
|
12
|
+
attribute :aggregate_id
|
13
|
+
attribute :message_id
|
14
|
+
attribute :created_at
|
15
|
+
validates_presence_of :aggregate_id, :message_id, :created_at
|
22
16
|
end
|
23
17
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model'
|
4
|
+
|
5
|
+
module Aggregates
|
6
|
+
# Defines an object that is an element of the domain.
|
7
|
+
class DomainObject
|
8
|
+
include ActiveModel::Model
|
9
|
+
include ActiveModel::Validations
|
10
|
+
include ActiveModel::Attributes
|
11
|
+
|
12
|
+
validate :validate_nested_fields
|
13
|
+
|
14
|
+
def to_json(*args)
|
15
|
+
json_data = attributes.merge({ JSON.create_id => self.class.name })
|
16
|
+
json_data.to_json(*args)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.json_create(arguments)
|
20
|
+
new(**arguments)
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def add_nested_errors_for(attribute, other_validator)
|
26
|
+
nested_errors = other_validator.errors
|
27
|
+
errors.messages[attribute] = nested_errors.messages
|
28
|
+
errors.details[attribute] = nested_errors.details
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate_nested_fields
|
32
|
+
attributes.each do |key, value|
|
33
|
+
add_nested_errors_for(key.to_sym, value) if value.is_a?(DomainObject) && !value.valid?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/aggregates/event.rb
CHANGED
@@ -6,6 +6,7 @@ module Aggregates
|
|
6
6
|
# For instance, if the user's email has changed, then you might create an event type called
|
7
7
|
# UserEmailChanged.
|
8
8
|
class Event < DomainMessage
|
9
|
-
attribute :sequence_number
|
9
|
+
attribute :sequence_number
|
10
|
+
validates_presence_of :sequence_number
|
10
11
|
end
|
11
12
|
end
|
@@ -6,20 +6,33 @@ module Aggregates
|
|
6
6
|
#
|
7
7
|
# There is likely no need to interact with this class directly.
|
8
8
|
class EventStream
|
9
|
-
def initialize(aggregate_id)
|
9
|
+
def initialize(storage_backend, event_processors, aggregate_id)
|
10
|
+
@storage_backend = storage_backend
|
11
|
+
@event_processors = event_processors
|
10
12
|
@aggregate_id = aggregate_id
|
11
|
-
@config = Configuration.instance
|
12
13
|
end
|
13
14
|
|
14
|
-
def load_events
|
15
|
-
@
|
15
|
+
def load_events(ending_at: nil)
|
16
|
+
events = @storage_backend.load_events_by_aggregate_id(@aggregate_id)
|
17
|
+
events = events.select { |event| event.created_at <= ending_at } if ending_at.present?
|
18
|
+
events
|
16
19
|
end
|
17
20
|
|
18
21
|
def publish(event)
|
19
|
-
|
20
|
-
|
22
|
+
send_to_event_processors(event)
|
23
|
+
store_event(event)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def send_to_event_processors(event)
|
29
|
+
@event_processors.each do |event_processor|
|
30
|
+
event_processor.process_event(event)
|
21
31
|
end
|
22
|
-
|
32
|
+
end
|
33
|
+
|
34
|
+
def store_event(event)
|
35
|
+
@storage_backend.store_event(event)
|
23
36
|
end
|
24
37
|
end
|
25
38
|
end
|
data/lib/aggregates/identity.rb
CHANGED
@@ -13,13 +13,11 @@ module Aggregates
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def store_command(command)
|
16
|
-
|
17
|
-
commands_for_aggregate_id << command
|
16
|
+
load_commands_by_aggregate_id(command.aggregate_id) << command
|
18
17
|
end
|
19
18
|
|
20
19
|
def store_event(event)
|
21
|
-
|
22
|
-
event_for_aggregate_id << event
|
20
|
+
load_events_by_aggregate_id(event.aggregate_id) << event
|
23
21
|
end
|
24
22
|
|
25
23
|
def load_events_by_aggregate_id(aggregate_id)
|
@@ -27,7 +27,7 @@ module Aggregates
|
|
27
27
|
host_class.extend(ClassMethods)
|
28
28
|
end
|
29
29
|
|
30
|
-
def
|
30
|
+
def with_message_handlers(message, &block)
|
31
31
|
search_class = message.class
|
32
32
|
while search_class != DomainMessage
|
33
33
|
handlers = self.class.message_mapping[search_class]
|
@@ -36,10 +36,16 @@ module Aggregates
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
39
|
+
def invoke_handlers(message, *additional_args)
|
40
|
+
results = []
|
41
|
+
with_message_handlers(message) do |handler|
|
42
|
+
results << instance_exec(message, *additional_args, &handler)
|
42
43
|
end
|
44
|
+
results
|
45
|
+
end
|
46
|
+
|
47
|
+
def handle_message(message)
|
48
|
+
invoke_handlers(message)
|
43
49
|
end
|
44
50
|
end
|
45
51
|
end
|
metadata
CHANGED
@@ -1,57 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aggregates
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zach Probst
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-07-
|
11
|
+
date: 2021-07-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: activemodel
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1
|
19
|
+
version: '6.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: dry-validation
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '1.6'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '1.6'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: zeitwerk
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '2.4'
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '2.4'
|
26
|
+
version: '6.1'
|
55
27
|
description:
|
56
28
|
email:
|
57
29
|
- zprobst@resilientvitality.com
|
@@ -64,16 +36,19 @@ files:
|
|
64
36
|
- LICENSE.md
|
65
37
|
- README.md
|
66
38
|
- lib/aggregates.rb
|
39
|
+
- lib/aggregates/aggregate_repository.rb
|
67
40
|
- lib/aggregates/aggregate_root.rb
|
68
41
|
- lib/aggregates/auditor.rb
|
69
42
|
- lib/aggregates/command.rb
|
70
43
|
- lib/aggregates/command_dispatcher.rb
|
44
|
+
- lib/aggregates/command_execution.rb
|
71
45
|
- lib/aggregates/command_filter.rb
|
72
46
|
- lib/aggregates/command_processor.rb
|
73
47
|
- lib/aggregates/command_validation_error.rb
|
74
|
-
- lib/aggregates/
|
48
|
+
- lib/aggregates/domain.rb
|
49
|
+
- lib/aggregates/domain_executor.rb
|
75
50
|
- lib/aggregates/domain_message.rb
|
76
|
-
- lib/aggregates/
|
51
|
+
- lib/aggregates/domain_object.rb
|
77
52
|
- lib/aggregates/event.rb
|
78
53
|
- lib/aggregates/event_processor.rb
|
79
54
|
- lib/aggregates/event_stream.rb
|
@@ -81,8 +56,7 @@ files:
|
|
81
56
|
- lib/aggregates/in_memory_storage_backend.rb
|
82
57
|
- lib/aggregates/message_processor.rb
|
83
58
|
- lib/aggregates/storage_backend.rb
|
84
|
-
- lib/aggregates/
|
85
|
-
- lib/aggregates/with_aggregate_helpers.rb
|
59
|
+
- lib/aggregates/value_object.rb
|
86
60
|
homepage: https://github.com/resilient-vitality/aggregates
|
87
61
|
licenses:
|
88
62
|
- MIT
|
@@ -1,37 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'singleton'
|
4
|
-
|
5
|
-
module Aggregates
|
6
|
-
# Stores all of the items needed to dictate the exact behavior needed by
|
7
|
-
# the application consuming the Aggregates gem.
|
8
|
-
class Configuration
|
9
|
-
include Singleton
|
10
|
-
|
11
|
-
attr_reader :command_processors, :event_processors,
|
12
|
-
:storage_backend, :command_filters
|
13
|
-
|
14
|
-
def initialize
|
15
|
-
@command_processors = []
|
16
|
-
@event_processors = []
|
17
|
-
@command_filters = []
|
18
|
-
@storage_backend = InMemoryStorageBackend.new
|
19
|
-
end
|
20
|
-
|
21
|
-
def filter_commands_with(command_filter)
|
22
|
-
@command_filters << command_filter
|
23
|
-
end
|
24
|
-
|
25
|
-
def store_with(storage_backend)
|
26
|
-
@storage_backend = storage_backend
|
27
|
-
end
|
28
|
-
|
29
|
-
def process_events_with(event_processor)
|
30
|
-
@event_processors << event_processor
|
31
|
-
end
|
32
|
-
|
33
|
-
def process_commands_with(command_processor)
|
34
|
-
@command_processors << command_processor
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
@@ -1,73 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# rubocop:disable Style/Documentation
|
4
|
-
|
5
|
-
module Aggregates
|
6
|
-
# rubocop:enable Style/Documentation
|
7
|
-
begin
|
8
|
-
require 'dynamoid'
|
9
|
-
|
10
|
-
# Extensions to the Aggregates gem that provide message storage on DynamoDB.
|
11
|
-
module Dynamoid
|
12
|
-
# Stores events in DynamoDB using `Dynamoid`
|
13
|
-
class DynamoEventStore
|
14
|
-
include ::Dynamoid::Document
|
15
|
-
|
16
|
-
field :aggregate_id
|
17
|
-
field :sequence_number, :integer
|
18
|
-
field :data
|
19
|
-
|
20
|
-
table name: :events, hash_key: :aggregate_id, range_key: :sequence_number, timestamps: true
|
21
|
-
|
22
|
-
def self.store!(event, data)
|
23
|
-
args = { aggregate_id: event.aggregate_id, sequence_number: event.sequence_number, data: data }
|
24
|
-
event = new args
|
25
|
-
event.save!
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
# Stores commands in DynamoDB using `Dynamoid`
|
30
|
-
class DynamoCommandStore
|
31
|
-
include ::Dynamoid::Document
|
32
|
-
|
33
|
-
field :aggregate_id
|
34
|
-
field :data
|
35
|
-
|
36
|
-
table name: :commands, hash_key: :aggregate_id, range_key: :created_at, timestamps: true
|
37
|
-
|
38
|
-
def self.store!(command, data)
|
39
|
-
args = { aggregate_id: command.aggregate_id, data: data }
|
40
|
-
command = new args
|
41
|
-
command.save!
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
# Stores messages on DynamoDB using the dynamoid gem.
|
46
|
-
class DynamoidStorageBackend < StorageBackend
|
47
|
-
def store_event(event)
|
48
|
-
data = message_to_json_string(event)
|
49
|
-
DynamoEventStore.store! event, data
|
50
|
-
end
|
51
|
-
|
52
|
-
def store_command(command)
|
53
|
-
data = message_to_json_string(command)
|
54
|
-
DynamoCommandStore.store! command, data
|
55
|
-
end
|
56
|
-
|
57
|
-
def load_events_by_aggregate_id(aggregate_id)
|
58
|
-
DynamoEventStore.where(aggregate_id: aggregate_id).all.map do |stored_event|
|
59
|
-
json_string_to_message stored_event.data
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def load_commands_by_aggregate_id(aggregate_id)
|
64
|
-
DynamoCommandStore.where(aggregate_id: aggregate_id).all.map do |stored_command|
|
65
|
-
json_string_to_message stored_command.data
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
rescue LoadError
|
71
|
-
# This is intentional to no do anything if it is not loadable.
|
72
|
-
end
|
73
|
-
end
|
@@ -1,22 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Aggregates
|
4
|
-
# Helper functions for running blocks with a specified aggregate.
|
5
|
-
module WithAggregateHelpers
|
6
|
-
# Class Methods to extend onto the host class.
|
7
|
-
module ClassMethods
|
8
|
-
def with_aggregate(type, command, &block)
|
9
|
-
aggregate_id = command.aggregate_id
|
10
|
-
with_aggregate_by_id(type, aggregate_id, &block)
|
11
|
-
end
|
12
|
-
|
13
|
-
def with_aggregate_by_id(type, aggregate_id)
|
14
|
-
yield type.get_by_id aggregate_id
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.included(host_class)
|
19
|
-
host_class.extend(ClassMethods)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|