aggregates 0.2.0 → 0.3.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/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
|