ruby_event_store-rom 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 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: []