ruby_event_store-rom 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: daa7bf0e1b646617b35f359cbef1a2160c486cd72cef8e6865f38bd2d6bc8dbf
4
+ data.tar.gz: 7262de3e82f5e5ed7de6955a4d4973ba1f04dc8b877c74b38b7f804867d26479
5
+ SHA512:
6
+ metadata.gz: 5ec50cdeb365d38b2420af3736fa994c40670a6a703677fb1348dd3c1e26909407b2d04376d43e156428e989aca08a7d508411625589efee0b5f39224c04506f
7
+ data.tar.gz: e25e06dcd18b2a4f071d89804ac7a68d1cb7275457f8114ddc22a8af9e0a42aa858833e50c3f81a0e69294913692a9357fd7b914e8e3f7aa014c3510cdbe65c6
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ Further changes can be tracked at [releases page](https://github.com/RailsEventStore/rails_event_store/releases).
2
+
3
+ ### 0.1.0 (03.04.2018)
4
+
5
+ * Implemented ROM SQL adapter
6
+ * Add `rom-sql` 2.4.0 dependency
7
+ * Add `rom-repository` 2.0.2 dependency
8
+ * Add `rom-changeset` 1.0.2 dependency
9
+ * Add `sequel` 4.49 dependency
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'ruby_event_store', path: '../ruby_event_store'
data/Makefile ADDED
@@ -0,0 +1,78 @@
1
+ GEM_VERSION = $(shell cat ../RES_VERSION)
2
+ GEM_NAME = ruby_event_store-rom
3
+ REQUIRE = $(GEM_NAME)
4
+ IGNORE = RubyEventStore::ROM::EventRepository\#handle_not_found_errors \
5
+ RubyEventStore::ROM::EventRepository\#handle_unique_violation_errors \
6
+ RubyEventStore::ROM::EventRepository\#has_event? \
7
+ RubyEventStore::ROM::EventRepository\#last_stream_event \
8
+ RubyEventStore::ROM::Repositories::Events\#find_nonexistent_pks \
9
+ RubyEventStore::ROM::Env\#handle_error \
10
+ RubyEventStore::ROM::Env\#initialize \
11
+ RubyEventStore::ROM::Env\#register_error_handler \
12
+ RubyEventStore::ROM::Env\#register_unit_of_work_options \
13
+ RubyEventStore::ROM::Env\#transaction \
14
+ RubyEventStore::ROM::UnitOfWork\#call \
15
+ RubyEventStore::ROM::UnitOfWork\#commit! \
16
+ RubyEventStore::ROM::SQL::Relations::StreamEntries\#normalize_stream_name \
17
+ RubyEventStore::ROM::SQL::IndexViolationDetector\#detect \
18
+ RubyEventStore::ROM::SQL::SpecHelper\#load_gateway_schema \
19
+ RubyEventStore::ROM::SQL::SpecHelper\#establish_gateway_connection \
20
+ RubyEventStore::ROM::SQL::SpecHelper\#drop_gateway_schema \
21
+ RubyEventStore::ROM::SQL::SpecHelper\#close_gateway_connection \
22
+ RubyEventStore::ROM::SQL::UnitOfWork\#commit!
23
+ SUBJECT ?= RubyEventStore::ROM*
24
+ DATABASE_URL ?= sqlite::memory:
25
+
26
+ install: ## Install gem dependencies
27
+ @echo "Installing gem dependencies"
28
+ @bundle install
29
+
30
+ remove-lock:
31
+ @echo "Removing resolved dependency versions"
32
+ -rm Gemfile.lock
33
+
34
+ reinstall: remove-lock install ## Removing resolved dependency versions
35
+
36
+ test: ## Run unit tests
37
+ @echo "Running unit tests"
38
+ @bundle exec rspec
39
+
40
+ test-fast: ## Run unit tests with --fail-fast
41
+ @echo "Running unit tests with --fail-fast"
42
+ @bundle exec rspec --fail-fast --order defined --backtrace
43
+
44
+ mutate: test ## Run mutation tests
45
+ @echo "Running mutation tests"
46
+ @DATABASE_URL=$(DATABASE_URL) bundle exec mutant --include lib \
47
+ $(addprefix --require ,$(REQUIRE)) \
48
+ $(addprefix --ignore-subject ,$(IGNORE)) \
49
+ --use rspec "$(SUBJECT)"
50
+
51
+ mutate-fast: ## Run mutation tests with --fail-fast
52
+ @echo "Running mutation tests with --fail-fast"
53
+ @DATABASE_URL=$(DATABASE_URL) bundle exec mutant --include lib \
54
+ $(addprefix --require ,$(REQUIRE)) \
55
+ $(addprefix --ignore-subject ,$(IGNORE)) \
56
+ --fail-fast \
57
+ --use rspec "$(SUBJECT)"
58
+
59
+ build:
60
+ @echo "Building gem package"
61
+ @gem build -V $(GEM_NAME).gemspec
62
+ @mkdir -p pkg/
63
+ @mv $(GEM_NAME)-$(GEM_VERSION).gem pkg/
64
+
65
+ push:
66
+ @echo "Pushing package to RubyGems"
67
+ @gem push -k dev_arkency pkg/$(GEM_NAME)-$(GEM_VERSION).gem
68
+
69
+ clean:
70
+ @echo "Removing previously built package"
71
+ -rm pkg/$(GEM_NAME)-$(GEM_VERSION).gem
72
+
73
+ help:
74
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
75
+
76
+ .PHONY: help
77
+ .DEFAULT_GOAL := help
78
+
data/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # RubyEventStore ROM Event Repository
2
+
3
+ A Ruby Object Model (ROM) implementation of events repository for [Ruby Event Store](https://github.com/RailsEventStore/rails_event_store).
4
+
5
+ This version of the ROM adapter supports [rom-sql](https://github.com/rom-rb/rom-sql) at this time. It is an alternative to the ActiveRecord `EventRepository` implementation used in `rails_event_store` gem.
6
+
7
+ [Read the docs to get started.](http://railseventstore.org/docs/repository/)
8
+
9
+ _Additonal backing stores via ROM are being tracked here: [#299](https://github.com/RailsEventStore/rails_event_store/issues/299)._
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'ruby_event_store/rom/adapters/sql/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task default: [:ci]
7
+
8
+ desc "Run CI tasks"
9
+ task ci: [:spec]
10
+
11
+ begin
12
+ require "rubocop/rake_task"
13
+
14
+ Rake::Task[:default].enhance [:rubocop]
15
+
16
+ RuboCop::RakeTask.new do |task|
17
+ task.options << "--display-cop-names"
18
+ end
19
+ rescue LoadError
20
+ end
@@ -0,0 +1,45 @@
1
+ require 'rom/sql'
2
+
3
+ ::ROM::SQL.migration do
4
+ change do
5
+ postgres = database_type =~ /postgres/
6
+ sqlite = database_type =~ /sqlite/
7
+
8
+ run 'CREATE EXTENSION IF NOT EXISTS pgcrypto;' if postgres
9
+
10
+ create_table? :event_store_events_in_streams do
11
+ primary_key :id, type: :Bignum, null: false
12
+
13
+ column :stream, String, null: false
14
+ column :position, Integer
15
+
16
+ if postgres
17
+ column :event_id, :uuid, null: false
18
+ else
19
+ column :event_id, String, size: 36, null: false
20
+ end
21
+
22
+ column :created_at, DateTime, null: false, index: 'index_event_store_events_in_streams_on_created_at'
23
+
24
+ index %i[stream position], unique: true, name: 'index_event_store_events_in_streams_on_stream_and_position'
25
+ index %i[stream event_id], unique: true, name: 'index_event_store_events_in_streams_on_stream_and_event_id'
26
+ end
27
+
28
+ create_table? :event_store_events do
29
+ if postgres
30
+ column :id, :uuid, default: Sequel.function(:gen_random_uuid), primary_key: true
31
+ else
32
+ column :id, String, size: 36, null: false, primary_key: true
33
+ end
34
+
35
+ column :event_type, String, null: false
36
+ column :metadata, String, text: true
37
+ column :data, String, text: true, null: false
38
+ column :created_at, DateTime, null: false, index: 'index_event_store_events_on_created_at'
39
+
40
+ if sqlite # TODO: Is this relevant without ActiveRecord?
41
+ index :id, unique: true
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1 @@
1
+ require 'ruby_event_store/rom'
@@ -0,0 +1,107 @@
1
+ require 'rom-changeset'
2
+ require 'rom-mapper'
3
+ require 'rom-repository'
4
+ require 'ruby_event_store'
5
+ require 'ruby_event_store/rom/event_repository'
6
+ require 'ruby_event_store/rom/tuple_uniqueness_error'
7
+ require 'ruby_event_store/rom/unit_of_work'
8
+ require 'ruby_event_store/rom/version'
9
+
10
+ module RubyEventStore
11
+ module ROM
12
+ class Env
13
+ attr_accessor :container
14
+
15
+ def initialize(container)
16
+ @container = container
17
+
18
+ container.register(:unique_violation_error_handlers, Set.new)
19
+ container.register(:not_found_error_handlers, Set.new)
20
+ container.register(:logger, Logger.new(STDOUT).tap { |logger| logger.level = Logger::WARN })
21
+ end
22
+
23
+ def logger
24
+ container[:logger]
25
+ end
26
+
27
+ def transaction(&block)
28
+ options = container[:unit_of_work_options].dup
29
+ options.delete(:class){UnitOfWork}.new(rom: self).call(**options, &block)
30
+ end
31
+
32
+ def register_unit_of_work_options(options)
33
+ container.register(:unit_of_work_options, options)
34
+ end
35
+
36
+ def register_error_handler(type, handler)
37
+ container[:"#{type}_error_handlers"] << handler
38
+ end
39
+
40
+ def handle_error(type, ex, *args)
41
+ container[:"#{type}_error_handlers"].each{ |h| h.call(ex, *args) }
42
+ raise ex
43
+ end
44
+ end
45
+
46
+ class << self
47
+ # Set to a default instance
48
+ attr_accessor :env
49
+
50
+ def configure(adapter_name, database_uri = ENV['DATABASE_URL'], &block)
51
+ if adapter_name.is_a?(::ROM::Configuration)
52
+ # Call config block manually
53
+ Env.new ::ROM.container(adapter_name.tap(&block), &block)
54
+ elsif database_uri.nil?
55
+ raise ArgumentError.new('Missing database URI argument or DATABASE_URL environment variable')
56
+ else
57
+ Env.new ::ROM.container(adapter_name, database_uri, &block)
58
+ end
59
+ end
60
+
61
+ def setup(*args, &block)
62
+ configure(*args) do |config|
63
+ setup_defaults(config)
64
+ block.call(config) if block
65
+ end.tap(&method(:configure_defaults))
66
+ end
67
+
68
+ private
69
+
70
+ def setup_defaults(config)
71
+ require_relative 'rom/repositories/stream_entries'
72
+ require_relative 'rom/repositories/events'
73
+
74
+ config.register_mapper(ROM::Mappers::EventToSerializedRecord)
75
+ config.register_mapper(ROM::Mappers::StreamEntryToSerializedRecord)
76
+
77
+ find_adapters(config.environment.gateways).each do |adapter|
78
+ adapter.setup(config)
79
+ end
80
+ end
81
+
82
+ def configure_defaults(env)
83
+ env.register_error_handler :not_found, -> (ex, event_id) {
84
+ case ex
85
+ when ::ROM::TupleCountMismatchError
86
+ raise EventNotFound.new(event_id)
87
+ end
88
+ }
89
+
90
+ find_adapters(env.container.gateways).each do |adapter|
91
+ adapter.configure(env)
92
+ end
93
+ end
94
+
95
+ def find_adapters(gateways)
96
+ # Setup for each kind of gateway class
97
+ gateways.values.map(&:class).uniq.map do |klass|
98
+ constant = klass.name.split('::')[1].to_sym
99
+
100
+ next unless RubyEventStore::ROM.constants.include?(constant)
101
+
102
+ RubyEventStore::ROM.const_get(constant)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,26 @@
1
+ module RubyEventStore
2
+ module ROM
3
+ module SQL
4
+ class IndexViolationDetector
5
+
6
+ MYSQL_PKEY_ERROR = "for key 'PRIMARY'"
7
+ POSTGRES_PKEY_ERROR = "event_store_events_pkey"
8
+ SQLITE3_PKEY_ERROR = "event_store_events.id"
9
+
10
+ MYSQL_INDEX_ERROR = "for key 'index_event_store_events_in_streams_on_stream_and_event_id'"
11
+ POSTGRES_INDEX_ERROR = "Key (stream, event_id)"
12
+ SQLITE3_INDEX_ERROR = "event_store_events_in_streams.stream, event_store_events_in_streams.event_id"
13
+
14
+ def detect(message)
15
+ message.include?(MYSQL_PKEY_ERROR) ||
16
+ message.include?(POSTGRES_PKEY_ERROR) ||
17
+ message.include?(SQLITE3_PKEY_ERROR) ||
18
+
19
+ message.include?(MYSQL_INDEX_ERROR) ||
20
+ message.include?(POSTGRES_INDEX_ERROR) ||
21
+ message.include?(SQLITE3_INDEX_ERROR)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ require 'rom/sql/rake_task'
2
+ require 'ruby_event_store/rom/sql'
3
+ load 'ruby_event_store/rom/adapters/sql/tasks/migration_tasks.rake'
@@ -0,0 +1,11 @@
1
+ module RubyEventStore
2
+ module ROM
3
+ module SQL
4
+ module Relations
5
+ class Events < ::ROM::Relation[:sql]
6
+ schema(:event_store_events, as: :events, infer: true)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,61 @@
1
+ module RubyEventStore
2
+ module ROM
3
+ module SQL
4
+ module Relations
5
+ class StreamEntries < ::ROM::Relation[:sql]
6
+ schema(:event_store_events_in_streams, as: :stream_entries, infer: true) do
7
+ associations do
8
+ belongs_to :events, as: :event, foreign_key: :event_id
9
+ end
10
+ end
11
+
12
+ alias_method :take, :limit
13
+
14
+ def by_stream(stream)
15
+ where(stream: stream.name)
16
+ end
17
+
18
+ SERIALIZED_GLOBAL_STREAM_NAME = 'all'.freeze
19
+
20
+ def by_stream(stream)
21
+ where(stream: normalize_stream_name(stream))
22
+ end
23
+
24
+ def by_stream_and_event_id(stream, event_id)
25
+ where(stream: normalize_stream_name(stream), event_id: event_id).one!
26
+ end
27
+
28
+ def max_position(stream)
29
+ by_stream(stream).select(:position).order(Sequel.desc(:position)).first
30
+ end
31
+
32
+ DIRECTION_MAP = {
33
+ forward: [:asc, :>],
34
+ backward: [:desc, :<]
35
+ }.freeze
36
+
37
+ def ordered(direction, stream, offset_entry_id = nil)
38
+ order, operator = DIRECTION_MAP[direction]
39
+
40
+ raise ArgumentError, 'Direction must be :forward or :backward' if order.nil?
41
+
42
+ order_columns = %i[position id]
43
+ order_columns.delete(:position) if stream.global?
44
+
45
+ query = by_stream(stream)
46
+ query = query.where { id.public_send(operator, offset_entry_id) } if offset_entry_id
47
+ query.order { |r| order_columns.map { |c| r[:stream_entries][c].public_send(order) } }
48
+ end
49
+
50
+ alias_method :take, :limit
51
+
52
+ private
53
+
54
+ def normalize_stream_name(stream)
55
+ stream.global? ? SERIALIZED_GLOBAL_STREAM_NAME : stream.name
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,18 @@
1
+ require 'ruby_event_store/rom/sql'
2
+
3
+ desc 'Setup ROM EventRespository environment'
4
+ task 'db:setup' do
5
+ ROM::SQL::RakeSupport.env = ::RubyEventStore::ROM.configure(:sql).container
6
+ end
7
+
8
+ desc "Copy RubyEventStore SQL migrations to db/migrate"
9
+ task 'db:copy_migrations' => 'db:setup' do
10
+ Dir[File.join(File.dirname(__FILE__), '../../../../../../db/migrate/*.rb')].each do |input|
11
+ name = File.basename(input, '.*').sub(/\d+_/, '')
12
+ output = ROM::SQL::RakeSupport.create_migration(name)
13
+
14
+ File.write output, File.read(input)
15
+
16
+ puts "<= migration file created #{output}"
17
+ end
18
+ end
@@ -0,0 +1,37 @@
1
+ module RubyEventStore
2
+ module ROM
3
+ module SQL
4
+ class UnitOfWork < ROM::UnitOfWork
5
+ def commit!(gateway, changesets, **options)
6
+ # Committing changesets concurrently causes MySQL deadlocks
7
+ # which are not caught and retried by Sequel's built-in
8
+ # :retry_on option. This appears to be a result of how ROM
9
+ # handles exceptions which don't bubble up so that Sequel
10
+ # can retry transactions with the :retry_on option when there's
11
+ # a deadlock.
12
+ #
13
+ # This is exacerbated by the fact that changesets insert multiple
14
+ # tuples with individual INSERT statements because ROM specifies
15
+ # to Sequel to return a list of primary keys created. The likelihood
16
+ # of a deadlock is reduced with batched INSERT statements.
17
+ #
18
+ # For this reason we need to manually insert changeset records to avoid
19
+ # MySQL deadlocks or to allow Sequel to retry transactions
20
+ # when the :retry_on option is specified.
21
+ options.merge!(
22
+ retry_on: Sequel::SerializationFailure,
23
+ before_retry: -> (num, ex) {
24
+ env.logger.warn("RETRY TRANSACTION [#{self.class.name} => #{ex.class.name}] #{ex.message}")
25
+ }
26
+ )
27
+
28
+ gateway.transaction(options) do
29
+ changesets.each do |changeset|
30
+ changeset.relation.multi_insert(changeset.to_a)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,91 @@
1
+ require 'ruby_event_store/rom/unit_of_work'
2
+
3
+ module RubyEventStore
4
+ module ROM
5
+ class EventRepository
6
+ def initialize(rom: ROM.env)
7
+ @rom = rom
8
+ @events = Repositories::Events.new(rom.container)
9
+ @stream_entries = Repositories::StreamEntries.new(rom.container)
10
+ end
11
+
12
+ def append_to_stream(events, stream, expected_version)
13
+ events = normalize_to_array(events)
14
+ event_ids = events.map(&:event_id)
15
+
16
+ @rom.transaction do |changesets|
17
+ # Create changesets inside transaction because
18
+ # we want to find the last position (a.k.a. version)
19
+ # again if the transaction is retried due to a
20
+ # deadlock in MySQL
21
+ changesets << @events.create_changeset(events)
22
+ changesets << @stream_entries.create_changeset(event_ids, stream, expected_version, global_stream: true)
23
+ end
24
+
25
+ self
26
+ rescue => ex
27
+ @rom.handle_error(:unique_violation, ex)
28
+ end
29
+
30
+ def link_to_stream(event_ids, stream, expected_version)
31
+ event_ids = normalize_to_array(event_ids)
32
+ nonexistent_ids = @events.find_nonexistent_pks(event_ids)
33
+
34
+ nonexistent_ids.each { |id| raise EventNotFound.new(id) }
35
+
36
+ @stream_entries.create_changeset(event_ids, stream, expected_version).commit
37
+
38
+ self
39
+ rescue => ex
40
+ @rom.handle_error(:unique_violation, ex)
41
+ end
42
+
43
+ def delete_stream(stream)
44
+ @stream_entries.delete(stream)
45
+ end
46
+
47
+ def has_event?(event_id)
48
+ @events.exist?(event_id)
49
+ rescue => ex
50
+ begin
51
+ @rom.handle_error(:not_found, ex, event_id)
52
+ rescue EventNotFound
53
+ false
54
+ end
55
+ end
56
+
57
+ def last_stream_event(stream)
58
+ Specification.new(self)
59
+ .stream(stream.name)
60
+ .limit(1)
61
+ .backward
62
+ .each
63
+ .first
64
+ end
65
+
66
+ def read_event(event_id)
67
+ @events.by_id(event_id)
68
+ rescue => ex
69
+ @rom.handle_error(:not_found, ex, event_id)
70
+ end
71
+
72
+ def read(specification)
73
+ raise ReservedInternalName if specification.stream_name.eql?(@stream_entries.stream_entries.class::SERIALIZED_GLOBAL_STREAM_NAME)
74
+
75
+ @events.read(
76
+ specification.direction,
77
+ specification.stream,
78
+ from: specification.start,
79
+ limit: (specification.count if specification.limit?)
80
+ )
81
+ end
82
+
83
+ private
84
+
85
+ def normalize_to_array(events)
86
+ return events if events.is_a?(Enumerable)
87
+ [events]
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,18 @@
1
+ require 'rom/transformer'
2
+
3
+ module RubyEventStore
4
+ module ROM
5
+ module Mappers
6
+ class EventToSerializedRecord < ::ROM::Transformer
7
+ relation :events
8
+ register_as :event_to_serialized_record
9
+
10
+ map_array do
11
+ rename_keys id: :event_id
12
+ accept_keys %i[event_id data metadata event_type]
13
+ constructor_inject RubyEventStore::SerializedRecord
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ require 'rom/transformer'
2
+
3
+ module RubyEventStore
4
+ module ROM
5
+ module Mappers
6
+ class StreamEntryToSerializedRecord < ::ROM::Transformer
7
+ relation :stream_entries
8
+ register_as :stream_entry_to_serialized_record
9
+
10
+ map_array do
11
+ unwrap :event, %i[data metadata event_type]
12
+ accept_keys %i[event_id data metadata event_type]
13
+ constructor_inject RubyEventStore::SerializedRecord
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,53 @@
1
+ require_relative '../mappers/event_to_serialized_record'
2
+
3
+ module RubyEventStore
4
+ module ROM
5
+ module Repositories
6
+ class Events < ::ROM::Repository[:events]
7
+ class Create < ::ROM::Changeset::Create
8
+ # Convert to Hash
9
+ map(&:to_h)
10
+
11
+ map do
12
+ rename_keys event_id: :id
13
+ accept_keys %i[id data metadata event_type]
14
+ end
15
+
16
+ map do |tuple|
17
+ Hash(created_at: Time.now).merge(tuple)
18
+ end
19
+ end
20
+
21
+ def create_changeset(serialized_records)
22
+ events.changeset(Create, serialized_records)
23
+ end
24
+
25
+ def find_nonexistent_pks(event_ids)
26
+ return event_ids unless event_ids.any?
27
+ event_ids - events.by_pk(event_ids).pluck(:id)
28
+ end
29
+
30
+ def exist?(event_id)
31
+ events.by_pk(event_id).exist?
32
+ end
33
+
34
+ def by_id(event_id)
35
+ events.map_with(:event_to_serialized_record).by_pk(event_id).one!
36
+ end
37
+
38
+ def read(direction, stream, from:, limit:)
39
+ unless from.equal?(:head)
40
+ offset_entry_id = stream_entries.by_stream_and_event_id(stream, from).fetch(:id)
41
+ end
42
+
43
+ stream_entries
44
+ .ordered(direction, stream, offset_entry_id)
45
+ .take(limit)
46
+ .combine(:event)
47
+ .map_with(:stream_entry_to_serialized_record) # Add `auto_struct: false` for Memory adapter
48
+ .each
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,48 @@
1
+ require_relative '../mappers/stream_entry_to_serialized_record'
2
+
3
+ module RubyEventStore
4
+ module ROM
5
+ module Repositories
6
+ class StreamEntries < ::ROM::Repository[:stream_entries]
7
+ class Create < ::ROM::Changeset::Create
8
+ map do |tuple|
9
+ Hash(created_at: Time.now).merge(tuple)
10
+ end
11
+ end
12
+
13
+ POSITION_SHIFT = 1.freeze
14
+
15
+ def create_changeset(event_ids, stream, expected_version, global_stream: nil)
16
+ resolved_version = expected_version.resolve_for(stream, ->(_stream) {
17
+ (stream_entries.max_position(stream) || {})[:position]
18
+ })
19
+
20
+ tuples = []
21
+
22
+ event_ids.each_with_index do |event_id, index|
23
+ tuples << {
24
+ stream: stream.name,
25
+ position: resolved_version && resolved_version + index + POSITION_SHIFT,
26
+ event_id: event_id
27
+ } unless stream.global?
28
+
29
+ tuples << {
30
+ stream: stream_entries.class::SERIALIZED_GLOBAL_STREAM_NAME,
31
+ event_id: event_id
32
+ } if global_stream
33
+ end
34
+
35
+ stream_entries.changeset(Create, tuples)
36
+ end
37
+
38
+ def delete(stream)
39
+ delete_changeset(stream).commit
40
+ end
41
+
42
+ def delete_changeset(stream)
43
+ stream_entries.by_stream(stream).changeset(:delete)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,81 @@
1
+ require 'rom/sql'
2
+ require 'ruby_event_store/rom'
3
+ require_relative 'adapters/sql/index_violation_detector'
4
+ require_relative 'adapters/sql/unit_of_work'
5
+ require_relative 'adapters/sql/relations/events'
6
+ require_relative 'adapters/sql/relations/stream_entries'
7
+
8
+ module RubyEventStore
9
+ module ROM
10
+ module SQL
11
+ class << self
12
+ def setup(config)
13
+ config.register_relation Relations::Events
14
+ config.register_relation Relations::StreamEntries
15
+ end
16
+
17
+ def configure(env)
18
+ # See: https://github.com/jeremyevans/sequel/blob/master/doc/transactions.rdoc
19
+ env.register_unit_of_work_options(
20
+ class: UnitOfWork,
21
+ savepoint: true
22
+ )
23
+
24
+ env.register_error_handler :unique_violation, -> ex {
25
+ case ex
26
+ when ::ROM::SQL::UniqueConstraintError, Sequel::UniqueConstraintViolation
27
+ raise EventDuplicatedInStream if IndexViolationDetector.new.detect(ex.message)
28
+ raise WrongExpectedEventVersion
29
+ end
30
+ }
31
+
32
+ env.register_error_handler :not_found, -> (ex, event_id) {
33
+ case ex
34
+ when ::ROM::TupleCountMismatchError
35
+ raise EventNotFound.new(event_id)
36
+ when Sequel::DatabaseError
37
+ raise ex unless ex.message =~ /PG::InvalidTextRepresentation.*uuid/
38
+ raise EventNotFound.new(event_id)
39
+ end
40
+ }
41
+ end
42
+ end
43
+
44
+ class SpecHelper
45
+ attr_reader :env
46
+
47
+ def initialize(rom: ROM.env)
48
+ @env = rom
49
+ end
50
+
51
+ def gateway
52
+ env.container.gateways.fetch(:default)
53
+ end
54
+
55
+ def establish_gateway_connection
56
+ # Manually preconnect because disconnecting and reconnecting
57
+ # seems to lose the "preconnect concurrently" setting
58
+ gateway.connection.pool.send(:preconnect, true)
59
+ end
60
+
61
+ def load_gateway_schema
62
+ gateway.run_migrations
63
+ end
64
+
65
+ def drop_gateway_schema
66
+ gateway.connection.drop_table?('event_store_events')
67
+ gateway.connection.drop_table?('event_store_events_in_streams')
68
+ gateway.connection.drop_table?('schema_migrations')
69
+ end
70
+
71
+ # See: https://github.com/rom-rb/rom-sql/blob/master/spec/shared/database_setup.rb
72
+ def close_gateway_connection
73
+ gateway.connection.disconnect
74
+ # Prevent the auto-reconnect when the test completed
75
+ # This will save from hardly reproducible connection run outs
76
+ gateway.connection.pool.available_connections.freeze
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,6 @@
1
+ module RubyEventStore
2
+ module ROM
3
+ class TupleUniquenessError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,23 @@
1
+ module RubyEventStore
2
+ module ROM
3
+ class UnitOfWork
4
+ attr_reader :env
5
+
6
+ def initialize(rom: ROM.env)
7
+ @env = rom
8
+ end
9
+
10
+ def call(**options)
11
+ gateway = @env.container.gateways.fetch(options.delete(:gateway){:default})
12
+
13
+ yield(changesets = [])
14
+
15
+ commit!(gateway, changesets, options)
16
+ end
17
+
18
+ def commit!(gateway, changesets, **options)
19
+ gateway.transaction(options) { changesets.each(&:commit) }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ module RubyEventStore
2
+ module ROM
3
+ VERSION = "0.28.0"
4
+ end
5
+ end
@@ -0,0 +1,44 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ruby_event_store/rom/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ruby_event_store-rom'
8
+ spec.version = RubyEventStore::ROM::VERSION
9
+ spec.licenses = ['MIT']
10
+ spec.authors = ['Joel Van Horn']
11
+ spec.email = ['joel@joelvanhorn.com']
12
+
13
+ spec.summary = %q{ROM events repository for Ruby Event Store}
14
+ spec.description = %q{Implementation of events repository based on ROM for Ruby Event Store'}
15
+ spec.homepage = 'https://railseventstore.org'
16
+ spec.metadata = {
17
+ "homepage_uri" => "https://railseventstore.org/",
18
+ "changelog_uri" => "https://github.com/RailsEventStore/rails_event_store/releases",
19
+ "source_code_uri" => "https://github.com/RailsEventStore/rails_event_store",
20
+ "bug_tracker_uri" => "https://github.com/RailsEventStore/rails_event_store/issues",
21
+ }
22
+
23
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ spec.bindir = 'exe'
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ['lib']
27
+
28
+ spec.add_development_dependency 'bundler', '~> 1.15'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
30
+ spec.add_development_dependency 'rspec', '~> 3.6'
31
+ spec.add_development_dependency 'sqlite3', '1.3.13'
32
+ spec.add_development_dependency 'pg', '0.21'
33
+ spec.add_development_dependency 'mysql2', '0.4.10'
34
+ spec.add_development_dependency 'mutant-rspec', '~> 0.8.14'
35
+ spec.add_development_dependency 'fakefs', '~> 0.11.2'
36
+ spec.add_development_dependency 'childprocess'
37
+ spec.add_development_dependency 'google-protobuf', '~> 3.5.1.2'
38
+
39
+ spec.add_dependency 'ruby_event_store', '= 0.28.0'
40
+ spec.add_dependency 'sequel', '>= 4.49'
41
+ spec.add_dependency 'rom-sql', '~> 2.4'
42
+ spec.add_dependency 'rom-repository', '~> 2.0'
43
+ spec.add_dependency 'rom-changeset', '~> 1.0'
44
+ end
metadata ADDED
@@ -0,0 +1,282 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_event_store-rom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.28.0
5
+ platform: ruby
6
+ authors:
7
+ - Joel Van Horn
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-05-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.3.13
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 1.3.13
69
+ - !ruby/object:Gem::Dependency
70
+ name: pg
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: '0.21'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: '0.21'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mysql2
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 0.4.10
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 0.4.10
97
+ - !ruby/object:Gem::Dependency
98
+ name: mutant-rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.8.14
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.8.14
111
+ - !ruby/object:Gem::Dependency
112
+ name: fakefs
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.11.2
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.11.2
125
+ - !ruby/object:Gem::Dependency
126
+ name: childprocess
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: google-protobuf
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 3.5.1.2
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 3.5.1.2
153
+ - !ruby/object:Gem::Dependency
154
+ name: ruby_event_store
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - '='
158
+ - !ruby/object:Gem::Version
159
+ version: 0.28.0
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - '='
165
+ - !ruby/object:Gem::Version
166
+ version: 0.28.0
167
+ - !ruby/object:Gem::Dependency
168
+ name: sequel
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '4.49'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '4.49'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rom-sql
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '2.4'
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '2.4'
195
+ - !ruby/object:Gem::Dependency
196
+ name: rom-repository
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '2.0'
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '2.0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: rom-changeset
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: '1.0'
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: '1.0'
223
+ description: Implementation of events repository based on ROM for Ruby Event Store'
224
+ email:
225
+ - joel@joelvanhorn.com
226
+ executables: []
227
+ extensions: []
228
+ extra_rdoc_files: []
229
+ files:
230
+ - CHANGELOG.md
231
+ - Gemfile
232
+ - Makefile
233
+ - README.md
234
+ - Rakefile
235
+ - db/migrate/20180327044629_create_ruby_event_store_tables.rb
236
+ - lib/ruby_event_store-rom.rb
237
+ - lib/ruby_event_store/rom.rb
238
+ - lib/ruby_event_store/rom/adapters/sql/index_violation_detector.rb
239
+ - lib/ruby_event_store/rom/adapters/sql/rake_task.rb
240
+ - lib/ruby_event_store/rom/adapters/sql/relations/events.rb
241
+ - lib/ruby_event_store/rom/adapters/sql/relations/stream_entries.rb
242
+ - lib/ruby_event_store/rom/adapters/sql/tasks/migration_tasks.rake
243
+ - lib/ruby_event_store/rom/adapters/sql/unit_of_work.rb
244
+ - lib/ruby_event_store/rom/event_repository.rb
245
+ - lib/ruby_event_store/rom/mappers/event_to_serialized_record.rb
246
+ - lib/ruby_event_store/rom/mappers/stream_entry_to_serialized_record.rb
247
+ - lib/ruby_event_store/rom/repositories/events.rb
248
+ - lib/ruby_event_store/rom/repositories/stream_entries.rb
249
+ - lib/ruby_event_store/rom/sql.rb
250
+ - lib/ruby_event_store/rom/tuple_uniqueness_error.rb
251
+ - lib/ruby_event_store/rom/unit_of_work.rb
252
+ - lib/ruby_event_store/rom/version.rb
253
+ - ruby_event_store-rom.gemspec
254
+ homepage: https://railseventstore.org
255
+ licenses:
256
+ - MIT
257
+ metadata:
258
+ homepage_uri: https://railseventstore.org/
259
+ changelog_uri: https://github.com/RailsEventStore/rails_event_store/releases
260
+ source_code_uri: https://github.com/RailsEventStore/rails_event_store
261
+ bug_tracker_uri: https://github.com/RailsEventStore/rails_event_store/issues
262
+ post_install_message:
263
+ rdoc_options: []
264
+ require_paths:
265
+ - lib
266
+ required_ruby_version: !ruby/object:Gem::Requirement
267
+ requirements:
268
+ - - ">="
269
+ - !ruby/object:Gem::Version
270
+ version: '0'
271
+ required_rubygems_version: !ruby/object:Gem::Requirement
272
+ requirements:
273
+ - - ">="
274
+ - !ruby/object:Gem::Version
275
+ version: '0'
276
+ requirements: []
277
+ rubyforge_project:
278
+ rubygems_version: 2.7.6
279
+ signing_key:
280
+ specification_version: 4
281
+ summary: ROM events repository for Ruby Event Store
282
+ test_files: []