rails-event-sourcing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6515057a49928dc1c71c0b4767bea5c0a825f2be5f411a8a35457e53d17a9380
4
+ data.tar.gz: bfe0bc40897dca758f5be3b40118ea2381c400da92f0b4b77597a75351a8f5b7
5
+ SHA512:
6
+ metadata.gz: 5b025b485eb1d0e456c1818b9d0698a5ab8336a13f21d391230e0ad09315bceaa685b418a046ded7134771e2f4217650236ae6f2df16f276061db54e61bce5e3
7
+ data.tar.gz: bba96d5db5376ed99fb9695faa2318645633feb6bb12439597932490b3cbb521724a7579d184fa2bcf141d661177c0f261b9f2f8dd95fbf4ade60c7e532cd879
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require rails_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ ---
2
+ inherit_from:
3
+ - https://relaxed.ruby.style/rubocop.yml
4
+
5
+ require:
6
+ - rubocop-rspec
7
+
8
+ AllCops:
9
+ Exclude:
10
+ - _misc/*
11
+ - bin/*
12
+ - spec/dummy/**/*
13
+ NewCops: enable
14
+ SuggestExtensions: false
15
+ TargetRubyVersion: 2.6
16
+
17
+ Naming/FileName:
18
+ Exclude:
19
+ - lib/rails-event-sourcing.rb
20
+
21
+ RSpec/ExampleLength:
22
+ Max: 8
23
+
24
+ RSpec/MultipleMemoizedHelpers:
25
+ Max: 8
26
+
27
+ Style/GuardClause:
28
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in rails-event-sourcing.gemspec
6
+ gemspec
7
+
8
+ gem 'rspec-rails'
9
+ gem 'simplecov'
10
+ gem 'sqlite3'
11
+
12
+ # Linters
13
+ gem 'rubocop', '~> 1.21'
14
+ gem 'rubocop-rspec'
15
+
16
+ # Tools
17
+ gem 'awesome_print'
18
+ gem 'pry-rails'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Mattia Roccoberton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # Rails Event Sourcing
2
+
3
+ This gem provides features to setup an event sourcing application using ActiveRecord.
4
+ ActiveJob is necessary only to use async callbacks.
5
+
6
+ > DISCLAIMER: this project is in alpha stage
7
+
8
+ The main components are:
9
+ - **event**: model used to track state changes for an entity;
10
+ - **command**: wrap events creation;
11
+ - **dispatcher**: events' callbacks (sync and async).
12
+
13
+ This gem adds a layer to handle events for the underlying application models. In short:
14
+ - an event model is created for each "event-ed" application model;
15
+ - every change to an application model (named _aggregate_ in the event perspective) is stored in an event record;
16
+ - querying application models is the same as usual;
17
+ - writing changes to application entities is applied creating events.
18
+
19
+ A sample workflow can be:
20
+
21
+ ```rb
22
+ # I have a plain Post model:
23
+ post = Post.find(1)
24
+ # When I need to update that post:
25
+ Posts::ChangedDescription.create!(post: post, description: 'My beautiful post content')
26
+ # When I need to create a new post:
27
+ Posts::Created.create!(title: 'New post!', description: 'Another beautiful post')
28
+ # I can query the events for an aggregated entity:
29
+ events = Posts::Event.events_for(post) # Posts::Event is usually a base class for all events for an aggregate (using STI)
30
+ # I can rollback to a specific version of the aggregated entity:
31
+ events[2].rollback! # the aggregated entity is restored to the specific state, the events above that point are removed
32
+ ```
33
+
34
+ The project is based on a [demo app](https://github.com/pcreux/event-sourcing-rails-todo-app-demo) proposed by [Philippe Creux](https://github.com/pcreux) and his video presentation for Rails Conf 2019:
35
+
36
+ [![Event Sourcing made Simple by Philippe Creux](https://img.youtube.com/vi/ulF6lEFvrKo/0.jpg)](https://www.youtube.com/watch?v=ulF6lEFvrKo "Event Sourcing made Simple by Philippe Creux")
37
+
38
+ Please :star: if you like it.
39
+
40
+ ## Usage
41
+
42
+ - Add to your Gemfile: `gem 'rails-event-sourcing'` (and execute `bundle`)
43
+ - Create a migration per model to store the related events, example for User:
44
+ `bin/rails generate migration CreateUserEvents type:string user:reference data:text metadata:text`
45
+ - Create the events, example for `Users::Created`:
46
+ ```rb
47
+ module Users
48
+ class Created < RailsEventSourcing::BaseEvent
49
+ self.table_name = 'user_events' # usually this fits better in a base class using STI
50
+
51
+ belongs_to :user, autosave: false
52
+
53
+ data_attributes :name
54
+
55
+ def apply(user)
56
+ # this method will be applied when the event is created
57
+ user.name = name
58
+ user
59
+ end
60
+ end
61
+ end
62
+ ```
63
+ - Invoke an event with: `Users::Created.create!(name: 'Some user')`
64
+ - Optionally create a Command, example:
65
+ ```rb
66
+ module Users
67
+ class CreateCommand
68
+ include RailsEventSourcing::Command
69
+
70
+ attributes :user, :name
71
+
72
+ def build_event
73
+ # this method will be applied when the command is executed
74
+ Users::Created.new(user_id: user.id, name: name)
75
+ end
76
+ end
77
+ end
78
+ ```
79
+ - Invoke a command with: `Users::CreateCommand.call(name: 'Some name')`
80
+
81
+ ## Examples
82
+
83
+ Please take a look at the [dummy app](spec/dummy/app) for a detailed example.
84
+
85
+ Events:
86
+ ```rb
87
+ TodoLists::Created.create!(name: 'My TODO 1')
88
+ TodoLists::NameUpdated.create!(name: 'My TODO!', todo_list: TodoList.first)
89
+ TodoItems::Created.create!(todo_list_id: TodoList.first.id, name: 'First item')
90
+ TodoItems::Completed.create!(todo_item: TodoItem.last)
91
+ ```
92
+
93
+ Commands:
94
+ ```rb
95
+ TodoLists::Create.call(name: 'My todo')
96
+ TodoItems::Create.call(todo_list: TodoList.first, name: 'Some task')
97
+ ```
98
+
99
+ Dispatchers:
100
+ ```rb
101
+ class TodoItemsDispatcher < RailsEventSourcing::EventDispatcher
102
+ on TodoItems::Created, trigger: ->(todo_item) { puts ">>> TodoItems::Created [##{todo_item.id}]" }
103
+ on TodoItems::Completed, async: Notifications::TodoItems::Completed
104
+ end
105
+ # Now when the event TodoItems::Created is created the trigger callback is executed
106
+ ```
107
+
108
+ ## To do
109
+
110
+ - [ ] Generators for events, commands and dispatchers
111
+ - [ ] Database specific optimizations
112
+ - [ ] Add more usage examples
113
+
114
+ ## Do you like it? Star it!
115
+
116
+ If you use this component just star it. A developer is more motivated to improve a project when there is some interest.
117
+
118
+ Or consider offering me a coffee, it's a small thing but it is greatly appreciated: [about me](https://www.blocknot.es/about-me).
119
+
120
+ ## Contributors
121
+
122
+ - [Mattia Roccoberton](https://www.blocknot.es): author
123
+
124
+ ## License
125
+
126
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "rails-event-sourcing"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/pry ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'pry' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("pry", "pry")
data/bin/rails ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENV['RAILS_ENV'] ||= 'test'
6
+
7
+ ENGINE_ROOT = File.expand_path('..', __dir__)
8
+ APP_PATH = File.expand_path('../spec/dummy/config/application', __dir__)
9
+
10
+ # Set up gems listed in the Gemfile.
11
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
12
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
13
+
14
+ require "rails/all"
15
+ require "rails/engine/commands"
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/rubocop ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsEventSourcing
4
+ # This is the BaseEvent class that all Events inherit from.
5
+ # It takes care of serializing `data` and `metadata` via json
6
+ # It defines setters and accessors for the defined `data_attributes`
7
+ # After create, it calls `apply` to apply changes.
8
+ class BaseEvent < ::ActiveRecord::Base
9
+ self.abstract_class = true
10
+
11
+ serialize :data, JSON
12
+ serialize :metadata, JSON
13
+
14
+ before_validation :preset_aggregate
15
+ before_create :apply_and_persist
16
+ after_create :dispatch
17
+
18
+ after_initialize do
19
+ self.data ||= {}
20
+ self.metadata ||= {}
21
+ end
22
+
23
+ scope :recent_first, -> { reorder('id DESC') }
24
+
25
+ def aggregate
26
+ public_send aggregate_name
27
+ end
28
+
29
+ def aggregate=(model)
30
+ public_send "#{aggregate_name}=", model
31
+ end
32
+
33
+ def aggregate_id=(id)
34
+ public_send "#{aggregate_name}_id=", id
35
+ end
36
+
37
+ # Apply the event to the aggregate passed in.
38
+ # Must return the aggregate.
39
+ def apply(aggregate)
40
+ raise NotImplementedError
41
+ end
42
+
43
+ def aggregate_id
44
+ public_send "#{aggregate_name}_id"
45
+ end
46
+
47
+ def build_aggregate
48
+ public_send "build_#{aggregate_name}"
49
+ end
50
+
51
+ # Rollback an aggregate entity to a specific version
52
+ #
53
+ # Update the aggregate with the changes up to the current event and
54
+ # destroys the events after
55
+ def rollback!
56
+ base_class = self.class.superclass == RailsEventSourcing::BaseEvent ? self.class : self.class.superclass
57
+ new_attributes = aggregate.class.new.attributes
58
+ preserve_columns = new_attributes.keys - base_class.reserved_column_names
59
+ new_attributes.slice!(*preserve_columns)
60
+ aggregate.assign_attributes(new_attributes)
61
+ aggregate.transaction do
62
+ base_class.events_for(aggregate).where('id > ?', id).destroy_all
63
+ base_class.events_for(aggregate).reorder('id ASC').each do |event|
64
+ event.apply(aggregate)
65
+ end
66
+ aggregate.save!
67
+ end
68
+ end
69
+
70
+ delegate :aggregate_name, to: :class
71
+
72
+ class << self
73
+ def aggregate_name
74
+ inferred_aggregate = reflect_on_all_associations(:belongs_to).first
75
+ raise "Events must belong to an aggregate" if inferred_aggregate.nil?
76
+
77
+ inferred_aggregate.name
78
+ end
79
+
80
+ # Define attributes to be serialize in the `data` column.
81
+ # It generates setters and getters for those.
82
+ #
83
+ # Example:
84
+ #
85
+ # class MyEvent < RailsEventSourcing::BaseEvent
86
+ # data_attributes :title, :description, :drop_id
87
+ # end
88
+ def data_attributes(*attrs)
89
+ @data_attributes ||= []
90
+
91
+ attrs.map(&:to_s).each do |attr|
92
+ @data_attributes << attr unless @data_attributes.include?(attr)
93
+
94
+ define_method attr do
95
+ self.data ||= {}
96
+ self.data[attr]
97
+ end
98
+
99
+ define_method "#{attr}=" do |arg|
100
+ self.data ||= {}
101
+ self.data[attr] = arg
102
+ end
103
+ end
104
+
105
+ @data_attributes
106
+ end
107
+
108
+ # Underscored class name by default. ex: "post/updated"
109
+ # Used when sending events to the data pipeline
110
+ def event_name
111
+ name.underscore
112
+ end
113
+
114
+ def events_for(aggregate)
115
+ where(aggregate_name => aggregate)
116
+ end
117
+
118
+ def reserved_column_names
119
+ %w[id created_at updated_at]
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ # Build aggregate when the event is creating an aggregate
126
+ def preset_aggregate
127
+ self.aggregate ||= build_aggregate
128
+ end
129
+
130
+ # Apply the transformation to the aggregate and save it
131
+ def apply_and_persist
132
+ aggregate.lock! if aggregate.persisted?
133
+ self.aggregate = apply(aggregate)
134
+ aggregate.save!
135
+ self.aggregate_id = aggregate.id if aggregate_id.nil?
136
+ end
137
+
138
+ def dispatch
139
+ Dispatcher.dispatch(self)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsEventSourcing
4
+ # The Base command mixin that commands include.
5
+ #
6
+ # A Command has the following public api.
7
+ #
8
+ # ```
9
+ # MyCommand.call(user: ..., post: ...) # shorthand to initialize, validate and execute the command
10
+ # command = MyCommand.new(user: ..., post: ...)
11
+ # command.valid? # true or false
12
+ # command.errors # +> <ActiveModel::Errors ... >
13
+ # command.call # validate and execute the command
14
+ # ```
15
+ #
16
+ # `call` will raise an `ActiveRecord::RecordInvalid` error if it fails validations.
17
+ #
18
+ # Commands including the `RailsEventSourcing::Command` mixin must:
19
+ # * list the attributes the command takes
20
+ # * implement `build_event` which returns a non-persisted event or nil for noop.
21
+ #
22
+ # Ex:
23
+ #
24
+ # ```
25
+ # class MyCommand
26
+ # include RailsEventSourcing::Command
27
+ #
28
+ # attributes :user, :post
29
+ #
30
+ # def build_event
31
+ # Event.new(...)
32
+ # end
33
+ # end
34
+ # ```
35
+ module Command
36
+ extend ActiveSupport::Concern
37
+
38
+ included do
39
+ include ActiveModel::Validations
40
+ end
41
+
42
+ class_methods do
43
+ # Run validations and persist the event.
44
+ #
45
+ # On success: returns the event
46
+ # On noop: returns nil
47
+ # On failure: raise an ActiveRecord::RecordInvalid error
48
+ def call(*args)
49
+ new(*args).call
50
+ end
51
+
52
+ # Define the attributes.
53
+ # They are set when initializing the command as keyword arguments and
54
+ # are all accessible as getter methods.
55
+ #
56
+ # ex: `attributes :post, :user, :ability`
57
+ def attributes(*args)
58
+ attr_reader(*args)
59
+
60
+ initialize_method_arguments = args.map { |arg| "#{arg}:" }.join(', ')
61
+ initialize_method_body = args.map { |arg| "@#{arg} = #{arg}" }.join(";")
62
+ initialize_definition = <<~CODE
63
+ def initialize(#{initialize_method_arguments})
64
+ #{initialize_method_body}
65
+ after_initialize
66
+ end
67
+ CODE
68
+
69
+ class_eval(initialize_definition)
70
+ end
71
+ end
72
+
73
+ def call
74
+ return nil if event.nil?
75
+ raise "The event should not be persisted at this stage!" if event.persisted?
76
+
77
+ validate!
78
+ execute!
79
+
80
+ event
81
+ end
82
+
83
+ # A new record or nil if noop
84
+ def event
85
+ @event ||= (noop? ? nil : build_event)
86
+ end
87
+
88
+ def noop?
89
+ false
90
+ end
91
+
92
+ private
93
+
94
+ # Save the event. Should not be overwritten by the command as side effects
95
+ # should be implemented via Reactors triggering other Events.
96
+ def execute!
97
+ event.save!
98
+ end
99
+
100
+ # Returns a new event record or nil if noop
101
+ def build_event
102
+ raise NotImplementedError
103
+ end
104
+
105
+ # Hook to set default values
106
+ def after_initialize
107
+ # noop
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsEventSourcing
4
+ # Dispatcher implementation used by Events on after save.
5
+ class Dispatcher
6
+ class << self
7
+ # Register Reactors to Events.
8
+ # * Reactors registered with `trigger` will be triggered synchronously
9
+ # * Reactors registered with `async` will be triggered asynchronously via a ActiveJob
10
+ #
11
+ # Example:
12
+ #
13
+ # on SomeEvent, trigger: ->(item) { puts "Callable block on #{item.id}" }
14
+ # on BaseEvent, trigger: LogEvent, async: TrackEvent
15
+ # on PledgeCancelled, PaymentFailed, async: [NotifyAdmin, CreateTask]
16
+ # on [PledgeCancelled, PaymentFailed], async: [NotifyAdmin, CreateTask]
17
+ #
18
+ def on(*events, trigger: [], async: [])
19
+ rules.register(events: events.flatten, sync: Array(trigger), async: Array(async))
20
+ end
21
+
22
+ # Dispatches events to matching Reactors once.
23
+ # Called by all events after they are created.
24
+ def dispatch(event)
25
+ reactors = rules.for(event)
26
+ reactors.sync.each { |reactor| reactor.call(event) }
27
+ reactors.async.each { |reactor| RailsEventSourcing::ReactorJob.perform_later(event, reactor.to_s) }
28
+ end
29
+
30
+ def rules
31
+ @@rules ||= RuleSet.new # rubocop:disable Style/ClassVars
32
+ end
33
+ end
34
+
35
+ class RuleSet
36
+ def initialize
37
+ @rules = Hash.new { |h, k| h[k] = ReactorSet.new }
38
+ end
39
+
40
+ def register(events:, sync:, async:)
41
+ events.each do |event|
42
+ @rules[event].add_sync(sync)
43
+ @rules[event].add_async(async)
44
+ end
45
+ end
46
+
47
+ # Return a ReactorSet containing all Reactors matching an Event
48
+ def for(event)
49
+ reactors = ReactorSet.new
50
+
51
+ @rules.each do |event_class, rule|
52
+ # Match event by class including ancestors. e.g. All events match a role for BaseEvent.
53
+ if event.is_a?(event_class)
54
+ reactors.add_sync(rule.sync)
55
+ reactors.add_async(rule.async)
56
+ end
57
+ end
58
+
59
+ reactors
60
+ end
61
+ end
62
+
63
+ # Contains sync and async reactors. Used to:
64
+ # * store reactors via Rules#register
65
+ # * return a set of matching reactors with Rules#for
66
+ class ReactorSet
67
+ attr_reader :sync, :async
68
+
69
+ def initialize
70
+ @sync = Set.new
71
+ @async = Set.new
72
+ end
73
+
74
+ def add_sync(reactors)
75
+ @sync += reactors
76
+ end
77
+
78
+ def add_async(reactors)
79
+ @async += reactors
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined? ActiveJob
4
+ module RailsEventSourcing
5
+ class ReactorJob < ActiveJob::Base
6
+ def perform(event, reactor_class)
7
+ reactor = reactor_class.constantize
8
+ if reactor.ancestors.include? RailsEventSourcing::BaseEvent
9
+ reactor.create!(aggregate: event.aggregate)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsEventSourcing
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rails-event-sourcing/version'
4
+
5
+ require_relative 'rails-event-sourcing/base_event'
6
+ require_relative 'rails-event-sourcing/command'
7
+ require_relative 'rails-event-sourcing/dispatcher'
8
+ require_relative 'rails-event-sourcing/reactor_job'
9
+
10
+ module RailsEventSourcing
11
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-event-sourcing
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mattia Roccoberton
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-11-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ description: Setup event sourcing in a Rails application easily
42
+ email:
43
+ - mat@blocknot.es
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".rspec"
49
+ - ".rubocop.yml"
50
+ - Gemfile
51
+ - LICENSE.txt
52
+ - README.md
53
+ - bin/console
54
+ - bin/pry
55
+ - bin/rails
56
+ - bin/rspec
57
+ - bin/rubocop
58
+ - bin/setup
59
+ - lib/rails-event-sourcing.rb
60
+ - lib/rails-event-sourcing/base_event.rb
61
+ - lib/rails-event-sourcing/command.rb
62
+ - lib/rails-event-sourcing/dispatcher.rb
63
+ - lib/rails-event-sourcing/reactor_job.rb
64
+ - lib/rails-event-sourcing/version.rb
65
+ homepage: https://github.com/blocknotes/rails-event-sourcing
66
+ licenses:
67
+ - MIT
68
+ metadata:
69
+ homepage_uri: https://github.com/blocknotes/rails-event-sourcing
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 2.6.0
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 3.0.3.1
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Rails Event Sourcing library
89
+ test_files: []