rails_simple_event_sourcing 1.0.10 → 1.0.12
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 +96 -12
- data/app/models/concerns/rails_simple_event_sourcing/read_only.rb +3 -3
- data/app/models/rails_simple_event_sourcing/event.rb +5 -0
- data/db/migrate/20231231133250_create_rails_simple_event_sourcing_events.rb +4 -2
- data/lib/rails_simple_event_sourcing/aggregate_repository.rb +1 -1
- data/lib/rails_simple_event_sourcing/command_handlers/base.rb +12 -2
- data/lib/rails_simple_event_sourcing/engine.rb +1 -0
- data/lib/rails_simple_event_sourcing/event_bus.rb +31 -0
- data/lib/rails_simple_event_sourcing/paginator.rb +16 -18
- data/lib/rails_simple_event_sourcing/result.rb +4 -0
- data/lib/rails_simple_event_sourcing/version.rb +1 -1
- metadata +4 -4
- data/app/helpers/rails_simple_event_sourcing/events_helper.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d389be62e627ddf2443c6e7d781d1abbab7aaacf7b908741a753e4dbe370c978
|
|
4
|
+
data.tar.gz: 4dea2cb2546bd903a566edff8f0fb192be5b02ae1f45f36004cf58ba7d30a64a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e7671967605ab8714cdb39dec920b7fc1449bf4c4adedd77cf409fa89db7497e2f462c1176decb149d9191b01cdd973e88708de836c8f7f88bef98328246608
|
|
7
|
+
data.tar.gz: a3543ef2e0dd176bb85099be1b4d65e9cc0f148644eb5011435920325447114218f84fff9fa6a4721d462e35c2d1568f9ba4665648ff27b26fc7d3cdab550526
|
data/README.md
CHANGED
|
@@ -22,6 +22,7 @@ If you need a more comprehensive solution, check out:
|
|
|
22
22
|
- [Metadata Tracking](#metadata-tracking)
|
|
23
23
|
- [Event Querying](#event-querying)
|
|
24
24
|
- [Events Viewer](#events-viewer)
|
|
25
|
+
- [Event Subscriptions](#event-subscriptions)
|
|
25
26
|
- [Testing](#testing)
|
|
26
27
|
- [Limitations](#limitations)
|
|
27
28
|
- [Troubleshooting](#troubleshooting)
|
|
@@ -40,12 +41,13 @@ If you need a more comprehensive solution, check out:
|
|
|
40
41
|
- **Simple Command Pattern** - Clear command → handler → event flow
|
|
41
42
|
- **PostgreSQL JSONB Storage** - Efficient JSON storage for event payloads and metadata
|
|
42
43
|
- **Built-in Events Viewer** - Web UI for browsing, searching, and inspecting events
|
|
44
|
+
- **Event Subscriptions** - React to events after they are committed (send emails, send webhooks, etc.)
|
|
43
45
|
- **Minimal Configuration** - Convention over configuration approach
|
|
44
46
|
|
|
45
47
|
## Requirements
|
|
46
48
|
|
|
47
|
-
- **Ruby**: 2
|
|
48
|
-
- **Rails**:
|
|
49
|
+
- **Ruby**: 3.2 or higher
|
|
50
|
+
- **Rails**: 7.1.2 or higher
|
|
49
51
|
- **Database**: PostgreSQL 9.4+ (requires JSONB support)
|
|
50
52
|
|
|
51
53
|
## Installation
|
|
@@ -175,6 +177,7 @@ Handlers can be discovered in two ways:
|
|
|
175
177
|
**Result Object:**
|
|
176
178
|
The `Result` class has three fields:
|
|
177
179
|
- `success?` - Boolean indicating if the operation succeeded
|
|
180
|
+
- `failure?` - Boolean indicating if the operation failed (inverse of `success?`)
|
|
178
181
|
- `data` - Data to return (usually the aggregate/model instance)
|
|
179
182
|
- `errors` - Array or hash of error messages when `success?` is false
|
|
180
183
|
|
|
@@ -192,7 +195,7 @@ It supports a chainable API for use in controllers:
|
|
|
192
195
|
- `on_success { |data| }` - Executes the block (yielding `data`) if the result is successful
|
|
193
196
|
- `on_failure { |errors| }` - Executes the block (yielding `errors`) if the result failed
|
|
194
197
|
|
|
195
|
-
Both methods return `self`, so they can be chained. The
|
|
198
|
+
Both methods return `self`, so they can be chained. The predicates `success?` and `failure?` remain available for use in conditionals and tests.
|
|
196
199
|
|
|
197
200
|
```ruby
|
|
198
201
|
result = RailsSimpleEventSourcing::Result.success(data: customer)
|
|
@@ -203,9 +206,10 @@ result
|
|
|
203
206
|
.on_failure { |errors| render json: { errors: }, status: :unprocessable_entity }
|
|
204
207
|
|
|
205
208
|
# Predicate (preferred in tests)
|
|
206
|
-
result.success?
|
|
207
|
-
result.
|
|
208
|
-
result.
|
|
209
|
+
result.success? # => true
|
|
210
|
+
result.failure? # => false
|
|
211
|
+
result.data # => #<Customer ...>
|
|
212
|
+
result.errors # => nil
|
|
209
213
|
```
|
|
210
214
|
|
|
211
215
|
**Helper Methods in Handlers:**
|
|
@@ -221,9 +225,9 @@ class Customer
|
|
|
221
225
|
class Create < RailsSimpleEventSourcing::CommandHandlers::Base
|
|
222
226
|
def call
|
|
223
227
|
event = Customer::Events::CustomerCreated.create(
|
|
224
|
-
first_name:
|
|
225
|
-
last_name:
|
|
226
|
-
email:
|
|
228
|
+
first_name: command.first_name,
|
|
229
|
+
last_name: command.last_name,
|
|
230
|
+
email: command.email,
|
|
227
231
|
created_at: Time.zone.now,
|
|
228
232
|
updated_at: Time.zone.now
|
|
229
233
|
)
|
|
@@ -243,9 +247,9 @@ class Customer
|
|
|
243
247
|
class Create < RailsSimpleEventSourcing::CommandHandlers::Base
|
|
244
248
|
def call
|
|
245
249
|
event = Customer::Events::CustomerCreated.create(
|
|
246
|
-
first_name:
|
|
247
|
-
last_name:
|
|
248
|
-
email:
|
|
250
|
+
first_name: command.first_name,
|
|
251
|
+
last_name: command.last_name,
|
|
252
|
+
email: command.email,
|
|
249
253
|
created_at: Time.zone.now,
|
|
250
254
|
updated_at: Time.zone.now
|
|
251
255
|
)
|
|
@@ -667,6 +671,86 @@ class Customer::Events::CustomerDeleted < RailsSimpleEventSourcing::Event
|
|
|
667
671
|
end
|
|
668
672
|
```
|
|
669
673
|
|
|
674
|
+
### Event Subscriptions
|
|
675
|
+
|
|
676
|
+
The `EventBus` lets you react to events after they are persisted and committed to the database. Subscribers run **after the transaction commits**, so they never execute against data that could later be rolled back.
|
|
677
|
+
|
|
678
|
+
**Registering subscribers:**
|
|
679
|
+
|
|
680
|
+
```ruby
|
|
681
|
+
# config/initializers/rails_simple_event_sourcing.rb
|
|
682
|
+
Rails.application.config.after_initialize do
|
|
683
|
+
RailsSimpleEventSourcing::EventBus.subscribe(
|
|
684
|
+
Customer::Events::CustomerCreated,
|
|
685
|
+
Subscribers::SendWelcomeEmail
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
RailsSimpleEventSourcing::EventBus.subscribe(
|
|
689
|
+
Customer::Events::CustomerCreated,
|
|
690
|
+
Subscribers::CreateStripeCustomer
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
RailsSimpleEventSourcing::EventBus.subscribe(
|
|
694
|
+
Customer::Events::CustomerDeleted,
|
|
695
|
+
Subscribers::CancelStripeSubscription
|
|
696
|
+
)
|
|
697
|
+
end
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
**Writing a subscriber:**
|
|
701
|
+
|
|
702
|
+
Any object that responds to `call(event)` works — a class with `.call`, a lambda, or a proc:
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
module Subscribers
|
|
706
|
+
class SendWelcomeEmail
|
|
707
|
+
def self.call(event)
|
|
708
|
+
WelcomeMailer.with(email: event.email).deliver_later
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
**Subscribing to all events:**
|
|
715
|
+
|
|
716
|
+
Subscribe to `RailsSimpleEventSourcing::Event` to receive every event regardless of type — useful for audit loggers or metrics:
|
|
717
|
+
|
|
718
|
+
```ruby
|
|
719
|
+
RailsSimpleEventSourcing::EventBus.subscribe(
|
|
720
|
+
RailsSimpleEventSourcing::Event,
|
|
721
|
+
Subscribers::AuditLogger
|
|
722
|
+
)
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
If you subscribe the same callable to both a specific event class and `RailsSimpleEventSourcing::Event`, it will be called twice — once for each subscription. This is intentional and consistent with standard pub/sub behaviour.
|
|
726
|
+
|
|
727
|
+
**Testing with EventBus:**
|
|
728
|
+
|
|
729
|
+
Call `RailsSimpleEventSourcing::EventBus.reset!` in your test `setup` to clear all subscriptions between tests:
|
|
730
|
+
|
|
731
|
+
```ruby
|
|
732
|
+
class MyTest < ActiveSupport::TestCase
|
|
733
|
+
setup do
|
|
734
|
+
RailsSimpleEventSourcing::EventBus.reset!
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
test "sends welcome email on customer created" do
|
|
738
|
+
emails = []
|
|
739
|
+
RailsSimpleEventSourcing::EventBus.subscribe(
|
|
740
|
+
Customer::Events::CustomerCreated,
|
|
741
|
+
->(event) { emails << event.email }
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
Customer::Events::CustomerCreated.create!(
|
|
745
|
+
first_name: "John", last_name: "Doe", email: "john@example.com",
|
|
746
|
+
created_at: Time.zone.now, updated_at: Time.zone.now
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
assert_includes emails, "john@example.com"
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
```
|
|
753
|
+
|
|
670
754
|
## Testing
|
|
671
755
|
|
|
672
756
|
### Setting Up Tests with Command Handler Registry
|
|
@@ -6,7 +6,7 @@ module RailsSimpleEventSourcing
|
|
|
6
6
|
|
|
7
7
|
included do
|
|
8
8
|
def readonly?
|
|
9
|
-
super || !write_access_enabled
|
|
9
|
+
super || !write_access_enabled?
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def enable_write_access!
|
|
@@ -19,8 +19,8 @@ module RailsSimpleEventSourcing
|
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
|
-
def write_access_enabled
|
|
23
|
-
@write_access_enabled
|
|
22
|
+
def write_access_enabled?
|
|
23
|
+
@write_access_enabled == true
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
end
|
|
@@ -20,6 +20,7 @@ module RailsSimpleEventSourcing
|
|
|
20
20
|
before_validation :setup_for_create, on: :create
|
|
21
21
|
before_save :persist_aggregate, if: :aggregate_defined?
|
|
22
22
|
after_create :disable_write_access!
|
|
23
|
+
after_commit :dispatch_to_event_bus, on: :create
|
|
23
24
|
|
|
24
25
|
def apply(aggregate)
|
|
25
26
|
payload.each do |key, value|
|
|
@@ -81,5 +82,9 @@ module RailsSimpleEventSourcing
|
|
|
81
82
|
def aggregate_repository
|
|
82
83
|
@aggregate_repository ||= AggregateRepository.new(aggregate_class)
|
|
83
84
|
end
|
|
85
|
+
|
|
86
|
+
def dispatch_to_event_bus
|
|
87
|
+
EventBus.dispatch(self)
|
|
88
|
+
end
|
|
84
89
|
end
|
|
85
90
|
end
|
|
@@ -7,7 +7,7 @@ class CreateRailsSimpleEventSourcingEvents < ActiveRecord::Migration[7.1]
|
|
|
7
7
|
t.string :type, null: false
|
|
8
8
|
t.string :event_type, null: false
|
|
9
9
|
t.string :aggregate_id
|
|
10
|
-
t.
|
|
10
|
+
t.bigint :version
|
|
11
11
|
t.jsonb :payload
|
|
12
12
|
t.jsonb :metadata
|
|
13
13
|
|
|
@@ -15,7 +15,9 @@ class CreateRailsSimpleEventSourcingEvents < ActiveRecord::Migration[7.1]
|
|
|
15
15
|
|
|
16
16
|
t.index :type
|
|
17
17
|
t.index :event_type
|
|
18
|
-
t.index %i[aggregate_id version],
|
|
18
|
+
t.index %i[eventable_type aggregate_id version],
|
|
19
|
+
unique: true,
|
|
20
|
+
name: 'index_events_on_eventable_type_and_aggregate_id_and_version'
|
|
19
21
|
t.index :payload, using: :gin
|
|
20
22
|
t.index :metadata, using: :gin
|
|
21
23
|
end
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
module RailsSimpleEventSourcing
|
|
4
4
|
module CommandHandlers
|
|
5
5
|
class Base
|
|
6
|
-
delegate :success, :failure, to: 'RailsSimpleEventSourcing::Result'
|
|
7
|
-
|
|
8
6
|
def initialize(command:)
|
|
9
7
|
@command = command
|
|
10
8
|
end
|
|
@@ -12,6 +10,18 @@ module RailsSimpleEventSourcing
|
|
|
12
10
|
def call
|
|
13
11
|
raise NotImplementedError, "You must implement #{self.class}#call"
|
|
14
12
|
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
attr_reader :command
|
|
17
|
+
|
|
18
|
+
def success(data: nil)
|
|
19
|
+
Result.success(data:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def failure(errors:)
|
|
23
|
+
Result.failure(errors:)
|
|
24
|
+
end
|
|
15
25
|
end
|
|
16
26
|
end
|
|
17
27
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsSimpleEventSourcing
|
|
4
|
+
class EventBus
|
|
5
|
+
@subscriptions = Concurrent::Map.new { |h, k| h[k] = Concurrent::Array.new }
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def subscribe(event_class, subscriber)
|
|
9
|
+
@subscriptions[event_class.to_s] << subscriber
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def dispatch(event)
|
|
13
|
+
ancestors_with_subscriptions(event).each do |subscriber|
|
|
14
|
+
subscriber.call(event)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def reset!
|
|
19
|
+
@subscriptions = Concurrent::Map.new { |h, k| h[k] = Concurrent::Array.new }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def ancestors_with_subscriptions(event)
|
|
25
|
+
event.class.ancestors
|
|
26
|
+
.select { |ancestor| @subscriptions.key?(ancestor.to_s) }
|
|
27
|
+
.flat_map { |ancestor| @subscriptions[ancestor.to_s] }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -37,33 +37,31 @@ module RailsSimpleEventSourcing
|
|
|
37
37
|
|
|
38
38
|
def fetch_records
|
|
39
39
|
if @cursor.nil?
|
|
40
|
-
|
|
40
|
+
fetch_forward(@scope.order(id: :desc), has_prev: false)
|
|
41
41
|
elsif @direction == :prev
|
|
42
|
-
|
|
42
|
+
fetch_backward(@scope.where('id > ?', @cursor).order(id: :asc))
|
|
43
43
|
else
|
|
44
|
-
|
|
44
|
+
fetch_forward(@scope.where(id: ...@cursor).order(id: :desc), has_prev: true)
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def
|
|
49
|
-
rows =
|
|
50
|
-
@has_prev =
|
|
51
|
-
@has_next =
|
|
52
|
-
rows
|
|
48
|
+
def fetch_forward(scoped_query, has_prev:)
|
|
49
|
+
rows, has_more = paginate(scoped_query)
|
|
50
|
+
@has_prev = has_prev
|
|
51
|
+
@has_next = has_more
|
|
52
|
+
rows
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
def
|
|
56
|
-
rows =
|
|
57
|
-
@
|
|
58
|
-
@
|
|
59
|
-
rows.
|
|
55
|
+
def fetch_backward(scoped_query)
|
|
56
|
+
rows, has_more = paginate(scoped_query)
|
|
57
|
+
@has_next = true
|
|
58
|
+
@has_prev = has_more
|
|
59
|
+
rows.reverse
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
def
|
|
63
|
-
rows =
|
|
64
|
-
@
|
|
65
|
-
@has_prev = rows.size > @per_page
|
|
66
|
-
rows.first(@per_page).reverse
|
|
62
|
+
def paginate(scoped_query)
|
|
63
|
+
rows = scoped_query.limit(@per_page + 1).to_a
|
|
64
|
+
[rows.first(@per_page), rows.size > @per_page]
|
|
67
65
|
end
|
|
68
66
|
end
|
|
69
67
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_simple_event_sourcing
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Damian Baćkowski
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: pg
|
|
@@ -94,7 +94,6 @@ files:
|
|
|
94
94
|
- app/controllers/concerns/rails_simple_event_sourcing/set_current_request_details.rb
|
|
95
95
|
- app/controllers/rails_simple_event_sourcing/application_controller.rb
|
|
96
96
|
- app/controllers/rails_simple_event_sourcing/events_controller.rb
|
|
97
|
-
- app/helpers/rails_simple_event_sourcing/events_helper.rb
|
|
98
97
|
- app/models/concerns/rails_simple_event_sourcing/aggregate_configuration.rb
|
|
99
98
|
- app/models/concerns/rails_simple_event_sourcing/event_attributes.rb
|
|
100
99
|
- app/models/concerns/rails_simple_event_sourcing/events.rb
|
|
@@ -116,6 +115,7 @@ files:
|
|
|
116
115
|
- lib/rails_simple_event_sourcing/commands/base.rb
|
|
117
116
|
- lib/rails_simple_event_sourcing/configuration.rb
|
|
118
117
|
- lib/rails_simple_event_sourcing/engine.rb
|
|
118
|
+
- lib/rails_simple_event_sourcing/event_bus.rb
|
|
119
119
|
- lib/rails_simple_event_sourcing/event_player.rb
|
|
120
120
|
- lib/rails_simple_event_sourcing/event_search.rb
|
|
121
121
|
- lib/rails_simple_event_sourcing/paginator.rb
|
|
@@ -135,7 +135,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
135
135
|
requirements:
|
|
136
136
|
- - ">="
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: '
|
|
138
|
+
version: '3.2'
|
|
139
139
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
140
140
|
requirements:
|
|
141
141
|
- - ">="
|