eventsimple 1.8.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +137 -136
- data/README.md +37 -527
- data/Rakefile +0 -3
- data/app/views/eventsimple/entities/show.html.erb +2 -2
- data/eventsimple.gemspec +1 -2
- data/lib/eventsimple/data_type.rb +64 -11
- data/lib/eventsimple/event.rb +21 -7
- data/lib/eventsimple/message.rb +181 -11
- data/lib/eventsimple/metadata.rb +2 -4
- data/lib/eventsimple/types/encrypted_type.rb +36 -0
- data/lib/eventsimple/types.rb +32 -0
- data/lib/eventsimple/version.rb +1 -1
- data/lib/eventsimple.rb +1 -3
- metadata +7 -20
- data/lib/dry_types.rb +0 -5
data/README.md
CHANGED
|
@@ -2,556 +2,66 @@
|
|
|
2
2
|
[](https://github.com/wealthsimple/eventsimple/actions/workflows/default.yml) [](https://rubygems.org/gems/eventsimple)
|
|
3
3
|
|
|
4
4
|
## What
|
|
5
|
-
Eventsimple implements a simple deterministic event driven system using ActiveRecord and ActiveJob
|
|
5
|
+
Eventsimple implements a simple deterministic event driven system using ActiveRecord and ActiveJob
|
|
6
6
|
|
|
7
7
|
Use Eventsimple to:
|
|
8
8
|
|
|
9
9
|
* Add Event Sourcing to your ActiveRecord models.
|
|
10
|
-
*
|
|
10
|
+
* Pub/Sub.
|
|
11
11
|
* Implement a transactional outbox.
|
|
12
|
-
* Store audit logs
|
|
12
|
+
* Store audit logs.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
Async workflows are handled using [ActiveJob](https://guides.rubyonrails.org/active_job_basics.html).
|
|
16
|
-
|
|
17
|
-
Typical events in Eventsimple are ActiveRecord models that look like this:
|
|
18
|
-
|
|
19
|
-
```ruby
|
|
20
|
-
<UserComponent::Events::Created
|
|
21
|
-
id: 1,
|
|
22
|
-
aggregate_id: 'user-123',
|
|
23
|
-
type: "Created",
|
|
24
|
-
data: {
|
|
25
|
-
name: "John doe",
|
|
26
|
-
email: "johndoe@example.com",
|
|
27
|
-
},
|
|
28
|
-
created_at: 2022-01-01T00:00:00.000000,
|
|
29
|
-
updated_at: 2022-01-01T00:00:00.000000,
|
|
30
|
-
>
|
|
31
|
-
|
|
32
|
-
<UserComponent::Events::Deleted
|
|
33
|
-
id: 1,
|
|
34
|
-
aggregate_id: 'user-123',
|
|
35
|
-
type: "Deleted",
|
|
36
|
-
created_at: 2022-01-01T00:30:00.000000,
|
|
37
|
-
updated_at: 2022-01-01T00:30:00.000000,
|
|
38
|
-
>
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Setup
|
|
42
|
-
|
|
43
|
-
Add the following line to your Gemfile and run `bundle install`:
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
gem 'eventsimple'
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
The eventsimple UI allows you to view and navigate event history. Add the following line to your routes.rb:
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
mount Eventsimple::Engine => '/eventsimple'
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Setup an initializer in `config/initializers/eventsimple.rb`:
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
Eventsimple.configure do |config|
|
|
59
|
-
# Optional: Register your dispatch classes here.
|
|
60
|
-
# Dispatch classes are used to register reactors to events.
|
|
61
|
-
# Reactors are used to implement side effects.
|
|
62
|
-
# See the Reactors section below for more details.
|
|
63
|
-
config.dispatchers = []
|
|
64
|
-
|
|
65
|
-
# Optional: Entity updates use optimistic locking to enforce sequential updates.
|
|
66
|
-
# Set the max number of times to retry on concurrency failures.
|
|
67
|
-
# Defaults to 2
|
|
68
|
-
config.max_concurrency_retries = 2
|
|
69
|
-
|
|
70
|
-
# Optional: the metadata column is used to store optional metadata associated with the event.
|
|
71
|
-
# The default implemention enforces a typed constraint on the metadata column
|
|
72
|
-
# with the following two properties: `actor_id` and `reason`
|
|
73
|
-
# Use a custom metadata class to override this behaviour.
|
|
74
|
-
# Defaults to `Eventsimple::Metadata`
|
|
75
|
-
config.metadata_klass = 'Eventsimple::Metadata'
|
|
76
|
-
|
|
77
|
-
# Optional: When using an ActiveJob adapter that writes to a different data store like redis,
|
|
78
|
-
# it is possible that the reactor is executed before the transaction persisting the event is committed. This can result in noisy errors when using processors like Sidekiq.
|
|
79
|
-
# Enable this option to retry the reactor inline if the event is not found.
|
|
80
|
-
# Defaults to false.
|
|
81
|
-
config.retry_reactor_on_record_not_found = true
|
|
82
|
-
end
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
If using `Sidekiq` as a backend to `ActiveJob` for async reactors, please add this setting to
|
|
86
|
-
`config/application.rb`:
|
|
87
|
-
```ruby
|
|
88
|
-
config.active_job.queue_adapter = :sidekiq
|
|
89
|
-
```
|
|
90
|
-
The jobs are pushed into a queue named `eventsimple`, so please add it to your
|
|
91
|
-
`sidekiq.yml` as follows:
|
|
92
|
-
```yml
|
|
93
|
-
:queues:
|
|
94
|
-
- [default, 10]
|
|
95
|
-
- [eventsimple, 10]
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Generate a migration and add `Eventsimple` to an existing ActiveRecord model.
|
|
99
|
-
|
|
100
|
-
```ruby
|
|
101
|
-
bundle exec rails generate eventsimple:event User
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
This will result in the following changes:
|
|
105
|
-
|
|
106
|
-
```ruby
|
|
107
|
-
# ActiveRecord Classes
|
|
108
|
-
class User < ApplicationRecord
|
|
109
|
-
extend Eventsimple::Entity
|
|
110
|
-
event_driven_by UserEvent, aggregate_id: :id
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
class UserEvent < ApplicationRecord
|
|
114
|
-
extend Eventsimple::Event
|
|
115
|
-
drives_events_for User, events_namespace: 'UserComponent::Events', aggregate_id: :id
|
|
116
|
-
end
|
|
117
|
-
# Change aggregate_id to the column that represents the unique primary key for your model.
|
|
118
|
-
|
|
119
|
-
# Data migration
|
|
120
|
-
create_table :user_events do |t|
|
|
121
|
-
# Change this to string if your aggregates primary key is a string type
|
|
122
|
-
t.bigint :aggregate_id, null: false, index: true
|
|
123
|
-
t.string :idempotency_key, null: true
|
|
124
|
-
t.string :type, null: false
|
|
125
|
-
t.json :data, null: false, default: {}
|
|
126
|
-
t.json :metadata, null: false, default: {}
|
|
127
|
-
|
|
128
|
-
t.timestamps
|
|
129
|
-
|
|
130
|
-
t.index :idempotency_key, unique: true
|
|
131
|
-
t.index :created_at
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
add_column :users, :lock_version, :integer
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Adding lock_version to the model enables [optimistic locking](https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html) and protects against concurrent updates to stale versions of the model. Eventsimple will automatically retry on concurrency failures.
|
|
138
|
-
|
|
139
|
-
`events_namespace` is an optional argument pointing to the directory where your events classes are defined. If you do not specify this argument, Eventsimple will store the full namespace of the event classes in the STI column.
|
|
140
|
-
|
|
141
|
-
### Event Table definition
|
|
142
|
-
|
|
143
|
-
| Column | Description |
|
|
144
|
-
| ------------- | ------------- |
|
|
145
|
-
| aggregate_id | Stores the primary key of the entity. |
|
|
146
|
-
| idempotency_key | Optional value which can be used to write events that have uniqueness constraints. |
|
|
147
|
-
| type | Used by rails to implement Single Table inheritance. Stores the event class name. |
|
|
148
|
-
| data | Stores the event payload |
|
|
149
|
-
| metadata | Stores optional metadata associated with the event |
|
|
150
|
-
|
|
151
|
-
## Usage
|
|
152
|
-
|
|
153
|
-
An example event:
|
|
154
|
-
|
|
155
|
-
```ruby
|
|
156
|
-
module UserComponent
|
|
157
|
-
module Events
|
|
158
|
-
class Created < UserEvent
|
|
159
|
-
# Optional: Rails by default will use JSON serialization for the data attribute. Use Eventsimple::DataType to serialize/deserialize the data attribute using the Message subclass below which uses dry-struct.
|
|
160
|
-
attribute :data, Eventsimple::DataType.new(self)
|
|
161
|
-
|
|
162
|
-
class Message < Eventsimple::Message
|
|
163
|
-
attribute :canonical_id, DryTypes::Strict::String
|
|
164
|
-
attribute :email, DryTypes::Strict::String
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Optional: Context specific validations that can be extended onto the model on event creation.
|
|
168
|
-
validates_with UserForm
|
|
169
|
-
|
|
170
|
-
# Optional: Implement state machine checks to determine if the event is allowed to be written.
|
|
171
|
-
# Will raise Eventsimple::InvalidTransition on failure.
|
|
172
|
-
def can_apply?(user)
|
|
173
|
-
user.new_record?
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Optional: Update the state of your model based on data in the event payload.
|
|
177
|
-
def apply(user)
|
|
178
|
-
user.canonical_id = data.canonical_id
|
|
179
|
-
user.email = data.email
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Write an event:
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
user = User.new
|
|
190
|
-
|
|
191
|
-
UserComponent::Events::Created.create(
|
|
192
|
-
user: user,
|
|
193
|
-
data: { canonical_id: 'user-123', email: 'johndoe@example.com' },
|
|
194
|
-
metadata: { actor_id: 'user-123' } # optional metadata
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
if user.errors.any?
|
|
198
|
-
# render user errors
|
|
199
|
-
else
|
|
200
|
-
# render success
|
|
201
|
-
end
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### Using Dry::Struct
|
|
205
|
-
The Eventsimple::Message class is a subclass of Dry::Struct. Some common options you can use are:
|
|
206
|
-
|
|
207
|
-
```ruby
|
|
208
|
-
class Message < Eventsimple::Message
|
|
209
|
-
# attribute key is required and can not be nil
|
|
210
|
-
attribute :canonical_id, DryTypes::Strict::String
|
|
211
|
-
|
|
212
|
-
# attribute key is required but can be nil
|
|
213
|
-
attribute :required_key, DryTypes::Strict::String.optional
|
|
214
|
-
|
|
215
|
-
# attribute key is not required and can also be nil
|
|
216
|
-
attribute? :optional_key, DryTypes::Strict::String.optional
|
|
217
|
-
|
|
218
|
-
# use default value if attribute key is missing or if value is nil
|
|
219
|
-
# Note this is not the typical behaviour for dry-struct and is a customization in the Eventsimple::Message class.
|
|
220
|
-
attribute :default_key, DryTypes::Strict::String.default('default')
|
|
221
|
-
end
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### Event Reactors
|
|
225
|
-
|
|
226
|
-
Callback to events can be defined as reactors in the dispatcher class.
|
|
227
|
-
Reactors may be `async` or `sync`, depending on the usecase.
|
|
228
|
-
|
|
229
|
-
#### Sync Reactors
|
|
230
|
-
Sync reactors are executed within the context of the event transaction block.
|
|
231
|
-
They should **only** contain business logic that make additional database writes.
|
|
232
|
-
|
|
233
|
-
This is because executing writes to other data stores, e.g API call or writes to kafka/sqs, will result in the transaction being non-deterministic.
|
|
234
|
-
|
|
235
|
-
#### Async Reactors
|
|
236
|
-
Async reactors are executed via ActiveJob. Eventsimple implements checks to enforce reliable eventually consistent behaviour.
|
|
237
|
-
|
|
238
|
-
Use Async reactors to kick off async workflows or writes to external data sources as a side effect of model updates.
|
|
239
|
-
|
|
240
|
-
Reactor example:
|
|
241
|
-
|
|
242
|
-
```ruby
|
|
243
|
-
# Register your dispatch classes in config/initializers/eventsimple.rb.
|
|
244
|
-
Eventsimple.configure do |config|
|
|
245
|
-
config.dispatchers = %w[
|
|
246
|
-
UserComponent::Dispatcher
|
|
247
|
-
]
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
# Register reactors in the dispatcher class.
|
|
251
|
-
class UserComponent::Dispatcher < Eventsimple::EventDispatcher
|
|
252
|
-
# one to one
|
|
253
|
-
on UserComponent::Events::Created,
|
|
254
|
-
async: UserComponent::Reactors::Created::SendNotification
|
|
255
|
-
|
|
256
|
-
# or many to many
|
|
257
|
-
on [
|
|
258
|
-
UserComponent::Events::Locked,
|
|
259
|
-
UserComponent::Events::Unlocked
|
|
260
|
-
], sync: [
|
|
261
|
-
UserComponent::Reactors::Locking::UpdateLockCounter,
|
|
262
|
-
UserComponent::Reactors::Locking::UpdateLockMetrics
|
|
263
|
-
]
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Reactor classes accept the event as the only argument in the constructor
|
|
267
|
-
# and must define a `call` method
|
|
268
|
-
module UserComponent::Reactors::Created < Eventsimple::Reactor
|
|
269
|
-
class SendNotification
|
|
270
|
-
def call(event)
|
|
271
|
-
user = event.aggregate
|
|
272
|
-
# do something
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
## Configuring an outbox consumer
|
|
279
|
-
|
|
280
|
-
For many use cases, async reactors are sufficient to handle workflows like making an API call or publishing to a message broker. However as reactors use ActiveJob, order is not guaranteed. For use cases requiring order, eventsimple provides an simple ordered outbox implementation.
|
|
281
|
-
|
|
282
|
-
The current implementation leverages a single advisory lock to guarantee write order. This will impact write throughput on the model. On a db.rg6.large Aurora instance for example, write throughput to the table is ~300 events per second.
|
|
283
|
-
|
|
284
|
-
### Setup an ordered outbox
|
|
285
|
-
|
|
286
|
-
Generate migration to setup the outbox cursor table. This table is used to track cursor positions.
|
|
287
|
-
|
|
288
|
-
```ruby
|
|
289
|
-
bundle exec rails g eventsimple:outbox:install
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
Create a consummer and processor class for the outbox.
|
|
14
|
+
### Write events to update models
|
|
293
15
|
|
|
294
16
|
```ruby
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
extend Eventsimple::Outbox::Consumer
|
|
300
|
-
|
|
301
|
-
identitfier 'UserComponent::Consumer'
|
|
302
|
-
consumes_event UserEvent
|
|
303
|
-
processor EventProcessor, concurrency: 5
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
```ruby
|
|
309
|
-
module UserComponent
|
|
310
|
-
class EventProcessor
|
|
311
|
-
def call(event)
|
|
312
|
-
Rails.logger.info("PROCESSING EVENT: #{event.id}")
|
|
313
|
-
end
|
|
314
|
-
end
|
|
315
|
-
end
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
### Usage
|
|
319
|
-
Create a rake task to run the consumer
|
|
320
|
-
|
|
321
|
-
```ruby
|
|
322
|
-
namespace :consumers do
|
|
323
|
-
desc 'Starts the user event outbox consumer'
|
|
324
|
-
task :user_events do
|
|
325
|
-
UserComponent::Consumer.start
|
|
326
|
-
end
|
|
17
|
+
class UserComponent::Events::Created < UserEvent
|
|
18
|
+
class Message < Eventsimple::Message
|
|
19
|
+
attribute :name, Eventsimple::Types::String
|
|
20
|
+
attribute :email, Eventsimple::Types::String
|
|
327
21
|
end
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
To set the cursor position to the latest event:
|
|
331
|
-
|
|
332
|
-
```ruby
|
|
333
|
-
Eventsimple::Outbox::Cursor.set('UserComponent::Consumer', UserEvent.last.id)
|
|
334
|
-
```
|
|
335
22
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
**`#enable_writes!`**
|
|
340
|
-
Write access on entities is disabled by default outside of writes via events. Use this method to enable writes on an entity.
|
|
341
|
-
|
|
342
|
-
```ruby
|
|
343
|
-
user = User.find_by(canonical_id: 'user-123')
|
|
344
|
-
user.enable_writes! do
|
|
345
|
-
user.reproject
|
|
346
|
-
user.save!
|
|
23
|
+
def can_apply?(user)
|
|
24
|
+
user.new_record?
|
|
347
25
|
end
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
If you are using FactoryBot, you can add the following in your rails_helper.rb to enable writes on the entity:
|
|
351
|
-
```ruby
|
|
352
|
-
FactoryBot.define do
|
|
353
|
-
after(:build) { |model| model.enable_writes! if model.class.ancestors.include?(Eventsimple::Entity::InstanceMethods) }
|
|
354
|
-
end
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
**`#reproject(at: nil)`**
|
|
358
|
-
|
|
359
|
-
Reproject an entity from events (rebuilds in memory but does not persist the entity).
|
|
360
|
-
|
|
361
|
-
```ruby
|
|
362
|
-
module UserComponent
|
|
363
|
-
module Events
|
|
364
|
-
class Created < UserEvent
|
|
365
|
-
# ...
|
|
366
|
-
|
|
367
|
-
def apply(user)
|
|
368
|
-
user.email = data.email
|
|
369
26
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
end
|
|
27
|
+
def apply(user)
|
|
28
|
+
user.name = data.name
|
|
29
|
+
user.email = data.email
|
|
374
30
|
end
|
|
375
31
|
end
|
|
376
32
|
|
|
377
|
-
|
|
378
|
-
user.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
Or reproject the model to inspect what it looked like at a particular point in time.
|
|
384
|
-
```ruby
|
|
385
|
-
user = User.find_by(canonical_id: 'user-123')
|
|
386
|
-
user.reproject(at: 1.day.ago)
|
|
387
|
-
user.changes
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
**`#projection_matches_events?`**
|
|
391
|
-
|
|
392
|
-
Verify that a reprojection of the model matches it's current state.
|
|
393
|
-
|
|
394
|
-
```ruby
|
|
395
|
-
user = User.find_by(canonical_id: 'user-123')
|
|
396
|
-
user.update(name: 'something_else')
|
|
397
|
-
user.projection_matches_events? => false
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
**`.ignored_for_projection`**
|
|
401
|
-
|
|
402
|
-
Skip properties on a model that are not managed by the event driven system. This will prevent a reset of the value in case of a reprojection.
|
|
403
|
-
Useful if the model that is being event driven has some properties that are managed through other mechanics.
|
|
404
|
-
|
|
405
|
-
`id` and `lock_version` columns are always ignored by default.
|
|
406
|
-
|
|
407
|
-
```ruby
|
|
408
|
-
class User
|
|
409
|
-
self.ignored_for_projection = %i[last_sign_in_at]
|
|
410
|
-
end
|
|
33
|
+
UserComponent::Events::Created.create!(
|
|
34
|
+
user: User.new,
|
|
35
|
+
data: { name: "John doe", email: "johndoe@example.com" }
|
|
36
|
+
)
|
|
411
37
|
```
|
|
412
38
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
### I want to add validations to my model.
|
|
416
|
-
|
|
417
|
-
You _can_ add conditional validations to the model as usual. For example to verify an email:
|
|
39
|
+
### Execute side effects using Reactors
|
|
418
40
|
|
|
419
41
|
```ruby
|
|
420
|
-
class
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
with: EMAIL_REGEX
|
|
425
|
-
}, if: :email_changed?
|
|
426
|
-
|
|
427
|
-
validate :allowed_emails, if: :email_changed?
|
|
428
|
-
|
|
429
|
-
def allowed_emails
|
|
430
|
-
return if EmailBlacklist.allowed?(email)
|
|
431
|
-
|
|
432
|
-
errors.add(:email, :invalid, value: email)
|
|
433
|
-
end
|
|
42
|
+
class UserComponent::Dispatcher < Eventsimple::Dispatcher
|
|
43
|
+
on(
|
|
44
|
+
UserComponent::Events::Created, async: SendWelcomeEmail
|
|
45
|
+
)
|
|
434
46
|
end
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
However, conditional validations tend to become more complex over time. An alternative approach can be to validate at the point _when_ a handle is being updated.
|
|
438
|
-
|
|
439
|
-
Consider extending the model with a mixin, to apply the validation only when the email is actually being set.
|
|
440
47
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
base.class_eval do
|
|
445
|
-
EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
|
446
|
-
|
|
447
|
-
validates :email, presence: true, format: {
|
|
448
|
-
with: EMAIL_REGEX
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
validate :allowed_emails, if: :email_changed?
|
|
452
|
-
|
|
453
|
-
def allowed_emails
|
|
454
|
-
return if EmailBlacklist.allowed?(email)
|
|
455
|
-
|
|
456
|
-
errors.add(:email, :invalid, value: email)
|
|
457
|
-
end
|
|
48
|
+
class UserComponent::Reactors::SendWelcomeEmail < Eventsimple::Reactor
|
|
49
|
+
def call(event)
|
|
50
|
+
EmailService.send_welcome_email(event.aggregate)
|
|
458
51
|
end
|
|
459
52
|
end
|
|
460
|
-
|
|
461
|
-
user = User.find_by(canonical_id: 'user-123').extend(UpdateEmailForm)
|
|
462
|
-
|
|
463
|
-
UserComponent::Events::EmailUpdated.create(user: user, data: { email: 'email' })
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
You can configure mixins in the event class itself, so that they are applied automatically at the point of event creating. The following example will extend the user with UpdateEmailForm on user create:
|
|
467
|
-
|
|
468
|
-
```ruby
|
|
469
|
-
class UserComponent::Events::Created < UserEvent
|
|
470
|
-
...
|
|
471
|
-
|
|
472
|
-
validates_with UpdateEmailForm
|
|
473
|
-
|
|
474
|
-
...
|
|
475
|
-
end
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
### I want to modify an existing event by adding a new attribute
|
|
479
|
-
New attributes should always be added as being either optional or required with a default value.
|
|
480
|
-
|
|
481
|
-
```ruby
|
|
482
|
-
class UserComponent::Events::Created < Eventsimple::Message
|
|
483
|
-
attribute :new_attribute_1, DryTypes::Strict::String.default('default')
|
|
484
|
-
attribute? :new_attribute_2, DryTypes::Strict::String.optional
|
|
485
|
-
end
|
|
486
53
|
```
|
|
487
54
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
To ensure old models are also in a consistent state, a data migration may be required to update the new attribute to the new default.
|
|
491
|
-
|
|
492
|
-
```ruby
|
|
493
|
-
# migration file
|
|
494
|
-
add_column :users, :new_attribute_1, :string, default: 'new_default'
|
|
495
|
-
|
|
496
|
-
User.where(new_attribute_1: nil).find_in_batches do |batch|
|
|
497
|
-
batch.update_all(new_attribute_1: 'new_default')
|
|
498
|
-
end
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
### I want to modify an event by removing a unused attribute
|
|
502
|
-
Simply remove the attribute in code and any usage references. Any persisted data in old events will be ignored going forward, so a data migration is not explicitly needed.
|
|
503
|
-
|
|
504
|
-
However if this is something that is required, we can follow up code removal with a data migration like:
|
|
505
|
-
|
|
506
|
-
```ruby
|
|
507
|
-
UserEvent.where(type: 'MyEventName').in_batches do |batch|
|
|
508
|
-
batch.update_all("data = data::jsonb - 'old_attribute_1' - 'old_attribute_2'")
|
|
509
|
-
end
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
### I want to remove an event that is not longer required
|
|
513
|
-
* If an event and any properties it sets are no longer required, we can delete the Event, any code references and the model columns.
|
|
514
|
-
* The persisted events will be ignored going forward, so a data migration is not explicitly needed.
|
|
515
|
-
|
|
516
|
-
However if this is something that is required, we can follow up code removal with a data migration like:
|
|
517
|
-
|
|
518
|
-
```ruby
|
|
519
|
-
# Remove all code references and then run the following migration:
|
|
520
|
-
|
|
521
|
-
UserEvent.where(type: 'MyEventName').in_batches do |batch|
|
|
522
|
-
batch.delete_all
|
|
523
|
-
end
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
### I want to ignore InvalidTransition errors
|
|
527
|
-
|
|
528
|
-
The InvalidTransition error is raised when the `can_apply?` method of an Event returns `false`. In many cases this indicates a bug in the code, but in some cases it is expected behaviour.
|
|
529
|
-
|
|
530
|
-
An example scenario for not wanting to raise the error is when the `can_apply?` method is primarily defending against redundant events from being written, perhaps when consuming messages from a message broker.
|
|
531
|
-
|
|
532
|
-
You can mute these errors by calling `rescue_invalid_transition` on the event class. This will cause the event to be ignored and the model to remain unchanged. Optionally, you can pass a block to handle the error.
|
|
533
|
-
|
|
534
|
-
```ruby
|
|
535
|
-
module FooComponent
|
|
536
|
-
module Events
|
|
537
|
-
class BarToTrue < FooEvent
|
|
538
|
-
rescue_invalid_transition do |error|
|
|
539
|
-
logger.info("Receive invalid transition error", error)
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
def can_apply?(foo)
|
|
543
|
-
!foo.bar
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
def apply(foo)
|
|
547
|
-
foo.bar = true
|
|
548
|
-
|
|
549
|
-
foo
|
|
550
|
-
end
|
|
551
|
-
end
|
|
552
|
-
end
|
|
553
|
-
end
|
|
554
|
-
```
|
|
55
|
+
## Quick Start
|
|
555
56
|
|
|
556
|
-
|
|
557
|
-
|
|
57
|
+
- **[Home](Home)** - Installation, configuration, and getting started
|
|
58
|
+
- **[Usage-Events](Usage-Events)** - How to create and use events
|
|
59
|
+
- **[Usage-Reactors](Usage-Reactors)** - Handle side effects with sync and async reactors
|
|
60
|
+
- **[Encryption](Encryption)** - Encrypt sensitive data in event messages
|
|
61
|
+
- **[Outbox-Pattern](Outbox-Pattern)** - Implement ordered event processing
|
|
62
|
+
- **[Best-Practices](Best-Practices)** - Development guidelines and best practices
|
|
63
|
+
- **[Testing](Testing)** - Testing best practices for events and reactors
|
|
64
|
+
- **[Helper-Methods](Helper-Methods)** - Convenience methods for common tasks
|
|
65
|
+
- **[Data-Migrations](Data-Migrations)** - Migrating event data
|
|
66
|
+
- **[Existing-Models](Existing-Models)** - Adding Eventsimple to existing models
|
|
67
|
+
- **[Factory-Bot-Compatibility](Factory-Bot-Compatibility)** - Using Factory Bot with Eventsimple
|
data/Rakefile
CHANGED
|
@@ -4,9 +4,6 @@ require "bundler/setup"
|
|
|
4
4
|
|
|
5
5
|
APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
|
|
6
6
|
Rake.load_rakefile 'spec/dummy/Rakefile'
|
|
7
|
-
load 'rails/tasks/engine.rake'
|
|
8
|
-
|
|
9
|
-
load 'rails/tasks/statistics.rake'
|
|
10
7
|
|
|
11
8
|
require 'bundler/gem_tasks'
|
|
12
9
|
require 'rspec/core/rake_task'
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
<th scope="row" colspan="2">Data</th>
|
|
62
62
|
</tr>
|
|
63
63
|
<% if @selected_event.data.present? %>
|
|
64
|
-
<% @selected_event.data.
|
|
64
|
+
<% @selected_event.data.attributes.each do |attr_name, attr_value| %>
|
|
65
65
|
<tr>
|
|
66
66
|
<td> <%= attr_name %></td>
|
|
67
67
|
<td><code class="entity-property"><%= attr_value %></code></td>
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
<tr>
|
|
73
73
|
<th scope="row" colspan="2">Metadata</th>
|
|
74
74
|
</tr>
|
|
75
|
-
<% @selected_event.metadata.
|
|
75
|
+
<% @selected_event.metadata.attributes.each do |attr_name, attr_value| %>
|
|
76
76
|
<tr>
|
|
77
77
|
<td> <%= attr_name %></td>
|
|
78
78
|
<td><code class="entity-property">: <%= attr_value %></code></td>
|
data/eventsimple.gemspec
CHANGED
|
@@ -24,8 +24,7 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
spec.require_paths = ['lib']
|
|
25
25
|
|
|
26
26
|
spec.add_runtime_dependency 'concurrent-ruby', '>= 1.2.3'
|
|
27
|
-
spec.add_runtime_dependency 'dry-
|
|
28
|
-
spec.add_runtime_dependency 'dry-types', '~> 1.7'
|
|
27
|
+
spec.add_runtime_dependency 'dry-types', '>= 1.7.0'
|
|
29
28
|
spec.add_runtime_dependency 'pg', '~> 1.4'
|
|
30
29
|
spec.add_runtime_dependency 'rails', '>= 7.0', '< 9.0'
|
|
31
30
|
spec.add_runtime_dependency 'retriable', '~> 3.1'
|