event_sourced_record 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Appraisals +4 -1
- data/Getting_Started.md +284 -0
- data/README.md +6 -2
- data/lib/event_sourced_record/event.rb +10 -0
- data/lib/event_sourced_record/version.rb +1 -1
- data/lib/generators/event_sourced_record/event_generator.rb +27 -10
- data/lib/generators/event_sourced_record/projection_generator.rb +23 -5
- data/lib/generators/event_sourced_record/templates/event_migration.ar3.rb +16 -0
- data/lib/generators/event_sourced_record/templates/projection_migration.ar3.rb +16 -0
- data/lib/generators/event_sourced_record/templates/projection_model.ar3.rb +11 -0
- data/lib/generators/event_sourced_record.rb +6 -0
- data/test/generators/event_generator_test.rb +10 -3
- data/test/generators/projection_generator_test.rb +14 -6
- data/test/test_helper.rb +7 -5
- metadata +8 -3
- /data/lib/generators/event_sourced_record/templates/{projection_model.rb → projection_model.ar4.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 11467089bb5e302e5ba7458442510f7c635a9665
|
4
|
+
data.tar.gz: 6eb6064de3952fc90fc8ddba4aea5cf8b417e220
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6d3a67f2edaf5c8d3907ea7eb2c9423fa39508542673acdcf3b881cabf05301b65893fa6057cfecf393e99f44f07f37afc7a5ab5d07049dea8b57a9ba24992e
|
7
|
+
data.tar.gz: d3ede2905cfacc55d72692e469c7d954eec68fa58b0221b542a38c76fdfea23eb6cc412f627941c7bdf15e36b05d97a0af2229b62bb08fba45b9ae8f780da532
|
data/Appraisals
CHANGED
data/Getting_Started.md
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
# Getting started with Event Sourced Record
|
2
|
+
|
3
|
+
This document is intended to teach you how to use Event Sourced Record, and explains some of the concepts behind event sourcing in general. It assumes you're already familiar with Rails.
|
4
|
+
|
5
|
+
Say you're starting a company that sells a shampoo subscription through the mail, and you want to use Event Sourcing to handle your subscription model.
|
6
|
+
|
7
|
+
## Requirements
|
8
|
+
|
9
|
+
Event Sourced Record supports Rails 3.2 and higher.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
gem 'event_sourced_record'
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install event_sourced_record
|
24
|
+
|
25
|
+
Event Sourced Record uses observers, so you'll need to add them to your Gemfile:
|
26
|
+
|
27
|
+
gem 'rails-observers'
|
28
|
+
|
29
|
+
## Generate your classes
|
30
|
+
|
31
|
+
You can use `rails generate event_sourced_record` to get started:
|
32
|
+
|
33
|
+
$ rails generate event_sourced_record Subscription \
|
34
|
+
user_id:integer bottles_per_shipment:integer \
|
35
|
+
bottles_left:integer
|
36
|
+
|
37
|
+
This generates two migrations, which you might as well run now:
|
38
|
+
|
39
|
+
$ rake db:migrate
|
40
|
+
|
41
|
+
This takes the same attribute list as `rails generate model`, but generates a number of different types of files. Let's look at them in turn:
|
42
|
+
|
43
|
+
### Subscription
|
44
|
+
|
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
|
+
|
47
|
+
class Subscription < ActiveRecord::Base
|
48
|
+
has_many :subscription_events
|
49
|
+
|
50
|
+
validates :uuid, uniqueness: true
|
51
|
+
end
|
52
|
+
|
53
|
+
`Subscription` holds data in a convenient form, but it's not responsible for changing its own state. That's the responsiblity of the calculator, using the associated events.
|
54
|
+
|
55
|
+
In Event Sourcing parlance, `Subscription` is a "projection", meaning that everything in it can be derived from data and logic that lives elsewhere.
|
56
|
+
|
57
|
+
### SubscriptionEvent
|
58
|
+
|
59
|
+
You might never end up showing this model to end-users, but in fact it's the authoritative data in this system. With all the events, you can rebuild the projections, but not the other way around.
|
60
|
+
|
61
|
+
`SubscriptionEvent` represents a timestamped event associated with a particular `Subscription`. Each event should be treated as read-only; it's meant to be written once and then never modified.
|
62
|
+
|
63
|
+
class SubscriptionEvent < ActiveRecord::Base
|
64
|
+
include EventSourcedRecord::Event
|
65
|
+
|
66
|
+
event_type :creation do
|
67
|
+
# attributes :user_id
|
68
|
+
#
|
69
|
+
# validates :user_id, presence: true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
### SubscriptionCalculator
|
74
|
+
|
75
|
+
This service class replays the event sequence to build a Subscription record that reflects current state. You'll flesh it out by adding methods that advance the state of the Subscription for each individual type of event.
|
76
|
+
|
77
|
+
class SubscriptionCalculator < EventSourcedRecord::Calculator
|
78
|
+
events :subscription_events
|
79
|
+
|
80
|
+
def advance_creation(event)
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
### SubscriptionEventObserver
|
87
|
+
|
88
|
+
You can run `SubscriptionCalculator` yourself whenever you like, but `SubscriptionEventObserver` takes care of a core use-case. It monitors `SubscriptionEvent` (and other event classes, as we'll see) and tells `SubscriptionCalculator` to build or rebuild the `Subscription` every time there's a new event class saved.
|
89
|
+
|
90
|
+
class SubscriptionEventObserver < ActiveRecord::Observer
|
91
|
+
observe :subscription_event
|
92
|
+
|
93
|
+
def after_create(event)
|
94
|
+
SubscriptionCalculator.new(event).run.save!
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
The generator registers this observer in `config/application.rb`:
|
99
|
+
|
100
|
+
config.active_record.observers = :subscription_event_observer
|
101
|
+
|
102
|
+
## Creation
|
103
|
+
|
104
|
+
The generated code starts you out with a `creation` event, but we'll want to define it to get some use out of it.
|
105
|
+
|
106
|
+
Above, we specified that subscriptions will have `user_id`, `bottles_per_shipment`, and `bottles_left`. During the creation, we assume we can get `user_id` from `current_user` in the controller, and `bottles_per_shipment` is something that the user will specify during the initial signup. Let's say, also, that sign up requires that you buy some bottles up-front, which we'll define on the initial event but not on `Subscription`.
|
107
|
+
|
108
|
+
### Define the creation event type
|
109
|
+
|
110
|
+
Fill out the `event_type` block in `subscription_event.rb`:
|
111
|
+
|
112
|
+
class SubscriptionEvent < ActiveRecord::Base
|
113
|
+
include EventSourcedRecord::Event
|
114
|
+
|
115
|
+
event_type :creation do
|
116
|
+
attributes :bottles_per_shipment, :bottles_purchased, :user_id
|
117
|
+
|
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
|
122
|
+
end
|
123
|
+
|
124
|
+
This lets you build and save events with the attributes `bottles_per_shipment`,
|
125
|
+
`bottles_purchased`, and `user_id`, and validates those attributes -- as long as the event type is set by using the auto-generated scope:
|
126
|
+
|
127
|
+
event = SubscriptionEvent.creation.new(
|
128
|
+
bottles_purchased: 6,
|
129
|
+
user_id: current_user.id
|
130
|
+
)
|
131
|
+
puts "Trying to purchase #{event.bottles_purchased} bottles"
|
132
|
+
event.valid? # false
|
133
|
+
event.errors[:bottles_per_shipment] # ["can't be blank", "is not a number"]
|
134
|
+
|
135
|
+
### Handle the creation in the calculator
|
136
|
+
|
137
|
+
Fill out the `advance_creation` method in `subscription_calculator.rb`:
|
138
|
+
|
139
|
+
class SubscriptionCalculator < EventSourcedRecord::Calculator
|
140
|
+
events :subscription_events
|
141
|
+
|
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
|
147
|
+
end
|
148
|
+
|
149
|
+
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
|
+
|
151
|
+
### Create a subscription (indirectly)
|
152
|
+
|
153
|
+
Creating a subscription is a matter of creating the event itself:
|
154
|
+
|
155
|
+
event = SubscriptionEvent.creation.new(
|
156
|
+
bottles_per_shipment: 1,
|
157
|
+
bottles_purchased: 6,
|
158
|
+
user_id: current_user.id
|
159
|
+
)
|
160
|
+
event.save!
|
161
|
+
subscription = Subscription.last
|
162
|
+
puts "Created subscription #{subscription.id}"
|
163
|
+
|
164
|
+
### What's happening here?
|
165
|
+
|
166
|
+
There's a lot going on here. If you're curious, here's what's happening under the hood:
|
167
|
+
|
168
|
+
1. `SubscriptionEvent` saves, provided its event type validations are satisfied.
|
169
|
+
1. `SubscriptionObserver` is notified that a `SubscriptionEvent` was saved, so it runs the calculator.
|
170
|
+
1. `SubscriptionCalculator` collects all associated events around the `Subscription`, orders them by `created_at`, and runs them in order.
|
171
|
+
1. In the case of creation, we don't actually have a `Subscription` at the time we run the calculator for the first time. So `SubscriptionCalculator` makes use of an auto-generated `subscription_uuid` attribute to tell subscriptions apart even when some of them have yet to be created in the database.
|
172
|
+
1. For each event, `SubscriptionCalculator` calls `advance_[event_type]`, which is responsible for updating the attributes on `@subscription` accordingly.
|
173
|
+
1. `SubscriptionObserver` takes the record returned by `SubscriptionCalculator` and saves it to the database.
|
174
|
+
|
175
|
+
This is a lot of indirection, which you don't have to understand right away. When you hit more complex situations later, you'll find this indirection will come in handy.
|
176
|
+
|
177
|
+
## Change the settings for a subscription
|
178
|
+
|
179
|
+
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
|
+
|
181
|
+
class SubscriptionEvent < ActiveRecord::Base
|
182
|
+
event_type :change_settings do
|
183
|
+
attributes :bottles_per_shipment
|
184
|
+
|
185
|
+
validates :bottles_per_shipment, numericality: true
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
And we add a method to handle this new event type in the calculator:
|
190
|
+
|
191
|
+
class SubscriptionCalculator < EventSourcedRecord::Calculator
|
192
|
+
def advance_change_settings(event)
|
193
|
+
@subscription.bottles_per_shipment = event.bottles_per_shipment
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
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
|
+
|
199
|
+
subscription.bottles_per_shipment # 1
|
200
|
+
SubscriptionEvent.change_settings.create!(
|
201
|
+
subscription_uuid: subscription.uuid, bottles_per_shipment: 2
|
202
|
+
)
|
203
|
+
subscription.reload
|
204
|
+
subscription.bottles_per_shipment # 2
|
205
|
+
|
206
|
+
## Shipment
|
207
|
+
|
208
|
+
We created `SubscriptionEvent` to store events about `Subscription`, but you may find you have other kinds of classes that are like events in that they are time-based and relatively immutable. Let's say that shipments of shampoo function this way in our system: As soon as they are inserted in the database they are applied against the associated subscription.
|
209
|
+
|
210
|
+
So let's create a `Shipment` class, using the standard Rails generator:
|
211
|
+
|
212
|
+
rails generate model Shipment subscription_id:integer num_bottles:integer
|
213
|
+
|
214
|
+
And let's say that everytime a `Shipment` goes out the door, we deduct `Shipment#num_bottles` from `bottles_left` on the associated `Subscription`. To do that we'll need to change the calculator, and the observer.
|
215
|
+
|
216
|
+
In `subscription_calculator.rb` we make two changes. First, we add `:shipments` to `events`, which tells the calculator to include `Shipment` as an event that needs to be considered. Then we add an `advance_shipment` method to handle each associated `Shipment`.
|
217
|
+
|
218
|
+
class SubscriptionCalculator < EventSourcedRecord::Calculator
|
219
|
+
events :subscription_events, :shipments
|
220
|
+
|
221
|
+
def advance_shipment(shipment)
|
222
|
+
@subscription.bottles_left -= shipment.num_bottles
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
In `subscription_event_observer.rb`, we add `:shipment` to `observer`, so the observer will know to fire when we create a shipment.
|
227
|
+
|
228
|
+
class SubscriptionEventObserver < ActiveRecord::Observer
|
229
|
+
observe :subscription_event, :shipment
|
230
|
+
|
231
|
+
def after_create(event)
|
232
|
+
SubscriptionCalculator.new(event).run.save!
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
Note that `SubscriptionEventObserver#after_create` didn't change. When a `Shipment` is created, `after_create` will treat that `Shipment` like another type of event.
|
237
|
+
|
238
|
+
## About calculators
|
239
|
+
|
240
|
+
To get the most benefit out of the calculator, you should make its work idempotent: That is, you should be to run it twice and have no side effects. And don't forget that every time you call `SubscriptionCalculator#run`, it runs through all events in order, even events that have been considered before. So things like sending emails, charging a credit card for a recurring order, or firing off analytic events should not happen anywhere inside `SubscriptionCalculator`.
|
241
|
+
|
242
|
+
The best way to think of it is that `Subscription` is a cache, and `SubscriptionCalculator` contains the logic that fills that cache. Just as you wouldn't want to send an email every time you saved data in Redis, you wouldn't want to send an email every time you called `SubscriptionCalculator#run`.
|
243
|
+
|
244
|
+
For this reason, you'll find that event sourcing fits nicely with architectural styles that move these sorts of side effects out of the model, such as [service classes](https://blog.engineyard.com/2014/keeping-your-rails-controllers-dry-with-services) or [Data Context Interaction](http://dci-in-ruby.info/).
|
245
|
+
|
246
|
+
## Extending Subscription in production
|
247
|
+
|
248
|
+
Over time, your application will change, and `Subscription` will change accordingly. It will handle new concepts which you will need to add to both pre-existing and future subscriptions.
|
249
|
+
|
250
|
+
Because `Subscription` is just a cache, event sourcing gives us a consistent way to handle both pre-existing and future records:
|
251
|
+
|
252
|
+
1. Write a migration if needed to add or change fields on the `subscriptions` table.
|
253
|
+
1. Modify `SubscriptionCalculator` to take the new fields into account.
|
254
|
+
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
|
261
|
+
|
262
|
+
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
|
+
|
264
|
+
## Debugging Subscription in production
|
265
|
+
|
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.
|
267
|
+
|
268
|
+
So when you find a bug, you can often fix it via this process:
|
269
|
+
|
270
|
+
1. Write a test to reproduce the bug.
|
271
|
+
1. Fix `SubscriptionCalculator` to fix the bug.
|
272
|
+
1. Merge and deploy to production.
|
273
|
+
1. Rebuild every `Subscription` with the code above.
|
274
|
+
|
275
|
+
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
|
+
|
277
|
+
## Reporting
|
278
|
+
|
279
|
+
Because `SubscriptionCalculator` rebuilds a record in sequence, it's a piece of cake to only partially rebuild up to a certain time, which can save a lot of time in generating retrospective reports:
|
280
|
+
|
281
|
+
calculator = SubscriptionCalculator.new(subscription)
|
282
|
+
sub_at_end_of_year = calculator.run(last_event_time: Date.new(2014,12,31))
|
283
|
+
|
284
|
+
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.
|
data/README.md
CHANGED
@@ -6,6 +6,10 @@ With Event Sourcing, every change to the state of an object is recorded as an im
|
|
6
6
|
|
7
7
|
For more, see Martin Fowler's writeup of the pattern: http://martinfowler.com/eaaDev/EventSourcing.html
|
8
8
|
|
9
|
+
## Requirements
|
10
|
+
|
11
|
+
Event Sourced Record supports Rails 3.2 and higher.
|
12
|
+
|
9
13
|
## Installation
|
10
14
|
|
11
15
|
Add this line to your application's Gemfile:
|
@@ -24,10 +28,10 @@ Event Sourced Record uses observers, so you'll need to add them to your Gemfile:
|
|
24
28
|
|
25
29
|
gem 'rails-observers'
|
26
30
|
|
27
|
-
Note that only Rails 4 is supported as of this writing. Rails 3 support is coming soon.
|
28
|
-
|
29
31
|
## Usage
|
30
32
|
|
33
|
+
See `Getting_Started.md` for a detailed example.
|
34
|
+
|
31
35
|
Generate the required classes with `rails generate event_sourced_record`:
|
32
36
|
|
33
37
|
rails generate event_sourced_record Subscription \
|
@@ -23,6 +23,16 @@ module EventSourcedRecord::Event
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
+
def respond_to?(meth, include_all = false)
|
27
|
+
if event_type_config && event_type_config.attributes.include?(meth)
|
28
|
+
true
|
29
|
+
elsif event_type_config && event_type_config.attributes.any? { |a| "#{a}=" == meth }
|
30
|
+
true
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
26
36
|
private
|
27
37
|
|
28
38
|
def ensure_data
|
@@ -1,18 +1,27 @@
|
|
1
|
-
|
1
|
+
require 'generators/event_sourced_record'
|
2
|
+
|
3
|
+
class EventSourcedRecord::EventGenerator < ActiveRecord::Generators::Base
|
2
4
|
source_root File.expand_path('../templates', __FILE__)
|
3
5
|
argument :attributes,
|
4
6
|
:type => :array, :default => []
|
5
7
|
|
6
8
|
def create_migration_file
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
9
|
+
ar_major_version = ActiveRecord::VERSION::MAJOR
|
10
|
+
if ar_major_version >= 4
|
11
|
+
attributes_str = attributes.map { |attr|
|
12
|
+
attr_banner = attr.name
|
13
|
+
attr_banner << ":#{attr.type}" if attr.type
|
14
|
+
attr_banner << ':index' if attr.has_index?
|
15
|
+
attr_banner
|
16
|
+
}.join(' ')
|
17
|
+
generate(
|
18
|
+
"migration", "create_#{event_table_name} #{attributes_str}"
|
19
|
+
)
|
20
|
+
else
|
21
|
+
migration_template(
|
22
|
+
"event_migration.ar3.rb", "db/migrate/create_#{event_table_name}.rb"
|
23
|
+
)
|
24
|
+
end
|
16
25
|
end
|
17
26
|
|
18
27
|
def create_model_file
|
@@ -22,10 +31,18 @@ class EventSourcedRecord::EventGenerator < Rails::Generators::NamedBase
|
|
22
31
|
)
|
23
32
|
end
|
24
33
|
|
34
|
+
def attributes_with_index
|
35
|
+
attributes.select { |a| a.has_index? || (a.reference? && options[:indexes]) }
|
36
|
+
end
|
37
|
+
|
25
38
|
hook_for :test_framework, as: :model
|
26
39
|
|
27
40
|
private
|
28
41
|
|
42
|
+
def event_migration_class_name
|
43
|
+
"create_#{event_table_name}".camelize
|
44
|
+
end
|
45
|
+
|
29
46
|
def event_class_name
|
30
47
|
class_name
|
31
48
|
end
|
@@ -1,21 +1,36 @@
|
|
1
|
-
|
1
|
+
require 'generators/event_sourced_record'
|
2
|
+
|
3
|
+
class EventSourcedRecord::ProjectionGenerator < ActiveRecord::Generators::Base
|
2
4
|
source_root File.expand_path('../templates', __FILE__)
|
3
5
|
argument :attributes,
|
4
6
|
:type => :array, :default => []
|
5
7
|
|
6
8
|
def create_migration_file
|
7
|
-
|
8
|
-
|
9
|
-
|
9
|
+
ar_major_version = ActiveRecord::VERSION::MAJOR
|
10
|
+
if ar_major_version >= 4
|
11
|
+
generate(
|
12
|
+
"migration", "create_#{projection_table_name} #{migration_attributes}"
|
13
|
+
)
|
14
|
+
else
|
15
|
+
migration_template(
|
16
|
+
"projection_migration.ar3.rb",
|
17
|
+
"db/migrate/create_#{projection_table_name}.rb"
|
18
|
+
)
|
19
|
+
end
|
10
20
|
end
|
11
21
|
|
12
22
|
def create_model_file
|
23
|
+
ar_major_version = ActiveRecord::VERSION::MAJOR
|
13
24
|
template(
|
14
|
-
|
25
|
+
"projection_model.ar#{ar_major_version}.rb",
|
15
26
|
File.join('app/models', class_path, "#{projection_file_name}.rb")
|
16
27
|
)
|
17
28
|
end
|
18
29
|
|
30
|
+
def attributes_with_index
|
31
|
+
attributes.select { |a| a.has_index? || (a.reference? && options[:indexes]) }
|
32
|
+
end
|
33
|
+
|
19
34
|
hook_for :test_framework, as: :model
|
20
35
|
|
21
36
|
private
|
@@ -31,6 +46,9 @@ class EventSourcedRecord::ProjectionGenerator < Rails::Generators::NamedBase
|
|
31
46
|
attr_strings.join(' ')
|
32
47
|
end
|
33
48
|
|
49
|
+
def projection_migration_class_name
|
50
|
+
"create_#{projection_table_name}".camelize
|
51
|
+
end
|
34
52
|
|
35
53
|
def projection_class_name
|
36
54
|
class_name
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class <%= event_migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :<%= event_table_name %> do |t|
|
4
|
+
<% attributes.each do |attribute| -%>
|
5
|
+
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
|
6
|
+
<% end -%>
|
7
|
+
<% if options[:timestamps] %>
|
8
|
+
t.timestamps
|
9
|
+
<% end -%>
|
10
|
+
end
|
11
|
+
<% attributes_with_index.each do |attribute| -%>
|
12
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
|
13
|
+
<% end -%>
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class <%= projection_migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :<%= projection_table_name %> do |t|
|
4
|
+
<% attributes.each do |attribute| -%>
|
5
|
+
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
|
6
|
+
<% end -%>
|
7
|
+
<% if options[:timestamps] %>
|
8
|
+
t.timestamps
|
9
|
+
<% end -%>
|
10
|
+
end
|
11
|
+
<% attributes_with_index.each do |attribute| -%>
|
12
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
|
13
|
+
<% end -%>
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<% module_namespacing do -%>
|
2
|
+
class <%= projection_class_name %> < <%= projection_parent_class_name.classify %>
|
3
|
+
<% attributes.select {|attr| attr.reference? }.each do |attribute| -%>
|
4
|
+
belongs_to :<%= attribute.name %>
|
5
|
+
<% end -%>
|
6
|
+
has_many :<%= file_name %>_events
|
7
|
+
|
8
|
+
validates :uuid, uniqueness: true
|
9
|
+
end
|
10
|
+
<% end -%>
|
11
|
+
|
@@ -0,0 +1,6 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
require 'generators/event_sourced_record/event_sourced_record_generator'
|
3
|
+
require 'generators/event_sourced_record/calculator_generator'
|
4
|
+
require 'generators/event_sourced_record/event_generator'
|
5
|
+
require 'generators/event_sourced_record/observer_generator'
|
6
|
+
require 'generators/event_sourced_record/projection_generator'
|
@@ -20,9 +20,16 @@ class EventSourcedRecord::EventGeneratorTest < Rails::Generators::TestCase
|
|
20
20
|
end
|
21
21
|
|
22
22
|
test "creates a migration for the event class" do
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
ar_major_version = ActiveRecord::VERSION::MAJOR
|
24
|
+
if ar_major_version >= 4
|
25
|
+
assert @generate_calls['migration'].include?(
|
26
|
+
"create_subscription_events subscription_uuid:string:index event_type:string data:text created_at:datetime"
|
27
|
+
)
|
28
|
+
else
|
29
|
+
assert_migration("db/migrate/create_subscription_events.rb") do |contents|
|
30
|
+
assert_match(/t.string :event_type/, contents)
|
31
|
+
end
|
32
|
+
end
|
26
33
|
end
|
27
34
|
|
28
35
|
test "creates a model for the event class" do
|
@@ -19,18 +19,26 @@ class EventSourcedRecord::ProjectionGeneratorTest < Rails::Generators::TestCase
|
|
19
19
|
end
|
20
20
|
|
21
21
|
test "creates a migration for the projection class" do
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
22
|
+
ar_major_version = ActiveRecord::VERSION::MAJOR
|
23
|
+
if ar_major_version >= 4
|
24
|
+
assert(
|
25
|
+
@generate_calls['migration'].include?(
|
26
|
+
"create_subscriptions user_id:integer bottles_per_shipment:integer bottles_left:integer uuid:string:uniq"
|
27
|
+
),
|
28
|
+
@generate_calls.inspect
|
29
|
+
)
|
30
|
+
else
|
31
|
+
assert_migration("db/migrate/create_subscriptions.rb") do |contents|
|
32
|
+
assert_match(/t.integer :user_id/, contents)
|
33
|
+
end
|
34
|
+
end
|
28
35
|
end
|
29
36
|
|
30
37
|
test "creates a model for the projection class" do
|
31
38
|
assert_file("app/models/subscription.rb") do |contents|
|
32
39
|
assert_match(/class Subscription < ActiveRecord::Base/, contents)
|
33
40
|
assert_match(/validates :uuid, uniqueness: true/, contents)
|
41
|
+
assert_no_match(/attr_accessible :bottles_left/, contents)
|
34
42
|
end
|
35
43
|
end
|
36
44
|
end
|
data/test/test_helper.rb
CHANGED
@@ -3,13 +3,15 @@ require 'rails/test_help'
|
|
3
3
|
require 'rails/generators/test_case'
|
4
4
|
require 'pry'
|
5
5
|
require 'mocha/test_unit'
|
6
|
+
#require 'rails/generators/active_record'
|
6
7
|
$: << 'lib'
|
7
8
|
require 'event_sourced_record'
|
8
|
-
require 'generators/event_sourced_record
|
9
|
-
require 'generators/event_sourced_record/
|
10
|
-
require 'generators/event_sourced_record/
|
11
|
-
require 'generators/event_sourced_record/
|
12
|
-
require 'generators/event_sourced_record/
|
9
|
+
require 'generators/event_sourced_record'
|
10
|
+
#require 'generators/event_sourced_record/event_sourced_record_generator'
|
11
|
+
#require 'generators/event_sourced_record/calculator_generator'
|
12
|
+
#require 'generators/event_sourced_record/event_generator'
|
13
|
+
#require 'generators/event_sourced_record/observer_generator'
|
14
|
+
#require 'generators/event_sourced_record/projection_generator'
|
13
15
|
require 'active_record'
|
14
16
|
|
15
17
|
ActiveRecord::Base.establish_connection(
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: event_sourced_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Francis Hwang
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-01-
|
11
|
+
date: 2015-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -146,6 +146,7 @@ files:
|
|
146
146
|
- ".gitignore"
|
147
147
|
- Appraisals
|
148
148
|
- Gemfile
|
149
|
+
- Getting_Started.md
|
149
150
|
- LICENSE.txt
|
150
151
|
- README.md
|
151
152
|
- Rakefile
|
@@ -155,6 +156,7 @@ files:
|
|
155
156
|
- lib/event_sourced_record/event.rb
|
156
157
|
- lib/event_sourced_record/event/event_type_config.rb
|
157
158
|
- lib/event_sourced_record/version.rb
|
159
|
+
- lib/generators/event_sourced_record.rb
|
158
160
|
- lib/generators/event_sourced_record/USAGE
|
159
161
|
- lib/generators/event_sourced_record/calculator_generator.rb
|
160
162
|
- lib/generators/event_sourced_record/event_generator.rb
|
@@ -162,9 +164,12 @@ files:
|
|
162
164
|
- lib/generators/event_sourced_record/observer_generator.rb
|
163
165
|
- lib/generators/event_sourced_record/projection_generator.rb
|
164
166
|
- lib/generators/event_sourced_record/templates/calculator.rb
|
167
|
+
- lib/generators/event_sourced_record/templates/event_migration.ar3.rb
|
165
168
|
- lib/generators/event_sourced_record/templates/event_model.rb
|
166
169
|
- lib/generators/event_sourced_record/templates/observer.rb
|
167
|
-
- lib/generators/event_sourced_record/templates/
|
170
|
+
- lib/generators/event_sourced_record/templates/projection_migration.ar3.rb
|
171
|
+
- lib/generators/event_sourced_record/templates/projection_model.ar3.rb
|
172
|
+
- lib/generators/event_sourced_record/templates/projection_model.ar4.rb
|
168
173
|
- lib/generators/rspec/service_generator.rb
|
169
174
|
- lib/generators/rspec/templates/service_spec.rb
|
170
175
|
- lib/generators/test_unit/service_generator.rb
|
/data/lib/generators/event_sourced_record/templates/{projection_model.rb → projection_model.ar4.rb}
RENAMED
File without changes
|