rails_simple_event_sourcing 1.0.7 → 1.0.9
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 +53 -40
- data/app/models/concerns/rails_simple_event_sourcing/read_only.rb +4 -0
- data/app/models/rails_simple_event_sourcing/event.rb +5 -1
- data/config/routes.rb +2 -2
- data/lib/rails_simple_event_sourcing/command_handler.rb +1 -1
- data/lib/rails_simple_event_sourcing/command_handlers/base.rb +3 -9
- data/lib/rails_simple_event_sourcing/commands/base.rb +0 -1
- data/lib/rails_simple_event_sourcing/engine.rb +1 -0
- data/lib/rails_simple_event_sourcing/result.rb +38 -1
- data/lib/rails_simple_event_sourcing/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2cdc3924ccf5cb03de4c576e06e36516187ab5743fdeacb7c4b543e1733af6ed
|
|
4
|
+
data.tar.gz: e8a6bcce2a7b918a9a55ffd37dcf11e31c4692c53678ae083a59213fdd39e29e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2825950884885d9366e2586ddada26b272fc9551ba92caefe9be08c78569ad5df7243bef7b6fcf51f3e6529fc93dba1b455aacb7e7ed61b94ce9c40bd9ac0868
|
|
7
|
+
data.tar.gz: b8b2ec4ceca8ace0ec9f51185ac373008c73a11c0aa536d650d844464822c2e4bf6aa919d797aff36807ceca23f366ebbadaf94dcd78230b0ace007d0efd33c6
|
data/README.md
CHANGED
|
@@ -173,15 +173,45 @@ Handlers can be discovered in two ways:
|
|
|
173
173
|
2. **Convention-based** - Using naming convention mapping (can be disabled via configuration)
|
|
174
174
|
|
|
175
175
|
**Result Object:**
|
|
176
|
-
The `Result`
|
|
176
|
+
The `Result` class has three fields:
|
|
177
177
|
- `success?` - Boolean indicating if the operation succeeded
|
|
178
178
|
- `data` - Data to return (usually the aggregate/model instance)
|
|
179
179
|
- `errors` - Array or hash of error messages when `success?` is false
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
Use the factory methods to build results:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
# Successful result
|
|
185
|
+
RailsSimpleEventSourcing::Result.success(data: customer)
|
|
186
|
+
|
|
187
|
+
# Failed result
|
|
188
|
+
RailsSimpleEventSourcing::Result.failure(errors: { email: ["already taken"] })
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
It supports a chainable API for use in controllers:
|
|
192
|
+
- `on_success { |data| }` - Executes the block (yielding `data`) if the result is successful
|
|
193
|
+
- `on_failure { |errors| }` - Executes the block (yielding `errors`) if the result failed
|
|
194
|
+
|
|
195
|
+
Both methods return `self`, so they can be chained. The predicate `success?` remains available for use in conditionals and tests.
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
result = RailsSimpleEventSourcing::Result.success(data: customer)
|
|
199
|
+
|
|
200
|
+
# Chainable (preferred in controllers)
|
|
201
|
+
result
|
|
202
|
+
.on_success { |data| render json: data }
|
|
203
|
+
.on_failure { |errors| render json: { errors: }, status: :unprocessable_entity }
|
|
204
|
+
|
|
205
|
+
# Predicate (preferred in tests)
|
|
206
|
+
result.success? # => true
|
|
207
|
+
result.data # => #<Customer ...>
|
|
208
|
+
result.errors # => nil
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Helper Methods in Handlers:**
|
|
212
|
+
Inside a command handler, `success` and `failure` are delegated to `RailsSimpleEventSourcing::Result`:
|
|
213
|
+
- `success(data:)` - delegates to `RailsSimpleEventSourcing::Result.success`
|
|
214
|
+
- `failure(errors:)` - delegates to `RailsSimpleEventSourcing::Result.failure`
|
|
185
215
|
|
|
186
216
|
**Example - Basic Handler:**
|
|
187
217
|
|
|
@@ -198,11 +228,7 @@ class Customer
|
|
|
198
228
|
updated_at: Time.zone.now
|
|
199
229
|
)
|
|
200
230
|
|
|
201
|
-
|
|
202
|
-
success_result(data: event.aggregate)
|
|
203
|
-
|
|
204
|
-
# Or create Result directly
|
|
205
|
-
# RailsSimpleEventSourcing::Result.new(success?: true, data: event.aggregate)
|
|
231
|
+
success(data: event.aggregate)
|
|
206
232
|
end
|
|
207
233
|
end
|
|
208
234
|
end
|
|
@@ -224,11 +250,11 @@ class Customer
|
|
|
224
250
|
updated_at: Time.zone.now
|
|
225
251
|
)
|
|
226
252
|
|
|
227
|
-
|
|
253
|
+
success(data: event.aggregate)
|
|
228
254
|
rescue ActiveRecord::RecordNotUnique
|
|
229
|
-
|
|
255
|
+
failure(errors: ["Email has already been taken"])
|
|
230
256
|
rescue StandardError => e
|
|
231
|
-
|
|
257
|
+
failure(errors: ["An error occurred: #{e.message}"])
|
|
232
258
|
end
|
|
233
259
|
end
|
|
234
260
|
end
|
|
@@ -358,13 +384,10 @@ class CustomersController < ApplicationController
|
|
|
358
384
|
last_name: params[:last_name],
|
|
359
385
|
email: params[:email]
|
|
360
386
|
)
|
|
361
|
-
handler = RailsSimpleEventSourcing::CommandHandler.new(cmd).call
|
|
362
387
|
|
|
363
|
-
|
|
364
|
-
render json:
|
|
365
|
-
|
|
366
|
-
render json: { errors: handler.errors }, status: :unprocessable_entity
|
|
367
|
-
end
|
|
388
|
+
RailsSimpleEventSourcing::CommandHandler.new(cmd).call
|
|
389
|
+
.on_success { |data| render json: data }
|
|
390
|
+
.on_failure { |errors| render json: { errors: }, status: :unprocessable_entity }
|
|
368
391
|
end
|
|
369
392
|
end
|
|
370
393
|
```
|
|
@@ -382,13 +405,10 @@ class CustomersController < ApplicationController
|
|
|
382
405
|
last_name: params[:last_name],
|
|
383
406
|
email: params[:email]
|
|
384
407
|
)
|
|
385
|
-
handler = RailsSimpleEventSourcing::CommandHandler.new(cmd).call
|
|
386
408
|
|
|
387
|
-
|
|
388
|
-
render json:
|
|
389
|
-
|
|
390
|
-
render json: { errors: handler.errors }, status: :unprocessable_entity
|
|
391
|
-
end
|
|
409
|
+
RailsSimpleEventSourcing::CommandHandler.new(cmd).call
|
|
410
|
+
.on_success { |data| render json: data }
|
|
411
|
+
.on_failure { |errors| render json: { errors: }, status: :unprocessable_entity }
|
|
392
412
|
end
|
|
393
413
|
end
|
|
394
414
|
```
|
|
@@ -399,13 +419,10 @@ end
|
|
|
399
419
|
class CustomersController < ApplicationController
|
|
400
420
|
def destroy
|
|
401
421
|
cmd = Customer::Commands::Delete.new(aggregate_id: params[:id])
|
|
402
|
-
handler = RailsSimpleEventSourcing::CommandHandler.new(cmd).call
|
|
403
422
|
|
|
404
|
-
|
|
405
|
-
head :no_content
|
|
406
|
-
|
|
407
|
-
render json: { errors: handler.errors }, status: :unprocessable_entity
|
|
408
|
-
end
|
|
423
|
+
RailsSimpleEventSourcing::CommandHandler.new(cmd).call
|
|
424
|
+
.on_success { head :no_content }
|
|
425
|
+
.on_failure { |errors| render json: { errors: }, status: :unprocessable_entity }
|
|
409
426
|
end
|
|
410
427
|
end
|
|
411
428
|
```
|
|
@@ -548,11 +565,11 @@ Mount the engine in your `config/routes.rb`:
|
|
|
548
565
|
|
|
549
566
|
```ruby
|
|
550
567
|
Rails.application.routes.draw do
|
|
551
|
-
mount RailsSimpleEventSourcing::Engine, at: "/
|
|
568
|
+
mount RailsSimpleEventSourcing::Engine, at: "/events"
|
|
552
569
|
end
|
|
553
570
|
```
|
|
554
571
|
|
|
555
|
-
Then navigate to `/
|
|
572
|
+
Then navigate to `/events` in your browser to access the viewer.
|
|
556
573
|
|
|
557
574
|
**Password Protection:**
|
|
558
575
|
|
|
@@ -564,11 +581,11 @@ In production you will likely want to restrict access to the events viewer.
|
|
|
564
581
|
Rails.application.routes.draw do
|
|
565
582
|
mount Rack::Auth::Basic.new(
|
|
566
583
|
RailsSimpleEventSourcing::Engine,
|
|
567
|
-
"
|
|
584
|
+
"Events"
|
|
568
585
|
) { |username, password|
|
|
569
586
|
ActiveSupport::SecurityUtils.secure_compare(username, "admin") &
|
|
570
587
|
ActiveSupport::SecurityUtils.secure_compare(password, Rails.application.credentials.event_sourcing_password || "secret")
|
|
571
|
-
}, at: "/
|
|
588
|
+
}, at: "/events"
|
|
572
589
|
end
|
|
573
590
|
```
|
|
574
591
|
|
|
@@ -577,7 +594,7 @@ end
|
|
|
577
594
|
```ruby
|
|
578
595
|
Rails.application.routes.draw do
|
|
579
596
|
authenticate :user, ->(user) { user.admin? } do
|
|
580
|
-
mount RailsSimpleEventSourcing::Engine, at: "/
|
|
597
|
+
mount RailsSimpleEventSourcing::Engine, at: "/events"
|
|
581
598
|
end
|
|
582
599
|
end
|
|
583
600
|
```
|
|
@@ -639,10 +656,6 @@ class Customer < ApplicationRecord
|
|
|
639
656
|
|
|
640
657
|
scope :active, -> { where(deleted_at: nil) }
|
|
641
658
|
scope :deleted, -> { where.not(deleted_at: nil) }
|
|
642
|
-
|
|
643
|
-
def soft_delete
|
|
644
|
-
update(deleted_at: Time.current)
|
|
645
|
-
end
|
|
646
659
|
end
|
|
647
660
|
|
|
648
661
|
# Event
|
|
@@ -14,10 +14,13 @@ module RailsSimpleEventSourcing
|
|
|
14
14
|
|
|
15
15
|
before_validation :setup_for_create, on: :create
|
|
16
16
|
before_save :persist_aggregate, if: :aggregate_defined?
|
|
17
|
+
after_create :disable_write_access!
|
|
17
18
|
|
|
18
19
|
def apply(aggregate)
|
|
19
20
|
payload.each do |key, value|
|
|
20
|
-
|
|
21
|
+
raise "Unknown attribute '#{key}' on #{aggregate.class}" unless aggregate.respond_to?("#{key}=")
|
|
22
|
+
|
|
23
|
+
aggregate.send("#{key}=", value)
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
|
|
@@ -67,6 +70,7 @@ module RailsSimpleEventSourcing
|
|
|
67
70
|
|
|
68
71
|
aggregate_repository.save!(@aggregate)
|
|
69
72
|
self.aggregate_id = @aggregate.id
|
|
73
|
+
@aggregate.disable_write_access!
|
|
70
74
|
end
|
|
71
75
|
|
|
72
76
|
def aggregate_repository
|
data/config/routes.rb
CHANGED
|
@@ -3,20 +3,14 @@
|
|
|
3
3
|
module RailsSimpleEventSourcing
|
|
4
4
|
module CommandHandlers
|
|
5
5
|
class Base
|
|
6
|
+
delegate :success, :failure, to: 'RailsSimpleEventSourcing::Result'
|
|
7
|
+
|
|
6
8
|
def initialize(command:)
|
|
7
9
|
@command = command
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def call
|
|
11
|
-
raise
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def success_result(data: nil)
|
|
15
|
-
RailsSimpleEventSourcing::Result.new(success?: true, data:)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def failure_result(errors:)
|
|
19
|
-
RailsSimpleEventSourcing::Result.new(success?: false, errors:)
|
|
13
|
+
raise NotImplementedError, "You must implement #{self.class}#call"
|
|
20
14
|
end
|
|
21
15
|
end
|
|
22
16
|
end
|
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RailsSimpleEventSourcing
|
|
4
|
-
Result
|
|
4
|
+
class Result
|
|
5
|
+
attr_reader :data, :errors
|
|
6
|
+
|
|
7
|
+
def self.success(data: nil)
|
|
8
|
+
new(success: true, data:)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.failure(errors:)
|
|
12
|
+
new(success: false, errors:)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(success:, data: nil, errors: nil)
|
|
16
|
+
raise ArgumentError, 'Successful result cannot have errors' if success && errors
|
|
17
|
+
raise ArgumentError, 'Failed result cannot have data' if !success && data
|
|
18
|
+
|
|
19
|
+
@success = success
|
|
20
|
+
@data = data
|
|
21
|
+
@errors = errors
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def success?
|
|
25
|
+
@success
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def on_success(&block)
|
|
29
|
+
raise ArgumentError, 'Block required' unless block
|
|
30
|
+
|
|
31
|
+
block.call(data) if success?
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def on_failure(&block)
|
|
36
|
+
raise ArgumentError, 'Block required' unless block
|
|
37
|
+
|
|
38
|
+
block.call(errors) unless success?
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
end
|
|
5
42
|
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.9
|
|
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-
|
|
11
|
+
date: 2026-03-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: pg
|
|
@@ -38,6 +38,20 @@ dependencies:
|
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: 7.1.2
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: concurrent-ruby
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.1'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.1'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: rubocop
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|