rails_simple_event_sourcing 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +268 -0
- data/Rakefile +8 -0
- data/app/assets/config/rails_simple_event_sourcing_manifest.js +0 -0
- data/app/controllers/concerns/rails_simple_event_sourcing/set_current_request_details.rb +29 -0
- data/app/models/concerns/rails_simple_event_sourcing/events.rb +12 -0
- data/app/models/concerns/rails_simple_event_sourcing/read_only.rb +17 -0
- data/app/models/rails_simple_event_sourcing/current_request.rb +7 -0
- data/app/models/rails_simple_event_sourcing/event.rb +86 -0
- data/app/models/rails_simple_event_sourcing.rb +7 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20231231133250_create_rails_simple_event_sourcing_events.rb +22 -0
- data/lib/rails_simple_event_sourcing/apply_with_returning_aggregate.rb +9 -0
- data/lib/rails_simple_event_sourcing/command_handler.rb +21 -0
- data/lib/rails_simple_event_sourcing/command_handlers/base.rb +15 -0
- data/lib/rails_simple_event_sourcing/commands/base.rb +12 -0
- data/lib/rails_simple_event_sourcing/engine.rb +13 -0
- data/lib/rails_simple_event_sourcing/result.rb +5 -0
- data/lib/rails_simple_event_sourcing/version.rb +5 -0
- data/lib/rails_simple_event_sourcing.rb +8 -0
- data/lib/tasks/rails_simple_event_sourcing_tasks.rake +6 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9789524ed9ede4a4a16561d587fbfc729fe825d0e4196390b6bd39d4196ed56d
|
4
|
+
data.tar.gz: 5bbc7f33e45dd59ce805b06cea72a84a4a8f63c852fc859270eef9f2b8d11cb7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4112017f8f52f621b401dc463be3673f635176bfcee2732a7a0502757d4472a2897328169a68ddea60e4e8ec9726068d45daa8f1f4d70e6b471d42efffbd0da7
|
7
|
+
data.tar.gz: cbe47f8933ff692665a019fce022e1f52bdc849067fb7ae10bbebef02a9fb3c5eb6222d28d3ce5205249e37466a6733544de7dbd388f4aae43fc4a1219cb96c2
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Damian Baćkowski
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
# RailsSimpleEventSourcing ![tests](https://github.com/dbackowski/rails_simple_event_sourcing/actions/workflows/minitest.yml/badge.svg) ![codecheck](https://github.com/dbackowski/rails_simple_event_sourcing/actions/workflows/codecheck.yml/badge.svg)
|
2
|
+
|
3
|
+
This is a very minimalist implementation of an event sourcing pattern, if you want a full-featured framework in ruby you can check out one of these:
|
4
|
+
- https://www.sequent.io
|
5
|
+
- https://railseventstore.org
|
6
|
+
|
7
|
+
I wanted to learn how to build this from scratch and also wanted to build something that would be very easy to use since most of the fully featured frameworks like the two above require a lot of configuration and learning.
|
8
|
+
|
9
|
+
### Important notice
|
10
|
+
|
11
|
+
This plugin will only work with Postgres database because it uses JSONB data type which is only supported by this database.
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
So how does it all work?
|
16
|
+
|
17
|
+
Let's start with the directory structure:
|
18
|
+
|
19
|
+
```
|
20
|
+
app/
|
21
|
+
├─ domain/
|
22
|
+
│ ├─ customer/
|
23
|
+
│ │ ├─ command_handlers/
|
24
|
+
│ │ │ ├─ create.rb
|
25
|
+
│ │ ├─ events/
|
26
|
+
│ │ │ ├─ customer_created.rb
|
27
|
+
│ │ ├─ commands/
|
28
|
+
│ │ │ ├─ create.rb
|
29
|
+
```
|
30
|
+
|
31
|
+
The name of the top directory can be different because Rails does not namespace it.
|
32
|
+
|
33
|
+
Based on the example above, the usage looks like this
|
34
|
+
|
35
|
+
Command -> Command Handler -> Create Event (which under the hood writes changes to the appropriate model)
|
36
|
+
|
37
|
+
Explanation of each of these blocks above:
|
38
|
+
|
39
|
+
- `Command` - is responsible for any action you want to take in your system, it is also responsible for validating the input parameters it takes (you can use the same validations you would normally use in models).
|
40
|
+
|
41
|
+
Example:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class Customer
|
45
|
+
module Commands
|
46
|
+
class Create < RailsSimpleEventSourcing::Commands::Base
|
47
|
+
attr_accessor :first_name, :last_name, :email
|
48
|
+
|
49
|
+
validates :first_name, presence: true
|
50
|
+
validates :last_name, presence: true
|
51
|
+
validates :email, presence: true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
- `CommandHandler` - is responsible for handling the passed command (it automatically checks if a command is valid), making additional API calls, doing additional business logic, and finally creating a proper event. This should always return the `RailsSimpleEventSourcing::Result` struct.
|
58
|
+
|
59
|
+
This struct has 3 keywords:
|
60
|
+
- `success?:` true/false (if everything went well, commands are automatically validated, but there might still be an API call here, etc., so you can return false if something went wrong)
|
61
|
+
- `data:` data that you want to return, eg. to the controller (in the example above the `event.aggregate` will return a proper instance of the Customer model)
|
62
|
+
- `errors:` in a scenario where you set `success?:false`, you can also return some related errors here (see: test/dummy/app/domain/customer/command_handlers/create.rb for an example)
|
63
|
+
|
64
|
+
Example:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
class Customer
|
68
|
+
module CommandHandlers
|
69
|
+
class Create < RailsSimpleEventSourcing::CommandHandlers::Base
|
70
|
+
def call
|
71
|
+
event = Customer::Events::CustomerCreated.create(
|
72
|
+
first_name: @command.first_name,
|
73
|
+
last_name: @command.last_name,
|
74
|
+
email: @command.email,
|
75
|
+
created_at: Time.zone.now,
|
76
|
+
updated_at: Time.zone.now
|
77
|
+
)
|
78
|
+
|
79
|
+
RailsSimpleEventSourcing::Result.new(success?: true, data: event.aggregate)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
- `Event` - is responsible for storing immutable data of your actions, you should use past tense for naming events since an event is something that has already happened (e.g. customer was created)
|
87
|
+
|
88
|
+
Example:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class Customer
|
92
|
+
module Events
|
93
|
+
class CustomerCreated < RailsSimpleEventSourcing::Event
|
94
|
+
aggregate_model_name Customer
|
95
|
+
event_attributes :first_name, :last_name, :email, :created_at, :updated_at
|
96
|
+
|
97
|
+
def apply(aggregate)
|
98
|
+
aggregate.id = aggregate_id
|
99
|
+
aggregate.first_name = first_name
|
100
|
+
aggregate.last_name = last_name
|
101
|
+
aggregate.email = email
|
102
|
+
aggregate.created_at = created_at
|
103
|
+
aggregate.updated_at = updated_at
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
In the example above:
|
111
|
+
- `aggregate_model_name` is used for the corresponding model (each model is normally set to read-only mode since the only way to modify it should be via events), this param is optional since you can have an event that is not applied to the model, e.g. UserLoginAlreadyTaken
|
112
|
+
- `event_attributes` - defines params that will be stored in the event and these params will be available to apply to the model via the `apply(aggregate)` method (where aggregate is an instance of your model passed in aggregate_model_name).
|
113
|
+
|
114
|
+
Here is an example of a custom controller that uses all the blocks described above:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class CustomersController < ApplicationController
|
118
|
+
def create
|
119
|
+
cmd = Customer::Commands::Create.new(
|
120
|
+
first_name: params[:first_name],
|
121
|
+
last_name: params[:last_name],
|
122
|
+
email: params[:email]
|
123
|
+
)
|
124
|
+
handler = RailsSimpleEventSourcing::CommandHandler.new(cmd).call
|
125
|
+
|
126
|
+
if handler.success?
|
127
|
+
render json: handler.data
|
128
|
+
else
|
129
|
+
render json: { errors: handler.errors }, status: :unprocessable_entity
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
Now, if you make an API call using curl:
|
136
|
+
|
137
|
+
```sh
|
138
|
+
curl -X POST http://localhost:3000/customers \
|
139
|
+
-H 'Content-Type: application/json' \
|
140
|
+
-d '{ "first_name": "John", "last_name": "Doe" }' | jq
|
141
|
+
```
|
142
|
+
|
143
|
+
You will get the response:
|
144
|
+
|
145
|
+
```json
|
146
|
+
{
|
147
|
+
"id": 1,
|
148
|
+
"first_name": "John",
|
149
|
+
"last_name": "Doe",
|
150
|
+
"created_at": "2024-08-03T16:52:30.829Z",
|
151
|
+
"updated_at": "2024-08-03T16:52:30.848Z"
|
152
|
+
}
|
153
|
+
```
|
154
|
+
|
155
|
+
Run `rails c` and do the following:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
Customer.last
|
159
|
+
=>
|
160
|
+
#<Customer:0x0000000107e20998
|
161
|
+
id: 1,
|
162
|
+
first_name: "John",
|
163
|
+
last_name: "Doe",
|
164
|
+
created_at: Sat, 03 Aug 2024 16:52:30.829043000 UTC +00:00,
|
165
|
+
updated_at: Sat, 03 Aug 2024 16:52:30.848243000 UTC +00:00>
|
166
|
+
Customer.last.events
|
167
|
+
[#<Customer::Events::CustomerCreated:0x0000000108dbcac8
|
168
|
+
id: 1,
|
169
|
+
type: "Customer::Events::CustomerCreated",
|
170
|
+
event_type: "Customer::Events::CustomerCreated",
|
171
|
+
aggregate_id: "1",
|
172
|
+
eventable_type: "Customer",
|
173
|
+
eventable_id: 1,
|
174
|
+
payload: {"last_name"=>"Doe", "created_at"=>"2024-08-03T16:58:59.952Z", "first_name"=>"John", "updated_at"=>"2024-08-03T16:58:59.952Z"},
|
175
|
+
metadata:
|
176
|
+
{"request_id"=>"2a40d4f9-509b-4b49-a39f-d978679fa5ef",
|
177
|
+
"request_ip"=>"::1",
|
178
|
+
"request_params"=>{"action"=>"create", "customer"=>{"last_name"=>"Doe", "first_name"=>"John"}, "last_name"=>"Doe", "controller"=>"customers", "first_name"=>"John"},
|
179
|
+
"request_user_agent"=>"curl/8.6.0"},
|
180
|
+
created_at: Sat, 03 Aug 2024 16:58:59.973815000 UTC +00:00,
|
181
|
+
updated_at: Sat, 03 Aug 2024 16:58:59.973815000 UTC +00:00>]
|
182
|
+
```
|
183
|
+
|
184
|
+
As you can see, customer has been created and if you check its `.events` relationship, you should see an event that created it.
|
185
|
+
This event has the same attributes in the payload as you set using the `event_attributes` method of the `Customer::Events::CustomerCreated` class.
|
186
|
+
There is also a metadata field, which is also defined as JSON, and you can store additional things in this field (this is just for information).
|
187
|
+
|
188
|
+
To have these metadata fields populated automatically, you need to include `RailsSimpleEventSourcing::SetCurrentRequestDetails` in your ApplicationController.
|
189
|
+
|
190
|
+
Example:
|
191
|
+
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
class ApplicationController < ActionController::Base
|
195
|
+
include RailsSimpleEventSourcing::SetCurrentRequestDetails
|
196
|
+
end
|
197
|
+
```
|
198
|
+
|
199
|
+
You can override metadata fields by defining the `event_metadata` method in the controller, this method should return a Hash which will be stored in the metadata field of the event.
|
200
|
+
|
201
|
+
By default, this method looks like this:
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
def event_metadata
|
205
|
+
parameter_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
|
206
|
+
|
207
|
+
{
|
208
|
+
request_id: request.uuid,
|
209
|
+
request_user_agent: request.user_agent,
|
210
|
+
request_referer: request.referer,
|
211
|
+
request_ip: request.ip,
|
212
|
+
request_params: parameter_filter.filter(request.params)
|
213
|
+
}
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
#### Important notice
|
218
|
+
|
219
|
+
The data stored in the events should be immutable (i.e., you shouldn't change it after it's created), so they have simple protection against accidental modification, which means that the model is marked as read-only.
|
220
|
+
|
221
|
+
The same goes for models, any model that should be updated by events should include `include RailsSimpleEventSourcing::Events`, this will give you access to the `.events` relation and you will have read-only protection as well (model should only be updated by creating an event).
|
222
|
+
|
223
|
+
Example:
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
class Customer < ApplicationRecord
|
227
|
+
include RailsSimpleEventSourcing::Events
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
One thing to note here is that it would be better to do soft-deletes (mark record as deleted) instead of deleting records from the DB, since every record has relations called `events` when you have all the events that were applied to it.
|
232
|
+
|
233
|
+
#### More examples
|
234
|
+
|
235
|
+
There is a sample application in the `test/dummy/app` directory so you can see how updates and deletes are handled.
|
236
|
+
|
237
|
+
## Installation
|
238
|
+
Add this line to your application's Gemfile:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
gem "rails_simple_event_sourcing"
|
242
|
+
```
|
243
|
+
|
244
|
+
And then execute:
|
245
|
+
```bash
|
246
|
+
$ bundle
|
247
|
+
```
|
248
|
+
|
249
|
+
Or install it yourself as:
|
250
|
+
```bash
|
251
|
+
$ gem install rails_simple_event_sourcing
|
252
|
+
```
|
253
|
+
|
254
|
+
Copy migration to your app:
|
255
|
+
```ruby
|
256
|
+
rails rails_simple_event_sourcing:install:migrations
|
257
|
+
```
|
258
|
+
|
259
|
+
And then run the migration in order to create the rails_simple_event_sourcing_events table (the table that will store the event log):
|
260
|
+
```ruby
|
261
|
+
rake db:migrate
|
262
|
+
```
|
263
|
+
|
264
|
+
## Contributing
|
265
|
+
Contribution directions go here.
|
266
|
+
|
267
|
+
## License
|
268
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
File without changes
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsSimpleEventSourcing
|
4
|
+
module SetCurrentRequestDetails
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_action :set_event_metadata
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def set_event_metadata
|
13
|
+
CurrentRequest.metadata = event_metadata
|
14
|
+
end
|
15
|
+
|
16
|
+
def event_metadata
|
17
|
+
parameter_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
|
18
|
+
|
19
|
+
{
|
20
|
+
request_id: request.uuid,
|
21
|
+
request_user_agent: request.user_agent,
|
22
|
+
request_referer: request.referer,
|
23
|
+
request_ip: request.ip,
|
24
|
+
request_params: parameter_filter.filter(request.params)
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsSimpleEventSourcing
|
4
|
+
module Events
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include ReadOnly
|
7
|
+
|
8
|
+
included do
|
9
|
+
has_many :events, class_name: 'RailsSimpleEventSourcing::Event', as: :eventable, dependent: :nullify
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsSimpleEventSourcing
|
4
|
+
module ReadOnly
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
def readonly?
|
9
|
+
super || !@write_access_enabled
|
10
|
+
end
|
11
|
+
|
12
|
+
def enable_write_access!
|
13
|
+
@write_access_enabled = true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsSimpleEventSourcing
|
4
|
+
class Event < ApplicationRecord
|
5
|
+
include ReadOnly
|
6
|
+
|
7
|
+
belongs_to :eventable, polymorphic: true, optional: true
|
8
|
+
|
9
|
+
alias aggregate eventable
|
10
|
+
|
11
|
+
after_initialize :initialize_event
|
12
|
+
before_validation :enable_write_access_on_self, if: :new_record?
|
13
|
+
before_validation :apply_on_aggregate, if: :aggregate_defined?
|
14
|
+
before_save :add_metadata
|
15
|
+
before_save :assing_aggregate_id_and_persist_aggregate, if: :aggregate_defined?
|
16
|
+
|
17
|
+
def self.aggregate_model_name(name)
|
18
|
+
singleton_class.instance_variable_set(:@aggregate_model_name, name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def aggregate_model_name
|
22
|
+
self.class.singleton_class.instance_variable_get(:@aggregate_model_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.event_attributes(*attributes)
|
26
|
+
@event_attributes ||= []
|
27
|
+
|
28
|
+
attributes.map(&:to_s).each do |attribute|
|
29
|
+
define_method attribute do
|
30
|
+
self.payload ||= {}
|
31
|
+
self.payload[attribute]
|
32
|
+
end
|
33
|
+
|
34
|
+
define_method "#{attribute}=" do |argument|
|
35
|
+
self.payload ||= {}
|
36
|
+
self.payload[attribute] = argument
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
@event_attributes
|
41
|
+
end
|
42
|
+
|
43
|
+
def apply(_aggregate)
|
44
|
+
raise NoMethodError, "You must implement #{self.class}#apply"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def aggregate_defined?
|
50
|
+
aggregate_model_name.present?
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize_event
|
54
|
+
self.class.prepend RailsSimpleEventSourcing::ApplyWithReturningAggregate
|
55
|
+
@aggregate = find_or_build_aggregate if aggregate_defined?
|
56
|
+
self.event_type = self.class
|
57
|
+
self.eventable = @aggregate
|
58
|
+
end
|
59
|
+
|
60
|
+
def enable_write_access_on_self
|
61
|
+
enable_write_access!
|
62
|
+
end
|
63
|
+
|
64
|
+
def apply_on_aggregate
|
65
|
+
@aggregate.enable_write_access!
|
66
|
+
apply(@aggregate)
|
67
|
+
end
|
68
|
+
|
69
|
+
def assing_aggregate_id_and_persist_aggregate
|
70
|
+
@aggregate.save! if aggregate_id.present?
|
71
|
+
self.aggregate_id = @aggregate.id
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_metadata
|
75
|
+
return if CurrentRequest.metadata.blank?
|
76
|
+
|
77
|
+
self.metadata = CurrentRequest.metadata.compact.presence
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_or_build_aggregate
|
81
|
+
return aggregate_model_name.find(aggregate_id).lock! if aggregate_id.present?
|
82
|
+
|
83
|
+
aggregate_model_name.new
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateRailsSimpleEventSourcingEvents < ActiveRecord::Migration[7.1]
|
4
|
+
def change
|
5
|
+
create_table :rails_simple_event_sourcing_events do |t|
|
6
|
+
t.references :eventable, polymorphic: true
|
7
|
+
t.string :type, null: false
|
8
|
+
t.string :event_type, null: false
|
9
|
+
t.string :aggregate_id
|
10
|
+
t.jsonb :payload
|
11
|
+
t.jsonb :metadata
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
|
15
|
+
t.index :type
|
16
|
+
t.index :event_type
|
17
|
+
t.index :aggregate_id
|
18
|
+
t.index :payload, using: :gin
|
19
|
+
t.index :metadata, using: :gin
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsSimpleEventSourcing
|
4
|
+
class CommandHandler
|
5
|
+
def initialize(command)
|
6
|
+
@command = command
|
7
|
+
end
|
8
|
+
|
9
|
+
def call
|
10
|
+
return Result.new(success?: false, errors: @command.errors) unless @command.valid?
|
11
|
+
|
12
|
+
initialize_command_handler.call
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def initialize_command_handler
|
18
|
+
@command.class.to_s.gsub('::Commands::', '::CommandHandlers::').constantize.new(command: @command)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsSimpleEventSourcing
|
4
|
+
module CommandHandlers
|
5
|
+
class Base
|
6
|
+
def initialize(command:)
|
7
|
+
@command = command
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
raise NoMethodError, "You must implement #{self.class}#call"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'command_handlers/base'
|
4
|
+
require_relative 'commands/base'
|
5
|
+
require_relative 'command_handler'
|
6
|
+
require_relative 'apply_with_returning_aggregate'
|
7
|
+
require_relative 'result'
|
8
|
+
|
9
|
+
module RailsSimpleEventSourcing
|
10
|
+
class Engine < ::Rails::Engine
|
11
|
+
isolate_namespace RailsSimpleEventSourcing
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails_simple_event_sourcing
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Damian Baćkowski
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pg
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 7.1.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 7.1.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Rails simple event sourcing engine.
|
70
|
+
email:
|
71
|
+
- damianbackowski@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- MIT-LICENSE
|
77
|
+
- README.md
|
78
|
+
- Rakefile
|
79
|
+
- app/assets/config/rails_simple_event_sourcing_manifest.js
|
80
|
+
- app/controllers/concerns/rails_simple_event_sourcing/set_current_request_details.rb
|
81
|
+
- app/models/concerns/rails_simple_event_sourcing/events.rb
|
82
|
+
- app/models/concerns/rails_simple_event_sourcing/read_only.rb
|
83
|
+
- app/models/rails_simple_event_sourcing.rb
|
84
|
+
- app/models/rails_simple_event_sourcing/current_request.rb
|
85
|
+
- app/models/rails_simple_event_sourcing/event.rb
|
86
|
+
- config/routes.rb
|
87
|
+
- db/migrate/20231231133250_create_rails_simple_event_sourcing_events.rb
|
88
|
+
- lib/rails_simple_event_sourcing.rb
|
89
|
+
- lib/rails_simple_event_sourcing/apply_with_returning_aggregate.rb
|
90
|
+
- lib/rails_simple_event_sourcing/command_handler.rb
|
91
|
+
- lib/rails_simple_event_sourcing/command_handlers/base.rb
|
92
|
+
- lib/rails_simple_event_sourcing/commands/base.rb
|
93
|
+
- lib/rails_simple_event_sourcing/engine.rb
|
94
|
+
- lib/rails_simple_event_sourcing/result.rb
|
95
|
+
- lib/rails_simple_event_sourcing/version.rb
|
96
|
+
- lib/tasks/rails_simple_event_sourcing_tasks.rake
|
97
|
+
homepage: https://github.com/dbackowski/rails_simple_event_sourcing
|
98
|
+
licenses:
|
99
|
+
- MIT
|
100
|
+
metadata:
|
101
|
+
homepage_uri: https://github.com/dbackowski/rails_simple_event_sourcing
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubygems_version: 3.1.6
|
118
|
+
signing_key:
|
119
|
+
specification_version: 4
|
120
|
+
summary: Rails engine for simple event sourcing.
|
121
|
+
test_files: []
|