Skip to content

Commit

Permalink
Merge pull request #753 from RailsEventStore/multiple-databases-repos…
Browse files Browse the repository at this point in the history
…itory-support

Multiple databases repository support
  • Loading branch information
mpraglowski authored Sep 15, 2020
2 parents f158ada + 0f26a6f commit 985faa6
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 21 deletions.
1 change: 1 addition & 0 deletions rails_event_store/spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'rails_event_store'
require 'example_invoicing_app'
require 'support/fake_configuration'
require 'active_record'
require_relative '../../support/helpers/rspec_defaults'
require_relative '../../support/helpers/mutant_timeout'
require_relative '../../support/helpers/migrator'
Expand Down
3 changes: 2 additions & 1 deletion rails_event_store_active_record/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ GEM_NAME = rails_event_store_active_record
REQUIRE = $(GEM_NAME)
IGNORE = RailsEventStoreActiveRecord::IndexViolationDetector\#detect \
RailsEventStoreActiveRecord::PgLinearizedEventRepository* \
RailsEventStoreActiveRecord::EventRepository\#update_messages
RailsEventStoreActiveRecord::EventRepository\#update_messages \
RailsEventStoreActiveRecord::WithAbstractBaseClass\#build_event_klass
SUBJECT ?= RailsEventStoreActiveRecord*
DATABASE_URL ?= sqlite3::memory:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
require 'rails_event_store_active_record/generators/limit_for_event_id_generator'
require 'rails_event_store_active_record/generators/binary_data_and_metadata_generator'
require 'rails_event_store_active_record/event'
require 'rails_event_store_active_record/with_default_models'
require 'rails_event_store_active_record/with_abstract_base_class'
require 'rails_event_store_active_record/event_repository'
require 'rails_event_store_active_record/batch_enumerator'
require 'rails_event_store_active_record/event_repository_reader'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ class EventRepository
POSITION_SHIFT = 1
SERIALIZED_GLOBAL_STREAM_NAME = "all".freeze

def initialize(serializer:)
@repo_reader = EventRepositoryReader.new(serializer)
def initialize(model_factory: WithDefaultModels.new, serializer:)
@serializer = serializer

@event_klass, @stream_klass = model_factory.call
@repo_reader = EventRepositoryReader.new(@event_klass, @stream_klass, serializer)
end

def append_to_stream(records, stream, expected_version)
Expand All @@ -20,20 +22,20 @@ def append_to_stream(records, stream, expected_version)
event_ids << serialized_record.event_id
end
add_to_stream(event_ids, stream, expected_version, true) do
Event.import(hashes)
@event_klass.import(hashes)
end
end

def link_to_stream(event_ids, stream, expected_version)
event_ids = Array(event_ids)
(event_ids - Event.where(id: event_ids).pluck(:id)).each do |id|
(event_ids - @event_klass.where(id: event_ids).pluck(:id)).each do |id|
raise RubyEventStore::EventNotFound.new(id)
end
add_to_stream(event_ids, stream, expected_version, nil)
end

def delete_stream(stream)
EventInStream.where(stream: stream.name).delete_all
@stream_klass.where(stream: stream.name).delete_all
end

def has_event?(event_id)
Expand All @@ -56,14 +58,14 @@ def update_messages(records)
hashes = Array(records).map{|record| serialized_record_hash(record.serialize(serializer)) }
for_update = records.map(&:event_id)
start_transaction do
existing = Event.where(id: for_update).pluck(:id)
existing = @event_klass.where(id: for_update).pluck(:id)
(for_update - existing).each{|id| raise RubyEventStore::EventNotFound.new(id) }
Event.import(hashes, on_duplicate_key_update: [:data, :metadata, :event_type])
@event_klass.import(hashes, on_duplicate_key_update: [:data, :metadata, :event_type])
end
end

def streams_of(event_id)
EventInStream.where(event_id: event_id)
@stream_klass.where(event_id: event_id)
.where.not(stream: SERIALIZED_GLOBAL_STREAM_NAME)
.pluck(:stream)
.map{|name| RubyEventStore::Stream.new(name)}
Expand All @@ -73,7 +75,7 @@ def streams_of(event_id)
attr_reader :serializer

def add_to_stream(event_ids, stream, expected_version, include_global)
last_stream_version = ->(stream_) { EventInStream.where(stream: stream_.name).order("position DESC").first.try(:position) }
last_stream_version = ->(stream_) { @stream_klass.where(stream: stream_.name).order("position DESC").first.try(:position) }
resolved_version = expected_version.resolve_for(stream, last_stream_version)

start_transaction do
Expand All @@ -94,7 +96,7 @@ def add_to_stream(event_ids, stream, expected_version, include_global)
collection
end
fill_ids(in_stream)
EventInStream.import(in_stream)
@stream_klass.import(in_stream)
end
self
rescue ActiveRecord::RecordNotUnique => e
Expand Down Expand Up @@ -132,7 +134,7 @@ def fill_ids(_in_stream)
end

def start_transaction(&block)
ActiveRecord::Base.transaction(requires_new: true, &block)
@event_klass.transaction(requires_new: true, &block)
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
module RailsEventStoreActiveRecord
class EventRepositoryReader

def initialize(serializer)
def initialize(event_klass, stream_klass, serializer)
@event_klass = event_klass
@stream_klass = stream_klass
@serializer = serializer
end

def has_event?(event_id)
Event.exists?(id: event_id)
@event_klass.exists?(id: event_id)
end

def last_stream_event(stream)
record_ = EventInStream.where(stream: stream.name).order('position DESC, id DESC').first
record_ = @stream_klass.where(stream: stream.name).order('position DESC, id DESC').first
record(record_) if record_
end

Expand Down Expand Up @@ -48,7 +50,7 @@ def count(spec)
attr_reader :serializer

def read_scope(spec)
stream = EventInStream.preload(:event).where(stream: normalize_stream_name(spec))
stream = @stream_klass.preload(:event).where(stream: normalize_stream_name(spec))
stream = stream.where(event_id: spec.with_ids) if spec.with_ids?
stream = stream.joins(:event).where(event_store_events: {event_type: spec.with_types}) if spec.with_types?
stream = stream.order(position: order(spec)) unless spec.stream.global?
Expand All @@ -75,12 +77,12 @@ def stop_offset_condition(specification, record_id)

def start_condition(specification)
start_offset_condition(specification,
EventInStream.find_by!(event_id: specification.start, stream: normalize_stream_name(specification)))
@stream_klass.find_by!(event_id: specification.start, stream: normalize_stream_name(specification)))
end

def stop_condition(specification)
stop_offset_condition(specification,
EventInStream.find_by!(event_id: specification.stop, stream: normalize_stream_name(specification)))
@stream_klass.find_by!(event_id: specification.stop, stream: normalize_stream_name(specification)))
end

def order(spec)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module RailsEventStoreActiveRecord
class WithAbstractBaseClass
def initialize(base_klass)
raise ArgumentError.new(
"#{base_klass} must be an abstract class that inherits from ActiveRecord::Base"
) unless base_klass < ActiveRecord::Base && base_klass.abstract_class?
@base_klass = base_klass
end

def call(instance_id: SecureRandom.hex)
[
build_event_klass(instance_id),
build_stream_klass(instance_id),
]
end

private
def build_event_klass(instance_id)
Object.const_set("Event_"+instance_id,
Class.new(@base_klass) do
self.table_name = 'event_store_events'
self.primary_key = 'id'
end
)
end

def build_stream_klass(instance_id)
Object.const_set("EventInStream_"+instance_id,
Class.new(@base_klass) do
self.table_name = 'event_store_events_in_streams'
belongs_to :event, class_name: "Event_"+instance_id
end
)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

module RailsEventStoreActiveRecord
class WithDefaultModels
def call
[Event, EventInStream]
end
end
end
26 changes: 25 additions & 1 deletion rails_event_store_active_record/spec/event_repository_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
require 'spec_helper'
require 'ruby_event_store'
require 'ruby_event_store/spec/event_repository_lint'
require 'rails_event_store_active_record/event'

module RailsEventStoreActiveRecord
class EventRepository
Expand Down Expand Up @@ -359,6 +358,31 @@ def fill_ids(in_stream)
expect(batches[1]).to eq([events[99]])
end

specify 'use default models' do
repository = EventRepository.new(serializer: YAML)
expect(repository.instance_variable_get(:@event_klass)).to be(Event)
expect(repository.instance_variable_get(:@stream_klass)).to be(EventInStream)
end

specify 'allows custom base class' do
repository = EventRepository.new(model_factory: WithAbstractBaseClass.new(CustomApplicationRecord), serializer: YAML)
expect(repository.instance_variable_get(:@event_klass).ancestors).to include(CustomApplicationRecord)
expect(repository.instance_variable_get(:@stream_klass).ancestors).to include(CustomApplicationRecord)
end

specify 'reading/writting works with custom base class' do
repository = EventRepository.new(model_factory: WithAbstractBaseClass.new(CustomApplicationRecord), serializer: YAML)
repository.append_to_stream(
[event = RubyEventStore::SRecord.new],
RubyEventStore::Stream.new(RubyEventStore::GLOBAL_STREAM),
RubyEventStore::ExpectedVersion.any
)
reader = RubyEventStore::SpecificationReader.new(repository, RubyEventStore::Mappers::NullMapper.new)
specification = RubyEventStore::Specification.new(reader)
read_event = repository.read(specification.result).first
expect(read_event).to eq(event)
end

def cleanup_concurrency_test
ActiveRecord::Base.connection_pool.disconnect!
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
require 'spec_helper'
require 'ruby_event_store'
require 'ruby_event_store/spec/event_repository_lint'
require 'rails_event_store_active_record/event'

module RailsEventStoreActiveRecord
class PgLinearizedEventRepository
Expand Down
9 changes: 8 additions & 1 deletion rails_event_store_active_record/spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@
require_relative '../../support/helpers/migrator'
require_relative '../../support/helpers/schema_helper'
require 'rails'
require 'active_record'


$verbose = ENV.has_key?('VERBOSE') ? true : false
ActiveRecord::Schema.verbose = $verbose

ENV['DATABASE_URL'] ||= 'sqlite3:db.sqlite3'

module RailsEventStoreActiveRecord
class CustomApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
end

RSpec::Matchers.define :contains_ids do |expected_ids|
match do |enum|
@actual = enum.map(&:event_id)
values_match?(expected_ids, @actual)
end
diffable
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
require 'spec_helper'
require 'ruby_event_store'
require 'ruby_event_store/spec/event_repository_lint'

module RailsEventStoreActiveRecord
RSpec.describe WithAbstractBaseClass do
include SchemaHelper

specify 'Base for event factory models must be an abstract class' do
NonAbstractClass = Class.new(ActiveRecord::Base)
expect {
WithAbstractBaseClass.new(NonAbstractClass)
}.to raise_error(ArgumentError)
.with_message('RailsEventStoreActiveRecord::NonAbstractClass must be an abstract class that inherits from ActiveRecord::Base')
end

specify 'Base for event factory models could not be the ActiveRecord::Base' do
expect {
WithAbstractBaseClass.new(ActiveRecord::Base)
}.to raise_error(ArgumentError)
.with_message('ActiveRecord::Base must be an abstract class that inherits from ActiveRecord::Base')
end

specify 'Base for event factory models must inherit from ActiveRecord::Base' do
expect {
WithAbstractBaseClass.new(Object)
}.to raise_error(ArgumentError)
.with_message('Object must be an abstract class that inherits from ActiveRecord::Base')
end

specify 'AR classes must have the same instance id' do
event_klass, stream_klass = WithAbstractBaseClass.new(CustomApplicationRecord).call

expect(event_klass.name).to match(/^Event_[a-z,0-9]{32}$/)
expect(stream_klass.name).to match(/^EventInStream_[a-z,0-9]{32}$/)
expect(event_klass.name[6..-1]).to eq(stream_klass.name[14..-1])
end

specify 'each factory must generate different AR classes' do
factory1 = WithAbstractBaseClass.new(CustomApplicationRecord)
factory2 = WithAbstractBaseClass.new(CustomApplicationRecord)
event_klass_1, stream_klass_1 = factory1.call
event_klass_2, stream_klass_2 = factory2.call
expect(event_klass_1).not_to eq(event_klass_2)
expect(stream_klass_1).not_to eq(stream_klass_2)
end

specify 'reading/writting works with base class' do
begin
establish_database_connection
load_database_schema

repository = EventRepository.new(model_factory: WithAbstractBaseClass.new(CustomApplicationRecord), serializer: YAML)
repository.append_to_stream(
[event = RubyEventStore::SRecord.new],
RubyEventStore::Stream.new(RubyEventStore::GLOBAL_STREAM),
RubyEventStore::ExpectedVersion.any
)
reader = RubyEventStore::SpecificationReader.new(repository, RubyEventStore::Mappers::NullMapper.new)
specification = RubyEventStore::Specification.new(reader)
read_event = repository.read(specification.result).first
expect(read_event).to eq(event)
ensure
drop_database
end
end
end
end
12 changes: 12 additions & 0 deletions rails_event_store_active_record/spec/with_default_models_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require 'spec_helper'

module RailsEventStoreActiveRecord
RSpec.describe WithDefaultModels do
specify do
event_klass, stream_klass = WithDefaultModels.new.call

expect(event_klass).to eq(Event)
expect(stream_klass).to eq(EventInStream)
end
end
end

0 comments on commit 985faa6

Please sign in to comment.