eventsimple 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +18 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +164 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +320 -0
  9. data/Guardfile +35 -0
  10. data/LICENSE +22 -0
  11. data/README.md +510 -0
  12. data/Rakefile +17 -0
  13. data/app/controllers/eventsimple/application_controller.rb +15 -0
  14. data/app/controllers/eventsimple/entities_controller.rb +59 -0
  15. data/app/controllers/eventsimple/home_controller.rb +5 -0
  16. data/app/controllers/eventsimple/models_controller.rb +10 -0
  17. data/app/views/eventsimple/entities/show.html.erb +109 -0
  18. data/app/views/eventsimple/home/index.html.erb +0 -0
  19. data/app/views/eventsimple/models/show.html.erb +19 -0
  20. data/app/views/eventsimple/shared/_header.html.erb +26 -0
  21. data/app/views/eventsimple/shared/_sidebar.html.erb +19 -0
  22. data/app/views/eventsimple/shared/_style.html.erb +105 -0
  23. data/app/views/layouts/eventsimple/application.html.erb +76 -0
  24. data/catalog-info.yaml +11 -0
  25. data/config/routes.rb +11 -0
  26. data/eventsimple.gemspec +42 -0
  27. data/lib/dry_types.rb +5 -0
  28. data/lib/eventsimple/configuration.rb +36 -0
  29. data/lib/eventsimple/data_type.rb +48 -0
  30. data/lib/eventsimple/dispatcher.rb +17 -0
  31. data/lib/eventsimple/engine.rb +37 -0
  32. data/lib/eventsimple/entity.rb +54 -0
  33. data/lib/eventsimple/event.rb +189 -0
  34. data/lib/eventsimple/event_dispatcher.rb +93 -0
  35. data/lib/eventsimple/generators/install_generator.rb +42 -0
  36. data/lib/eventsimple/generators/outbox/install_generator.rb +31 -0
  37. data/lib/eventsimple/generators/outbox/templates/create_outbox_cursor.erb +13 -0
  38. data/lib/eventsimple/generators/templates/create_events.erb +21 -0
  39. data/lib/eventsimple/generators/templates/event.erb +8 -0
  40. data/lib/eventsimple/invalid_transition.rb +14 -0
  41. data/lib/eventsimple/message.rb +23 -0
  42. data/lib/eventsimple/metadata.rb +11 -0
  43. data/lib/eventsimple/metadata_type.rb +38 -0
  44. data/lib/eventsimple/outbox/consumer.rb +52 -0
  45. data/lib/eventsimple/outbox/models/cursor.rb +25 -0
  46. data/lib/eventsimple/reactor_worker.rb +18 -0
  47. data/lib/eventsimple/support/spec_helpers.rb +47 -0
  48. data/lib/eventsimple/version.rb +5 -0
  49. data/lib/eventsimple.rb +41 -0
  50. data/log/development.log +0 -0
  51. data/sonar-project.properties +4 -0
  52. metadata +304 -0
data/README.md ADDED
@@ -0,0 +1,510 @@
1
+ # Eventsimple
2
+ [![Github Actions Badge](https://github.com/wealthsimple/eventsimple/actions/workflows/main.yml/badge.svg)](https://github.com/wealthsimple/eventsimple/actions)
3
+
4
+ ## What
5
+ Eventsimple implements a simple deterministic event driven system using ActiveRecord and Sidekiq.
6
+
7
+ Use Eventsimple to:
8
+
9
+ * Add Event Sourcing to your ActiveRecord models.
10
+ * Implement Pub/Sub.
11
+ * Implement a transactional outbox.
12
+ * Store audit logs of changes to your ActiveRecord objects.
13
+
14
+ Eventsimple uses standard Rails features like [Single Table Inheritance](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html) and [Optimistic Locking](https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html) to implement a simple event driven system.
15
+ Async workflows are handled using [Sidekiq](https://github.com/sidekiq/sidekiq).
16
+
17
+ Typical events in Eventsimple are ActiveRecord models using STI and look like this:
18
+
19
+ ```ruby
20
+ <UserComponent::Events::Created
21
+ id: 1,
22
+ aggregate_id: 'user-123',
23
+ type: "Created",
24
+ data: {
25
+ name: "John doe",
26
+ email: "johndoe@example.com",
27
+ },
28
+ created_at: 2022-01-01T00:00:00.000000,
29
+ updated_at: 2022-01-01T00:00:00.000000,
30
+ >
31
+
32
+ <UserComponent::Events::Deleted
33
+ id: 1,
34
+ aggregate_id: 'user-123',
35
+ type: "Deleted",
36
+ created_at: 2022-01-01T00:30:00.000000,
37
+ updated_at: 2022-01-01T00:30:00.000000,
38
+ >
39
+ ```
40
+
41
+ ## Setup
42
+
43
+ Add the following line to your Gemfile:
44
+
45
+ ```
46
+ gem 'eventsimple'
47
+ ```
48
+ Then run `bundle install`
49
+
50
+ The eventsimple UI allows you to view and navigate event history. Add the following line to your routes.rb to use it:
51
+
52
+ ```
53
+ mount Eventsimple::Engine => '/eventsimple'
54
+ ```
55
+
56
+ Generate a migration and add `Eventsimple` to an existing ActiveRecord model.
57
+
58
+ ```ruby
59
+ bundle exec rails generate eventsimple:event User
60
+ ```
61
+
62
+ This will result in the following changes:
63
+
64
+ ```ruby
65
+ # ActiveRecord Classes
66
+ class User < ApplicationRecord
67
+ extend Eventsimple::Entity
68
+ event_driven_by UserEvent, aggregate_id: :id
69
+ end
70
+
71
+ class UserEvent < ApplicationRecord
72
+ extend Eventsimple::Event
73
+ drives_events_for User, events_namespace: 'UserComponent::Events', aggregate_id: :id
74
+ end
75
+ # Change aggregate_id to the column that represents the unique primary key for your model.
76
+
77
+ # Data migration
78
+ create_table :user_events do |t|
79
+ # Change this to string if your aggregates primary key is a string type
80
+ t.bigint :aggregate_id, null: false, index: true
81
+ t.string :idempotency_key, null: true
82
+ t.string :type, null: false
83
+ t.json :data, null: false, default: {}
84
+ t.json :metadata, null: false, default: {}
85
+
86
+ t.timestamps
87
+
88
+ t.index :idempotency_key, unique: true
89
+ end
90
+
91
+ add_column :users, :lock_version, :integer
92
+ ```
93
+
94
+ Adding lock_version to the model enables [optimistic locking](https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html) and protects against concurrent updates to stale versions of the model. Eventsimple will automatically retry on concurrency failures.
95
+
96
+ `events_namespace` is an optional argument pointing to the directory where your events classes are defined. If you do not specify this argument, Eventsimple will store the full namespace of the event classes in the STI type column.
97
+
98
+ ### Event Table definition
99
+
100
+ | Column | Description |
101
+ | ------------- | ------------- |
102
+ | aggregate_id | Stores the primary key of the entity. |
103
+ | idempotency_key | Optional value which can be used to write events that have uniqueness constraints. |
104
+ | type | Used by rails to implement Single Table inheritance. Stores the event class name. |
105
+ | data | Stores the event payload |
106
+ | metadata | Stores optional metadata associated with the event |
107
+
108
+ ## Usage
109
+
110
+ An example event:
111
+
112
+ ```ruby
113
+ module UserComponent
114
+ module Events
115
+ class Created < UserEvent
116
+ # Optional: Rails by default will use JSON serialization for the data attribute. Use Eventsimple::DataType to serialize/deserialize the data attribute using the Message subclass below which uses dry-struct.
117
+ attribute :data, Eventsimple::DataType.new(self)
118
+
119
+ class Message < Eventsimple::Message
120
+ attribute :canonical_id, DryTypes::Strict::String
121
+ attribute :email, DryTypes::Strict::String
122
+ end
123
+
124
+ # Optional: Context specific validations that can be extended onto the model on event creation.
125
+ validates_with UserForm
126
+
127
+ # Optional: Implement state machine checks to determine if the event is allowed to be written.
128
+ # Will raise Eventsimple::InvalidTransition on failure.
129
+ def can_apply?(user)
130
+ user.new_record?
131
+ end
132
+
133
+ # Optional: Update the state of your model based on data in the event payload.
134
+ def apply(user)
135
+ user.canonical_id = data.canonical_id
136
+ user.email = data.email
137
+
138
+ user
139
+ end
140
+ end
141
+ end
142
+ end
143
+ ```
144
+
145
+ Write an event:
146
+
147
+ ```ruby
148
+ user = User.new
149
+
150
+ UserComponent::Events::Created.create(
151
+ user: user,
152
+ data: { canonical_id: 'user-123', email: 'johndoe@example.com' },
153
+ metadata: { actor_id: 'user-123' } # optional metadata
154
+ )
155
+
156
+ if user.errors.any?
157
+ # render user errors
158
+ else
159
+ # render success
160
+ end
161
+ ```
162
+
163
+ ### Using Dry::Struct
164
+ The Eventsimple::Message class is a subclass of Dry::Struct. Some common options you can use are:
165
+
166
+ ```ruby
167
+ class Message < Eventsimple::Message
168
+ # attribute key is required and can not be nil
169
+ attribute :canonical_id, DryTypes::Strict::String
170
+
171
+ # attribute key is required but can be nil
172
+ attribute :required_key, DryTypes::Strict::String.optional
173
+
174
+ # attribute key is not required and can also be nil
175
+ attribute? :optional_key, DryTypes::Strict::String.optional
176
+
177
+ # use default value if attribute key is missing or if value is nil
178
+ # Note this is not the typical behaviour for dry-struct and is a customization in the Eventsimple::Message class.
179
+ attribute :default_key, DryTypes::Strict::String.default('default')
180
+ end
181
+ ```
182
+
183
+ ### Event Reactors
184
+
185
+ Callback to events can be defined as reactors in the dispatcher class.
186
+ Reactors may be `async` or `sync`, depending on the usecase.
187
+
188
+ #### Sync Reactors
189
+ Sync reactors are executed within the context of the event transaction block.
190
+ They should **only** contain business logic that make additional database writes.
191
+
192
+ This is because executing writes to other data stores, e.g API call or writes to kafka/sqs, will result in the transaction being non-deterministic.
193
+
194
+ #### Async Reactors
195
+ Async reactors are executed via Sidekiq. Eventsimple implements checks to enforce reliable eventually consistent behaviour.
196
+
197
+ Use Async reactors to kick off async workflows or writes to external data sources as a side effect of model updates.
198
+
199
+ Reactor example:
200
+
201
+ ```ruby
202
+ # Register your dispatch class in an initializer.
203
+ Eventsimple.configure do |config|
204
+ config.dispatchers = %w[
205
+ UserComponent::Dispatcher
206
+ ]
207
+ end
208
+
209
+ # Register reactors in the dispatcher class.
210
+ class UserComponent::Dispatcher < Eventsimple::EventDispatcher
211
+ # one to one
212
+ on UserComponent::Events::Created,
213
+ async: UserComponent::Reactors::Created::SendNotification
214
+
215
+ # or many to many
216
+ on [
217
+ UserComponent::Events::Locked,
218
+ UserComponent::Events::Unlocked
219
+ ], sync: [
220
+ UserComponent::Reactors::Locking::UpdateLockCounter,
221
+ UserComponent::Reactors::Locking::UpdateLockMetrics
222
+ ]
223
+ end
224
+
225
+ # Reactor classes accept the event as the only argument in the constructor
226
+ # and must define a `call` method
227
+ module UserComponent::Reactors::Created
228
+ class SendNotification
229
+ def initialize(event)
230
+ @event = event
231
+ @user = event.aggregate
232
+ end
233
+ attr_reader :event, :user
234
+
235
+ def call
236
+ # do something
237
+ end
238
+ end
239
+ end
240
+ ```
241
+
242
+ ## Configuring an outbox consumer
243
+
244
+ For many use cases, async reactors are sufficient to handle workflows like making an API call or publishing to a message broker.
245
+ However since reactors use Sidekiq, order is not guaranteed.
246
+
247
+ Eventsimple provides an outbox implementation with order and eventual consistency guarantees.
248
+
249
+ **Caveat**: The current implementation leverages a single advisory lock to guarantee write order. This will significantly impact write throughput on the model. On a standard Aurora instance for example, write throughput is limited to ~300 events per second.
250
+
251
+ For more information on why an advisory lock is required:
252
+ https://github.com/pawelpacana/account-basics
253
+
254
+ ### Setup an ordered outbox
255
+
256
+ Generate migration to setup the outbox cursor table. This table is used to track cursor positions.
257
+
258
+ ```ruby
259
+ bundle exec rails g eventsimple:outbox:install
260
+ ```
261
+
262
+ Create a consummer and processor class for the outbox.
263
+ Note: The presence of the consumer class moves all writes to the respective events table to be written using an advisory lock.
264
+
265
+ Only a single outbox consumer per events table is supported. **DO NOT** create multiple consumers for the same events table.
266
+
267
+ ```ruby
268
+ require 'eventsimple/outbox/consumer'
269
+
270
+ module UserComponent
271
+ class Consumer
272
+ extend Eventsimple::Outbox::Consumer
273
+
274
+ consumes_event UserEvent
275
+ processor EventProcessor
276
+ end
277
+ end
278
+ ```
279
+
280
+ ```ruby
281
+ module UserComponent
282
+ class EventProcessor
283
+ def initialize(event)
284
+ @event = event
285
+ end
286
+ attr_reader :event
287
+
288
+ def call
289
+ Rails.logger.info("PROCESSING EVENT: #{event.id}")
290
+ end
291
+ end
292
+ end
293
+ ```
294
+
295
+ ### Usage
296
+ Create a rake task to run the consumer
297
+
298
+ ```ruby
299
+ namespace :consumers do
300
+ desc 'Starts the user event outbox consumer'
301
+ task :user_events do
302
+ UserComponent::Consumer.start
303
+ end
304
+ end
305
+ ```
306
+
307
+ ## Helper methods
308
+ Some convenience methods are provided to help with common use cases.
309
+
310
+ **`#reproject(at: nil)`**
311
+
312
+ Reproject an entity from events (rebuilds in memory but does not persist the entity).
313
+
314
+ ```ruby
315
+ module UserComponent
316
+ module Events
317
+ class Created < UserEvent
318
+ # ...
319
+
320
+ def apply(user)
321
+ user.email = data.email
322
+
323
+ # Changes the projection to start tracking a sign up timestamp.
324
+ user.signed_up_at = self.created_at
325
+ end
326
+ end
327
+ end
328
+ end
329
+
330
+ user = User.find_by(canonical_id: 'user-123')
331
+ user.reproject
332
+ user.changes # => { sign_up_at: [nil, "2022-01-01 00:00:00 UTC"] }
333
+ user.save!
334
+ ```
335
+
336
+ Or reproject the model to inspect what it looked like at a particular point in time.
337
+ ```ruby
338
+ user = User.find_by(canonical_id: 'user-123')
339
+ user.reproject(at: 1.day.ago)
340
+ user.changes
341
+ ```
342
+
343
+ **`#projection_matches_events?`**
344
+
345
+ Verify that a reprojection of the model matches it's current state.
346
+
347
+ ```ruby
348
+ user = User.find_by(canonical_id: 'user-123')
349
+ user.update(name: 'something_else')
350
+ user.projection_matches_events? => false
351
+ ```
352
+
353
+ **`.ignored_for_projection`**
354
+
355
+ Skip properties on a model that are not managed by the event driven system. This will prevent a reset of the value in case of a reprojection.
356
+ Useful if the model that is being event driven has some properties that are managed through other mechanics.
357
+
358
+ `id` and `lock_version` columns are always ignored by default.
359
+
360
+ ```ruby
361
+ class User
362
+ self.ignored_for_projection = %i[last_sign_in_at]
363
+ end
364
+ ```
365
+
366
+ ## Common Use cases
367
+
368
+ ### I want to add validations to my model.
369
+
370
+ You _can_ add conditional validations to the model as usual. For example to verify an email:
371
+
372
+ ```ruby
373
+ class User
374
+ EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
375
+
376
+ validates :email, presence: true, format: {
377
+ with: EMAIL_REGEX
378
+ }, if: :email_changed?
379
+
380
+ validate :allowed_emails, if: :email_changed?
381
+
382
+ def allowed_emails
383
+ return if EmailBlacklist.allowed?(email)
384
+
385
+ errors.add(:email, :invalid, value: email)
386
+ end
387
+ end
388
+ ```
389
+
390
+ However, conditional validations tend to become more complex over time. An alternative approach can be to validate at the point _when_ a handle is being updated.
391
+
392
+ Consider extending the model with a mixin, to apply the validation only when the email is actually being set.
393
+
394
+ ```ruby
395
+ module UpdateEmailForm
396
+ def self.extended(base)
397
+ base.class_eval do
398
+ EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
399
+
400
+ validates :email, presence: true, format: {
401
+ with: EMAIL_REGEX
402
+ }
403
+
404
+ validate :allowed_emails, if: :email_changed?
405
+
406
+ def allowed_emails
407
+ return if EmailBlacklist.allowed?(email)
408
+
409
+ errors.add(:email, :invalid, value: email)
410
+ end
411
+ end
412
+ end
413
+
414
+ user = User.find_by(canonical_id: 'user-123').extend(UpdateEmailForm)
415
+
416
+ UserComponent::Events::EmailUpdated.create(user: user, data: { email: 'email' })
417
+ ```
418
+
419
+ You can configure mixins in the event class itself, so that they are applied automatically at the point of event creating. The following example will extend the user with UpdateEmailForm on user create:
420
+
421
+ ```ruby
422
+ class UserComponent::Events::Created < UserEvent
423
+ ...
424
+
425
+ validates_with UpdateEmailForm
426
+
427
+ ...
428
+ end
429
+ ```
430
+
431
+ ### I want to modify an existing event by adding a new attribute
432
+ New attributes should always be added as being either optional or required with a default value.
433
+
434
+ ```ruby
435
+ class UserComponent::Events::Created < Eventsimple::Message
436
+ attribute :new_attribute_1, DryTypes::Strict::String.default('default')
437
+ attribute? :new_attribute_2, DryTypes::Strict::String.optional
438
+ end
439
+ ```
440
+
441
+ This guarantees compatibility with older events which do not contain this attribute. Old events will be loaded with the attribute being either nil or the new default.
442
+
443
+ To ensure old models are also in a consistent state, a data migration may be required to update the new attribute to the new default.
444
+
445
+ ```ruby
446
+ # migration file
447
+ add_column :users, :new_attribute_1, :string, default: 'new_default'
448
+
449
+ User.where(new_attribute_1: nil).find_in_batches do |batch|
450
+ batch.update_all(new_attribute_1: 'new_default')
451
+ end
452
+ ```
453
+
454
+ ### I want to modify an event by removing a unused attribute
455
+ Simply remove the attribute in code and any usage references. Any persisted data in old events will be ignored going forward, so a data migration is not explicitly needed.
456
+
457
+ However if this is something that is required, we can follow up code removal with a data migration like:
458
+
459
+ ```ruby
460
+ UserEvent.where(type: 'MyEventName').in_batches do |batch|
461
+ batch.update_all("data = data::jsonb - 'old_attribute_1' - 'old_attribute_2'")
462
+ end
463
+ ```
464
+
465
+ ### I want to remove an event that is not longer required
466
+ * If an event and any properties it sets are no longer required, we can delete the Event, any code references and the model columns.
467
+ * The persisted events will be ignored going forward, so a data migration is not explicitly needed.
468
+
469
+ However if this is something that is required, we can follow up code removal with a data migration like:
470
+
471
+ ```ruby
472
+ # Remove all code references and then run the following migration:
473
+
474
+ UserEvent.where(type: 'MyEventName').in_batches do |batch|
475
+ batch.delete_all
476
+ end
477
+ ```
478
+
479
+ ### I want to ignore InvalidTransition errors
480
+
481
+ The InvalidTransition error is raised when the `can_apply?` method of an Event returns `false`. In many cases this indicates a bug in the code, but in some cases it is expected behaviour.
482
+
483
+ An example scenario for not wanting to raise the error is when the `can_apply?` method is primarily defending against redundant events from being written, perhaps when consuming messages from a message broker.
484
+
485
+ You can mute these errors by calling `rescue_invalid_transition` on the event class. This will cause the event to be ignored and the model to remain unchanged. Optionally, you can pass a block to handle the error.
486
+
487
+ ```ruby
488
+ module FooComponent
489
+ module Events
490
+ class BarToTrue < FooEvent
491
+ rescue_invalid_transition do |error|
492
+ logger.info("Receive invalid transition error", error)
493
+ end
494
+
495
+ def can_apply?(foo)
496
+ !foo.bar
497
+ end
498
+
499
+ def apply(foo)
500
+ foo.bar = true
501
+
502
+ foo
503
+ end
504
+ end
505
+ end
506
+ end
507
+ ```
508
+
509
+ ### Credits
510
+ Special credits to [kickstarter](https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224) and [Eventide Project](https://github.com/eventide-project) for much of the inspiration for this gem.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
6
+ load 'rails/tasks/engine.rake'
7
+
8
+ load 'rails/tasks/statistics.rake'
9
+
10
+ require 'bundler/gem_tasks'
11
+ require 'rspec/core/rake_task'
12
+ require "rubocop/rake_task"
13
+
14
+ RSpec::Core::RakeTask.new(:spec)
15
+ RuboCop::RakeTask.new
16
+
17
+ task default: %i[spec rubocop]
@@ -0,0 +1,15 @@
1
+ module Eventsimple
2
+ class ApplicationController < ActionController::Base
3
+ helper_method :event_class_names
4
+
5
+ def event_class_names
6
+ @event_class_names ||= event_classes.map(&:name)
7
+ end
8
+
9
+ def event_classes
10
+ Rails.application.eager_load!
11
+
12
+ Eventsimple.configuration.ui_visible_models
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,59 @@
1
+ module Eventsimple
2
+ class EntitiesController < ApplicationController
3
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
4
+ def show
5
+ @model_name = params[:model_name]
6
+ @model_class = event_classes.find { |d| d.name == @model_name }
7
+ @aggregate_id = params[:id]
8
+ @event_id = params[:e] || -1
9
+ @tab_id = params[:t] == 'event' ? 'event' : 'entity'
10
+
11
+ primary_key = @model_class.event_class._aggregate_id
12
+ @entity = @model_class.find_by!(primary_key => @aggregate_id)
13
+ @entity_event_history = @entity.events.reverse
14
+
15
+ @selected_event = @entity_event_history.find { |e|
16
+ e.id == @event_id.to_i
17
+ } || @entity_event_history.first
18
+
19
+ previous_index = @entity_event_history.find_index { |e| e.id == @selected_event.id } + 1
20
+ @previous_event = @entity_event_history[previous_index]
21
+
22
+ @entity_changes = changes
23
+ rescue StandardError => e
24
+ @error_message = e.message
25
+ render html: '', layout: true
26
+ end
27
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
28
+
29
+ private
30
+
31
+ def changes
32
+ current_attributes.map do |attr_name, *|
33
+ {
34
+ label: attr_name,
35
+ current_value: current_attributes[attr_name],
36
+ historical_value: previous_attributes[attr_name],
37
+ is_changed: current_attributes[attr_name] != previous_attributes[attr_name],
38
+ }
39
+ end
40
+ end
41
+
42
+ def current_attributes
43
+ @current_attributes ||= @entity.reproject(at: @selected_event.created_at).attributes.except(
44
+ 'lock_version',
45
+ )
46
+ end
47
+
48
+ def previous_attributes
49
+ @previous_attributes ||=
50
+ if @previous_event
51
+ @entity.reproject(at: @previous_event.created_at).attributes.except(
52
+ 'lock_version',
53
+ )
54
+ else
55
+ @model_class.column_defaults
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,5 @@
1
+ module Eventsimple
2
+ class HomeController < ApplicationController
3
+ def index; end
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ module Eventsimple
2
+ class ModelsController < ApplicationController
3
+ def show
4
+ @model_name = params[:name]
5
+
6
+ model_event_class = event_classes.find { |d| d.name == @model_name }.event_class
7
+ @latest_entities = model_event_class.last(20).reverse
8
+ end
9
+ end
10
+ end