diff --git a/contrib/multiple_databases_repository_sample_app/.gitignore b/contrib/multiple_databases_repository_sample_app/.gitignore
new file mode 100644
index 0000000000..c4bd440cfd
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/.gitignore
@@ -0,0 +1,20 @@
+!/log/.keep
+!/storage/.keep
+!/tmp/.keep
+.byebug_history
+.yarn-integrity
+/.bundle
+/config/master.key
+/db/*.sqlite3
+/db/*.sqlite3-journal
+/elm-stuff
+/log/*
+/node_modules
+/public/assets
+/public/packs
+/public/packs-test
+/storage/*
+/tmp/*
+/yarn-error.log
+coverage
+yarn-debug.log*
diff --git a/contrib/multiple_databases_repository_sample_app/.rspec b/contrib/multiple_databases_repository_sample_app/.rspec
new file mode 100644
index 0000000000..c99d2e7396
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/.rspec
@@ -0,0 +1 @@
+--require spec_helper
diff --git a/contrib/multiple_databases_repository_sample_app/Gemfile b/contrib/multiple_databases_repository_sample_app/Gemfile
new file mode 100644
index 0000000000..ecdd2e1742
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/Gemfile
@@ -0,0 +1,38 @@
+source 'https://rubygems.org'
+git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ruby '2.6.6'
+
+# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
+gem 'rails', '~> 6.0.3', '>= 6.0.3.1'
+# Use sqlite3 as the database for Active Record
+gem 'sqlite3', '~> 1.4'
+# Use Puma as the app server
+gem 'puma', '~> 4.1'
+# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
+gem 'jbuilder', '~> 2.7'
+# Use Active Model has_secure_password
+# gem 'bcrypt', '~> 3.1.7'
+
+group :development, :test do
+ # Call 'byebug' anywhere in the code to stop execution and get a debugger console
+ gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
+ gem 'rspec-rails'
+ gem 'database_cleaner'
+ gem 'rails_event_store-rspec'
+end
+
+group :development do
+ # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
+ gem 'web-console', '>= 3.3.0'
+end
+
+# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+
+gem 'dry-struct'
+gem 'dry-types'
+
+gem 'ruby_event_store', path: '../../ruby_event_store'
+gem 'rails_event_store_active_record', path: '../../rails_event_store_active_record'
+gem 'rails_event_store', path: '../../rails_event_store'
diff --git a/contrib/multiple_databases_repository_sample_app/README.md b/contrib/multiple_databases_repository_sample_app/README.md
new file mode 100644
index 0000000000..b600c3f026
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/README.md
@@ -0,0 +1,5 @@
+# Domain description
+
+TDB
+
+[Event storming board](https://miro.com/app/embed/o9J_krLAGIQ=/?)
diff --git a/contrib/multiple_databases_repository_sample_app/Rakefile b/contrib/multiple_databases_repository_sample_app/Rakefile
new file mode 100644
index 0000000000..e85f913914
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/Rakefile
@@ -0,0 +1,6 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require_relative 'config/application'
+
+Rails.application.load_tasks
diff --git a/contrib/multiple_databases_repository_sample_app/app/assets/config/manifest.js b/contrib/multiple_databases_repository_sample_app/app/assets/config/manifest.js
new file mode 100644
index 0000000000..591819335f
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/app/assets/config/manifest.js
@@ -0,0 +1,2 @@
+//= link_tree ../images
+//= link_directory ../stylesheets .css
diff --git a/contrib/multiple_databases_repository_sample_app/app/assets/stylesheets/application.css b/contrib/multiple_databases_repository_sample_app/app/assets/stylesheets/application.css
new file mode 100644
index 0000000000..d05ea0f511
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/app/assets/stylesheets/application.css
@@ -0,0 +1,15 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
+ * vendor/assets/stylesheets directory can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
+ * files in this directory. Styles in this file should be added after the last require_* statement.
+ * It is generally better to create a new file per style scope.
+ *
+ *= require_tree .
+ *= require_self
+ */
diff --git a/contrib/multiple_databases_repository_sample_app/app/controllers/application_controller.rb b/contrib/multiple_databases_repository_sample_app/app/controllers/application_controller.rb
new file mode 100644
index 0000000000..09705d12ab
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/app/controllers/application_controller.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::Base
+end
diff --git a/contrib/multiple_databases_repository_sample_app/app/helpers/application_helper.rb b/contrib/multiple_databases_repository_sample_app/app/helpers/application_helper.rb
new file mode 100644
index 0000000000..de6be7945c
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/app/helpers/application_helper.rb
@@ -0,0 +1,2 @@
+module ApplicationHelper
+end
diff --git a/contrib/multiple_databases_repository_sample_app/app/jobs/application_job.rb b/contrib/multiple_databases_repository_sample_app/app/jobs/application_job.rb
new file mode 100644
index 0000000000..d394c3d106
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/app/jobs/application_job.rb
@@ -0,0 +1,7 @@
+class ApplicationJob < ActiveJob::Base
+ # Automatically retry jobs that encountered a deadlock
+ # retry_on ActiveRecord::Deadlocked
+
+ # Most jobs are safe to ignore if the underlying records are no longer available
+ # discard_on ActiveJob::DeserializationError
+end
diff --git a/contrib/multiple_databases_repository_sample_app/app/models/application_record.rb b/contrib/multiple_databases_repository_sample_app/app/models/application_record.rb
new file mode 100644
index 0000000000..10a4cba84d
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/app/models/application_record.rb
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
diff --git a/contrib/multiple_databases_repository_sample_app/app/views/layouts/application.html.erb b/contrib/multiple_databases_repository_sample_app/app/views/layouts/application.html.erb
new file mode 100644
index 0000000000..f06473dab7
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/app/views/layouts/application.html.erb
@@ -0,0 +1,14 @@
+
+
+
+ Sample
+ <%= csrf_meta_tags %>
+ <%= csp_meta_tag %>
+
+ <%= stylesheet_link_tag 'application', media: 'all' %>
+
+
+
+ <%= yield %>
+
+
diff --git a/contrib/multiple_databases_repository_sample_app/config.ru b/contrib/multiple_databases_repository_sample_app/config.ru
new file mode 100644
index 0000000000..f7ba0b527b
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config.ru
@@ -0,0 +1,5 @@
+# This file is used by Rack-based servers to start the application.
+
+require_relative 'config/environment'
+
+run Rails.application
diff --git a/contrib/multiple_databases_repository_sample_app/config/application.rb b/contrib/multiple_databases_repository_sample_app/config/application.rb
new file mode 100644
index 0000000000..c2fe0ab597
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/application.rb
@@ -0,0 +1,38 @@
+require_relative 'boot'
+
+require "rails"
+# Pick the frameworks you want:
+require "active_model/railtie"
+require "active_job/railtie"
+require "active_record/railtie"
+# require "active_storage/engine"
+require "action_controller/railtie"
+# require "action_mailer/railtie"
+# require "action_mailbox/engine"
+# require "action_text/engine"
+require "action_view/railtie"
+# require "action_cable/engine"
+# require "sprockets/railtie"
+# require "rails/test_unit/railtie"
+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+module Sample
+ class Application < Rails::Application
+ # Initialize configuration defaults for originally generated Rails version.
+ config.load_defaults 6.0
+
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration can go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded after loading
+ # the framework and any gems in your application.
+ config.paths.add 'orders/lib', eager_load: true
+ config.paths.add 'payments/lib', eager_load: true
+ config.paths.add 'shipping/lib', eager_load: true
+
+ # Don't generate system test files.
+ config.generators.system_tests = nil
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/config/boot.rb b/contrib/multiple_databases_repository_sample_app/config/boot.rb
new file mode 100644
index 0000000000..30f5120df6
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/boot.rb
@@ -0,0 +1,3 @@
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/contrib/multiple_databases_repository_sample_app/config/credentials.yml.enc b/contrib/multiple_databases_repository_sample_app/config/credentials.yml.enc
new file mode 100644
index 0000000000..7a43605f93
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/credentials.yml.enc
@@ -0,0 +1 @@
+d1WzgYIe3R/fpHfTZfMP5wrp9uDbDMQc2SBiBqg5sqbHZKnTNktELx1YrV9xCcJqUa/V7RBEvsfXBAuu/5luTXkK/sKKL81mlcNuy57i09y7LjQ+ojRB6aWpTcquXM8RSDimSmjKQd9QKDNLxRKGK4wHhBWDa4+4urG2n72+6iVH4JvycVFv5Wgzkhe+su22rraZOKg7LLE9Nvrwrwf1Is1mDHJdxTZqnS4JzWljGbCGFO5AXhyEPp/zHJ/B1zooCcEeO/TUVpxU9OB1bjhIslKY0S+JFPsheY56eYv6WOEpt9EZAPNe4ZEj71H131kVZyD9SZajq+bUCJePJuKjMRjL2w/pkrKXXi9yz4W4bDsKvM2Oz4CNOtXePYe9G+YauExNYEO79oghgxz1UFUQEy1WGZfi/pj6I2mj--BY9vm19PlrY42f7d--DMlEvIHGiG+9KuB6ArAYkA==
\ No newline at end of file
diff --git a/contrib/multiple_databases_repository_sample_app/config/database.yml b/contrib/multiple_databases_repository_sample_app/config/database.yml
new file mode 100644
index 0000000000..38f4810f4d
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/database.yml
@@ -0,0 +1,64 @@
+# SQLite. Versions 3.8.0 and up are supported.
+# gem install sqlite3
+#
+# Ensure the SQLite 3 gem is defined in your Gemfile
+# gem 'sqlite3'
+#
+default: &default
+ adapter: sqlite3
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
+
+development:
+ primary:
+ <<: *default
+ database: db/development.sqlite3
+ orders:
+ <<: *default
+ database: db/orders_development.sqlite3
+ migrations_paths: db/orders
+ payments:
+ <<: *default
+ database: db/payments_development.sqlite3
+ migrations_paths: db/payments
+ shipping:
+ <<: *default
+ database: db/shipping_development.sqlite3
+ migrations_paths: db/shipping
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ primary:
+ <<: *default
+ database: db/test.sqlite3
+ orders:
+ <<: *default
+ database: db/orders_test.sqlite3
+ migrations_paths: db/orders
+ payments:
+ <<: *default
+ database: db/payments_test.sqlite3
+ migrations_paths: db/payments
+ shipping:
+ <<: *default
+ database: db/shipping_test.sqlite3
+ migrations_paths: db/shipping
+
+production:
+ primary:
+ <<: *default
+ database: db/production.sqlite3
+ orders:
+ <<: *default
+ database: db/orders_production.sqlite3
+ migrations_paths: db/orders
+ payments:
+ <<: *default
+ database: db/payments_production.sqlite3
+ migrations_paths: db/payments
+ shipping:
+ <<: *default
+ database: db/shipping_production.sqlite3
+ migrations_paths: db/shipping
diff --git a/contrib/multiple_databases_repository_sample_app/config/environment.rb b/contrib/multiple_databases_repository_sample_app/config/environment.rb
new file mode 100644
index 0000000000..426333bb46
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative 'application'
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/contrib/multiple_databases_repository_sample_app/config/environments/development.rb b/contrib/multiple_databases_repository_sample_app/config/environments/development.rb
new file mode 100644
index 0000000000..7eafa4acaf
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/environments/development.rb
@@ -0,0 +1,49 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded on
+ # every request. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports.
+ config.consider_all_requests_local = true
+
+ # Enable/disable caching. By default caching is disabled.
+ # Run rails dev:cache to toggle caching.
+ if Rails.root.join('tmp', 'caching-dev.txt').exist?
+ config.action_controller.perform_caching = true
+ config.action_controller.enable_fragment_cache_logging = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{2.days.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Highlight code that triggered database queries in logs.
+ config.active_record.verbose_query_logs = true
+
+
+ # Raises error for missing translations.
+ # config.action_view.raise_on_missing_translations = true
+
+ # Use an evented file watcher to asynchronously detect changes in source code,
+ # routes, locales, etc. This feature depends on the listen gem.
+ # config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+
+ config.number_generator_factory = ->{ Orders::NumberGenerator.new }
+end
diff --git a/contrib/multiple_databases_repository_sample_app/config/environments/production.rb b/contrib/multiple_databases_repository_sample_app/config/environments/production.rb
new file mode 100644
index 0000000000..adfbaa8a5a
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/environments/production.rb
@@ -0,0 +1,94 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
+ # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
+ # config.require_master_key = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.action_controller.asset_host = 'http://assets.example.com'
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :debug
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [ :request_id ]
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Use a real queuing backend for Active Job (and separate queues per environment).
+ # config.active_job.queue_adapter = :resque
+ # config.active_job.queue_name_prefix = "sample_production"
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners.
+ config.active_support.deprecation = :notify
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require 'syslog/logger'
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
+
+ if ENV["RAILS_LOG_TO_STDOUT"].present?
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+
+ # Inserts middleware to perform automatic connection switching.
+ # The `database_selector` hash is used to pass options to the DatabaseSelector
+ # middleware. The `delay` is used to determine how long to wait after a write
+ # to send a subsequent read to the primary.
+ #
+ # The `database_resolver` class is used by the middleware to determine which
+ # database is appropriate to use based on the time delay.
+ #
+ # The `database_resolver_context` class is used by the middleware to set
+ # timestamps for the last write to the primary. The resolver uses the context
+ # class timestamps to determine how long to wait before reading from the
+ # replica.
+ #
+ # By default Rails will store a last write timestamp in the session. The
+ # DatabaseSelector middleware is designed as such you can define your own
+ # strategy for connection switching and pass that into the middleware through
+ # these configuration options.
+ # config.active_record.database_selector = { delay: 2.seconds }
+ # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
+ # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+
+ config.number_generator_factory = -> { Orders::NumberGenerator.new }
+end
diff --git a/contrib/multiple_databases_repository_sample_app/config/environments/test.rb b/contrib/multiple_databases_repository_sample_app/config/environments/test.rb
new file mode 100644
index 0000000000..1308f3401a
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/environments/test.rb
@@ -0,0 +1,40 @@
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ config.cache_classes = true
+
+ # Do not eager load code on boot. This avoids loading your whole application
+ # just for the purpose of running a single test. If you are using a tool that
+ # preloads Rails for running tests, you may have to set it to true.
+ config.eager_load = false
+
+ # Configure public file server for tests with Cache-Control for performance.
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
+ }
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+ config.cache_store = :null_store
+
+ # Raise exceptions instead of rendering exception templates.
+ config.action_dispatch.show_exceptions = false
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Raises error for missing translations.
+ # config.action_view.raise_on_missing_translations = true
+
+ config.number_generator_factory = ->{ Orders::FakeNumberGenerator.new }
+end
diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/application_controller_renderer.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/application_controller_renderer.rb
new file mode 100644
index 0000000000..89d2efab2b
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/initializers/application_controller_renderer.rb
@@ -0,0 +1,8 @@
+# Be sure to restart your server when you modify this file.
+
+# ActiveSupport::Reloader.to_prepare do
+# ApplicationController.renderer.defaults.merge!(
+# http_host: 'example.org',
+# https: false
+# )
+# end
diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/backtrace_silencers.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/backtrace_silencers.rb
new file mode 100644
index 0000000000..59385cdf37
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/initializers/backtrace_silencers.rb
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/content_security_policy.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/content_security_policy.rb
new file mode 100644
index 0000000000..41c43016f1
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/initializers/content_security_policy.rb
@@ -0,0 +1,28 @@
+# Be sure to restart your server when you modify this file.
+
+# Define an application-wide content security policy
+# For further information see the following documentation
+# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+
+# Rails.application.config.content_security_policy do |policy|
+# policy.default_src :self, :https
+# policy.font_src :self, :https, :data
+# policy.img_src :self, :https, :data
+# policy.object_src :none
+# policy.script_src :self, :https
+# policy.style_src :self, :https
+
+# # Specify URI for violation reports
+# # policy.report_uri "/csp-violation-report-endpoint"
+# end
+
+# If you are using UJS then enable automatic nonce generation
+# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
+
+# Set the nonce only to specific directives
+# Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
+
+# Report CSP violations to a specified URI
+# For further information see the following documentation:
+# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
+# Rails.application.config.content_security_policy_report_only = true
diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/cookies_serializer.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/cookies_serializer.rb
new file mode 100644
index 0000000000..5a6a32d371
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/initializers/cookies_serializer.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file.
+
+# Specify a serializer for the signed and encrypted cookie jars.
+# Valid options are :json, :marshal, and :hybrid.
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/filter_parameter_logging.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000000..4a994e1e7b
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Configure sensitive parameters which will be filtered from the log file.
+Rails.application.config.filter_parameters += [:password]
diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/inflections.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/inflections.rb
new file mode 100644
index 0000000000..ac033bf9dc
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/initializers/inflections.rb
@@ -0,0 +1,16 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.acronym 'RESTful'
+# end
diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/mime_types.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/mime_types.rb
new file mode 100644
index 0000000000..dc1899682b
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/initializers/mime_types.rb
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/rails_event_store.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/rails_event_store.rb
new file mode 100644
index 0000000000..f76b95061d
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/initializers/rails_event_store.rb
@@ -0,0 +1,41 @@
+require 'rails_event_store'
+require 'aggregate_root'
+require 'arkency/command_bus'
+
+Rails.configuration.to_prepare do
+ events_class_remapping = {}
+
+ Rails.configuration.event_repository =
+ RailsEventStoreActiveRecord::EventRepository.new(serializer: JSON)
+ Rails.configuration.event_store = RailsEventStore::Client.new(
+ repository: Rails.configuration.event_repository,
+ mapper: RubyEventStore::Mappers::Default.new(
+ events_class_remapping: events_class_remapping
+ ),
+ dispatcher: RubyEventStore::ComposedDispatcher.new(
+ RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)),
+ RubyEventStore::Dispatcher.new),
+ )
+ Rails.configuration.command_bus = Arkency::CommandBus.new
+
+ AggregateRoot.configure do |config|
+ config.default_event_store = Rails.configuration.event_store
+ end
+
+ # Subscribe event handlers below
+ # Rails.configuration.event_store.tap do |store|
+ # store.subscribe(InvoiceReadModel.new, to: [InvoicePrinted])
+ # store.subscribe(->(event) { SendOrderConfirmation.new.call(event) }, to: [OrderSubmitted])
+ # store.subscribe_to_all_events(->(event) { Rails.logger.info(event.type) })
+ # end
+
+ # Register command handlers below
+ # Rails.configuration.command_bus.tap do |bus|
+ # bus.register(PrintInvoice, Invoicing::OnPrint.new)
+ # bus.register(SubmitOrder, ->(cmd) { Orders::OnSubmitOrder.new.call(cmd) })
+ # end
+
+ Orders.setup(Rails.configuration)
+ Payments.setup(Rails.configuration)
+ Shipping.setup(Rails.configuration)
+end
diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/wrap_parameters.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/wrap_parameters.rb
new file mode 100644
index 0000000000..bbfc3961bf
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/initializers/wrap_parameters.rb
@@ -0,0 +1,14 @@
+# Be sure to restart your server when you modify this file.
+
+# This file contains settings for ActionController::ParamsWrapper which
+# is enabled by default.
+
+# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
+ActiveSupport.on_load(:action_controller) do
+ wrap_parameters format: [:json]
+end
+
+# To enable root element in JSON for ActiveRecord objects.
+# ActiveSupport.on_load(:active_record) do
+# self.include_root_in_json = true
+# end
diff --git a/contrib/multiple_databases_repository_sample_app/config/locales/en.yml b/contrib/multiple_databases_repository_sample_app/config/locales/en.yml
new file mode 100644
index 0000000000..cf9b342d0a
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/locales/en.yml
@@ -0,0 +1,33 @@
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+# I18n.t 'hello'
+#
+# In views, this is aliased to just `t`:
+#
+# <%= t('hello') %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+# I18n.locale = :es
+#
+# This would use the information in config/locales/es.yml.
+#
+# The following keys must be escaped otherwise they will not be retrieved by
+# the default I18n backend:
+#
+# true, false, on, off, yes, no
+#
+# Instead, surround them with single quotes.
+#
+# en:
+# 'true': 'foo'
+#
+# To learn more, please read the Rails Internationalization guide
+# available at https://guides.rubyonrails.org/i18n.html.
+
+en:
+ hello: "Hello world"
diff --git a/contrib/multiple_databases_repository_sample_app/config/puma.rb b/contrib/multiple_databases_repository_sample_app/config/puma.rb
new file mode 100644
index 0000000000..5ed4437744
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/puma.rb
@@ -0,0 +1,38 @@
+# Puma can serve each request in a thread from an internal thread pool.
+# The `threads` method setting takes two numbers: a minimum and maximum.
+# Any libraries that use thread pools should be configured to match
+# the maximum value specified for Puma. Default is set to 5 threads for minimum
+# and maximum; this matches the default thread size of Active Record.
+#
+max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
+min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
+threads min_threads_count, max_threads_count
+
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
+#
+port ENV.fetch("PORT") { 3000 }
+
+# Specifies the `environment` that Puma will run in.
+#
+environment ENV.fetch("RAILS_ENV") { "development" }
+
+# Specifies the `pidfile` that Puma will use.
+pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
+
+# Specifies the number of `workers` to boot in clustered mode.
+# Workers are forked web server processes. If using threads and workers together
+# the concurrency of the application would be max `threads` * `workers`.
+# Workers do not work on JRuby or Windows (both of which do not support
+# processes).
+#
+# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
+
+# Use the `preload_app!` method when specifying a `workers` number.
+# This directive tells Puma to first boot the application and load code
+# before forking the application. This takes advantage of Copy On Write
+# process behavior so workers use less memory.
+#
+# preload_app!
+
+# Allow puma to be restarted by `rails restart` command.
+plugin :tmp_restart
diff --git a/contrib/multiple_databases_repository_sample_app/config/routes.rb b/contrib/multiple_databases_repository_sample_app/config/routes.rb
new file mode 100644
index 0000000000..96a6bd1396
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/config/routes.rb
@@ -0,0 +1,4 @@
+Rails.application.routes.draw do
+ mount RailsEventStore::Browser => '/res' if Rails.env.development?
+ # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
+end
diff --git a/contrib/multiple_databases_repository_sample_app/db/migrate/20200604152933_create_event_store_events.rb b/contrib/multiple_databases_repository_sample_app/db/migrate/20200604152933_create_event_store_events.rb
new file mode 100644
index 0000000000..c583aaea5b
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/db/migrate/20200604152933_create_event_store_events.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class CreateEventStoreEvents < ActiveRecord::Migration[4.2]
+ def change
+ postgres = ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
+ sqlite = ActiveRecord::Base.connection.adapter_name == "SQLite"
+ rails_42 = Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("5.0.0")
+ enable_extension "pgcrypto" if postgres
+ create_table(:event_store_events_in_streams, force: false) do |t|
+ t.string :stream, null: false
+ t.integer :position, null: true
+ if postgres
+ t.references :event, null: false, type: :uuid
+ else
+ t.references :event, null: false, type: :string, limit: 36
+ end
+ t.datetime :created_at, null: false
+ end
+ add_index :event_store_events_in_streams, [:stream, :position], unique: true
+ add_index :event_store_events_in_streams, [:created_at]
+ add_index :event_store_events_in_streams, [:stream, :event_id], unique: true
+
+ if postgres
+ create_table(:event_store_events, id: :uuid, default: 'gen_random_uuid()', force: false) do |t|
+ t.string :event_type, null: false
+ t.binary :metadata
+ t.binary :data, null: false
+ t.datetime :created_at, null: false
+ end
+ else
+ create_table(:event_store_events, id: false, force: false) do |t|
+ t.string :id, limit: 36, primary_key: true, null: false
+ t.string :event_type, null: false
+ t.binary :metadata
+ t.binary :data, null: false
+ t.datetime :created_at, null: false
+ end
+ if sqlite && rails_42
+ add_index :event_store_events, :id, unique: true
+ end
+ end
+ add_index :event_store_events, :created_at
+ add_index :event_store_events, :event_type
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/db/orders/20200604152933_create_event_store_events.rb b/contrib/multiple_databases_repository_sample_app/db/orders/20200604152933_create_event_store_events.rb
new file mode 100644
index 0000000000..c583aaea5b
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/db/orders/20200604152933_create_event_store_events.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class CreateEventStoreEvents < ActiveRecord::Migration[4.2]
+ def change
+ postgres = ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
+ sqlite = ActiveRecord::Base.connection.adapter_name == "SQLite"
+ rails_42 = Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("5.0.0")
+ enable_extension "pgcrypto" if postgres
+ create_table(:event_store_events_in_streams, force: false) do |t|
+ t.string :stream, null: false
+ t.integer :position, null: true
+ if postgres
+ t.references :event, null: false, type: :uuid
+ else
+ t.references :event, null: false, type: :string, limit: 36
+ end
+ t.datetime :created_at, null: false
+ end
+ add_index :event_store_events_in_streams, [:stream, :position], unique: true
+ add_index :event_store_events_in_streams, [:created_at]
+ add_index :event_store_events_in_streams, [:stream, :event_id], unique: true
+
+ if postgres
+ create_table(:event_store_events, id: :uuid, default: 'gen_random_uuid()', force: false) do |t|
+ t.string :event_type, null: false
+ t.binary :metadata
+ t.binary :data, null: false
+ t.datetime :created_at, null: false
+ end
+ else
+ create_table(:event_store_events, id: false, force: false) do |t|
+ t.string :id, limit: 36, primary_key: true, null: false
+ t.string :event_type, null: false
+ t.binary :metadata
+ t.binary :data, null: false
+ t.datetime :created_at, null: false
+ end
+ if sqlite && rails_42
+ add_index :event_store_events, :id, unique: true
+ end
+ end
+ add_index :event_store_events, :created_at
+ add_index :event_store_events, :event_type
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/db/orders_schema.rb b/contrib/multiple_databases_repository_sample_app/db/orders_schema.rb
new file mode 100644
index 0000000000..01923995c3
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/db/orders_schema.rb
@@ -0,0 +1,34 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `rails
+# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 2020_06_04_152933) do
+
+ create_table "event_store_events", id: :string, limit: 36, force: :cascade do |t|
+ t.string "event_type", null: false
+ t.binary "metadata"
+ t.binary "data", null: false
+ t.datetime "created_at", null: false
+ t.index ["created_at"], name: "index_event_store_events_on_created_at"
+ t.index ["event_type"], name: "index_event_store_events_on_event_type"
+ end
+
+ create_table "event_store_events_in_streams", force: :cascade do |t|
+ t.string "stream", null: false
+ t.integer "position"
+ t.string "event_id", limit: 36, null: false
+ t.datetime "created_at", null: false
+ t.index ["created_at"], name: "index_event_store_events_in_streams_on_created_at"
+ t.index ["stream", "event_id"], name: "index_event_store_events_in_streams_on_stream_and_event_id", unique: true
+ t.index ["stream", "position"], name: "index_event_store_events_in_streams_on_stream_and_position", unique: true
+ end
+
+end
diff --git a/contrib/multiple_databases_repository_sample_app/db/payments/20200604152933_create_event_store_events.rb b/contrib/multiple_databases_repository_sample_app/db/payments/20200604152933_create_event_store_events.rb
new file mode 100644
index 0000000000..c583aaea5b
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/db/payments/20200604152933_create_event_store_events.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class CreateEventStoreEvents < ActiveRecord::Migration[4.2]
+ def change
+ postgres = ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
+ sqlite = ActiveRecord::Base.connection.adapter_name == "SQLite"
+ rails_42 = Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("5.0.0")
+ enable_extension "pgcrypto" if postgres
+ create_table(:event_store_events_in_streams, force: false) do |t|
+ t.string :stream, null: false
+ t.integer :position, null: true
+ if postgres
+ t.references :event, null: false, type: :uuid
+ else
+ t.references :event, null: false, type: :string, limit: 36
+ end
+ t.datetime :created_at, null: false
+ end
+ add_index :event_store_events_in_streams, [:stream, :position], unique: true
+ add_index :event_store_events_in_streams, [:created_at]
+ add_index :event_store_events_in_streams, [:stream, :event_id], unique: true
+
+ if postgres
+ create_table(:event_store_events, id: :uuid, default: 'gen_random_uuid()', force: false) do |t|
+ t.string :event_type, null: false
+ t.binary :metadata
+ t.binary :data, null: false
+ t.datetime :created_at, null: false
+ end
+ else
+ create_table(:event_store_events, id: false, force: false) do |t|
+ t.string :id, limit: 36, primary_key: true, null: false
+ t.string :event_type, null: false
+ t.binary :metadata
+ t.binary :data, null: false
+ t.datetime :created_at, null: false
+ end
+ if sqlite && rails_42
+ add_index :event_store_events, :id, unique: true
+ end
+ end
+ add_index :event_store_events, :created_at
+ add_index :event_store_events, :event_type
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/db/payments_schema.rb b/contrib/multiple_databases_repository_sample_app/db/payments_schema.rb
new file mode 100644
index 0000000000..01923995c3
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/db/payments_schema.rb
@@ -0,0 +1,34 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `rails
+# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 2020_06_04_152933) do
+
+ create_table "event_store_events", id: :string, limit: 36, force: :cascade do |t|
+ t.string "event_type", null: false
+ t.binary "metadata"
+ t.binary "data", null: false
+ t.datetime "created_at", null: false
+ t.index ["created_at"], name: "index_event_store_events_on_created_at"
+ t.index ["event_type"], name: "index_event_store_events_on_event_type"
+ end
+
+ create_table "event_store_events_in_streams", force: :cascade do |t|
+ t.string "stream", null: false
+ t.integer "position"
+ t.string "event_id", limit: 36, null: false
+ t.datetime "created_at", null: false
+ t.index ["created_at"], name: "index_event_store_events_in_streams_on_created_at"
+ t.index ["stream", "event_id"], name: "index_event_store_events_in_streams_on_stream_and_event_id", unique: true
+ t.index ["stream", "position"], name: "index_event_store_events_in_streams_on_stream_and_position", unique: true
+ end
+
+end
diff --git a/contrib/multiple_databases_repository_sample_app/db/schema.rb b/contrib/multiple_databases_repository_sample_app/db/schema.rb
new file mode 100644
index 0000000000..01923995c3
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/db/schema.rb
@@ -0,0 +1,34 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `rails
+# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 2020_06_04_152933) do
+
+ create_table "event_store_events", id: :string, limit: 36, force: :cascade do |t|
+ t.string "event_type", null: false
+ t.binary "metadata"
+ t.binary "data", null: false
+ t.datetime "created_at", null: false
+ t.index ["created_at"], name: "index_event_store_events_on_created_at"
+ t.index ["event_type"], name: "index_event_store_events_on_event_type"
+ end
+
+ create_table "event_store_events_in_streams", force: :cascade do |t|
+ t.string "stream", null: false
+ t.integer "position"
+ t.string "event_id", limit: 36, null: false
+ t.datetime "created_at", null: false
+ t.index ["created_at"], name: "index_event_store_events_in_streams_on_created_at"
+ t.index ["stream", "event_id"], name: "index_event_store_events_in_streams_on_stream_and_event_id", unique: true
+ t.index ["stream", "position"], name: "index_event_store_events_in_streams_on_stream_and_position", unique: true
+ end
+
+end
diff --git a/contrib/multiple_databases_repository_sample_app/db/seeds.rb b/contrib/multiple_databases_repository_sample_app/db/seeds.rb
new file mode 100644
index 0000000000..1beea2accd
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/db/seeds.rb
@@ -0,0 +1,7 @@
+# This file should contain all the record creation needed to seed the database with its default values.
+# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
+#
+# Examples:
+#
+# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
+# Character.create(name: 'Luke', movie: movies.first)
diff --git a/contrib/multiple_databases_repository_sample_app/db/shipping/20200604152933_create_event_store_events.rb b/contrib/multiple_databases_repository_sample_app/db/shipping/20200604152933_create_event_store_events.rb
new file mode 100644
index 0000000000..c583aaea5b
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/db/shipping/20200604152933_create_event_store_events.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class CreateEventStoreEvents < ActiveRecord::Migration[4.2]
+ def change
+ postgres = ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
+ sqlite = ActiveRecord::Base.connection.adapter_name == "SQLite"
+ rails_42 = Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("5.0.0")
+ enable_extension "pgcrypto" if postgres
+ create_table(:event_store_events_in_streams, force: false) do |t|
+ t.string :stream, null: false
+ t.integer :position, null: true
+ if postgres
+ t.references :event, null: false, type: :uuid
+ else
+ t.references :event, null: false, type: :string, limit: 36
+ end
+ t.datetime :created_at, null: false
+ end
+ add_index :event_store_events_in_streams, [:stream, :position], unique: true
+ add_index :event_store_events_in_streams, [:created_at]
+ add_index :event_store_events_in_streams, [:stream, :event_id], unique: true
+
+ if postgres
+ create_table(:event_store_events, id: :uuid, default: 'gen_random_uuid()', force: false) do |t|
+ t.string :event_type, null: false
+ t.binary :metadata
+ t.binary :data, null: false
+ t.datetime :created_at, null: false
+ end
+ else
+ create_table(:event_store_events, id: false, force: false) do |t|
+ t.string :id, limit: 36, primary_key: true, null: false
+ t.string :event_type, null: false
+ t.binary :metadata
+ t.binary :data, null: false
+ t.datetime :created_at, null: false
+ end
+ if sqlite && rails_42
+ add_index :event_store_events, :id, unique: true
+ end
+ end
+ add_index :event_store_events, :created_at
+ add_index :event_store_events, :event_type
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/db/shipping/20200622154807_shipping_process_state.rb b/contrib/multiple_databases_repository_sample_app/db/shipping/20200622154807_shipping_process_state.rb
new file mode 100644
index 0000000000..06c5dd105e
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/db/shipping/20200622154807_shipping_process_state.rb
@@ -0,0 +1,9 @@
+class ShippingProcessState < ActiveRecord::Migration[6.0]
+ def change
+ create_table :shipping_process_state, id: :string, primary_key: :order_id do |t|
+ t.integer :customer_id
+ t.integer :delivery_address_id
+ t.binary :states
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/db/shipping_schema.rb b/contrib/multiple_databases_repository_sample_app/db/shipping_schema.rb
new file mode 100644
index 0000000000..8345c08e49
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/db/shipping_schema.rb
@@ -0,0 +1,40 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `rails
+# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 2020_06_22_154807) do
+
+ create_table "event_store_events", id: :string, limit: 36, force: :cascade do |t|
+ t.string "event_type", null: false
+ t.binary "metadata"
+ t.binary "data", null: false
+ t.datetime "created_at", null: false
+ t.index ["created_at"], name: "index_event_store_events_on_created_at"
+ t.index ["event_type"], name: "index_event_store_events_on_event_type"
+ end
+
+ create_table "event_store_events_in_streams", force: :cascade do |t|
+ t.string "stream", null: false
+ t.integer "position"
+ t.string "event_id", limit: 36, null: false
+ t.datetime "created_at", null: false
+ t.index ["created_at"], name: "index_event_store_events_in_streams_on_created_at"
+ t.index ["stream", "event_id"], name: "index_event_store_events_in_streams_on_stream_and_event_id", unique: true
+ t.index ["stream", "position"], name: "index_event_store_events_in_streams_on_stream_and_position", unique: true
+ end
+
+ create_table "shipping_process_state", primary_key: "order_id", id: :string, force: :cascade do |t|
+ t.integer "customer_id"
+ t.integer "delivery_address_id"
+ t.binary "states"
+ end
+
+end
diff --git a/contrib/multiple_databases_repository_sample_app/lib/command.rb b/contrib/multiple_databases_repository_sample_app/lib/command.rb
new file mode 100644
index 0000000000..e8d4cee961
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/lib/command.rb
@@ -0,0 +1,11 @@
+require 'dry-struct'
+
+class Command < Dry::Struct::Value
+ Invalid = Class.new(StandardError)
+
+ def self.new(*)
+ super
+ rescue Dry::Struct::Error => doh
+ raise Invalid, doh
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/lib/command_handler.rb b/contrib/multiple_databases_repository_sample_app/lib/command_handler.rb
new file mode 100644
index 0000000000..9132e92f7e
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/lib/command_handler.rb
@@ -0,0 +1,21 @@
+module CommandHandler
+ def initialize(event_store)
+ @repository = AggregateRoot::InstrumentedRepository.new(
+ AggregateRoot::Repository.new(event_store),
+ ActiveSupport::Notifications
+ )
+ end
+
+ def with_aggregate(aggregate, aggregate_id, &block)
+ stream = stream_name(aggregate.class, aggregate_id)
+ repository.with_aggregate(aggregate, stream, &block)
+ end
+
+ private
+ attr_reader :repository
+
+ def stream_name(aggregate_class, aggregate_id)
+ "#{aggregate_class.name}$#{aggregate_id}"
+ end
+end
+
diff --git a/contrib/multiple_databases_repository_sample_app/lib/event.rb b/contrib/multiple_databases_repository_sample_app/lib/event.rb
new file mode 100644
index 0000000000..c4d59f83b0
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/lib/event.rb
@@ -0,0 +1,55 @@
+require "ruby_event_store"
+require 'time'
+require 'json'
+require 'dry-struct'
+require 'dry-types'
+require 'rspec/core'
+
+module Types
+ include Dry::Types()
+
+ EventId = Types::Coercible::String.default { SecureRandom.uuid }
+ Metadata = Types.Constructor(RubyEventStore::Metadata) { |value| RubyEventStore::Metadata.new(value.to_h) }.default { RubyEventStore::Metadata.new }
+end
+
+
+class Event < Dry::Struct
+ transform_keys(&:to_sym)
+
+ attribute :event_id, Types::EventId
+ attribute :metadata, Types::Metadata
+ alias :message_id :event_id
+
+ def self.new(data: {}, metadata: {}, **args)
+ super(args.merge(data).merge(metadata: metadata))
+ end
+
+ def self.inherited(klass)
+ super
+ klass.define_singleton_method(:event_type) do |value|
+ klass.define_method(:event_type) do
+ value
+ end
+ end
+ end
+
+ def timestamp
+ metadata[:timestamp] && Time.parse(metadata[:timestamp])
+ end
+
+ def data
+ to_h.reject{|k,_| [:event_id, :metadata].include?(k) }
+ end
+
+ def event_type
+ self.class.name
+ end
+
+ def ==(other_event)
+ other_event.instance_of?(self.class) &&
+ other_event.event_id.eql?(event_id) &&
+ other_event.data.eql?(data)
+ end
+
+ alias_method :eql?, :==
+end
diff --git a/contrib/multiple_databases_repository_sample_app/lib/types.rb b/contrib/multiple_databases_repository_sample_app/lib/types.rb
new file mode 100644
index 0000000000..955f08da96
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/lib/types.rb
@@ -0,0 +1,8 @@
+require 'dry-types'
+
+module Types
+ ID = Types::Strict::Integer
+ UUID = Types::Strict::String.constrained(format: /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\z/i)
+ TransactionId = Types::Strict::String.constrained(format: /\A[0-9a-fA-F]{32}\z/i)
+ OrderNumber = Types::Strict::String.constrained(format: /\A\d{4}\/\d{2}\/\d+\z/i)
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders.rb
new file mode 100644
index 0000000000..dc6f64cee8
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require_relative 'orders/application_record'
+require_relative '../../lib/event'
+require_relative '../../lib/types'
+require_relative '../../lib/command'
+require_relative '../../lib/command_handler'
+
+require_dependency 'orders/add_item_to_basket'
+require_dependency 'orders/remove_item_from_basket'
+require_dependency 'orders/submit_order'
+
+require_dependency 'orders/item_added_to_basket'
+require_dependency 'orders/item_removed_from_basket'
+require_dependency 'orders/order_submitted'
+
+require_dependency 'orders/on_add_item_to_basket'
+require_dependency 'orders/on_remove_item_from_basket'
+require_dependency 'orders/on_submit_order'
+
+require_dependency 'orders/order'
+require_dependency 'orders/order_line'
+require_dependency 'orders/number_generator'
+require_dependency 'orders/fake_number_generator'
+
+module Orders
+ def self.setup(config)
+ @@command_bus = config.command_bus
+ @@public_event_store = RailsEventStore::Client.new(
+ repository: config.event_repository,
+ mapper: RubyEventStore::Mappers::Default.new(
+ events_class_remapping: events_class_remapping
+ ),
+ dispatcher: RubyEventStore::ComposedDispatcher.new(
+ RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)),
+ RubyEventStore::Dispatcher.new),
+ )
+ @@module_event_store = RailsEventStore::Client.new(
+ repository: RailsEventStoreActiveRecord::EventRepository.new(
+ model_factory: RailsEventStoreActiveRecord::WithAbstractBaseClass.new(Orders::ApplicationRecord), serializer: JSON),
+ dispatcher: RubyEventStore::ComposedDispatcher.new(
+ RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)),
+ RubyEventStore::Dispatcher.new),
+ )
+
+ # Subscribe public event handlers below
+ public_event_store.tap do |store|
+ end
+
+ # Subscribe private event handlers below
+ event_store.tap do |store|
+ store.subscribe(PrepareOrderProcess.new(store, command_bus), to: [
+ ItemAddedToBasket,
+ ItemRemovedFromBasket,
+ OrderSubmitted,
+ ])
+ end
+
+ # Register commands handled by this module below
+ command_bus.tap do |bus|
+ bus.register(Orders::SubmitOrder, Orders::OnSubmitOrder.new(event_store, number_generator_factory: config.number_generator_factory))
+ bus.register(Orders::AddItemToBasket, Orders::OnAddItemToBasket.new(event_store))
+ bus.register(Orders::RemoveItemFromBasket, Orders::OnRemoveItemFromBasket.new(event_store))
+ bus.register(Orders::PlaceOrder, ->(cmd) { public_event_store.publish(OrderPlaced.new(**cmd)) })
+ end
+ end
+
+ def self.events_class_remapping
+ {
+ 'new-order' => 'Orders::OrderPlaced',
+ }
+ end
+
+ def self.command_bus
+ @@command_bus
+ end
+
+ def self.public_event_store
+ @@public_event_store
+ end
+
+ def self.event_store
+ @@module_event_store
+ end
+
+ def self.setup?
+ command_bus && event_store && public_event_store
+ end
+
+ class OrderPlaced < Event
+ event_type 'new-order'
+ attribute :order_id, Types::UUID
+ attribute :customer_id, Types::ID
+ attribute :delivery_address_id, Types::ID
+ attribute :payment_method_id, Types::ID
+ attribute :amount, Types::Coercible::Decimal
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/.keep b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/add_item_to_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/add_item_to_basket.rb
new file mode 100644
index 0000000000..f0e4ab35de
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/add_item_to_basket.rb
@@ -0,0 +1,6 @@
+module Orders
+ class AddItemToBasket < Command
+ attribute :order_id, Types::UUID
+ attribute :product_id, Types::Coercible::Integer
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/application_record.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/application_record.rb
new file mode 100644
index 0000000000..28321d9aa2
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/application_record.rb
@@ -0,0 +1,8 @@
+require 'active_record'
+
+module Orders
+ class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+ connects_to database: { writing: :orders, reading: :orders }
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/fake_number_generator.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/fake_number_generator.rb
new file mode 100644
index 0000000000..eb10681ddf
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/fake_number_generator.rb
@@ -0,0 +1,7 @@
+module Orders
+ class FakeNumberGenerator
+ def call
+ "2019/01/60"
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_added_to_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_added_to_basket.rb
new file mode 100644
index 0000000000..9eed977c17
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_added_to_basket.rb
@@ -0,0 +1,6 @@
+module Orders
+ class ItemAddedToBasket < Event
+ attribute :order_id, Types::UUID
+ attribute :product_id, Types::ID
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_removed_from_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_removed_from_basket.rb
new file mode 100644
index 0000000000..5005735db3
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_removed_from_basket.rb
@@ -0,0 +1,6 @@
+module Orders
+ class ItemRemovedFromBasket < Event
+ attribute :order_id, Types::UUID
+ attribute :product_id, Types::ID
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/number_generator.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/number_generator.rb
new file mode 100644
index 0000000000..ed60d6cf92
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/number_generator.rb
@@ -0,0 +1,7 @@
+module Orders
+ class NumberGenerator
+ def call
+ Time.current.strftime("%Y/%m/#{SecureRandom.random_number(100)}")
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_add_item_to_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_add_item_to_basket.rb
new file mode 100644
index 0000000000..7f9ae9b052
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_add_item_to_basket.rb
@@ -0,0 +1,11 @@
+module Orders
+ class OnAddItemToBasket
+ include CommandHandler
+
+ def call(command)
+ with_aggregate(Order.new(command.order_id), command.order_id) do |order|
+ order.add_item(command.product_id)
+ end
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_remove_item_from_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_remove_item_from_basket.rb
new file mode 100644
index 0000000000..b287f29b1d
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_remove_item_from_basket.rb
@@ -0,0 +1,11 @@
+module Orders
+ class OnRemoveItemFromBasket
+ include CommandHandler
+
+ def call(command)
+ with_aggregate(Order.new(command.order_id), command.order_id) do |order|
+ order.remove_item(command.product_id)
+ end
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_submit_order.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_submit_order.rb
new file mode 100644
index 0000000000..1b872aa019
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_submit_order.rb
@@ -0,0 +1,29 @@
+module Orders
+ class OnSubmitOrder
+ include CommandHandler
+
+ def initialize(event_store, number_generator_factory:)
+ @repository = AggregateRoot::InstrumentedRepository.new(
+ AggregateRoot::Repository.new(event_store),
+ ActiveSupport::Notifications
+ )
+ @number_generator = number_generator_factory.call
+ end
+
+ def call(command)
+ with_aggregate(Order.new(command.order_id), command.order_id) do |order|
+ order_number = number_generator.call
+ order.submit(
+ order_number,
+ command.customer_id,
+ command.delivery_address_id,
+ command.payment_method_id
+ )
+ end
+ end
+
+ private
+
+ attr_accessor :number_generator
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order.rb
new file mode 100644
index 0000000000..7389784ba3
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order.rb
@@ -0,0 +1,77 @@
+require 'aggregate_root'
+
+module Orders
+ class Order
+ include AggregateRoot
+
+ AlreadySubmitted = Class.new(StandardError)
+ NotSubmitted = Class.new(StandardError)
+ MissingCustomer = Class.new(StandardError)
+
+ def initialize(id)
+ @id = id
+ @state = :draft
+ @order_lines = []
+ end
+
+ def submit(order_number, customer_id, delivery_address_id, payment_method_id)
+ raise AlreadySubmitted if @state == :submitted
+ raise MissingCustomer unless customer_id
+ apply OrderSubmitted.new(data: {
+ order_id: @id,
+ order_number: order_number,
+ customer_id: customer_id,
+ delivery_address_id: delivery_address_id,
+ payment_method_id: payment_method_id,
+ })
+ end
+
+ def add_item(product_id)
+ raise AlreadySubmitted unless @state == :draft
+ apply ItemAddedToBasket.new(data: {order_id: @id, product_id: product_id})
+ end
+
+ def remove_item(product_id)
+ raise AlreadySubmitted unless @state == :draft
+ apply ItemRemovedFromBasket.new(data: {order_id: @id, product_id: product_id})
+ end
+
+ on OrderSubmitted do |event|
+ @customer_id = event.data[:customer_id]
+ @number = event.data[:order_number]
+ @state = :submitted
+ end
+
+ on ItemAddedToBasket do |event|
+ product_id = event.data[:product_id]
+ order_line = find_order_line(product_id)
+ unless order_line
+ order_line = create_order_line(product_id)
+ @order_lines << order_line
+ end
+ order_line.increase_quantity
+ end
+
+ on ItemRemovedFromBasket do |event|
+ product_id = event.data[:product_id]
+ order_line = find_order_line(product_id)
+ return unless order_line
+ order_line.decrease_quantity
+ remove_order_line(order_line) if order_line.empty?
+ end
+
+ private
+
+ def find_order_line(product_id)
+ @order_lines.select { |line| line.product_id == product_id }.first
+ end
+
+ def create_order_line(product_id)
+ OrderLine.new(product_id)
+ end
+
+ def remove_order_line(order_line)
+ @order_lines.delete(order_line)
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_line.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_line.rb
new file mode 100644
index 0000000000..b565444568
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_line.rb
@@ -0,0 +1,31 @@
+module Orders
+ class OrderLine
+ include Comparable
+ attr_reader :product_id
+
+ def initialize(product_id)
+ @product_id = product_id
+ @quantity = 0
+ end
+
+ def increase_quantity
+ @quantity += 1
+ end
+
+ def decrease_quantity
+ @quantity -= 1
+ end
+
+ def empty?
+ @quantity == 0
+ end
+
+ def <=>(other)
+ self.product_id <=> other.product_id
+ end
+
+ private
+
+ attr_accessor :quantity
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_submitted.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_submitted.rb
new file mode 100644
index 0000000000..4d29900b80
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_submitted.rb
@@ -0,0 +1,9 @@
+module Orders
+ class OrderSubmitted < Event
+ attribute :order_id, Types::UUID
+ attribute :order_number, Types::OrderNumber
+ attribute :customer_id, Types::ID
+ attribute :delivery_address_id, Types::ID
+ attribute :payment_method_id, Types::ID
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/place_order.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/place_order.rb
new file mode 100644
index 0000000000..45f92d0571
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/place_order.rb
@@ -0,0 +1,9 @@
+module Orders
+ class PlaceOrder < Command
+ attribute :order_id, Types::UUID
+ attribute :customer_id, Types::Coercible::Integer
+ attribute :delivery_address_id, Types::Coercible::Integer
+ attribute :payment_method_id, Types::Coercible::Integer
+ attribute :amount, Types::Decimal
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/prepare_order_process.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/prepare_order_process.rb
new file mode 100644
index 0000000000..7137f27286
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/prepare_order_process.rb
@@ -0,0 +1,64 @@
+module Orders
+ class PrepareOrderProcess
+ def initialize(event_store, bus)
+ @event_store = event_store
+ @bus = bus
+ end
+
+ def call(event)
+ with_linked(event) do |state|
+ bus.call(state.command) if state.done?
+ end
+ end
+
+ private
+ attr_reader :event_store, :bus
+
+ class State
+ def initialize(events)
+ @data = { amount: 0.to_d }
+ events.each{|e| apply(e)}
+ end
+
+ def done?
+ data[:amount] > 0 && data[:order_id]
+ end
+
+ def command
+ Orders::PlaceOrder.new(**data)
+ end
+
+ private
+ attr_reader :data
+
+ def apply(event)
+ case event
+ when Orders::ItemAddedToBasket
+ data[:amount] += 10.to_d
+ when Orders::ItemRemovedFromBasket
+ data[:amount] -= 10.to_d
+ when Orders::OrderSubmitted
+ data.merge!(event.data.slice(
+ :order_id,
+ :order_number,
+ :customer_id,
+ :delivery_address_id,
+ :payment_method_id
+ ))
+ else
+ raise ArgumentError.new("Not suported domain event")
+ end
+ end
+ end
+
+ def with_linked(event)
+ stream = "PreparationProcess$#{event.order_id}"
+ event_store.link(
+ event.event_id,
+ stream_name: stream
+ )
+ yield State.new(event_store.read.stream(stream))
+ rescue RubyEventStore::EventDuplicatedInStream
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/remove_item_from_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/remove_item_from_basket.rb
new file mode 100644
index 0000000000..2c8b065bb1
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/remove_item_from_basket.rb
@@ -0,0 +1,6 @@
+module Orders
+ class RemoveItemFromBasket < Command
+ attribute :order_id, Types::UUID
+ attribute :product_id, Types::Coercible::Integer
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/submit_order.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/submit_order.rb
new file mode 100644
index 0000000000..62ea8645a1
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/submit_order.rb
@@ -0,0 +1,8 @@
+module Orders
+ class SubmitOrder < Command
+ attribute :order_id, Types::UUID
+ attribute :customer_id, Types::Coercible::Integer
+ attribute :delivery_address_id, Types::Coercible::Integer
+ attribute :payment_method_id, Types::Coercible::Integer
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/add_item_to_basket_spec.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/add_item_to_basket_spec.rb
new file mode 100644
index 0000000000..f1486db5ea
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/spec/add_item_to_basket_spec.rb
@@ -0,0 +1,38 @@
+require_relative 'spec_helper'
+
+module Orders
+ RSpec.describe AddItemToBasket do
+ let(:aggregate_id) { SecureRandom.uuid }
+ let(:stream) { "Orders::Order$#{aggregate_id}" }
+ let(:customer_id) { 997 }
+ let(:address_id) { 998 }
+ let(:payment_method_id) { 999 }
+ let(:product_id) { 123 }
+ let(:order_number) { "2019/01/60" }
+
+ it 'item is added to draft order' do
+ Orders.act(AddItemToBasket.new(order_id: aggregate_id, product_id: product_id))
+ expect(Orders.event_store).to have_published(
+ an_event(ItemAddedToBasket)
+ .with_data(order_id: aggregate_id, product_id: product_id)
+ )
+ end
+
+ it 'no add allowed to submitted order' do
+ Orders.arrange(stream, [
+ ItemAddedToBasket.new(data: {order_id: aggregate_id, product_id: product_id}),
+ OrderSubmitted.new(data: {
+ order_id: aggregate_id,
+ order_number: order_number,
+ customer_id: customer_id,
+ delivery_address_id: address_id,
+ payment_method_id: payment_method_id,
+ })
+ ])
+
+ expect do
+ Orders.act(AddItemToBasket.new(order_id: aggregate_id, product_id: product_id))
+ end.to raise_error(Order::AlreadySubmitted)
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/order_spec.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/order_spec.rb
new file mode 100644
index 0000000000..cdbec04180
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/spec/order_spec.rb
@@ -0,0 +1,6 @@
+require_relative 'spec_helper'
+
+module Orders
+ RSpec.describe Order do
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/prepare_order_process_spec.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/prepare_order_process_spec.rb
new file mode 100644
index 0000000000..2e6f04ded4
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/spec/prepare_order_process_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+module Orders
+ RSpec.describe PrepareOrderProcess do
+ let(:order_id) { SecureRandom.uuid }
+ let(:stream) { "Orders::Order$#{order_id}" }
+ let(:customer_id) { 997 }
+ let(:address_id) { 998 }
+ let(:payment_method_id) { 999 }
+ let(:product_id) { 123 }
+ let(:order_number) { "2019/01/60" }
+
+ it 'works' do
+ Orders.act(AddItemToBasket.new(order_id: order_id, product_id: product_id))
+ Orders.act(AddItemToBasket.new(order_id: order_id, product_id: product_id))
+ Orders.act(SubmitOrder.new(
+ order_id: order_id,
+ customer_id: customer_id,
+ delivery_address_id: address_id,
+ payment_method_id: payment_method_id,
+ ))
+
+ expect(Orders.public_event_store).to have_published(
+ an_event(Orders::OrderPlaced).with_data(
+ order_id: order_id,
+ customer_id: customer_id,
+ delivery_address_id: address_id,
+ payment_method_id: payment_method_id,
+ amount: 20.to_d,
+ )
+ )
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/remove_item_from_basket_spec.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/remove_item_from_basket_spec.rb
new file mode 100644
index 0000000000..d634f20dc1
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/spec/remove_item_from_basket_spec.rb
@@ -0,0 +1,39 @@
+require_relative 'spec_helper'
+
+module Orders
+ RSpec.describe RemoveItemFromBasket do
+ let(:aggregate_id) { SecureRandom.uuid }
+ let(:stream) { "Orders::Order$#{aggregate_id}" }
+ let(:customer_id) { 997 }
+ let(:address_id) { 998 }
+ let(:payment_method_id) { 999 }
+ let(:product_id) { 123 }
+ let(:order_number) { "2019/01/60" }
+
+ it 'item is removed from draft order' do
+ Orders.arrange(stream, [ItemAddedToBasket.new(data: {order_id: aggregate_id, product_id: product_id})])
+ Orders.act(RemoveItemFromBasket.new(order_id: aggregate_id, product_id: product_id))
+ expect(Orders.event_store).to have_published(
+ an_event(ItemRemovedFromBasket)
+ .with_data(order_id: aggregate_id, product_id: product_id)
+ )
+ end
+
+ it 'no remove allowed to created order' do
+ Orders.arrange(stream, [
+ ItemAddedToBasket.new(data: {order_id: aggregate_id, product_id: product_id}),
+ OrderSubmitted.new(data: {
+ order_id: aggregate_id,
+ order_number: order_number,
+ customer_id: customer_id,
+ delivery_address_id: address_id,
+ payment_method_id: payment_method_id,
+ })
+ ])
+
+ expect do
+ Orders.act(RemoveItemFromBasket.new(order_id: aggregate_id, product_id: product_id))
+ end.to raise_error(Order::AlreadySubmitted)
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/spec_helper.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/spec_helper.rb
new file mode 100644
index 0000000000..6f6b503cfc
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/spec/spec_helper.rb
@@ -0,0 +1,22 @@
+ENV['RAILS_ENV'] = 'test'
+
+require_relative '../lib/orders'
+
+module Orders
+ def self.arrange(stream, events, event_store: Orders.event_store)
+ event_store.append(events, stream_name: stream)
+ end
+
+ def self.act(command, bus: Orders.command_bus)
+ bus.call(command)
+ end
+end
+
+unless Orders.setup?
+ Configuration = Struct.new(:event_repository, :command_bus, :number_generator_factory)
+ Orders.setup(Configuration.new(
+ RubyEventStore::InMemoryRepository.new,
+ Arkency::CommandBus.new,
+ ->{ Orders::FakeNumberGenerator.new },
+ ))
+end
diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/submit_order_spec.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/submit_order_spec.rb
new file mode 100644
index 0000000000..9c95b66086
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/orders/spec/submit_order_spec.rb
@@ -0,0 +1,68 @@
+require_relative 'spec_helper'
+
+module Orders
+ RSpec.describe SubmitOrder do
+ let(:aggregate_id) { SecureRandom.uuid }
+ let(:stream) { "Orders::Order$#{aggregate_id}" }
+ let(:customer_id) { 997 }
+ let(:address_id) { 998 }
+ let(:payment_method_id) { 999 }
+ let(:product_id) { 123 }
+ let(:order_number) { "2019/01/60" }
+
+ it 'order is submitted' do
+ Orders.arrange(stream, [ItemAddedToBasket.new(data: {order_id: aggregate_id, product_id: product_id})])
+ Orders.act(SubmitOrder.new(
+ order_id: aggregate_id,
+ customer_id: customer_id,
+ delivery_address_id: address_id,
+ payment_method_id: payment_method_id,
+ ))
+
+ expect(Orders.event_store).to have_published(
+ an_event(OrderSubmitted)
+ .with_data(
+ order_id: aggregate_id,
+ order_number: order_number,
+ customer_id: customer_id,
+ delivery_address_id: address_id,
+ payment_method_id: payment_method_id,
+ )
+ )
+ end
+
+ it 'could not create order where customer is not given' do
+ expect do
+ Orders.act(SubmitOrder.new(
+ order_id: aggregate_id,
+ customer_id: nil,
+ delivery_addredd_id: nil,
+ payment_method_id: nil
+ ))
+ end.to raise_error(Command::Invalid)
+ end
+
+ it 'already created order could not be created again' do
+ another_customer_id = 998
+ Orders.arrange(stream, [
+ ItemAddedToBasket.new(data: {order_id: aggregate_id, product_id: product_id}),
+ OrderSubmitted.new(data: {
+ order_id: aggregate_id,
+ order_number: order_number,
+ customer_id: customer_id,
+ delivery_address_id: address_id,
+ payment_method_id: payment_method_id,
+ })
+ ])
+
+ expect do
+ Orders.act(SubmitOrder.new(
+ order_id: aggregate_id,
+ customer_id: another_customer_id,
+ delivery_address_id: address_id,
+ payment_method_id: payment_method_id,
+ ))
+ end.to raise_error(Order::AlreadySubmitted)
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments.rb
new file mode 100644
index 0000000000..2d6b430a15
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require_relative 'payments/application_record'
+require_relative '../../lib/event'
+require_relative '../../lib/types'
+require_relative '../../lib/command'
+require_relative '../../lib/command_handler'
+
+require_dependency 'payments/payment_authorized'
+require_dependency 'payments/payment_captured'
+require_dependency 'payments/payment_released'
+require_dependency 'payments/payment_expired'
+
+require_dependency 'payments/authorize_payment'
+require_dependency 'payments/capture_payment'
+require_dependency 'payments/release_payment'
+require_dependency 'payments/set_payment_as_expired'
+require_dependency 'payments/complete_payment'
+
+require_dependency 'payments/on_authorize_payment'
+require_dependency 'payments/on_capture_payment'
+require_dependency 'payments/on_release_payment'
+require_dependency 'payments/on_expire_payment'
+require_dependency 'payments/on_complete_payment'
+
+require_dependency 'payments/payment'
+require_dependency 'payments/payment_process'
+
+module Payments
+ def self.setup(config)
+ @@command_bus = config.command_bus
+ @@public_event_store = RailsEventStore::Client.new(
+ repository: config.event_repository,
+ mapper: RubyEventStore::Mappers::Default.new(
+ events_class_remapping: events_class_remapping
+ ),
+ dispatcher: RubyEventStore::ComposedDispatcher.new(
+ RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)),
+ RubyEventStore::Dispatcher.new),
+ )
+ @@module_event_store = RailsEventStore::Client.new(
+ repository: RailsEventStoreActiveRecord::EventRepository.new(
+ model_factory: RailsEventStoreActiveRecord::WithAbstractBaseClass.new(Payments::ApplicationRecord), serializer: JSON),
+ dispatcher: RubyEventStore::ComposedDispatcher.new(
+ RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)),
+ RubyEventStore::Dispatcher.new),
+ )
+
+ # Subscribe public event handlers below
+ public_event_store.tap do |store|
+ store.subscribe(InitiatePayment.new(command_bus), to: ['new-order'])
+ end
+
+ # Subscribe private event handlers below
+ event_store.tap do |store|
+ store.subscribe(PaymentProcess.new(store, command_bus), to: [
+ PaymentAuthorized,
+ PaymentCaptured,
+ PaymentExpired,
+ ])
+ end
+
+ # Register commands handled by this module below
+ command_bus.tap do |bus|
+ bus.register(Payments::AuthorizePayment, Payments::OnAuthorizePayment.new(event_store))
+ bus.register(Payments::SetPaymentAsExpired, Payments::OnExpirePayment.new(event_store))
+ bus.register(Payments::CapturePayment, Payments::OnCapturePayment.new(event_store))
+ bus.register(Payments::ReleasePayment, Payments::OnReleasePayment.new(event_store))
+ bus.register(Payments::CompletePayment, Payments::OnCompletePayment.new(public_event_store))
+ end
+ end
+
+ def self.events_class_remapping
+ {
+ 'new-order' => 'Payments::PaymentInitiated',
+ 'payment-completed' => 'Payments::PaymentCompleted',
+ }
+ end
+
+ def self.command_bus
+ @@command_bus rescue nil
+ end
+
+ def self.public_event_store
+ @@public_event_store rescue nil
+ end
+
+ def self.event_store
+ @@module_event_store rescue nil
+ end
+
+ def self.setup?
+ command_bus && event_store && public_event_store
+ end
+
+ class PaymentInitiated < Event
+ event_type 'new-order'
+ attribute :order_id, Types::UUID
+ attribute :amount, Types::Coercible::Decimal
+ end
+
+ class PaymentCompleted < Event
+ event_type 'payment-completed'
+ attribute :transaction_id, Types::Coercible::String
+ attribute :order_id, Types::UUID
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/.keep b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/application_record.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/application_record.rb
new file mode 100644
index 0000000000..fa51873d0e
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/application_record.rb
@@ -0,0 +1,6 @@
+module Payments
+ class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+ connects_to database: { writing: :payments, reading: :payments }
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/authorize_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/authorize_payment.rb
new file mode 100644
index 0000000000..c3a8293113
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/authorize_payment.rb
@@ -0,0 +1,7 @@
+module Payments
+ class AuthorizePayment < Command
+ attribute :transaction_id, Types::Coercible::String
+ attribute :order_id, Types::UUID
+ attribute :amount, Types::Decimal
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/capture_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/capture_payment.rb
new file mode 100644
index 0000000000..e210af707c
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/capture_payment.rb
@@ -0,0 +1,5 @@
+module Payments
+ class CapturePayment < Command
+ attribute :transaction_id, Types::Coercible::String
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/complete_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/complete_payment.rb
new file mode 100644
index 0000000000..eba2e0f109
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/complete_payment.rb
@@ -0,0 +1,6 @@
+module Payments
+ class CompletePayment < Command
+ attribute :transaction_id, Types::Coercible::String
+ attribute :order_id, Types::UUID
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/initiate_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/initiate_payment.rb
new file mode 100644
index 0000000000..8a94af7f66
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/initiate_payment.rb
@@ -0,0 +1,13 @@
+module Payments
+ class InitiatePayment
+ def initialize(bus)
+ @bus = bus
+ end
+
+ def call(event)
+ @bus.call(AuthorizePayment.new(**event.data.merge(
+ transaction_id: SecureRandom.hex(16),
+ )))
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_authorize_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_authorize_payment.rb
new file mode 100644
index 0000000000..d609c0a26f
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_authorize_payment.rb
@@ -0,0 +1,11 @@
+module Payments
+ class OnAuthorizePayment
+ include CommandHandler
+
+ def call(command)
+ with_aggregate(Payment.new, command.transaction_id) do |payment|
+ payment.authorize(command.transaction_id, command.order_id, command.amount)
+ end
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_capture_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_capture_payment.rb
new file mode 100644
index 0000000000..fd7b5f3307
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_capture_payment.rb
@@ -0,0 +1,11 @@
+module Payments
+ class OnCapturePayment
+ include CommandHandler
+
+ def call(command)
+ with_aggregate(Payment.new, command.transaction_id) do |payment|
+ payment.capture
+ end
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_complete_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_complete_payment.rb
new file mode 100644
index 0000000000..dd4df82ee8
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_complete_payment.rb
@@ -0,0 +1,14 @@
+module Payments
+ class OnCompletePayment
+ def initialize(event_store)
+ @event_store = event_store
+ end
+
+ def call(command)
+ event_store.publish(Payments::PaymentCompleted.new(**command))
+ end
+
+ private
+ attr_reader :event_store
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_expire_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_expire_payment.rb
new file mode 100644
index 0000000000..2a6ac21f1c
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_expire_payment.rb
@@ -0,0 +1,11 @@
+module Payments
+ class OnExpirePayment
+ include CommandHandler
+
+ def call(command)
+ with_aggregate(Payment.new, command.transaction_id) do |payment|
+ payment.expire
+ end
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_release_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_release_payment.rb
new file mode 100644
index 0000000000..022acbfc7c
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_release_payment.rb
@@ -0,0 +1,11 @@
+module Payments
+ class OnReleasePayment
+ include CommandHandler
+
+ def call(command)
+ with_aggregate(Payment.new, command.transaction_id) do |payment|
+ payment.release
+ end
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment.rb
new file mode 100644
index 0000000000..403d9ff0e2
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment.rb
@@ -0,0 +1,91 @@
+module Payments
+ class Payment
+ include AggregateRoot
+
+ AlreadyAuthorized = Class.new(StandardError)
+ NotAuthorized = Class.new(StandardError)
+ AlreadyCaptured = Class.new(StandardError)
+ AlreadyReleased = Class.new(StandardError)
+
+ def initialize
+ @state = nil
+ end
+
+ def authorize(transaction_id, order_id, amount)
+ raise AlreadyAuthorized unless initiated?
+ apply(PaymentAuthorized.new(data: {
+ transaction_id: transaction_id,
+ order_id: order_id,
+ amount: amount
+ }))
+ end
+
+ def expire
+ raise NotAuthorized unless authorized?
+ raise AlreadyCaptured if captured?
+ raise AlreadyReleased if released?
+ apply(PaymentExpired.new(data: {
+ transaction_id: @transaction_id,
+ }))
+ end
+
+ def capture
+ raise NotAuthorized unless authorized?
+ raise AlreadyCaptured if captured?
+ apply(PaymentCaptured.new(data: {
+ transaction_id: @transaction_id,
+ }))
+ end
+
+ def release
+ raise NotAuthorized unless authorized?
+ raise AlreadyCaptured if captured?
+ raise AlreadyReleased if released?
+ apply(PaymentReleased.new(data: {
+ transaction_id: @transaction_id,
+ }))
+ end
+
+ private
+
+ on PaymentAuthorized do |event|
+ @state = :authorized
+ @transaction_id = event.transaction_id
+ @order_id = event.order_id
+ @amount = event.amount
+ end
+
+ on PaymentExpired do |event|
+ @state = :expired
+ end
+
+ on PaymentCaptured do |event|
+ @state = :captured
+ end
+
+ on PaymentReleased do |event|
+ @state = :released
+ end
+
+ def initiated?
+ @state == nil
+ end
+
+ def authorized?
+ @state != nil
+ end
+
+ def expired?
+ @state == :expired
+ end
+
+ def captured?
+ @state == :captured
+ end
+
+ def released?
+ @state == :released
+ end
+ end
+end
+
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_authorized.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_authorized.rb
new file mode 100644
index 0000000000..663740160b
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_authorized.rb
@@ -0,0 +1,7 @@
+module Payments
+ class PaymentAuthorized < Event
+ attribute :order_id, Types::UUID
+ attribute :transaction_id, Types::TransactionId
+ attribute :amount, Types::Coercible::Decimal
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_captured.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_captured.rb
new file mode 100644
index 0000000000..5834f3d901
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_captured.rb
@@ -0,0 +1,5 @@
+module Payments
+ class PaymentCaptured < Event
+ attribute :transaction_id, Types::TransactionId
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_expired.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_expired.rb
new file mode 100644
index 0000000000..8b96e1d4c7
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_expired.rb
@@ -0,0 +1,5 @@
+module Payments
+ class PaymentExpired < Event
+ attribute :transaction_id, Types::TransactionId
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_process.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_process.rb
new file mode 100644
index 0000000000..cc0b66a22b
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_process.rb
@@ -0,0 +1,75 @@
+module Payments
+ class PaymentProcess
+ def initialize(event_store, bus)
+ @event_store = event_store
+ @bus = bus
+ end
+
+ def call(event)
+ with_linked(event) do |state|
+ bus.call(state.command) if state.done?
+ end
+ end
+
+ private
+ attr_reader :event_store, :bus
+
+ class State
+ def initialize(events)
+ @data = {}
+ @state = nil
+ @action = nil
+ events.each{|e| apply(e)}
+ end
+
+ def done?
+ !!action
+ end
+
+ def command
+ case action
+ when :complete
+ Payments::CompletePayment.new(**data)
+ when :release
+ Payments::ReleasePayment.new(**data.slice(:transaction_id))
+ else
+ raise ArgumentError.new("Unknown action")
+ end
+ end
+
+ private
+ attr_reader :data, :state, :action
+
+ def apply(event)
+ case event
+ when Payments::PaymentAuthorized
+ data[:transaction_id] = event.transaction_id
+ data[:order_id] = event.order_id
+ @state = :authorized
+ when Payments::PaymentCaptured
+ @action = :complete if authorized?
+ @state = :captured
+ when Payments::PaymentExpired
+ @action = :release if authorized?
+ @state = :expired
+ else
+ raise ArgumentError.new("Not suported domain event")
+ end
+ end
+
+ def authorized?
+ state == :authorized
+ end
+ end
+
+ def with_linked(event)
+ stream = "PaymentProcess$#{event.transaction_id}"
+ event_store.link(
+ event.event_id,
+ stream_name: stream
+ )
+ yield State.new(event_store.read.stream(stream))
+ rescue RubyEventStore::EventDuplicatedInStream
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_released.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_released.rb
new file mode 100644
index 0000000000..0f85f9b77e
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_released.rb
@@ -0,0 +1,5 @@
+module Payments
+ class PaymentReleased < Event
+ attribute :transaction_id, Types::TransactionId
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/release_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/release_payment.rb
new file mode 100644
index 0000000000..6e7ab47ed6
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/release_payment.rb
@@ -0,0 +1,5 @@
+module Payments
+ class ReleasePayment < Command
+ attribute :transaction_id, Types::Coercible::String
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/set_payment_as_expired.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/set_payment_as_expired.rb
new file mode 100644
index 0000000000..9fa24c75a4
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/set_payment_as_expired.rb
@@ -0,0 +1,5 @@
+module Payments
+ class SetPaymentAsExpired < Command
+ attribute :transaction_id, Types::Coercible::String
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/initiate_payment_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/initiate_payment_spec.rb
new file mode 100644
index 0000000000..08e532997d
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/spec/initiate_payment_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+module Payments
+ RSpec.describe InitiatePayment do
+ let(:order_id) { SecureRandom.uuid }
+
+ it 'works' do
+ Payments.public_event_store.publish(
+ PaymentInitiated.new(
+ order_id: order_id,
+ amount: 20.to_d,
+ )
+ )
+
+ expect(Payments.event_store).to have_published(
+ an_event(PaymentAuthorized).with_data(
+ transaction_id: kind_of(String),
+ order_id: order_id,
+ amount: 20.to_d,
+ )
+ )
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/on_authorize_payment_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/on_authorize_payment_spec.rb
new file mode 100644
index 0000000000..e95775c24c
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/spec/on_authorize_payment_spec.rb
@@ -0,0 +1,22 @@
+require_relative 'spec_helper'
+
+module Payments
+ RSpec.describe OnAuthorizePayment do
+ it 'authorize payment' do
+ transaction_id = SecureRandom.hex(16)
+ order_id = SecureRandom.uuid
+
+ Payments.act(AuthorizePayment.new(transaction_id: transaction_id, order_id: order_id, amount: 20.to_d))
+
+ expect(Payments.event_store).to have_published(
+ an_event(PaymentAuthorized)
+ .with_data(
+ transaction_id: transaction_id,
+ order_id: order_id,
+ amount: 20.to_d
+ )
+ )
+ end
+ end
+end
+
diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/on_capture_payment_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/on_capture_payment_spec.rb
new file mode 100644
index 0000000000..455ec0fd50
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/spec/on_capture_payment_spec.rb
@@ -0,0 +1,19 @@
+require_relative 'spec_helper'
+
+module Payments
+ RSpec.describe OnCapturePayment do
+ it 'capture payment' do
+ transaction_id = SecureRandom.hex(16)
+ order_id = SecureRandom.uuid
+ stream = "Payments::Payment$#{transaction_id}"
+
+ Payments.arrange(stream, [PaymentAuthorized.new(data: {transaction_id: transaction_id, order_id: order_id, amount: 20.to_d})])
+ Payments.act(CapturePayment.new(transaction_id: transaction_id))
+
+ expect(Payments.event_store).to have_published(
+ an_event(PaymentCaptured)
+ .with_data(transaction_id: transaction_id)
+ )
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/on_release_payment_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/on_release_payment_spec.rb
new file mode 100644
index 0000000000..0584d79939
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/spec/on_release_payment_spec.rb
@@ -0,0 +1,19 @@
+require_relative 'spec_helper'
+
+module Payments
+ RSpec.describe OnReleasePayment do
+ it 'release payment' do
+ transaction_id = SecureRandom.hex(16)
+ order_id = SecureRandom.uuid
+ stream = "Payments::Payment$#{transaction_id}"
+
+ Payments.arrange(stream, [PaymentAuthorized.new(data: {transaction_id: transaction_id, order_id: order_id, amount: 20.to_d})])
+ Payments.act(ReleasePayment.new(transaction_id: transaction_id))
+
+ expect(Payments.event_store).to have_published(
+ an_event(PaymentReleased)
+ .with_data(transaction_id: transaction_id)
+ )
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/payment_process_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/payment_process_spec.rb
new file mode 100644
index 0000000000..2c90a69e5d
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/spec/payment_process_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+module Payments
+ RSpec.describe PaymentProcess do
+ let(:transaction_id) { SecureRandom.hex(16) }
+ let(:order_id) { SecureRandom.uuid }
+
+ it 'complete captured payment' do
+ Payments.act(AuthorizePayment.new(transaction_id: transaction_id, order_id: order_id, amount: 20.to_d))
+ Payments.act(CapturePayment.new(transaction_id: transaction_id, order_id: order_id))
+
+ expect(Payments.public_event_store).to have_published(
+ an_event(Payments::PaymentCompleted).with_data(
+ transaction_id: transaction_id,
+ order_id: order_id,
+ )
+ )
+ end
+
+ it 'release expired payment' do
+ Payments.act(AuthorizePayment.new(transaction_id: transaction_id, order_id: order_id, amount: 20.to_d))
+ Payments.act(SetPaymentAsExpired.new(transaction_id: transaction_id))
+
+ expect(Payments.public_event_store).not_to have_published(
+ an_event(Payments::PaymentCompleted)
+ )
+
+ expect(Payments.event_store).to have_published(
+ an_event(Payments::PaymentReleased).with_data(
+ transaction_id: transaction_id,
+ )
+ )
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/payment_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/payment_spec.rb
new file mode 100644
index 0000000000..f0126f3f4d
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/spec/payment_spec.rb
@@ -0,0 +1,112 @@
+require_relative 'spec_helper'
+
+module Payments
+ RSpec.describe Payment do
+ it 'authorize' do
+ payment = Payment.new
+ expect do
+ payment.authorize(transaction_id, order_id, 20.to_d)
+ end.to apply(
+ an_event(PaymentAuthorized)
+ .with_data(
+ transaction_id: transaction_id,
+ order_id: order_id,
+ amount: 20.to_d,
+ )
+ ).in(payment)
+ end
+
+ it 'should not allow for double authorization' do
+ expect do
+ authorized_payment.authorize(transaction_id, order_id, 20.to_d)
+ end.to raise_error(Payment::AlreadyAuthorized)
+ end
+
+ it 'should capture authorized payment' do
+ payment = authorized_payment
+
+ expect do
+ payment.capture
+ end.to apply(
+ an_event(PaymentCaptured)
+ .with_data(transaction_id: transaction_id)
+ ).in(payment)
+ end
+
+ it 'must not capture not authorized payment' do
+ expect do
+ Payment.new.capture
+ end.to raise_error(Payment::NotAuthorized)
+ end
+
+ it 'should not allow for double capture' do
+ expect do
+ captured_payment.capture
+ end.to raise_error(Payment::AlreadyCaptured)
+ end
+
+ it 'authorization could be released' do
+ payment = authorized_payment
+
+ expect do
+ payment.release
+ end.to apply(
+ an_event(PaymentReleased)
+ .with_data(transaction_id: transaction_id)
+ ).in(payment)
+ end
+
+ it 'must not release not captured payment' do
+ expect do
+ captured_payment.release
+ end.to raise_error(Payment::AlreadyCaptured)
+ end
+
+ it 'must not release not authorized payment' do
+ expect do
+ Payment.new.release
+ end.to raise_error(Payment::NotAuthorized)
+ end
+
+ it 'should not allow for double release' do
+ expect do
+ released_payment.release
+ end.to raise_error(Payment::AlreadyReleased)
+ end
+
+ let(:transaction_id) { SecureRandom.hex(16) }
+ let(:order_id) { SecureRandom.uuid }
+
+ def authorized_payment
+ Payment.new.tap do |payment|
+ payment.apply(
+ PaymentAuthorized.new(data: {
+ transaction_id: transaction_id,
+ order_id: order_id,
+ amount: 20.to_d,
+ })
+ )
+ end
+ end
+
+ def captured_payment
+ authorized_payment.tap do |payment|
+ payment.apply(
+ PaymentCaptured.new(data: {
+ transaction_id: transaction_id,
+ })
+ )
+ end
+ end
+
+ def released_payment
+ captured_payment.tap do |payment|
+ payment.apply(
+ PaymentReleased.new(data: {
+ transaction_id: transaction_id,
+ })
+ )
+ end
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/spec_helper.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/spec_helper.rb
new file mode 100644
index 0000000000..5fb442c0ff
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/payments/spec/spec_helper.rb
@@ -0,0 +1,37 @@
+require 'active_record'
+
+ENV['RAILS_ENV'] ||= 'test'
+if ActiveRecord::Base.configurations.empty?
+ configuration = YAML::load(File.open('../config/database.yml'))
+ ActiveRecord::Base.configurations = configuration
+ ActiveRecord::Base.establish_connection(configuration[ENV['RAILS_ENV']]["payments"])
+ begin
+ ActiveRecord::Migration.maintain_test_schema!
+ rescue ActiveRecord::PendingMigrationError => e
+ puts e.to_s.strip
+ exit 1
+ end
+ require 'arkency/command_bus'
+ require 'rails_event_store'
+ require 'aggregate_root'
+end
+
+require_relative '../lib/payments'
+
+module Payments
+ def self.arrange(stream, events, event_store: Payments.event_store)
+ event_store.append(events, stream_name: stream)
+ end
+
+ def self.act(command, bus: Payments.command_bus)
+ bus.call(command)
+ end
+end
+
+unless Payments.setup?
+ Configuration = Struct.new(:event_repository, :command_bus)
+ Payments.setup(Configuration.new(
+ RubyEventStore::InMemoryRepository.new,
+ Arkency::CommandBus.new,
+ ))
+end
diff --git a/contrib/multiple_databases_repository_sample_app/public/404.html b/contrib/multiple_databases_repository_sample_app/public/404.html
new file mode 100644
index 0000000000..2be3af26fc
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/public/404.html
@@ -0,0 +1,67 @@
+
+
+
+ The page you were looking for doesn't exist (404)
+
+
+
+
+
+
+
+
+
The page you were looking for doesn't exist.
+
You may have mistyped the address or the page may have moved.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/contrib/multiple_databases_repository_sample_app/public/422.html b/contrib/multiple_databases_repository_sample_app/public/422.html
new file mode 100644
index 0000000000..c08eac0d1d
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/public/422.html
@@ -0,0 +1,67 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
+
+
The change you wanted was rejected.
+
Maybe you tried to change something you didn't have access to.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/contrib/multiple_databases_repository_sample_app/public/500.html b/contrib/multiple_databases_repository_sample_app/public/500.html
new file mode 100644
index 0000000000..78a030af22
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/public/500.html
@@ -0,0 +1,66 @@
+
+
+
+ We're sorry, but something went wrong (500)
+
+
+
+
+
+
+
+
+
We're sorry, but something went wrong.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/contrib/multiple_databases_repository_sample_app/public/apple-touch-icon-precomposed.png b/contrib/multiple_databases_repository_sample_app/public/apple-touch-icon-precomposed.png
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/contrib/multiple_databases_repository_sample_app/public/apple-touch-icon.png b/contrib/multiple_databases_repository_sample_app/public/apple-touch-icon.png
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/contrib/multiple_databases_repository_sample_app/public/favicon.ico b/contrib/multiple_databases_repository_sample_app/public/favicon.ico
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/contrib/multiple_databases_repository_sample_app/public/robots.txt b/contrib/multiple_databases_repository_sample_app/public/robots.txt
new file mode 100644
index 0000000000..c19f78ab68
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/public/robots.txt
@@ -0,0 +1 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping.rb
new file mode 100644
index 0000000000..4db2c0e274
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require_relative 'shipping/application_record'
+require_relative '../../lib/event'
+require_relative '../../lib/types'
+require_relative '../../lib/command'
+require_relative '../../lib/command_handler'
+
+require_dependency 'shipping/package_shipped'
+
+require_dependency 'shipping/ship_package'
+
+require_dependency 'shipping/on_ship_package'
+
+require_dependency 'shipping/package'
+require_dependency 'shipping/shipping_process'
+
+module Shipping
+ def self.setup(config)
+ @@command_bus = config.command_bus
+ @@public_event_store = RailsEventStore::Client.new(
+ repository: config.event_repository,
+ mapper: RubyEventStore::Mappers::Default.new(
+ events_class_remapping: events_class_remapping
+ ),
+ dispatcher: RubyEventStore::ComposedDispatcher.new(
+ RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)),
+ RubyEventStore::Dispatcher.new),
+ )
+ @@module_event_store = RailsEventStore::Client.new(
+ repository: RailsEventStoreActiveRecord::EventRepository.new(
+ model_factory: RailsEventStoreActiveRecord::WithAbstractBaseClass.new(Shipping::ApplicationRecord), serializer: JSON),
+ dispatcher: RubyEventStore::ComposedDispatcher.new(
+ RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)),
+ RubyEventStore::Dispatcher.new),
+ )
+
+ # Subscribe public event handlers below
+ public_event_store.tap do |store|
+ store.subscribe(ShippingProcess.new(event_store, command_bus), to: ['new-order'])
+ store.subscribe(ShippingProcess.new(event_store, command_bus), to: ['payment-completed'])
+ end
+
+ # Subscribe private event handlers below
+ event_store.tap do |store|
+ store.subscribe(->(ev) {
+ public_event_store.publish(OrderSent.new(**ev.data.slice(
+ :order_id, :tracking_number, :estimated_delivery_date))
+ )
+ }, to: [Shipping::PackageShipped]
+ )
+ end
+
+ # Register commands handled by this module below
+ command_bus.tap do |bus|
+ bus.register(Shipping::ShipPackage, Shipping::OnShipPackage.new(event_store))
+ end
+ end
+
+ def self.events_class_remapping
+ {
+ 'new-order' => 'Shipping::OrderPlaced',
+ 'payment-completed' => 'Shipping::OrderPaid',
+ 'order-sent' => 'Shipping::OrderSent',
+ }
+ end
+
+ def self.command_bus
+ @@command_bus
+ end
+
+ def self.public_event_store
+ @@public_event_store
+ end
+
+ def self.event_store
+ @@module_event_store
+ end
+
+ def self.setup?
+ command_bus && event_store && public_event_store
+ end
+
+ class OrderPlaced < Event
+ event_type 'new-order'
+ attribute :order_id, Types::UUID
+ attribute :customer_id, Types::ID
+ attribute :delivery_address_id, Types::ID
+ end
+
+ class OrderPaid < Event
+ event_type 'payment-completed'
+ attribute :order_id, Types::UUID
+ end
+
+ class OrderSent < Event
+ event_type 'order-sent'
+ attribute :order_id, Types::UUID
+ attribute :tracking_number, Types::String
+ attribute :estimated_delivery_date, Types::JSON::Date
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/.keep b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/application_record.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/application_record.rb
new file mode 100644
index 0000000000..2e348c816b
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/application_record.rb
@@ -0,0 +1,8 @@
+require 'active_record'
+
+module Shipping
+ class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+ connects_to database: { writing: :shipping, reading: :shipping }
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/on_ship_package.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/on_ship_package.rb
new file mode 100644
index 0000000000..100d855062
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/on_ship_package.rb
@@ -0,0 +1,14 @@
+module Shipping
+ class OnShipPackage
+ include CommandHandler
+
+ def call(command)
+ with_aggregate(Package.new(command.order_id), command.order_id) do |package|
+ package.ship_to(
+ command.customer_id,
+ command.delivery_address_id,
+ )
+ end
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package.rb
new file mode 100644
index 0000000000..1e180c0e9d
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package.rb
@@ -0,0 +1,28 @@
+module Shipping
+ class Package
+ include AggregateRoot
+
+ def initialize(order_id)
+ @order_id = order_id
+ end
+
+ def ship_to(customer_id, address_id)
+ apply(PackageShipped.new(data: {
+ order_id: order_id,
+ customer_id: customer_id,
+ delivery_address_id: address_id,
+ tracking_number: SecureRandom.hex(16),
+ estimated_delivery_date: 2.days.from_now.to_date,
+ }))
+ end
+
+ private
+ attr_reader :order_id
+
+ on PackageShipped do |event|
+ @state = :ready
+ @tracking_number = event.tracking_number
+ end
+ end
+end
+
diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package_shipped.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package_shipped.rb
new file mode 100644
index 0000000000..d453e7b4df
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package_shipped.rb
@@ -0,0 +1,9 @@
+module Shipping
+ class PackageShipped < Event
+ attribute :order_id, Types::UUID
+ attribute :customer_id, Types::ID
+ attribute :delivery_address_id, Types::ID
+ attribute :tracking_number, Types::String
+ attribute :estimated_delivery_date, Types::JSON::Date
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/ship_package.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/ship_package.rb
new file mode 100644
index 0000000000..792c884cfc
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/ship_package.rb
@@ -0,0 +1,7 @@
+module Shipping
+ class ShipPackage < Command
+ attribute :order_id, Types::UUID
+ attribute :customer_id, Types::ID
+ attribute :delivery_address_id, Types::ID
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/shipping_process.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/shipping_process.rb
new file mode 100644
index 0000000000..0ca3391889
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/shipping_process.rb
@@ -0,0 +1,60 @@
+module Shipping
+ class ShippingProcess
+ def initialize(event_store, bus)
+ @event_store = event_store
+ @bus = bus
+ end
+
+ def call(event)
+ with_linked(event) do |state|
+ bus.call(state.command) if state.done?
+ end
+ end
+
+ private
+ attr_reader :event_store, :bus
+
+ class State < ApplicationRecord
+ self.table_name = :shipping_process_state
+ serialize :states, Array
+
+ def intialize
+ self.states = []
+ end
+
+ def done?
+ states.include?(:placed) && states.include?(:paid)
+ end
+
+ def command
+ Shipping::ShipPackage.new(
+ order_id: order_id,
+ customer_id: customer_id,
+ delivery_address_id: delivery_address_id,
+ )
+ end
+
+ def apply(event)
+ case event
+ when Shipping::OrderPlaced
+ self.customer_id = event.customer_id
+ self.delivery_address_id = event.delivery_address_id
+ states << :placed
+ when Shipping::OrderPaid
+ states << :paid
+ else
+ raise ArgumentError.new("Not suported domain event")
+ end
+ end
+ end
+
+ def with_linked(event)
+ State.transaction do
+ state = State.lock.find_or_create_by!(order_id: event.order_id)
+ state.apply(event)
+ state.save!
+ yield state
+ end
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_process_spec.rb b/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_process_spec.rb
new file mode 100644
index 0000000000..a4ba2b7016
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_process_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+module Shipping
+ RSpec.describe ShippingProcess do
+ let(:order_id) { SecureRandom.uuid }
+ let(:customer_id) { 123 }
+ let(:address_id) { 999 }
+
+ it 'works' do
+ Shipping.public_event_store.publish(OrderPlaced.new(
+ order_id: order_id,
+ customer_id: customer_id,
+ delivery_address_id: address_id,
+ ))
+ Shipping.public_event_store.publish(OrderPaid.new(
+ order_id: order_id,
+ ))
+
+ expect(Shipping.event_store).to have_published(
+ an_event(Shipping::PackageShipped).with_data(
+ order_id: order_id,
+ customer_id: customer_id,
+ delivery_address_id: address_id,
+ tracking_number: kind_of(String),
+ estimated_delivery_date: 2.days.from_now.to_date,
+ )
+ )
+
+ expect(Shipping.public_event_store).to have_published(
+ an_event(Shipping::OrderSent).with_data(
+ order_id: order_id,
+ tracking_number: kind_of(String),
+ estimated_delivery_date: 2.days.from_now.to_date,
+ )
+ )
+ end
+ end
+end
diff --git a/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_spec.rb b/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_spec.rb
new file mode 100644
index 0000000000..ed529f09ac
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_spec.rb
@@ -0,0 +1,4 @@
+require_relative 'spec_helper'
+
+RSpec.describe Shipping do
+end
diff --git a/contrib/multiple_databases_repository_sample_app/shipping/spec/spec_helper.rb b/contrib/multiple_databases_repository_sample_app/shipping/spec/spec_helper.rb
new file mode 100644
index 0000000000..aaa2235dc0
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/shipping/spec/spec_helper.rb
@@ -0,0 +1,21 @@
+ENV['RAILS_ENV'] ||= 'test'
+
+require_relative '../lib/shipping'
+
+module Shipping
+ def self.arrange(stream, events, event_store: Shipping.event_store)
+ event_store.append(events, stream_name: stream)
+ end
+
+ def self.act(command, bus: Shipping.command_bus)
+ bus.call(command)
+ end
+end
+
+unless Shipping.setup?
+ Configuration = Struct.new(:event_repository, :command_bus)
+ Shipping.setup(Configuration.new(
+ RubyEventStore::InMemoryRepository.new,
+ Arkency::CommandBus.new,
+ ))
+end
diff --git a/contrib/multiple_databases_repository_sample_app/spec/orders_spec.rb b/contrib/multiple_databases_repository_sample_app/spec/orders_spec.rb
new file mode 100644
index 0000000000..752c4215fe
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/spec/orders_spec.rb
@@ -0,0 +1,6 @@
+require 'rails_helper'
+
+path = Rails.root.join('orders/spec')
+Dir.glob("#{path}/**/*_spec.rb") do |file|
+ require file
+end
diff --git a/contrib/multiple_databases_repository_sample_app/spec/payments_spec.rb b/contrib/multiple_databases_repository_sample_app/spec/payments_spec.rb
new file mode 100644
index 0000000000..095a3c45cd
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/spec/payments_spec.rb
@@ -0,0 +1,6 @@
+require 'rails_helper'
+
+path = Rails.root.join('payments/spec')
+Dir.glob("#{path}/**/*_spec.rb") do |file|
+ require file
+end
diff --git a/contrib/multiple_databases_repository_sample_app/spec/rails_helper.rb b/contrib/multiple_databases_repository_sample_app/spec/rails_helper.rb
new file mode 100644
index 0000000000..00345af7c0
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/spec/rails_helper.rb
@@ -0,0 +1,64 @@
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+require 'spec_helper'
+ENV['RAILS_ENV'] ||= 'test'
+require File.expand_path('../config/environment', __dir__)
+# Prevent database truncation if the environment is production
+abort("The Rails environment is running in production mode!") if Rails.env.production?
+require 'rspec/rails'
+# Add additional requires below this line. Rails is not loaded until this point!
+
+# Requires supporting ruby files with custom matchers and macros, etc, in
+# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
+# run as spec files by default. This means that files in spec/support that end
+# in _spec.rb will both be required and run as specs, causing the specs to be
+# run twice. It is recommended that you do not name files matching this glob to
+# end with _spec.rb. You can configure this pattern with the --pattern
+# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
+#
+# The following line is provided for convenience purposes. It has the downside
+# of increasing the boot-up time by auto-requiring all files in the support
+# directory. Alternatively, in the individual `*_spec.rb` files, manually
+# require only the support files necessary.
+#
+# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
+
+# Checks for pending migrations and applies them before tests are run.
+# If you are not using ActiveRecord, you can remove these lines.
+begin
+ ActiveRecord::Migration.maintain_test_schema!
+rescue ActiveRecord::PendingMigrationError => e
+ puts e.to_s.strip
+ exit 1
+end
+RSpec.configure do |config|
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
+
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # examples within a transaction, remove the following line or assign false
+ # instead of true.
+ config.use_transactional_fixtures = true
+
+ # You can uncomment this line to turn off ActiveRecord support entirely.
+ # config.use_active_record = false
+
+ # RSpec Rails can automatically mix in different behaviours to your tests
+ # based on their file location, for example enabling you to call `get` and
+ # `post` in specs under `spec/controllers`.
+ #
+ # You can disable this behaviour by removing the line below, and instead
+ # explicitly tag your specs with their type, e.g.:
+ #
+ # RSpec.describe UsersController, type: :controller do
+ # # ...
+ # end
+ #
+ # The different available types are documented in the features, such as in
+ # https://relishapp.com/rspec/rspec-rails/docs
+ config.infer_spec_type_from_file_location!
+
+ # Filter lines from Rails gems in backtraces.
+ config.filter_rails_from_backtrace!
+ # arbitrary gems may also be filtered via:
+ # config.filter_gems_from_backtrace("gem name")
+end
diff --git a/contrib/multiple_databases_repository_sample_app/spec/shipping_spec.rb b/contrib/multiple_databases_repository_sample_app/spec/shipping_spec.rb
new file mode 100644
index 0000000000..35528697e3
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/spec/shipping_spec.rb
@@ -0,0 +1,6 @@
+require 'rails_helper'
+
+path = Rails.root.join('shipping/spec')
+Dir.glob("#{path}/**/*_spec.rb") do |file|
+ require file
+end
diff --git a/contrib/multiple_databases_repository_sample_app/spec/spec_helper.rb b/contrib/multiple_databases_repository_sample_app/spec/spec_helper.rb
new file mode 100644
index 0000000000..ce33d66df6
--- /dev/null
+++ b/contrib/multiple_databases_repository_sample_app/spec/spec_helper.rb
@@ -0,0 +1,96 @@
+# This file was generated by the `rails generate rspec:install` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause
+# this file to always be loaded, without a need to explicitly require it in any
+# files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need
+# it.
+#
+# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ # rspec-expectations config goes here. You can use an alternate
+ # assertion/expectation library such as wrong or the stdlib/minitest
+ # assertions if you prefer.
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`, e.g.:
+ # be_bigger_than(2).and_smaller_than(4).description
+ # # => "be bigger than 2 and smaller than 4"
+ # ...rather than:
+ # # => "be bigger than 2"
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ # rspec-mocks config goes here. You can use an alternate test double
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
+ # have no way to turn it off -- the option exists only for backwards
+ # compatibility in RSpec 3). It causes shared context metadata to be
+ # inherited by the metadata hash of host groups and examples, rather than
+ # triggering implicit auto-inclusion in groups with matching metadata.
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+
+# The settings below are suggested to provide a good initial experience
+# with RSpec, but feel free to customize to your heart's content.
+=begin
+ # This allows you to limit a spec run to individual examples or groups
+ # you care about by tagging them with `:focus` metadata. When nothing
+ # is tagged with `:focus`, all examples get run. RSpec also provides
+ # aliases for `it`, `describe`, and `context` that include `:focus`
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+ config.filter_run_when_matching :focus
+
+ # Allows RSpec to persist some state between runs in order to support
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
+ # you configure your source control system to ignore this file.
+ config.example_status_persistence_file_path = "spec/examples.txt"
+
+ # Limits the available syntax to the non-monkey patched syntax that is
+ # recommended. For more details, see:
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
+ config.disable_monkey_patching!
+
+ # Many RSpec users commonly either run the entire suite or an individual
+ # file, and it's useful to allow more verbose output when running an
+ # individual spec file.
+ if config.files_to_run.one?
+ # Use the documentation formatter for detailed output,
+ # unless a formatter has already been configured
+ # (e.g. via a command-line flag).
+ config.default_formatter = "doc"
+ end
+
+ # Print the 10 slowest examples and example groups at the
+ # end of the spec run, to help surface which specs are running
+ # particularly slow.
+ config.profile_examples = 10
+
+ # Run specs in random order to surface order dependencies. If you find an
+ # order dependency and want to debug it, you can fix the order by providing
+ # the seed, which is printed after each run.
+ # --seed 1234
+ config.order = :random
+
+ # Seed global randomization in this process using the `--seed` CLI option.
+ # Setting this allows you to use `--seed` to deterministically reproduce
+ # test failures related to randomization by passing the same `--seed` value
+ # as the one that triggered the failure.
+ Kernel.srand config.seed
+=end
+end