event_sourced_record 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Getting_Started.md +48 -40
- data/README.md +24 -2
- data/lib/event_sourced_record/version.rb +1 -1
- data/lib/generators/event_sourced_record/event_generator.rb +8 -0
- data/lib/generators/event_sourced_record/event_sourced_record_generator.rb +11 -0
- data/lib/generators/event_sourced_record/projection_generator.rb +8 -0
- data/lib/generators/event_sourced_record/templates/event_model.rb +3 -0
- data/lib/generators/event_sourced_record/templates/event_sourced_record.rake +8 -0
- data/lib/generators/event_sourced_record/templates/projection_model.ar3.rb +4 -1
- data/lib/generators/event_sourced_record/templates/projection_model.ar4.rb +4 -1
- data/test/generators/event_generator_test.rb +4 -0
- data/test/generators/event_sourced_record_generator_test.rb +13 -0
- data/test/generators/projection_generator_test.rb +4 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b7ec38af25ac7107acc8351ccbc38b7a7844296
|
4
|
+
data.tar.gz: c55c45d7a1ed2e00a85b23da21451eba99854cb8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c289c63573fce64a567ff6acb8283d810f0ad141bd7b28044457ffb34cce09fba2483fceebda9450c951459b3bdd5050630d16e8313c6824c88ca6b06a9896fb
|
7
|
+
data.tar.gz: c647debe68752c5146dce9f11427477b1233715ef47a6e286fb3757b99907bd6de14b1394a5acefaa35233d78e5a5e4dd248398de351816243e0ce0ac7c5f51f
|
data/Getting_Started.md
CHANGED
@@ -45,7 +45,10 @@ This takes the same attribute list as `rails generate model`, but generates a nu
|
|
45
45
|
This is the model that you'd create in a typical Rails application, but here you'll find that we don't do much with it directly.
|
46
46
|
|
47
47
|
class Subscription < ActiveRecord::Base
|
48
|
-
has_many :
|
48
|
+
has_many :events,
|
49
|
+
class_name: 'SubscriptionEvent',
|
50
|
+
foreign_key: 'subscription_uuid',
|
51
|
+
primary_key: 'uuid'
|
49
52
|
|
50
53
|
validates :uuid, uniqueness: true
|
51
54
|
end
|
@@ -63,6 +66,9 @@ You might never end up showing this model to end-users, but in fact it's the aut
|
|
63
66
|
class SubscriptionEvent < ActiveRecord::Base
|
64
67
|
include EventSourcedRecord::Event
|
65
68
|
|
69
|
+
belongs_to :subscription,
|
70
|
+
foreign_key: 'subscription_uuid', primary_key: 'uuid'
|
71
|
+
|
66
72
|
event_type :creation do
|
67
73
|
# attributes :user_id
|
68
74
|
#
|
@@ -99,6 +105,20 @@ The generator registers this observer in `config/application.rb`:
|
|
99
105
|
|
100
106
|
config.active_record.observers = :subscription_event_observer
|
101
107
|
|
108
|
+
### A Subscription rake file
|
109
|
+
|
110
|
+
The Rake file gives you a convenient command to rebuild every
|
111
|
+
`Subscription` whenever necessary. Since you'll be building
|
112
|
+
`SubscriptionCalculator` to be idempotent, this will be a fairly safe operation.
|
113
|
+
|
114
|
+
namespace :subscription do
|
115
|
+
task :recalculate => :environment do
|
116
|
+
Subscription.all.each do |subscription|
|
117
|
+
SubscriptionCalculator.new(subscription).run.save!
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
102
122
|
## Creation
|
103
123
|
|
104
124
|
The generated code starts you out with a `creation` event, but we'll want to define it to get some use out of it.
|
@@ -107,18 +127,14 @@ Above, we specified that subscriptions will have `user_id`, `bottles_per_shipmen
|
|
107
127
|
|
108
128
|
### Define the creation event type
|
109
129
|
|
110
|
-
Fill out the `event_type` block in `
|
130
|
+
Fill out the `event_type` block in `SubscriptionEvent`:
|
111
131
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
event_type :creation do
|
116
|
-
attributes :bottles_per_shipment, :bottles_purchased, :user_id
|
132
|
+
event_type :creation do
|
133
|
+
attributes :bottles_per_shipment, :bottles_purchased, :user_id
|
117
134
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
end
|
135
|
+
validates :bottles_per_shipment, presence: true, numericality: true
|
136
|
+
validates :bottles_purchased, presence: true, numericality: true
|
137
|
+
validates :user_id, presence: true
|
122
138
|
end
|
123
139
|
|
124
140
|
This lets you build and save events with the attributes `bottles_per_shipment`,
|
@@ -134,21 +150,17 @@ This lets you build and save events with the attributes `bottles_per_shipment`,
|
|
134
150
|
|
135
151
|
### Handle the creation in the calculator
|
136
152
|
|
137
|
-
Fill out the `advance_creation` method in `
|
138
|
-
|
139
|
-
class SubscriptionCalculator < EventSourcedRecord::Calculator
|
140
|
-
events :subscription_events
|
153
|
+
Fill out the `advance_creation` method in `SubscriptionCalculator`:
|
141
154
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
end
|
155
|
+
def advance_creation(event)
|
156
|
+
@subscription.user_id = event.user_id
|
157
|
+
@subscription.bottles_per_shipment = event.bottles_per_shipment
|
158
|
+
@subscription.bottles_left = event.bottles_purchased
|
147
159
|
end
|
148
160
|
|
149
161
|
Note that for `user_id` and `bottles_per_shipment` we simply copy the field from the event to the subscription, but in the case of `bottles_purchased`, that is translated to `Subscription#bottles_left`. This field will go up and down over time.
|
150
162
|
|
151
|
-
### Create a subscription
|
163
|
+
### Create a subscription, indirectly
|
152
164
|
|
153
165
|
Creating a subscription is a matter of creating the event itself:
|
154
166
|
|
@@ -178,26 +190,22 @@ This is a lot of indirection, which you don't have to understand right away. Wh
|
|
178
190
|
|
179
191
|
Occasionally, subscribers will want to change their settings. Let's say in this case we'll only allow them to change `bottles_per_shipment`. So we add a new event type in `SubscriptionEvent`:
|
180
192
|
|
181
|
-
|
182
|
-
|
183
|
-
attributes :bottles_per_shipment
|
193
|
+
event_type :change_settings do
|
194
|
+
attributes :bottles_per_shipment
|
184
195
|
|
185
|
-
|
186
|
-
end
|
196
|
+
validates :bottles_per_shipment, numericality: true
|
187
197
|
end
|
188
198
|
|
189
|
-
And we add a method to handle this new event type
|
199
|
+
And we add a method to handle this new event type to `SubscriptionCalculator`:
|
190
200
|
|
191
|
-
|
192
|
-
|
193
|
-
@subscription.bottles_per_shipment = event.bottles_per_shipment
|
194
|
-
end
|
201
|
+
def advance_change_settings(event)
|
202
|
+
@subscription.bottles_per_shipment = event.bottles_per_shipment
|
195
203
|
end
|
196
204
|
|
197
205
|
If we want to change the subscription from the last example from 1 bottle per shipment to 2 bottles per shipment, this looks like this:
|
198
206
|
|
199
207
|
subscription.bottles_per_shipment # 1
|
200
|
-
|
208
|
+
subscription.events.change_settings.create!(
|
201
209
|
subscription_uuid: subscription.uuid, bottles_per_shipment: 2
|
202
210
|
)
|
203
211
|
subscription.reload
|
@@ -217,6 +225,8 @@ In `subscription_calculator.rb` we make two changes. First, we add `:shipments`
|
|
217
225
|
|
218
226
|
class SubscriptionCalculator < EventSourcedRecord::Calculator
|
219
227
|
events :subscription_events, :shipments
|
228
|
+
|
229
|
+
# Other methods omitted
|
220
230
|
|
221
231
|
def advance_shipment(shipment)
|
222
232
|
@subscription.bottles_left -= shipment.num_bottles
|
@@ -252,25 +262,22 @@ Because `Subscription` is just a cache, event sourcing gives us a consistent way
|
|
252
262
|
1. Write a migration if needed to add or change fields on the `subscriptions` table.
|
253
263
|
1. Modify `SubscriptionCalculator` to take the new fields into account.
|
254
264
|
1. Test, merge, and deploy to production.
|
255
|
-
1. Rebuild every `Subscription
|
256
|
-
|
257
|
-
|
258
|
-
Subscription.all.each do |subscription|
|
259
|
-
SubscriptionCalculator.new(subscription).run.save!
|
260
|
-
end
|
265
|
+
1. Rebuild every `Subscription` by running `rake
|
266
|
+
subscription:recalculate`.
|
261
267
|
|
262
268
|
Because you have designed `SubscriptionCalculator` to be idempotent, it is safe to re-run at any time you want -- for example, if you want to fill in a database column that didn't exist before.
|
263
269
|
|
264
270
|
## Debugging Subscription in production
|
265
271
|
|
266
|
-
The same process can work for fixing many bugs in production. Events themselves are usually a simple act of recording the user's intention. Errors usually emerge in
|
272
|
+
The same process can work for fixing many bugs in production. Events themselves are usually a simple act of recording the user's intention. Errors usually emerge in the calculator, which is responsible for the tougher work of interpreting a sequence of events.
|
267
273
|
|
268
274
|
So when you find a bug, you can often fix it via this process:
|
269
275
|
|
270
276
|
1. Write a test to reproduce the bug.
|
271
277
|
1. Fix `SubscriptionCalculator` to fix the bug.
|
272
278
|
1. Merge and deploy to production.
|
273
|
-
1. Rebuild every `Subscription`
|
279
|
+
1. Rebuild every `Subscription` by running `rake
|
280
|
+
subscription:recalculate`.
|
274
281
|
|
275
282
|
Remember, `SubscriptionCalculator` is idempotent, so this process will fix all the records affected by the bug will leaving the others unchanged. It will also spare you the agonizing effort of picking through data in `rails console` to guess which records are broken. Why go to the trouble? Just re-run the whole thing and move on.
|
276
283
|
|
@@ -282,3 +289,4 @@ Because `SubscriptionCalculator` rebuilds a record in sequence, it's a piece of
|
|
282
289
|
sub_at_end_of_year = calculator.run(last_event_time: Date.new(2014,12,31))
|
283
290
|
|
284
291
|
Note that the returned instance is un-saved, and shouldn't be saved to `subscriptions`, but it's now a piece of cake to take those values and send them to whatever reporting system you have in place.
|
292
|
+
|
data/README.md
CHANGED
@@ -38,7 +38,9 @@ Generate the required classes with `rails generate event_sourced_record`:
|
|
38
38
|
user_id:integer bottles_per_shipment:integer \
|
39
39
|
bottles_left:integer
|
40
40
|
|
41
|
-
The argument list is the same as with `rails generate model`. This will
|
41
|
+
The argument list is the same as with `rails generate model`. This will
|
42
|
+
generate the event model, the projection, the calculator, the observer,
|
43
|
+
and a Rake file.
|
42
44
|
|
43
45
|
### Event model
|
44
46
|
|
@@ -47,6 +49,9 @@ The event model is an ActiveRecord model but it can act significantly different
|
|
47
49
|
class SubscriptionEvent < ActiveRecord::Base
|
48
50
|
include EventSourcedRecord::Event
|
49
51
|
|
52
|
+
belongs_to :subscription,
|
53
|
+
foreign_key: 'subscription_uuid', primary_key: 'uuid'
|
54
|
+
|
50
55
|
event_type :creation do
|
51
56
|
attributes :bottles_per_shipment, :bottles_purchased, :user_id
|
52
57
|
|
@@ -73,7 +78,10 @@ The easiest way to create these records is with the scopes that are automaticall
|
|
73
78
|
The projection is the ActiveRecord model that is generated deterministically with the data in the timestamped events combined with the logic in the calculator. Projections shouldn't have any code for modifying themselves, as that will be done externally. Accordingly, projections end up being fairly small classes:
|
74
79
|
|
75
80
|
class Subscription < ActiveRecord::Base
|
76
|
-
has_many :
|
81
|
+
has_many :events,
|
82
|
+
class_name: 'SubscriptionEvent',
|
83
|
+
foreign_key: 'subscription_uuid',
|
84
|
+
primary_key: 'uuid'
|
77
85
|
|
78
86
|
validates :uuid, uniqueness: true
|
79
87
|
end
|
@@ -125,6 +133,20 @@ If you use other models as events, simply add them to the `observe` method:
|
|
125
133
|
class SubscriptionEventObserver < ActiveRecord::Observer
|
126
134
|
observe :subscription_event, :shipment
|
127
135
|
|
136
|
+
## Rake file
|
137
|
+
|
138
|
+
The Rake file gives you a convenient command to rebuild every
|
139
|
+
projection whenever necessary. Since you'll be building the calculator
|
140
|
+
to be idempotent, this will be a fairly safe operation.
|
141
|
+
|
142
|
+
namespace :subscription do
|
143
|
+
task :recalculate => :environment do
|
144
|
+
Subscription.all.each do |subscription|
|
145
|
+
SubscriptionCalculator.new(subscription).run.save!
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
128
150
|
## Contributing
|
129
151
|
|
130
152
|
1. Fork it ( https://github.com/[my-github-username]/event_sourced_record/fork )
|
@@ -39,6 +39,14 @@ class EventSourcedRecord::EventGenerator < ActiveRecord::Generators::Base
|
|
39
39
|
|
40
40
|
private
|
41
41
|
|
42
|
+
def belongs_to_foreign_key
|
43
|
+
belongs_to_name + '_uuid'
|
44
|
+
end
|
45
|
+
|
46
|
+
def belongs_to_name
|
47
|
+
file_name.gsub(/_event/, '')
|
48
|
+
end
|
49
|
+
|
42
50
|
def event_migration_class_name
|
43
51
|
"create_#{event_table_name}".camelize
|
44
52
|
end
|
@@ -33,9 +33,20 @@ class EventSourcedRecord::EventSourcedRecordGenerator < Rails::Generators::Named
|
|
33
33
|
generate "event_sourced_record:projection", "#{file_name} #{projection_attributes}"
|
34
34
|
end
|
35
35
|
|
36
|
+
def create_rake_file
|
37
|
+
template(
|
38
|
+
"event_sourced_record.rake",
|
39
|
+
File.join("lib/tasks", class_path, "#{file_name}.rake")
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
36
43
|
protected
|
37
44
|
|
38
45
|
def calculator_class_name
|
39
46
|
class_name + 'Calculator'
|
40
47
|
end
|
48
|
+
|
49
|
+
def projection_class_name
|
50
|
+
class_name
|
51
|
+
end
|
41
52
|
end
|
@@ -35,6 +35,14 @@ class EventSourcedRecord::ProjectionGenerator < ActiveRecord::Generators::Base
|
|
35
35
|
|
36
36
|
private
|
37
37
|
|
38
|
+
def event_class_name
|
39
|
+
projection_class_name + 'Event'
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_many_foreign_key
|
43
|
+
file_name + '_uuid'
|
44
|
+
end
|
45
|
+
|
38
46
|
def migration_attributes
|
39
47
|
attr_strings = attributes.map { |attr|
|
40
48
|
attr_string = attr.name
|
@@ -3,7 +3,10 @@ class <%= projection_class_name %> < <%= projection_parent_class_name.classify %
|
|
3
3
|
<% attributes.select {|attr| attr.reference? }.each do |attribute| -%>
|
4
4
|
belongs_to :<%= attribute.name %>
|
5
5
|
<% end -%>
|
6
|
-
has_many
|
6
|
+
has_many :events,
|
7
|
+
class_name: '<%= event_class_name %>',
|
8
|
+
foreign_key: '<%= has_many_foreign_key %>',
|
9
|
+
primary_key: 'uuid'
|
7
10
|
|
8
11
|
validates :uuid, uniqueness: true
|
9
12
|
end
|
@@ -3,7 +3,10 @@ class <%= projection_class_name %> < <%= projection_parent_class_name.classify %
|
|
3
3
|
<% attributes.select(&:reference?).each do |attribute| -%>
|
4
4
|
belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
|
5
5
|
<% end -%>
|
6
|
-
has_many
|
6
|
+
has_many :events,
|
7
|
+
class_name: '<%= event_class_name %>',
|
8
|
+
foreign_key: '<%= has_many_foreign_key %>',
|
9
|
+
primary_key: 'uuid'
|
7
10
|
<% if attributes.any?(&:password_digest?) -%>
|
8
11
|
has_secure_password
|
9
12
|
<% end -%>
|
@@ -36,6 +36,10 @@ class EventSourcedRecord::EventGeneratorTest < Rails::Generators::TestCase
|
|
36
36
|
assert_file("app/models/subscription_event.rb") do |contents|
|
37
37
|
assert_match(/class SubscriptionEvent < ActiveRecord::Base/, contents)
|
38
38
|
assert_match(/include EventSourcedRecord::Event/, contents)
|
39
|
+
assert_match(/belongs_to :subscription,/, contents)
|
40
|
+
assert_match(
|
41
|
+
/foreign_key: 'subscription_uuid', primary_key: 'uuid'/, contents
|
42
|
+
)
|
39
43
|
assert_match(/event_type :creation do/, contents)
|
40
44
|
end
|
41
45
|
end
|
@@ -35,4 +35,17 @@ class EventSourcedRecord::EventSourcedRecordGeneratorTest < Rails::Generators::T
|
|
35
35
|
"shampoo_subscription_calculator"
|
36
36
|
)
|
37
37
|
end
|
38
|
+
|
39
|
+
test "creates a rake file" do
|
40
|
+
assert_file("lib/tasks/shampoo_subscription.rake") do |contents|
|
41
|
+
assert_match(/namespace :shampoo_subscription do/, contents)
|
42
|
+
assert_match(
|
43
|
+
/ShampooSubscription.all.each do |shampoo_subscription|/, contents
|
44
|
+
)
|
45
|
+
assert_match(
|
46
|
+
/ShampooSubscriptionCalculator.new\(shampoo_subscription\).run.save!/,
|
47
|
+
contents
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
38
51
|
end
|
@@ -39,6 +39,10 @@ class EventSourcedRecord::ProjectionGeneratorTest < Rails::Generators::TestCase
|
|
39
39
|
assert_match(/class Subscription < ActiveRecord::Base/, contents)
|
40
40
|
assert_match(/validates :uuid, uniqueness: true/, contents)
|
41
41
|
assert_no_match(/attr_accessible :bottles_left/, contents)
|
42
|
+
assert_match(/has_many :events,/, contents)
|
43
|
+
assert_match(/class_name: 'SubscriptionEvent',/, contents)
|
44
|
+
assert_match(/foreign_key: 'subscription_uuid',/, contents)
|
45
|
+
assert_match(/primary_key: 'uuid'/, contents)
|
42
46
|
end
|
43
47
|
end
|
44
48
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: event_sourced_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Francis Hwang
|
@@ -166,6 +166,7 @@ files:
|
|
166
166
|
- lib/generators/event_sourced_record/templates/calculator.rb
|
167
167
|
- lib/generators/event_sourced_record/templates/event_migration.ar3.rb
|
168
168
|
- lib/generators/event_sourced_record/templates/event_model.rb
|
169
|
+
- lib/generators/event_sourced_record/templates/event_sourced_record.rake
|
169
170
|
- lib/generators/event_sourced_record/templates/observer.rb
|
170
171
|
- lib/generators/event_sourced_record/templates/projection_migration.ar3.rb
|
171
172
|
- lib/generators/event_sourced_record/templates/projection_model.ar3.rb
|