rails-event-sourcing 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []