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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 572708800666a6ac438befd0004e290e42cde341
4
- data.tar.gz: bd813aa2a7f22f68fd9578c4793725c8e990a57f
3
+ metadata.gz: 5b7ec38af25ac7107acc8351ccbc38b7a7844296
4
+ data.tar.gz: c55c45d7a1ed2e00a85b23da21451eba99854cb8
5
5
  SHA512:
6
- metadata.gz: c7dd4787019fa7924f435bf9e62515a77ee3df7f201a13763868296fea317ace10f1bdcc4fe660b49eeaf7254358d6017f5725d76e05bde3cdffa61c34236e02
7
- data.tar.gz: 6271f822d88dd64ebc65d0c22800596a5674393354b0110b0e64b17c598f7329c19673dddf6ab1171954ed80b1d95d43667eef85135dfdb9bc37278ad635273c
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 :subscription_events
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 `subscription_event.rb`:
130
+ Fill out the `event_type` block in `SubscriptionEvent`:
111
131
 
112
- class SubscriptionEvent < ActiveRecord::Base
113
- include EventSourcedRecord::Event
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
- validates :bottles_per_shipment, presence: true, numericality: true
119
- validates :bottles_purchased, presence: true, numericality: true
120
- validates :user_id, presence: true
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 `subscription_calculator.rb`:
138
-
139
- class SubscriptionCalculator < EventSourcedRecord::Calculator
140
- events :subscription_events
153
+ Fill out the `advance_creation` method in `SubscriptionCalculator`:
141
154
 
142
- def advance_creation(event)
143
- @subscription.user_id = event.user_id
144
- @subscription.bottles_per_shipment = event.bottles_per_shipment
145
- @subscription.bottles_left = event.bottles_purchased
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 (indirectly)
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
- class SubscriptionEvent < ActiveRecord::Base
182
- event_type :change_settings do
183
- attributes :bottles_per_shipment
193
+ event_type :change_settings do
194
+ attributes :bottles_per_shipment
184
195
 
185
- validates :bottles_per_shipment, numericality: true
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 in the calculator:
199
+ And we add a method to handle this new event type to `SubscriptionCalculator`:
190
200
 
191
- class SubscriptionCalculator < EventSourcedRecord::Calculator
192
- def advance_change_settings(event)
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
- SubscriptionEvent.change_settings.create!(
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`, with code such as:
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 `SubscriptionCalculator`, which is responsible for the tougher work of interpreting a sequence of events.
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` with the code above.
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 generate the event model, the projection, the calculator, and the observer.
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 :subscription_events
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 )
@@ -1,3 +1,3 @@
1
1
  module EventSourcedRecord
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -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
@@ -2,6 +2,9 @@
2
2
  class <%= event_class_name %> < ActiveRecord::Base
3
3
  include EventSourcedRecord::Event
4
4
 
5
+ belongs_to :<%= belongs_to_name %>,
6
+ foreign_key: '<%= belongs_to_foreign_key %>', primary_key: 'uuid'
7
+
5
8
  event_type :creation do
6
9
  # attributes :user_id
7
10
  #
@@ -0,0 +1,8 @@
1
+ namespace :<%= file_name %> do
2
+ task :recalculate => :environment do
3
+ <%= projection_class_name %>.all.each do |<%= file_name %>|
4
+ <%= calculator_class_name%>.new(<%= file_name %>).run.save!
5
+ end
6
+ end
7
+ end
8
+
@@ -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 :<%= file_name %>_events
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 :<%= file_name %>_events
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.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