diff --git a/aggregate_root/lib/aggregate_root.rb b/aggregate_root/lib/aggregate_root.rb index 87a1bd3bde..8ac8282ff5 100644 --- a/aggregate_root/lib/aggregate_root.rb +++ b/aggregate_root/lib/aggregate_root.rb @@ -2,7 +2,6 @@ require_relative "aggregate_root/version" require_relative "aggregate_root/configuration" -require_relative "aggregate_root/transform" require_relative "aggregate_root/default_apply_strategy" require_relative "aggregate_root/repository" require_relative "aggregate_root/instrumented_repository" @@ -81,7 +80,6 @@ def marshal_load(vars) def self.with_default_apply_strategy Module.new do def self.included(host_class) - host_class.extend OnDSL host_class.include AggregateRoot.with_strategy(-> { DefaultApplyStrategy.new }) end end @@ -90,7 +88,8 @@ def self.included(host_class) def self.with_strategy(strategy) Module.new do def self.included(host_class) - host_class.extend Constructor + host_class.extend OnDSL + host_class.extend Constructor host_class.include AggregateMethods end diff --git a/aggregate_root/lib/aggregate_root/configuration.rb b/aggregate_root/lib/aggregate_root/configuration.rb deleted file mode 100644 index 6935658048..0000000000 --- a/aggregate_root/lib/aggregate_root/configuration.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module AggregateRoot - class << self - attr_accessor :configuration - end - - def self.configure - self.configuration ||= Configuration.new - yield(configuration) - end - - class Configuration - attr_accessor :default_event_store - end -end diff --git a/aggregate_root/lib/aggregate_root/default_apply_strategy.rb b/aggregate_root/lib/aggregate_root/default_apply_strategy.rb index 3046aa2aeb..ff3ffa147c 100644 --- a/aggregate_root/lib/aggregate_root/default_apply_strategy.rb +++ b/aggregate_root/lib/aggregate_root/default_apply_strategy.rb @@ -2,6 +2,7 @@ module AggregateRoot MissingHandler = Class.new(StandardError) + NullHandler = Proc.new {} class DefaultApplyStrategy def initialize(strict: true) @@ -9,30 +10,24 @@ def initialize(strict: true) end def call(aggregate, event) - name = handler_name(aggregate, event) - if aggregate.respond_to?(name, true) - aggregate.method(name).call(event) - else - raise MissingHandler.new("Missing handler method #{name} on aggregate #{aggregate.class}") if strict - end + on_handler(aggregate, event.event_type)[event] end private - def handler_name(aggregate, event) - on_dsl_handler_name(aggregate, event.event_type) || apply_handler_name(event.event_type) - end - - def on_dsl_handler_name(aggregate, event_type) - aggregate.class.on_methods[event_type] if aggregate.class.respond_to?(:on_methods) + def on_handler(aggregate, event_type) + on_method_name = aggregate.class.on_methods.fetch(event_type) + aggregate.method(on_method_name) + rescue KeyError + missing_handler(aggregate, event_type) end - def apply_handler_name(event_type) - "apply_#{Transform.to_snake_case(event_type(event_type))}" - end - - def event_type(event_type) - event_type.split(/::|\./).last + def missing_handler(aggregate, event_type) + if strict + raise MissingHandler.new("Missing handler method on aggregate #{aggregate.class} for #{event_type}") + else + NullHandler + end end attr_reader :strict, :on_methods diff --git a/aggregate_root/lib/aggregate_root/repository.rb b/aggregate_root/lib/aggregate_root/repository.rb index ad73120d43..09e2ea9575 100644 --- a/aggregate_root/lib/aggregate_root/repository.rb +++ b/aggregate_root/lib/aggregate_root/repository.rb @@ -2,7 +2,7 @@ module AggregateRoot class Repository - def initialize(event_store = default_event_store) + def initialize(event_store) @event_store = event_store end @@ -29,9 +29,5 @@ def with_aggregate(aggregate, stream_name, &block) private attr_reader :event_store - - def default_event_store - AggregateRoot.configuration.default_event_store - end end end diff --git a/aggregate_root/lib/aggregate_root/transform.rb b/aggregate_root/lib/aggregate_root/transform.rb deleted file mode 100644 index 39643b1e2e..0000000000 --- a/aggregate_root/lib/aggregate_root/transform.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module AggregateRoot - class Transform - def self.to_snake_case(name) - name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase - end - end -end diff --git a/aggregate_root/spec/aggregate_root_spec.rb b/aggregate_root/spec/aggregate_root_spec.rb index 78f8fba56d..3ccdec835f 100644 --- a/aggregate_root/spec/aggregate_root_spec.rb +++ b/aggregate_root/spec/aggregate_root_spec.rb @@ -31,12 +31,14 @@ def expire private - def apply_order_created(_event) - @status = :created + on Orders::Events::OrderCreated do |event| + @status = :created + @created_at = event.valid_at end - def apply_order_expired(_event) - @status = :expired + on Orders::Events::OrderExpired do |event| + @status = :expired + @expired_at = event.valid_at end end end @@ -44,9 +46,8 @@ def apply_order_expired(_event) it "should have ability to apply event on itself" do order = order_klass.new(uuid) order_created = Orders::Events::OrderCreated.new - - expect(order).to receive(:"apply_order_created").with(order_created).and_call_original order.apply(order_created) + expect(order.status).to eq :created expect(order.unpublished_events.to_a).to eq([order_created]) end @@ -56,6 +57,12 @@ def apply_order_expired(_event) expect(order.unpublished_events.to_a).to be_empty end + it "should raise error for missing apply method based on a default apply strategy" do + order = order_klass.new(uuid) + spanish_inquisition = Orders::Events::SpanishInquisition.new + expect { order.apply(spanish_inquisition) }.to raise_error(AggregateRoot::MissingHandler, "Missing handler method on aggregate #{order_klass} for Orders::Events::SpanishInquisition") + end + it "should receive a method call based on a default apply strategy" do order = order_klass.new(uuid) order_created = Orders::Events::OrderCreated.new @@ -64,15 +71,6 @@ def apply_order_expired(_event) expect(order.status).to eq :created end - it "should raise error for missing apply method based on a default apply strategy" do - order = order_klass.new(uuid) - spanish_inquisition = Orders::Events::SpanishInquisition.new - expect { order.apply(spanish_inquisition) }.to raise_error( - AggregateRoot::MissingHandler, - "Missing handler method apply_spanish_inquisition on aggregate #{order_klass}" - ) - end - it "should ignore missing apply method based on a default non-strict apply strategy" do klass = Class.new { include AggregateRoot.with_strategy(-> { AggregateRoot::DefaultApplyStrategy.new(strict: false) }) } diff --git a/aggregate_root/spec/instrumented_repository_spec.rb b/aggregate_root/spec/instrumented_repository_spec.rb index 344723482f..2a212d89fb 100644 --- a/aggregate_root/spec/instrumented_repository_spec.rb +++ b/aggregate_root/spec/instrumented_repository_spec.rb @@ -27,11 +27,11 @@ def expire private - def apply_order_created(_event) + on Orders::Events::OrderCreated do |_event| @status = :created end - def apply_order_expired(_event) + on Orders::Events::OrderExpired do |_event| @status = :expired end end diff --git a/aggregate_root/spec/repository_spec.rb b/aggregate_root/spec/repository_spec.rb index e2ec51d7a8..c8ca929c5c 100644 --- a/aggregate_root/spec/repository_spec.rb +++ b/aggregate_root/spec/repository_spec.rb @@ -34,51 +34,16 @@ def expire private - def apply_order_created(_event) + on Orders::Events::OrderCreated do |_event| @status = :created end - def apply_order_expired(_event) + on Orders::Events::OrderExpired do |_event| @status = :expired end end end - def with_default_event_store(store) - previous = AggregateRoot.configuration.default_event_store - AggregateRoot.configure { |config| config.default_event_store = store } - yield - AggregateRoot.configure { |config| config.default_event_store = previous } - end - - describe "#initialize" do - it "should use default client if event_store not provided" do - with_default_event_store(event_store) do - repository = AggregateRoot::Repository.new - - order = repository.load(order_klass.new(uuid), stream_name) - order_created = Orders::Events::OrderCreated.new - order.apply(order_created) - repository.store(order, stream_name) - - expect(event_store.read.stream(stream_name).to_a).to eq [order_created] - end - end - - it "should prefer provided event_store client" do - with_default_event_store(double(:event_store)) do - repository = AggregateRoot::Repository.new(event_store) - - order = repository.load(order_klass.new(uuid), stream_name) - order_created = Orders::Events::OrderCreated.new - order.apply(order_created) - repository.store(order, stream_name) - - expect(event_store.read.stream(stream_name).to_a).to eq [order_created] - end - end - end - describe "#load" do specify do event_store.publish(Orders::Events::OrderCreated.new, stream_name: stream_name) diff --git a/aggregate_root/spec/spec_helper.rb b/aggregate_root/spec/spec_helper.rb index 70dab3ce72..22e40dfcf1 100644 --- a/aggregate_root/spec/spec_helper.rb +++ b/aggregate_root/spec/spec_helper.rb @@ -4,8 +4,6 @@ require "ruby_event_store" require_relative "../../support/helpers/rspec_defaults" -RSpec.configure { |spec| spec.before(:each) { AggregateRoot.configure { |config| config.default_event_store = nil } } } - module Orders module Events OrderCreated = Class.new(RubyEventStore::Event) diff --git a/aggregate_root/spec/transform_spec.rb b/aggregate_root/spec/transform_spec.rb deleted file mode 100644 index 61ce216c4a..0000000000 --- a/aggregate_root/spec/transform_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module AggregateRoot - ::RSpec.describe Transform do - specify { expect(Transform.to_snake_case("OrderSubmitted")).to eq("order_submitted") } - specify { expect(Transform.to_snake_case("SHA1ChecksumComputed")).to eq("sha1_checksum_computed") } - specify { expect(Transform.to_snake_case("OKROfPSAInQ1Reached")).to eq("okr_of_psa_in_q1_reached") } - specify { expect(Transform.to_snake_case("EncryptedWithRot13")).to eq("encrypted_with_rot13") } - end -end diff --git a/rails_event_store/lib/rails_event_store/all.rb b/rails_event_store/lib/rails_event_store/all.rb index b366ee4b29..255dde5995 100644 --- a/rails_event_store/lib/rails_event_store/all.rb +++ b/rails_event_store/lib/rails_event_store/all.rb @@ -10,23 +10,3 @@ require_relative "version" require_relative "railtie" require_relative "browser" - -module RailsEventStore - Event = RubyEventStore::Event - InMemoryRepository = RubyEventStore::InMemoryRepository - Subscriptions = RubyEventStore::Subscriptions - Projection = RubyEventStore::Projection - WrongExpectedEventVersion = RubyEventStore::WrongExpectedEventVersion - InvalidExpectedVersion = RubyEventStore::InvalidExpectedVersion - IncorrectStreamData = RubyEventStore::IncorrectStreamData - EventNotFound = RubyEventStore::EventNotFound - SubscriberNotExist = RubyEventStore::SubscriberNotExist - InvalidHandler = RubyEventStore::InvalidHandler - InvalidPageStart = RubyEventStore::InvalidPageStart - InvalidPageStop = RubyEventStore::InvalidPageStop - InvalidPageSize = RubyEventStore::InvalidPageSize - CorrelatedCommands = RubyEventStore::CorrelatedCommands - GLOBAL_STREAM = RubyEventStore::GLOBAL_STREAM - PAGE_SIZE = RubyEventStore::PAGE_SIZE - ImmediateAsyncDispatcher = RubyEventStore::ImmediateAsyncDispatcher -end diff --git a/rails_event_store/spec/active_job_scheduler_spec.rb b/rails_event_store/spec/active_job_scheduler_spec.rb index 905543fcf6..6e7a11d1e1 100644 --- a/rails_event_store/spec/active_job_scheduler_spec.rb +++ b/rails_event_store/spec/active_job_scheduler_spec.rb @@ -23,9 +23,7 @@ module RailsEventStore it_behaves_like :scheduler, ActiveJobScheduler.new(serializer: RubyEventStore::Serializers::YAML) it_behaves_like :scheduler, ActiveJobScheduler.new(serializer: RubyEventStore::NULL) - let(:event) do - TimeEnrichment.with(Event.new(event_id: "83c3187f-84f6-4da7-8206-73af5aca7cc8"), timestamp: Time.utc(2019, 9, 30)) - end + let(:event) { TimeEnrichment.with(RubyEventStore::Event.new(event_id: "83c3187f-84f6-4da7-8206-73af5aca7cc8"), timestamp: Time.utc(2019, 9, 30)) } let(:record) { RubyEventStore::Mappers::Default.new.event_to_record(event) } describe "#verify" do diff --git a/rails_event_store/spec/after_commit_async_dispatcher_spec.rb b/rails_event_store/spec/after_commit_async_dispatcher_spec.rb index f2d3871ab1..59e799e574 100644 --- a/rails_event_store/spec/after_commit_async_dispatcher_spec.rb +++ b/rails_event_store/spec/after_commit_async_dispatcher_spec.rb @@ -12,7 +12,7 @@ class DummyRecord < ActiveRecord::Base it_behaves_like :dispatcher, AfterCommitAsyncDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: RubyEventStore::Serializers::YAML)) - let(:event) { TimeEnrichment.with(RailsEventStore::Event.new(event_id: "83c3187f-84f6-4da7-8206-73af5aca7cc8")) } + let(:event) { TimeEnrichment.with(RubyEventStore::Event.new(event_id: "83c3187f-84f6-4da7-8206-73af5aca7cc8")) } let(:record) { RubyEventStore::Mappers::Default.new.event_to_record(event) } let(:serialized_record) { record.serialize(RubyEventStore::Serializers::YAML).to_h.transform_keys(&:to_s) } diff --git a/rails_event_store/spec/async_handler_helpers_spec.rb b/rails_event_store/spec/async_handler_helpers_spec.rb index 271bd847cc..b407d25dd8 100644 --- a/rails_event_store/spec/async_handler_helpers_spec.rb +++ b/rails_event_store/spec/async_handler_helpers_spec.rb @@ -78,14 +78,14 @@ def perform(_event) specify "with defaults" do HandlerWithDefaults.prepend RailsEventStore::AsyncHandler event_store.subscribe_to_all_events(HandlerWithDefaults) - event_store.publish(ev = RailsEventStore::Event.new) + event_store.publish(ev = RubyEventStore::Event.new) expect($queue.pop).to eq(ev) end specify "with specified event store" do HandlerWithAnotherEventStore.prepend RailsEventStore::AsyncHandler.with(event_store: another_event_store) event_store.subscribe_to_all_events(HandlerWithAnotherEventStore) - event_store.publish(ev = RailsEventStore::Event.new) + event_store.publish(ev = RubyEventStore::Event.new) expect($queue.pop).to eq(ev) end @@ -95,7 +95,7 @@ def perform(_event) event_store_locator: -> { another_event_store } ) another_event_store.subscribe_to_all_events(HandlerWithEventStoreLocator) - another_event_store.publish(ev = RailsEventStore::Event.new) + another_event_store.publish(ev = RubyEventStore::Event.new) expect($queue.pop).to eq(ev) end @@ -105,20 +105,20 @@ def perform(_event) serializer: JSON ) json_event_store.subscribe_to_all_events(HandlerWithSpecifiedSerializer) - json_event_store.publish(ev = RailsEventStore::Event.new) + json_event_store.publish(ev = RubyEventStore::Event.new) expect($queue.pop).to eq(ev) end specify "default dispatcher can into ActiveJob" do event_store.subscribe_to_all_events(MyLovelyAsyncHandler) - event_store.publish(ev = RailsEventStore::Event.new) + event_store.publish(ev = RubyEventStore::Event.new) expect($queue.pop).to eq(ev) end specify "ActiveJob with AsyncHandler prepended" do HandlerWithHelper.prepend RailsEventStore::AsyncHandler event_store.subscribe_to_all_events(HandlerWithHelper) - event_store.publish(ev = RailsEventStore::Event.new) + event_store.publish(ev = RubyEventStore::Event.new) expect($queue.pop).to eq(ev) end @@ -127,7 +127,7 @@ def perform(_event) HandlerA.prepend RailsEventStore::CorrelatedHandler HandlerA.prepend RailsEventStore::AsyncHandler event_store.subscribe_to_all_events(HandlerA) - event_store.publish(ev = RailsEventStore::Event.new) + event_store.publish(ev = RubyEventStore::Event.new) expect($queue.pop).to eq({ correlation_id: ev.correlation_id, causation_id: ev.event_id }) end @@ -136,13 +136,13 @@ def perform(_event) HandlerB.prepend RailsEventStore::CorrelatedHandler HandlerB.prepend RailsEventStore::AsyncHandler event_store.subscribe_to_all_events(HandlerB) - event_store.publish(ev = RailsEventStore::Event.new(metadata: { correlation_id: "COID", causation_id: "CAID" })) + event_store.publish(ev = RubyEventStore::Event.new(metadata: { correlation_id: "COID", causation_id: "CAID" })) expect($queue.pop).to eq({ correlation_id: "COID", causation_id: ev.event_id }) end specify "ActiveJob with sidekiq adapter that requires serialization", mutant: false do ActiveJob::Base.queue_adapter = :sidekiq - ev = RailsEventStore::Event.new + ev = RubyEventStore::Event.new Sidekiq::Testing.fake! do event_store.subscribe_to_all_events(MyLovelyAsyncHandler) event_store.publish(ev) @@ -155,7 +155,8 @@ def perform(_event) HandlerB = Class.new(MetadataHandler) HandlerB.prepend RailsEventStore::CorrelatedHandler HandlerB.prepend RailsEventStore::AsyncHandler - event_store.append(ev = RailsEventStore::Event.new) + HandlerB.metadata = nil + event_store.append(ev = RubyEventStore::Event.new) ev = event_store.read.event(ev.event_id) HandlerB.perform_now(serialize_without_correlation_id(ev)) expect($queue.pop).to eq({ correlation_id: nil, causation_id: ev.event_id }) diff --git a/rails_event_store/spec/client_spec.rb b/rails_event_store/spec/client_spec.rb index 9dde8727d4..e53e8fc0b2 100644 --- a/rails_event_store/spec/client_spec.rb +++ b/rails_event_store/spec/client_spec.rb @@ -3,7 +3,7 @@ module RailsEventStore ::RSpec.describe Client do - TestEvent = Class.new(RailsEventStore::Event) + TestEvent = Class.new(RubyEventStore::Event) specify "has default request metadata proc if no custom one provided" do client = Client.new @@ -20,7 +20,9 @@ module RailsEventStore end specify "published event metadata will be enriched by metadata provided in request metadata when executed inside a with_request_metadata block" do - client = Client.new(repository: InMemoryRepository.new) + client = Client.new( + repository: RubyEventStore::InMemoryRepository.new, + ) event = TestEvent.new client.with_request_metadata( "action_dispatch.request_id" => "dummy_id", @@ -34,7 +36,7 @@ module RailsEventStore end specify "wraps repository into instrumentation" do - client = Client.new(repository: InMemoryRepository.new) + client = Client.new(repository: RubyEventStore::InMemoryRepository.new) received_notifications = 0 ActiveSupport::Notifications.subscribe("append_to_stream.repository.rails_event_store") do @@ -47,7 +49,10 @@ module RailsEventStore end specify "wraps mapper into instrumentation" do - client = Client.new(repository: InMemoryRepository.new, mapper: RubyEventStore::Mappers::NullMapper.new) + client = Client.new( + repository: RubyEventStore::InMemoryRepository.new, + mapper: RubyEventStore::Mappers::NullMapper.new + ) received_notifications = 0 ActiveSupport::Notifications.subscribe("serialize.mapper.rails_event_store") { received_notifications += 1 } diff --git a/rails_event_store/spec/example_invoicing_app.rb b/rails_event_store/spec/example_invoicing_app.rb index 3d35acf01d..2fb59b6a66 100644 --- a/rails_event_store/spec/example_invoicing_app.rb +++ b/rails_event_store/spec/example_invoicing_app.rb @@ -1,10 +1,10 @@ -class OrderCreated < RailsEventStore::Event +class OrderCreated < RubyEventStore::Event end -class ProductAdded < RailsEventStore::Event +class ProductAdded < RubyEventStore::Event end -class PriceChanged < RailsEventStore::Event +class PriceChanged < RubyEventStore::Event end class InvoiceReadModel diff --git a/rails_event_store/spec/spec_helper.rb b/rails_event_store/spec/spec_helper.rb index 506284e86e..105601e1e7 100644 --- a/rails_event_store/spec/spec_helper.rb +++ b/rails_event_store/spec/spec_helper.rb @@ -36,7 +36,7 @@ ActiveJob::Base.logger = nil unless $verbose ActiveRecord::Schema.verbose = $verbose -DummyEvent = Class.new(RailsEventStore::Event) +DummyEvent = Class.new(RubyEventStore::Event) module GeneratorHelper def dummy_app_name diff --git a/railseventstore.org/config.rb b/railseventstore.org/config.rb index 0af2eb298c..d3abe211ee 100644 --- a/railseventstore.org/config.rb +++ b/railseventstore.org/config.rb @@ -39,4 +39,5 @@ def precompiled_template(locals) page "/" page "/docs/v1/*", locals: { version: "v1" }, layout: "documentation" page "/docs/v2/*", locals: { version: "v2" }, layout: "documentation" +page "/docs/v3/*", locals: { version: "v3" }, layout: "documentation" page "*", locals: { version: "v2" } diff --git a/railseventstore.org/source/docs/v3/serialization.html.md.erb b/railseventstore.org/source/docs/v3/serialization.html.md.erb new file mode 100644 index 0000000000..d90e8edab3 --- /dev/null +++ b/railseventstore.org/source/docs/v3/serialization.html.md.erb @@ -0,0 +1,134 @@ +--- +title: Event serialization formats +--- + +By default RailsEventStore will use `YAML` as a +serialization format. The reason is that `YAML` is available out of box +and can serialize and deserialize data types which are not easily +handled in other formats. + +However, if you don't like `YAML` or you have different needs you can +choose to use different serializers or even replace mappers entirely. + +## Configuring a different serializer + +You can pass a different `serializer` as a dependency when [instantiating +the client](/docs/v2/install). + +Here is an example on how to configure RailsEventStore to serialize +events' `data` and `metadata` using `Marshal`. + +```ruby +# config/environments/*.rb + +Rails.application.configure do + config.to_prepare do + Rails.configuration.event_store = RailsEventStore::Client.new( + repository: RailsEventStoreActiveRecord::EventRepository.new(serializer: Marshal) + ) + end +end +``` + +The provided `serializer` must respond to `load` and `dump`. + +Serialization is needed not only when writing to and reading from storage, but also when scheduling events for background processing by async handlers: + +```ruby +Rails.configuration.event_store = RailsEventStore::Client.new( + dispatcher: RubyEventStore::ComposedDispatcher.new( + RailsEventStore::AfterCommitAsyncDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: Marshal)), + RubyEventStore::Dispatcher.new + ) + ) +``` + +```ruby +class SomeHandler < ActiveJob::Base + include RailsEventStore::AsyncHandler.with(serializer: Marshal) + + def perform(event) + # ... + end +end +``` + +## Configuring for Postgres JSON/B data type + +In Postgres database, you can store your events data and metadata in json or jsonb format. + +To generate migration containing event table schemas run + +```console +$ rails generate rails_event_store_active_record:migration --data-type=jsonb +``` + +Next, in your `RailsEventStore::Client` initialization, set repository serialization to ` RailsEventStoreActiveRecord::EventRepository.new(serializer: JSON)` + +```ruby +# config/environments/*.rb + +Rails.application.configure do + config.to_prepare do + Rails.configuration.event_store = RailsEventStore::Client.new( + repository: RubyEventStore::ActiveRecord::EventRepository.new(serializer: JSON) + ) + end +end +``` + +Using the `JSON` serializer (or any compatible, e.g. `ActiveSupport::JSON`) will serialize the event data and metadata. +It will do otherwise when reading. +ActiveRecord serialization will be skipped. +Database itself expect data to be json already. + + diff --git a/railseventstore.org/source/partials/_documentation_nav_v3.erb b/railseventstore.org/source/partials/_documentation_nav_v3.erb new file mode 100644 index 0000000000..734110247a --- /dev/null +++ b/railseventstore.org/source/partials/_documentation_nav_v3.erb @@ -0,0 +1,21 @@ +

+ Getting started +

+ +

+ Core concepts +

+ +

+ Advanced topics +

+ +

+ Common usage patterns +

+ \ No newline at end of file diff --git a/ruby_event_store-active_record/Gemfile b/ruby_event_store-active_record/Gemfile index fb17e08762..3acd431ac0 100644 --- a/ruby_event_store-active_record/Gemfile +++ b/ruby_event_store-active_record/Gemfile @@ -7,3 +7,4 @@ eval_gemfile "../support/bundler/Gemfile.database" gem "ruby_event_store", path: ".." gem "childprocess" + diff --git a/ruby_event_store-active_record/lib/ruby_event_store/active_record/event.rb b/ruby_event_store-active_record/lib/ruby_event_store/active_record/event.rb index 0536d7c21b..56eab9e396 100644 --- a/ruby_event_store-active_record/lib/ruby_event_store/active_record/event.rb +++ b/ruby_event_store-active_record/lib/ruby_event_store/active_record/event.rb @@ -7,6 +7,15 @@ module ActiveRecord class Event < ::ActiveRecord::Base self.primary_key = :id self.table_name = "event_store_events" + + if Gem::Version.new(::ActiveRecord::VERSION::STRING) >= Gem::Version.new("6.1.0") + skip_json_serialization = ->(initial_column_type) do + %i[json jsonb].include?(initial_column_type.type) ? ActiveModel::Type::Value.new : initial_column_type + end + + attribute :data, skip_json_serialization + attribute :metadata, skip_json_serialization + end end private_constant :Event diff --git a/ruby_event_store-active_record/spec/event_repository_spec.rb b/ruby_event_store-active_record/spec/event_repository_spec.rb index 380f70c31e..1672d7c91f 100644 --- a/ruby_event_store-active_record/spec/event_repository_spec.rb +++ b/ruby_event_store-active_record/spec/event_repository_spec.rb @@ -6,7 +6,17 @@ module RubyEventStore module ActiveRecord ::RSpec.describe EventRepository do helper = SpecHelper.new - mk_repository = -> { EventRepository.new(serializer: RubyEventStore::Serializers::YAML) } + + mk_repository = -> { + serializer = + case ENV["DATA_TYPE"] + when /json/ + JSON + else + RubyEventStore::Serializers::YAML + end + EventRepository.new(serializer: serializer) + } it_behaves_like :event_repository, mk_repository, helper diff --git a/ruby_event_store-active_record/spec/pg_linearized_event_repository_spec.rb b/ruby_event_store-active_record/spec/pg_linearized_event_repository_spec.rb index 1faa423135..5e8151c237 100644 --- a/ruby_event_store-active_record/spec/pg_linearized_event_repository_spec.rb +++ b/ruby_event_store-active_record/spec/pg_linearized_event_repository_spec.rb @@ -6,7 +6,17 @@ module RubyEventStore module ActiveRecord ::RSpec.describe PgLinearizedEventRepository do helper = SpecHelper.new - mk_repository = -> { PgLinearizedEventRepository.new(serializer: RubyEventStore::Serializers::YAML) } + + mk_repository = -> { + serializer = + case ENV["DATA_TYPE"] + when /json/ + JSON + else + RubyEventStore::Serializers::YAML + end + PgLinearizedEventRepository.new(serializer: serializer) + } it_behaves_like :event_repository, mk_repository, helper diff --git a/ruby_event_store-active_record/spec/skip_ar_serialization_spec.rb b/ruby_event_store-active_record/spec/skip_ar_serialization_spec.rb new file mode 100644 index 0000000000..fb4bf2de13 --- /dev/null +++ b/ruby_event_store-active_record/spec/skip_ar_serialization_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "spec_helper" +require "ruby_event_store" +require "ruby_event_store/spec/event_repository_lint" + +module RubyEventStore + module ActiveRecord + ::RSpec.describe "Skip ActiveRecord serialization of data and metadata for json(b) columns" do + helper = SpecHelper.new + mk_repository = -> { EventRepository.new(serializer: JSON) } + + let(:repository) { mk_repository.call } + let(:specification) do + RubyEventStore::Specification.new( + RubyEventStore::SpecificationReader.new(repository, RubyEventStore::Mappers::NullMapper.new) + ) + end + + around(:each) { |example| helper.run_lifecycle { example.run } } + + specify do + repository.append_to_stream( + [RubyEventStore::SRecord.new(data: { "foo" => "bar" })], + RubyEventStore::Stream.new("stream"), + RubyEventStore::ExpectedVersion.auto + ) + + record = repository.read(specification.result).first + expect(record.data).to eq({ "foo" => "bar" }) + expect( + ::ActiveRecord::Base + .connection + .execute("SELECT data ->> 'foo' as foo FROM event_store_events ORDER BY created_at DESC") + .first[ + "foo" + ] + ).to eq("bar") + end if ENV["DATABASE_URL"].include?("postgres") && %w[json jsonb].include?(ENV["DATA_TYPE"]) + + specify do + repository.append_to_stream( + [RubyEventStore::SRecord.new(data: { "foo" => "bar" })], + RubyEventStore::Stream.new("stream"), + RubyEventStore::ExpectedVersion.auto + ) + + record = repository.read(specification.result).first + expect(record.data).to eq({ "foo" => "bar" }) + end + end + end +end diff --git a/ruby_event_store/lib/ruby_event_store.rb b/ruby_event_store/lib/ruby_event_store.rb index 340793eaef..2bb210eaf7 100644 --- a/ruby_event_store/lib/ruby_event_store.rb +++ b/ruby_event_store/lib/ruby_event_store.rb @@ -23,7 +23,6 @@ require_relative "ruby_event_store/mappers/in_memory_encryption_key_repository" require_relative "ruby_event_store/mappers/transformation/domain_event" require_relative "ruby_event_store/mappers/transformation/encryption" -require_relative "ruby_event_store/mappers/transformation/event_class_remapper" require_relative "ruby_event_store/mappers/transformation/upcast" require_relative "ruby_event_store/mappers/transformation/stringify_metadata_keys" require_relative "ruby_event_store/mappers/transformation/symbolize_metadata_keys" @@ -34,7 +33,6 @@ require_relative "ruby_event_store/mappers/forgotten_data" require_relative "ruby_event_store/mappers/encryption_mapper" require_relative "ruby_event_store/mappers/instrumented_mapper" -require_relative "ruby_event_store/mappers/json_mapper" require_relative "ruby_event_store/mappers/null_mapper" require_relative "ruby_event_store/serializers/yaml" require_relative "ruby_event_store/batch_enumerator" diff --git a/ruby_event_store/lib/ruby_event_store/client.rb b/ruby_event_store/lib/ruby_event_store/client.rb index 18dc13a020..5a5c1fa8e6 100644 --- a/ruby_event_store/lib/ruby_event_store/client.rb +++ b/ruby_event_store/lib/ruby_event_store/client.rb @@ -28,6 +28,8 @@ def initialize( # @param expected_version [:any, :auto, :none, Integer] controls optimistic locking strategy. {http://railseventstore.org/docs/expected_version/ Read more} # @return [self] def publish(events, stream_name: GLOBAL_STREAM, expected_version: :any) + assert_nil_events(events) + enriched_events = enrich_events_metadata(events) records = transform(enriched_events) append_records_to_stream(records, stream_name: stream_name, expected_version: expected_version) @@ -44,6 +46,8 @@ def publish(events, stream_name: GLOBAL_STREAM, expected_version: :any) # @param (see #publish) # @return [self] def append(events, stream_name: GLOBAL_STREAM, expected_version: :any) + assert_nil_events(events) + append_records_to_stream( transform(enrich_events_metadata(events)), stream_name: stream_name, @@ -60,6 +64,8 @@ def append(events, stream_name: GLOBAL_STREAM, expected_version: :any) # @param expected_version (see #publish) # @return [self] def link(event_ids, stream_name:, expected_version: :any) + assert_nil_events(event_ids) + repository.link_to_stream(Array(event_ids), Stream.new(stream_name), ExpectedVersion.new(expected_version)) self end @@ -334,6 +340,12 @@ def inspect private + def assert_nil_events(events) + raise ArgumentError, "Event cannot be `nil`" if events.nil? + events = Array(events) + raise ArgumentError, "Event cannot be `nil`" if events.any?(&:nil?) + end + def transform(events) events.map { |ev| mapper.event_to_record(ev) } end diff --git a/ruby_event_store/lib/ruby_event_store/mappers/default.rb b/ruby_event_store/lib/ruby_event_store/mappers/default.rb index 259fd8b555..70ffec09bb 100644 --- a/ruby_event_store/lib/ruby_event_store/mappers/default.rb +++ b/ruby_event_store/lib/ruby_event_store/mappers/default.rb @@ -3,13 +3,10 @@ module RubyEventStore module Mappers class Default < PipelineMapper - def initialize(events_class_remapping: {}) - super( - Pipeline.new( - Transformation::EventClassRemapper.new(events_class_remapping), - Transformation::SymbolizeMetadataKeys.new - ) - ) + def initialize + super(Pipeline.new( + Transformation::SymbolizeMetadataKeys.new, + )) end end end diff --git a/ruby_event_store/lib/ruby_event_store/mappers/json_mapper.rb b/ruby_event_store/lib/ruby_event_store/mappers/json_mapper.rb index 93f64773d0..26e61354a4 100644 --- a/ruby_event_store/lib/ruby_event_store/mappers/json_mapper.rb +++ b/ruby_event_store/lib/ruby_event_store/mappers/json_mapper.rb @@ -3,7 +3,7 @@ module RubyEventStore module Mappers class JSONMapper < Default - def initialize(events_class_remapping: {}) + def initialize warn <<~EOW Please replace RubyEventStore::Mappers::JSONMapper with RubyEventStore::Mappers::Default diff --git a/ruby_event_store/lib/ruby_event_store/mappers/transformation/event_class_remapper.rb b/ruby_event_store/lib/ruby_event_store/mappers/transformation/event_class_remapper.rb deleted file mode 100644 index 7f4084864d..0000000000 --- a/ruby_event_store/lib/ruby_event_store/mappers/transformation/event_class_remapper.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module RubyEventStore - module Mappers - module Transformation - class EventClassRemapper - def initialize(class_map) - @class_map = class_map - end - - def dump(record) - record - end - - def load(record) - Record.new( - event_id: record.event_id, - event_type: class_map[record.event_type] || record.event_type, - data: record.data, - metadata: record.metadata, - timestamp: record.timestamp, - valid_at: record.valid_at - ) - end - - private - - attr_reader :class_map - end - end - end -end diff --git a/ruby_event_store/spec/client_spec.rb b/ruby_event_store/spec/client_spec.rb index ef914c220c..7818663409 100644 --- a/ruby_event_store/spec/client_spec.rb +++ b/ruby_event_store/spec/client_spec.rb @@ -19,10 +19,57 @@ module RubyEventStore expect(client.publish(TestEvent.new)).to eq(client) end + specify "publish with no events, fail if nil" do + client.publish([], stream_name: stream) + + expect { + client.publish(nil, stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect { + client.publish([nil], stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect(client.read.stream(stream).to_a).to be_empty + end + specify "append returns client when success" do expect(client.append(TestEvent.new, stream_name: stream)).to eq(client) end + specify "append with no events, fail if nil" do + client.append([], stream_name: stream) + + expect { + client.append(nil, stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect { + client.append([nil], stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect(client.read.stream(stream).to_a).to be_empty + end + + specify "link returns self when success" do + client.append(event = TestEvent.new) + expect(client.link(event.event_id, stream_name: stream)).to eq(client) + end + + specify "link with no events, fail if nil" do + client.link([], stream_name: stream) + + expect { + client.link(nil, stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect { + client.link([nil], stream_name: stream) + }.to raise_error(ArgumentError, "Event cannot be `nil`") + + expect(client.read.stream(stream).to_a).to be_empty + end + specify "append to default stream when not specified" do expect(client.append(test_event = TestEvent.new)).to eq(client) expect(client.read.limit(100).to_a).to eq([test_event]) @@ -198,7 +245,7 @@ module RubyEventStore expect(published[2].metadata[:valid_at]).to be_a Time end - specify "event's metadata takes precedence over with_metadata" do + specify "event's metadata takes precedence over with_metadata" do client.with_metadata(request_ip: "127.0.0.1") do client.publish(@event = TestEvent.new(metadata: { request_ip: "1.2.3.4" })) end diff --git a/ruby_event_store/spec/mappers/default_spec.rb b/ruby_event_store/spec/mappers/default_spec.rb index 9c61f0cb74..572e0beb42 100644 --- a/ruby_event_store/spec/mappers/default_spec.rb +++ b/ruby_event_store/spec/mappers/default_spec.rb @@ -55,25 +55,6 @@ module Mappers expect(event.metadata[:valid_at]).to eq(time) end - specify "#record_to_event its using events class remapping" do - subject = Default.new(events_class_remapping: { "EventNameBeforeRefactor" => "SomethingHappened" }) - record = - Record.new( - event_id: domain_event.event_id, - data: { - some_attribute: 5 - }, - metadata: { - some_meta: 1 - }, - event_type: "EventNameBeforeRefactor", - timestamp: time, - valid_at: time - ) - event = subject.record_to_event(record) - expect(event).to eq(domain_event) - end - specify "metadata keys are symbolized" do record = Record.new( diff --git a/ruby_event_store/spec/mappers/transformation/event_class_remapper_spec.rb b/ruby_event_store/spec/mappers/transformation/event_class_remapper_spec.rb deleted file mode 100644 index 19a83ec1f8..0000000000 --- a/ruby_event_store/spec/mappers/transformation/event_class_remapper_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require "spec_helper" - -module RubyEventStore - module Mappers - module Transformation - ::RSpec.describe EventClassRemapper do - let(:time) { Time.now.utc } - let(:uuid) { SecureRandom.uuid } - def record(event_type: "TestEvent") - Record.new( - event_id: uuid, - metadata: { - some: "meta" - }, - data: { - some: "value" - }, - event_type: event_type, - timestamp: time, - valid_at: time - ) - end - let(:changeable_record) { record(event_type: "EventNameBeforeRefactor") } - let(:changed_record) { record(event_type: "SomethingHappened") } - let(:class_map) { { "EventNameBeforeRefactor" => "SomethingHappened" } } - - specify "#dump" do - expect(EventClassRemapper.new(class_map).dump(record)).to eq(record) - expect(EventClassRemapper.new(class_map).dump(record)).to eq(record) - end - - specify "#load" do - expect(EventClassRemapper.new(class_map).load(record)).to eq(record) - expect(EventClassRemapper.new(class_map).load(changeable_record)).to eq(changed_record) - end - end - end - end -end diff --git a/ruby_event_store/spec/projection_spec.rb b/ruby_event_store/spec/projection_spec.rb index b46ff08288..dc20f16a73 100644 --- a/ruby_event_store/spec/projection_spec.rb +++ b/ruby_event_store/spec/projection_spec.rb @@ -309,30 +309,5 @@ module RubyEventStore expect(state).to eq({}) end - - specify "supports event class remapping" do - event_store = - RubyEventStore::Client.new( - repository: repository, - mapper: Mappers::Default.new(events_class_remapping: { MoneyInvested.to_s => MoneyLost.to_s }) - ) - event_store.append(MoneyInvested.new(data: { amount: 1 })) - - balance = - Projection - .from_all_streams - .init(-> { { total: 0 } }) - .when(MoneyLost, ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store) - expect(balance).to eq(total: 0) - - balance = - Projection - .from_all_streams - .init(-> { { total: 0 } }) - .when([MoneyLost, MoneyInvested], ->(state, event) { state[:total] -= event.data[:amount] }) - .run(event_store) - expect(balance).to eq(total: -1) - end end end