Giter Site home page Giter Site logo

railseventstore / rails_event_store Goto Github PK

View Code? Open in Web Editor NEW
1.4K 34.0 120.0 13.01 MB

A Ruby implementation of an Event Store based on Active Record

Home Page: http://railseventstore.org

License: MIT License

Ruby 81.84% Makefile 1.14% Elm 6.85% JavaScript 0.15% CSS 1.34% HTML 8.33% Dockerfile 0.02% Nix 0.33% Shell 0.01%
ruby event-sourcing rails ddd aggregate-root event-driven domain-driven-design event-driven-architecture cqrs-es cqrs

rails_event_store's Introduction

Rails Event Store

Rails Event Store (RES) is a library for publishing, consuming, storing and retrieving events. It's your best companion for going with an event-driven architecture for your Rails application.

You can use it:

  • as your Publish-Subscribe bus
  • to decouple core business logic from external concerns in Hexagonal style architectures
  • as an alternative to ActiveRecord callbacks and Observers
  • as a communication layer between loosely coupled components
  • to react to published events synchronously or asynchronously
  • to extract side-effects (notifications, metrics etc) from your controllers and services into event handlers
  • to build an audit-log
  • to create read-models
  • to implement event-sourcing

Documentation

Documentation, tutorials and code samples are available at https://railseventstore.org.

Code status

This single repository hosts several gems and website with documentation โ€” see the contribution guide.

We're aiming for 100% mutation coverage in this project. This is why:

Whenever you fix a bug or add a new feature, we require that the coverage doesn't go down.

RailsEventStore gems

Name CI Version Downloads
rails_event_store GitHub Workflow Status Gem Gem
rails_event_store_active_record GitHub Workflow Status Gem Gem
ruby_event_store-active_record GitHub Workflow Status Gem Gem
ruby_event_store GitHub Workflow Status Gem Gem
ruby_event_store-browser GitHub Workflow Status Gem Gem
ruby_event_store-rspec GitHub Workflow Status Gem Gem
aggregate_root GitHub Workflow Status Gem Gem

Contributed gems

Name CI Version Downloads
ruby_event_store-outbox GitHub Workflow Status Gem Gem
ruby_event_store-protobuf GitHub Workflow Status Gem Gem
ruby_event_store-profiler GitHub Workflow Status Gem Gem
ruby_event_store-flipper GitHub Workflow Status Gem Gem
ruby_event_store-transformations GitHub Workflow Status Gem Gem
ruby_event_store-rom GitHub Workflow Status Gem Gem
ruby_event_store-sidekiq_scheduler GitHub Workflow Status Gem Gem

Unreleased contributed gems

Name CI
ruby_event_store-newrelic GitHub Workflow Status
minitest-ruby_event_store GitHub Workflow Status
dres_rails GitHub Workflow Status
dres_client GitHub Workflow Status

About

Arkency

This repository is funded and maintained by arkency. Make sure to check out our Rails Architect Masterclass training and long-term support plans available.

rails_event_store's People

Contributors

aesyondu avatar aguthstuditemps avatar anderslemke avatar andrzejkrzywda avatar antonpaisov avatar dependabot-preview[bot] avatar dependabot[bot] avatar dneilroth avatar fidel avatar gottfrois avatar jakubkosinski avatar jandudulski avatar joelvh avatar killavus avatar lukaszreszke avatar maciejkorsan avatar mbj avatar mlomnicki avatar mostlyobvious avatar mpraglowski avatar paneq avatar pjurewicz avatar pkondzior avatar porbas avatar rybex avatar scottbartell avatar swistak35 avatar tomaszpatrzek avatar tomaszwro avatar voter101 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rails_event_store's Issues

Consider using prepend module pattern to set constructor values in AggregateRoot

Consider this code:

module AggregateRoot
  def apply(event)
    apply_strategy.(self, event)
    unpublished_events << event
  end

  def load(stream_name, event_store: default_event_store)
    @loaded_from_stream_name = stream_name
    events = event_store.read_stream_events_forward(stream_name)
    events.each do |event|
      apply(event)
    end
    @version = events.size - 1
    @unpublished_events = nil
    self
  end

  def store(stream_name = loaded_from_stream_name, event_store: default_event_store)
    event_store.publish_events(unpublished_events, stream_name: stream_name, expected_version: version)
    @version += unpublished_events.size
    @unpublished_events = nil
  end

  private
  attr_reader :loaded_from_stream_name

  def unpublished_events
    @unpublished_events ||= []
  end

  def version
    @version ||= -1
  end

It uses lazy methods to create instance variables at first usage:

  def unpublished_events
    @unpublished_events ||= []
  end

  def version
    @version ||= -1
  end

But that's because we don't have a constructor because our logic is implemented as module.

What if we did something like:

module X
  module Constructor
    def initialize
      puts "C"
      super if defined?(super)
    end
  end
  def self.included(klass)
    klass.prepend(Constructor)
  end
end

class A
  include X

  def initialize
    puts "A"
  end
end

A.new

to initialize defaults such as:

@unpublished_events = []
@version = -1

Would that make sense or make things more convoluted?

Inspired by mutation testing:

evil:AggregateRoot#store:/home/travis/build/RailsEventStore/rails_event_store/aggregate_root/lib/aggregate_root.rb:23:b9e31
@@ -1,6 +1,6 @@
 def store(stream_name = loaded_from_stream_name, event_store: default_event_store)
   event_store.publish_events(unpublished_events, stream_name: stream_name, expected_version: version)
   @version += unpublished_events.size
-  @unpublished_events = []
+  @unpublished_events = nil
 end

Current master brach does not have @version but after #86 it will have

Transaction and create hook problem

I have an observer in which i create an event and subscribe on this event like this

class ChangedStateOfOrderObserver < ActiveRecord::Observer
  observe :order

  def after_save(order)
    if order.state_changed?
      publish_changed_state_event(order)
    end
  end

  def publish_changed_state_event(order)
    client = RailsEventStore::Client.new
    stream_name = "order_#{order.id}"
    event_data = {
        data: { order_id: order.id, state: order.state }
    }
    event = Orders::StateChanged.new(event_data)

    notifiers = []

    notifiers << Managers::OrderAcceptedNotifier.new if order.accepted?
    notifiers << Managers::OrderCreatedNotifier.new if order.new?
    notifiers << Clients::OrderStateChangedNotifier.new

    notifiers.each do |n|
      client.subscribe(n, [event.class.name])
    end

    client.publish_event(event, stream_name)
  end
end

Clients::OrderStateChangedNotifier.new - in this class I send push notification to clients to inform that order's state has been changed.
I run into bug, when if push notificatin can not be performed due to some ssl bugs, order' save function rolls back and app crashes.

What do you think, is it correct behavior that save is rollbacked? I think problem with notifications should not interfere with default work flow of programm, how can I fix it?

undefined method `failure_message' for HaveApplied

Posted originally by @paneq

Failures:

  1) Product product registered
     Failure/Error:
       expect(product).to have_applied(
         an_event(ProductRegistered).with_data(
           store_id: 1,
         ).strict
       )
     
     NoMethodError:
       undefined method `failure_message' for #<RailsEventStore::RSpec::HaveApplied:0x000000023c9180>

What is the best plase for subscription

I have a order model, in which I want to add event publishing on create callback. Should I add subscribtion to the same place where event is triggered? See the example below

def on_create  
    .... 
    notifiers << Managers::OrderCreatedNotifier.new if order.new?

    notifiers.each do |n|
      client.subscribe(n, [event.class.name])
    end

    client.publish_event(event, stream_name)
end

Enable ActiveJob handlers by default in rails_event_store

Right now we have this code ready but it is not enabled as default dispatcher:

module RailsEventStore
  class Client < RubyEventStore::Client
    def initialize(repository: RailsEventStore.event_repository,
                   event_broker: EventBroker.new,
                   page_size: PAGE_SIZE)
      capture_metadata = ->{ Thread.current[:rails_event_store] }
      super(repository: repository,
            event_broker: event_broker,
            page_size: page_size,
            metadata_proc: capture_metadata)
    end
  end
end

We could detect if active_job is enabled and in such cass pass ActiveJobDispatcher to EventBroker.

ActiveJobDispatcher or standard Dispatcher could be wrapped in instrumentation proxy #90 .

Related to #124 as well

Add license field to gemspec

It would facilitate identifying the license using rubygems.org (https://rubygems.org/gems/rails_event_store) and tools like license_finder, if the license field was added to the gemspec.

It seems to be LGPLv3 at the moment, although other related projects like rails_event_store_active_record are MIT.

Consistent MIT licensing for rails_event_store_active_record, rails_event_store and ruby_event_store would be our preference - but your choice obviously.

Global configuration leads to unexpected behaviour

RES version: current master

es1 = RailsEventStore::Client.new
es2 = RailsEventStore::Client.new

class Handler
 def handle_event(event)
   puts "handler fired..."
 end
end

OrderExpired = Class.new(RailsEventStore::Event)

es1.subscribe(Handler.new, [OrderExpired])
es2.publish_event(OrderExpired.new)

# => handler fired...

Would you expect that the Handler is being fired? It is, because both es1 and es2 share the same
instance of PubSub::Broker. This leads to bizzare failures. You instantiate a new RES instance in every test but subscribed handlers are retained.

The problem is due to this line which is needed for global configuration here

Proposed solution: drop global configuration ๐Ÿ˜„

Discussion: Support for Refactoring use case for Aggregate

Theoretically can happen situation when you would like to move aggregate between namespaces or you choose wrong name for it. We don't want to modify our source of true (event store), but we are using aggregate names as part of identification of stream.

In such use case this example code lets to re-define name used for constructing stream id:

module Loans
  class Loan
    include Infra::AggregateRoot
    include ::Base::Contracts

    set_aggregate_name 'Loans::Loan'
    ...
  end
end
module Infra
  module AggregateRoot
    extend ActiveSupport::Concern

    included do
      include ::AggregateRoot
      extend AggregateName
    end

    module AggregateName
      # rubocop:disable Style/AccessorMethodName
      def set_aggregate_name(aggregate_name)
        @aggregate_name = aggregate_name
      end

      def aggregate_name
        @aggregate_name
      end
    end
  end
end
module Infra
  module WithAggregate
    include Contracts::Core
    include Contracts::Builtin

    private

    def with_aggregate(aggregate_class, aggregate_id, **args)
      aggregate = build(aggregate_class, aggregate_id, **args)
      yield aggregate
      aggregate.store
      aggregate
    end

    def build(aggregate_class, aggregate_id, **args)
      name = aggregate_class.aggregate_name || aggregate_class.name
      stream = "#{name}$#{aggregate_id}"
      aggregate_class.new(aggregate_id: aggregate_id, **args)
        .load(stream, event_store: Rails.configuration.core.event_store)
    end
  end
end

Event Store as queue

Implement Event Store in a way that would allow clients to safely iterate over global stream of all events with pagination and no race conditions. The client would remember its position in the stream of events in its own side.

New release scheme

Right now we have different version numbers for indiviudal gems, that is each gem has individual versioning and changelog. Currently released:

  • rails_event_store 0.14.5
  • ruby_event_store 0.14.0
  • rails_event_store_active_record 0.6.13
  • aggregate_root 0.4.0

Then there's rails_event_store gem that depends on the rest. Given that dependencies are defined with ~> it is not unusual to bump its version because of the dependency change.

On the other hand there's Rails way of doing releases - each gem included in rails gets the same version number of Rails release, even with no changes.
That makes it slightly easier to push related chanegs across all gems with less confusion and to prepare single changelog of what's included with each release. It might be also more convenient with monorepo (see #74) to git tag releases where each component has the same version.

Proposal: release rails_event_store, ruby_event_store, rails_event_store_active_record and aggregate_root together starting with 0.15.0 version.

Decide what should be returned by AggregateRoot#apply

Current code

module AggregateRoot
  def apply(event)
    apply_strategy.(self, event)
    unpublished_events << event
  end

However there is no test checking for it and manual mutation testing still passed all tests:

module AggregateRoot
  def apply(event)
    apply_strategy.(self, event)
    unpublished_events << event
    nil
  end

Returns all unpublished_events might sound good but if someone wants to test events created by calling a command and set up a state using other commands, it's not easy to extract domain events created only by last action. Example:

class Product
  def supply(quantity)
    raise NotRegistered unless @store_id
    apply(ProductSupplied.new(data: {
      store_id: @store_id,
      sku: @sku,
      quantity: quantity,
    }))
  end
end

  specify "product supplied" do
    product.register(store_id: 1, sku: "BOOK")
    product.supply(30)

Since product.unpublished_events is private (#108) and product.supply returns all unpublished_events there is no easy way to test only events generated by product.supply.

My proposal:

module AggregateRoot
  def apply(*events)
    events.each do |event|
      apply_strategy.(self, event)
      unpublished_events << event
    end
  end
  • allow applying multiple events (useful sometimes)
  • return only applied events instead of all unpublished_events

Run mutant on one Ruby version only

Testing with mutant takes time (a lot of it). Yet mutating on each Ruby version in matrix doesn't uncover new facts about missing code coverage. Unit tests should be enough to verify that functionality works on given Ruby verison.

Proposal: run make mutate on latest Ruby only

Feature proposal have_applied(...events).only

Posted originally by @paneq

One of the best additional features of testing with domain events is the ability to check what has not happened.

Currently have_applied and have_published use RSpec::Matchers::BuiltIn::Include matcher which does not guarantee there this and only this. That's especially important in terms of have_applied.

It would be nice to have API such as expect(obj).to have_applied(...events).only and to know that there are those and only those events listed in unpublished_events.

Maybe only should be a default behavior even? and .softly or .not_exclusively should to it otherwise?

Consider (re)moving git tags < 0.15.0

Since rewriting commit SHAs tags before rewrite point to wrong commits. This might be confusing at least.

We could probably remove those tags (no value from them now).

Additionally (if that makes sense) I could imagine adjusting (re-adding) them to point to new SHAs.

Discussion: Support for Refactoring use case for Events

We have here few use cases:

  1. Renaming / moving event class
  2. Modification of existing event (ex. adding new attribute)

In order to support both use case we have done example customizations:

  1. Renaming / moving event class

In such use case we had to implement custom repository:

module Infra
  # Clone of RailsEventStoreActiveRecord::EventRepository
  # with changes:
  #   - constructor is accepting 'events_name_mapping' keyword with hash
  #     configuration of renaming mapping
  #   - with modified version of build_event_entity method, which is using this mapping
  #   - with modified version of read_all_streams_forward method, with support for event_types
  #     (used in rebuilding of read models, to reduce scope)
  class EventRepository < RailsEventStoreActiveRecord::EventRepository
    def initialize(events_rename_mapping: {})
      @events_rename_mapping = events_rename_mapping
      super()
    end

    # improved version, please take a look documentation on top
    def read_all_streams_forward(start_event_id, count, event_types)
      stream = adapter
      unless start_event_id.equal?(:head)
        starting_event = adapter.find_by(event_id: start_event_id)
        stream = stream.where('id > ?', starting_event)
      end

      scope = stream.order('id ASC')
      scope = scope.where(event_type: event_types) if event_types
      scope = scope.limit(count)
      scope.map(&method(:build_event_entity))
    end

    private

    attr_reader :events_rename_mapping

    # improved version, please take a look documentation on top
    def build_event_entity(record)
      return nil unless record
      event_type = events_rename_mapping.fetch(record.event_type) { record.event_type }
      event_type.constantize.new(
        event_id: record.event_id,
        metadata: record.metadata,
        data:     record.data
      )
    end
  end
end

Configure using of this repository by our configuration:

EVENTS_RENAME_MAPPING = {
  # Example mapping in case of refactoring (move or rename)
  "Loans::Events::LoanGivenEvent" => "Loans::Events::SomeGivenEvent"
}

class CoreConfiguration
  def initialize(repository: Infra::EventRepository.new(events_rename_mapping: EVENTS_RENAME_MAPPING),
                 event_store: RailsEventStore::Client.new(repository: repository),
                 command_bus: Infra::CommandBus.new,
                 command_injector: Infra::CommandInjector.new(command_bus: command_bus))
    @event_store      = event_store
    @command_bus      = command_bus
    @command_injector = command_injector

    configure_aggregate_root(event_store)
    setup_event_handler_strategy
    setup_read_models(event_store)
    register_event_handlers(event_store)
    register_command_handlers(command_bus)
  end

  def configure_aggregate_root(event_store)
    AggregateRoot.configure do |config|
      config.default_event_store = event_store
    end
  end
  ...

  attr_reader :event_store,
              :command_bus,
              :all_command_handlers,
              :all_read_models,
              :all_event_handlers,
              :command_injector
end
  1. modification of existing event (ex. adding new attribute, changing type of existing one, etc)
module Loans
  module Events
    class LoanGivenEvent < ::Infra::Event
      version 4

      attribute :loan_number,     Loans::Types::LoanNumber
      attribute :loan_conditions, Loans::Types::LoanConditions

      def self.convert_from_v1_to_v2(event)
        puts "\n#{self} convert: v1 -> v2\n"
        puts event.inspect
        event
      end

      def self.convert_from_v2_to_v3(event)
        puts "\n#{self} convert: v2 -> v3\n"
        puts event.inspect
        event
      end

      def self.convert_from_v3_to_v4(event)
        puts "\n#{self} convert: v3 -> v4\n"
        puts event.inspect
        event
      end
    end
  end
end

by default version is equal 1 implicitly:

module Loans
  module Events
    class LoanGivenEvent < ::Infra::Event
      attribute :loan_number,     Loans::Types::LoanNumber
      attribute :loan_conditions, Loans::Types::LoanConditions
    end
  end
end
module Infra
  class Event < Dry::Struct
    include ::Base::Contracts

    class VersionConverter
      include ::Base::Contracts

      SpecEventAttributes = KeywordArgs[event_id: String, data: Hash, metadata: Hash]

      Contract SpecEventAttributes, ClassOf[Event] => SpecEventAttributes
      def call(event_attributes, klass)
        event_version     = event_version(event_attributes, klass)
        outdated_versions = (event_version..(klass.version - 1)).to_a
        outdated_versions.inject(event_attributes) do |attributes, current_version|
          upgrade(klass, attributes, from: current_version)
        end
      end

      private

      Contract SpecEventAttributes, Any => Integer
      def event_version(event_attributes, _klass)
        event_attributes.dig(:metadata, :version) || INITIAL_VERSION
      end
   

      Contract ClassOf[Event], SpecEventAttributes, KeywordArgs[from: Integer] => SpecEventAttributes
      def upgrade(klass, event_attributes, from:)
        event_version = from
        upgraded_event_version = event_version + 1
        new_data =
          klass
            .method("convert_from_v#{event_version}_to_v#{upgraded_event_version}")
            .call(event_attributes[:data])
        event_attributes
          .merge(data: new_data)
          .deep_merge(metadata: { version: upgraded_event_version })
          .slice(:data, :metadata, :event_id)
      end
    end

    INITIAL_VERSION = 1
    Contract Or[nil, Integer] => Integer
    def self.version(version = nil)
      if version
        @version = version
      else
        @version || INITIAL_VERSION
      end
    end

    ...
  end
end

RSpec warning if spec description is empty

specify do
  expect(es).to have_published(an_event(OrderPaid))
end
rspec --format documentation order_spec.rb

raises this warning


specify { expect(object).to matcher }

or this:

it { is_expected.to matcher }

RSpec expects the matcher to have a #description method. You should either
add a String to the example this matcher is being used in, or give it a
description method. Then you won't have to suffer this lengthy warning again.

Expected:
RSpec itself should generate the description, without any warnings

Benchmarks for RES

The discussion on the implementation of optimistic locking made me think that it would be beneficial to have an automated way to benchmark RES. Reasons are pretty obvious

  • to make sure there are no performance regressions
  • to have an ability to compare solutions and pick the best one
  • to prove or disprove that a certain patch brings performance benefits

I came up with the list of the following requirements:

  • benchmark real-world scenarios (as much as possible)
  • make benchmarks reproducible
  • make it easy to compare multiple versions of RES
  • make it easy to specify RES source and version. I.e. branch, rubygems release, path on the disk, etc

This is an initial attempt to implement such solution https://github.com/mlomnicki/res-benchmarks

For now there are 2 benchmarks

  • publish events to stream
  • read 50 events from stream

It benchmarks the following RES versions

  • v0.9 to v0.15
  • master branch
  • locking_friendly branch

The benchmarks have already proved to be useful. The "read" benchmark discovered that there was a major regression in RES 0.12.

Reading events
https://benchmark.fyi/T

As you can see the last version where reading events is efficient is 0.11. In 0.12 reading from stream became 42x slower.

No surprises when it comes to publishing events
Publishing
http://benchmark.fyi/U

I'd like to get some feedback from you. In particular:

  • do you think it's useful?
  • what to benchmark?
  • should the code live in a separate repo? moved to the RES organization? added to the rails_event_store repo?

Obviosuly there's a huge room for improvement

  • show results on nice charts
  • plug into CI and benchmark every commit and/or release
  • benchmark with mysql and postgres

Feedback is more than welcome

Discussion: Fault tolerance for errors on event handlers

Current implementation of PubSub Broker is not fault tolerant, which means that if one error handler will fail the "broadcasting" will stop processing event by other handlers which were added after failing one.

https://github.com/RailsEventStore/rails_event_store/blob/master/ruby_event_store/lib/ruby_event_store/pub_sub/broker.rb#L26

Because of current implementation of the broker I had to install such error isolation layer:

Configuration:

...
  def setup_event_handler_strategy
    Rails.configuration.event_handler_strategy =
      %w[development test].include?(Rails.env) ? :throw : :notify
  end
....

Usage of Error handling strategy:

module Infra
  module EventHandlers
    include Contracts::Core
    include Contracts::Builtin

    class UnknownHandlerStrategy < StandardError
    end

    Contract Config => Any
    def connect(event_handler_config)
      @configs ||= []
      @configs << event_handler_config.handlers
    end

    Contract KeywordArgs[event_store: RailsEventStore::Client,
                         event_handler_error_strategy: Optional[EventHandlerErrorStrategy]] => Any
    def register(event_store: Rails.configuration.core.event_store,
                 event_handler_error_strategy: EventHandlerErrorStrategy.new)
      configs.flatten.each do |c|
        events  = c.fetch(:events)
        handler = event_handler_error_strategy.build_error_handler(c.fetch(:handler), events)
        event_store.subscribe(handler, events)
      end
    end

    attr_reader :configs
  end
end

Implementation of Error handling strategy :

module Infra
  class EventHandlerErrorStrategy
    def initialize(event_handler_strategy: Rails.configuration.event_handler_strategy)
      @strategy = event_handler_strategy
    end

    def build_error_handler(handler, events)
      send_handler_info(handler, events)
      case strategy
      when :notify
        ->(event) { notify_error_handler(handler, event) }
      when :throw
        ->(event) { handler.call(event) }
      else
        raise UnknownHandlerStrategy
      end
    end

    private

    def send_handler_info(handler, events)
      for_events    = "  - for events: #{events.inspect}"
      with_strategy = "  - with '#{strategy}' strategy"
      message = "event handler registered #{handler}\n#{for_events}\n#{with_strategy}"
      if %w[development test].include?(Rails.env)
        puts message
      else
        Rails.logger.info message
      end
    end

    def notify_error_handler(handler, event)
      handler.call(event)
    rescue StandardError => ex
      Airbrake.notify(ex)
    end

    attr_reader :strategy
  end
end

AggregateRoot::Debugging

class Product
  include AggregateRoot
  include AggregateRoot::Debugging if Rails.env.development? # never use in production

AggregateRoot::Debugging could hook into apply and display (puts) and log (Rails.logger.debug) applied event and the state after applying.

I believe it could be useful.

Cleanup ruby_event_store repo

This repository is now a placeholder to redirect to the monorepo (via link in README). There are however some discussions and branches to take care of:

  • migrate (or close) issues
  • migrate (or close) pull requests
  • rebase (or not) and remove branches

Issue with optional command attributes

Let's say we have the following command:

module Commands
  class CreateOrder
    include ActiveModel::Model
    include ActiveModel::Validations

    attr_accessor :order_id, :customer_id, :some_optional_attribute

    validates :order_id, :customer_id, presence: true
  end
end

Now in my domain:

# ...
def apply_events_order_created(event)
  self.order_id = event.order_id
  self.customer_id = event.customer_id
  self.some_optional_attribute = event.some_optional_attribute
end
# ...

The way the event handles data currently is by defining a getter method for each attributes. If the optional attribute is not present when we create the event, it will throw a NoMethodError exception when trying to access event.some_optional_attribute.

I think it should return nil.

What are your thoughts on this?

Consider automating RSpec configuration

Posted originally by @pawelpacana

Right now there's a manual process for adding matchers into the project besides plain gem install. It's good when you consciously decided to control every aspect of a depedency being added to the project. Yet most of the time I'd imagine there's not much to tweak.

Let's have a look how Capybara does it: https://github.com/teamcapybara/capybara#using-capybara-with-rspec

So when you want matchers, you require them:

require `capybara/rspec`

Same could work for RES matchers.

require `rails_event_store/rspec`

Since the matchers live in a separate gem named rails_event_store-rspec, bundler should require it automatically by mere presence in Gemfile (or should we advise to require: 'rails_event_store/rspec' there?). As a "pro" user, you could add require: false to disable it and proceed manually. For the rest, good enough default would kick in.

This is what happens after require: https://github.com/teamcapybara/capybara/blob/master/lib/capybara/rspec.rb. Basically all we advise to do yourself.

Finally, Capybara does not depend on RSpec: https://github.com/teamcapybara/capybara/blob/master/capybara.gemspec#L24-L29

If we drop this runtime dependency as well, maybe rails_event_store could have matchers as a dependency without making your project depend on rspec when you don't have it.

Error when trying to #read_all_streams

Hey, after update to 0.2.0 the es_client.read_all_streams fails for me (it worked on 0.1.4).

class OrderPlaced < RailsEventStore::Event; end

it "fails RailsEventStore" do
  es_client = RailsEventStore::Client.new
  expect(es_client.read_all_streams).to eql({}) # => true
  order_id = SecureRandom.uuid
  stream_name = "order_#{order_id}"
  event_data = {
    data: {
      id: order_id,
      hodor: "HODOR",
    },
    event_id: SecureRandom.uuid,
  }
  event = OrderPlaced.new(event_data)
  es_client.publish_event(event, stream_name)
  es_client.read_all_streams
end

Last line raises:

NoMethodError: undefined method `stream' for #<RailsEventStore::Event:0x007fbd22885518>
from /Users/hodak/.rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/ruby_event_store-0.2.0/lib/ruby_event_store/actions/read_all_streams.rb:10:in `block in call'

Which points to this line in ruby_event_store: https://github.com/arkency/ruby_event_store/blob/2cab39a85a8431dd682a0c71008d32b87d66ee4d/lib/ruby_event_store/actions/read_all_streams.rb#L10

Which makes sense, because when I read repository manually, it doesn't seem to have stream attribute:

[4] pry(#<RSpec::ExampleGroups::ApiOrdersController::ValidParams>)> RailsEventStore::Repositories::EventRepository.new.get_all_events
=> [#<RailsEventStore::Event:0x007fbd223257d0
  @data={:id=>"cf5ecc88-e7b7-46b9-88f3-1eb2e2fd1c8d", :hodor=>"HODOR"},
  @event_id="2e8f45ea-b1c3-48c5-be1e-2226ec5f84a0",
  @event_type="OrderPlaced",
  @metadata={:published_at=>2016-02-24 17:52:58 UTC}>]

rails/generators as an explicit dependency

While trying to use rails_event_store outside rails rails/generators dependency is missing. I think that it should be explicitly required to prevent people from adding it as a dev_dependency in libraries which could be potentially created within an RES ecosystem.

 c/R/rails_event_store-rspec $$ make test
Running basic tests - beware: this won't guarantee build pass

An error occurred while loading ./spec/rails_event_store/rspec/event_matcher_spec.rb.
Failure/Error: require "rails_event_store"

LoadError:
  cannot load such file -- rails/generators
# ./spec/rails_event_store/rspec/event_matcher_spec.rb:2:in `require'
# ./spec/rails_event_store/rspec/event_matcher_spec.rb:2:in `<top (required)>'
No examples found.

Randomized with seed 40509

Finished in 0.00031 seconds (files took 0.09792 seconds to load)
0 examples, 0 failures, 1 error occurred outside of examples

Randomized with seed 40509

make: *** [test] Error 1

Migration to new DB schema

  • Verify data migrated
    • positions
    • created_at
  • Check on all DBs
    • postgres
    • mysql
    • sqlite
  • Verify proper schema at the end
  • v1-v2 migration generator by calling rake res:v1tov2schema_migration
  • Fix random with SQL

Documenting changes for each release

So far we've used CHANGELOG.md in each gem to document changes. We also seem to track documentation changes separately in http://railseventstore.org/docs/changelog/.

From an end-user perspective I'm not that interested in looking for what were the changes in dependencies of rails_event_store, I'd expect to have single place to figure out what changed once I update the top-level rails_event_store gem.

Yet some gems make sense to be used on it's own (ruby_event_store, aggregate_root). So per-gem changelogs are not bad after all.

Last but not least - github offers a feature to document release changes. See how https://github.com/webpack/webpack/releases is structured. The advantage of having release descriptions outside of published code atrifact (pushed gem) has an advantage of ability to fix it post-release.

What are your thoughts on changelogs?

Update documentation in a gitbook

Right now docs are a little outdated I suppose:

  • Still showing handle_event which is deprecated in favor of call (subscription part)
  • Lack of dynamic subscriptions

I think it's necessary to keep docs up to date. I leave this issue as-is, so maybe someone who has a little time can make a PR for it.

Logging

I'd like to start a discussion about the concept of logging around the event stores.

RailsEventStore is meant to help Rails people to start being event-driven. That's a great thing. Events do introduce indirection of the control flow. They also make debugging different.

One thing I'm wondering about is to have some automatic logging for some important places in the lifecycle of the event store:

  • initialization
  • setup of subscribers
  • publishing
  • notifying subscribers
  • warnings (for example when event is published but nothing is handling it), this may be a separate concern, though

Given that RailsEventStore is an umbrella which automates things for Rails, we could have the Rails.logger being a default dependency. However, it's possible that the EventStore is also run without Rails (my current case) and then it should be possible to inject my own logger (probably via constructor?).

Anyone else felt the need for having logs around the events?

uninitialized constant RSpec::Matchers when running rake

Steps to reproduce

Gemfile:

gem 'rails_event_store'

group :test do
   gem 'rspec-rails'
   gem 'rails_event_store-rspec'
end
RAILS_ENV=test rake db:create

rake aborted!
NameError: uninitialized constant RSpec::Matchers
/Users/michal/.rvm/gems/ruby-2.3.3/gems/rails_event_store-rspec-0.17.0/lib/rails_event_store/rspec/be_event.rb:115:in `<class:BeEvent>'
/Users/michal/.rvm/gems/ruby-2.3.3/gems/rails_event_store-rspec-0.17.0/lib/rails_event_store/rspec/be_event.rb:3:in `<module:RSpec>'
/Users/michal/.rvm/gems/ruby-2.3.3/gems/rails_event_store-rspec-0.17.0/lib/rails_event_store/rspec/be_event.rb:2:in `<module:RailsEventStore>'
/Users/michal/.rvm/gems/ruby-2.3.3/gems/rails_event_store-rspec-0.17.0/lib/rails_event_store/rspec/be_event.rb:1:in `<top (required)>'
/Users/michal/.rvm/gems/ruby-2.3.3/gems/rails_event_store-rspec-0.17.0/lib/rails_event_store/rspec.rb:8:in `<top (required)>'

or have a look at the sample app to reproduce the issue https://github.com/mlomnicki/res-rspec-error

The error goes away if I add 'rspec' to the Gemfile (rspec-rails is not enough)

group :test do
   gem 'rspec'
   gem 'rspec-rails'
   gem 'rails_event_store-rspec'
end

Allow to verify events generated in a block of code

Posted originally by @paneq

Imagine that you test an AggregateRoot and you use commands to set up its state but you never publish them.

You would like to verify events generated by a particular action but without the need to care about events generated by previous actions.

your choices:

product = Product.new
product.register(store_id: 1, sku: "BOOK")
product.unpublished_events.clear # unpublished_events is private right now inAggregateRoot but it should be public

expect(product).to have_applied(
  an_event(ProductSupplied).with_data(
    store_id: 1,
    sku: "BOOK",
    quantity: 30,
  ).strict
).only # `only` from https://github.com/RailsEventStore/rails_event_store-rspec/issues/5

What if instead we could do:

    expect do
      product.supply(20)
    end.to have_applied(an_event(ProductSupplied).with_data(
      store_id: 1,
      sku: "BOOK",
      quantity: 30,
    ).strict).only.on(product)

or something similar?

or maybe everything would be easier if we had

product = Product.new
product.register(store_id: 1, sku: "BOOK")
events = product.supply(30)
expect(events).to include(...)

Misleading documentation potentially (RailsEventStore::RSpec)?

Posted originally by @paneq

It is sometimes important to ensure no additional events have been published. Luckliy there's a modifier to cover that usecase.

expect(event_store).not_to have_published(an_event(OrderPlaced)).once
expect(event_store).to have_published(an_event(OrderPlaced)).exactly(2).times

That's what the documentation says. But...

But looking at the implementation

      def matches_count(events, expected, count)
        return true unless count
        raise NotSupported if expected.size > 1

        expected.all? do |event_or_matcher|
          events.select { |e| event_or_matcher === e }.size.equal?(count)
        end
      end

it seems that once does not guarantee that no additional events have been published, just that the listed events have been published once.

It seems that the author wanted something similar to .only proposed in #5 ?

Cleanup aggregate_root repo

This repository is now a placeholder to redirect to the monorepo (via link in README). There are however some discussions and branches to take care of:

  • migrate (or close) issues
  • rebase (or not) and remove branches

Merge all repos into one

Problem:

RES is made up of 5 repos

  • ruby_event_store
  • rails_event_store
  • rails_event_store_active_record
  • aggregate_root
  • railseventstore.org (website/docs)

Pros:

  • clean separation. You can use ruby_event_store without rails_event_store and its dependencies
  • faster tests. You can test aggregate_root without running tests for rails_event_store_active_record
  • 5 repos look and feel more fancy than one. The project looks more enterprisey

Cons:

  • the code is hard to navigate and understand. You have to jump between multiple repos and gems
  • if you make a change you don't know how it affects other components/gems. Unless you create a metarepo you can't run tests for all components at once
  • bigger changes that touch multiple components must be split into very small commits, one per repo. You can't easily see the correlation between changes.
  • Commits in separate repos force you to open separate pull requests. This makes code review a pain
  • People are less likely to contribute because the code isn't straightforward to navigate and understand

Solution:

Maybe it makes sense to split the code into gems but separate repos are pain in the ass. How about we adapt the same approach as Rails and put all the gems into one repo? https://github.com/rails/rails

Thoughts?

In rails 5.1 migration throws exception

be rake db:migrate
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Rails release the migration was written for:

  class CreateEventStoreEvents < ActiveRecord::Migration[4.2]
/Users/andriytyurnikov/Code/andriytyurnikov/blog_es_rails/db/migrate/20170713193136_create_event_store_events.rb:1:in `<top (required)>'
/Users/andriytyurnikov/.rbenv/versions/2.4.1/bin/bundle:22:in `load'
/Users/andriytyurnikov/.rbenv/versions/2.4.1/bin/bundle:22:in `<main>'
StandardError: Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Rails release the migration was written for:

  class CreateEventStoreEvents < ActiveRecord::Migration[4.2]
/Users/andriytyurnikov/Code/andriytyurnikov/blog_es_rails/db/migrate/20170713193136_create_event_store_events.rb:1:in `<top (required)>'
/Users/andriytyurnikov/.rbenv/versions/2.4.1/bin/bundle:22:in `load'
/Users/andriytyurnikov/.rbenv/versions/2.4.1/bin/bundle:22:in `<main>'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)

Using Event Store instead of db

Is it possible to use Greg's Event Store instead of the db? As far as I can see it's mandatory to migrate the database provided.

Add CodeClimate metrics

While merging repositories the source repo for metrics change as well. Before re-adding these metrics it's worth validating that we actually need them.

We already strive for 100% mutant coverage. That gives:

  • better code coverage metric
  • quite a pressure to refactor code to pass mutant and so far that has been great driver for quality

I've never actually looked into CC hints and I also remember that getting 4.0 score isn't hard.

Thoughts?

Events data

Hey guys,

What are your thoughts regarding how event classes are defined? Today we put our event data under a data key. Unfortunately we then have to define some helper methods to access this data as method calls instead of hash attributes:

class SomeEvent < RailsEventStore::Event  
  def uid
    data.fetch(:uid)
  end

  def foo
    data.fetch(:foo)
  end

  def self.create(uid, foo)
    new(data: { uid: uid, foo: foo })
  end
end

Maybe we could drop the data namespace and define our events using activemodel:

class SomeEvent < RailsEventStore::Event  
  include ActiveModel::Model

  attr_accessor :uid, :foo

  def self.create(uid, foo)
    new(uid: uid, foo: foo)
  end
end

The include could then be part of RailsEventStore::Event class. The downside is with attributes conflicts is the one defined by you (metadata) but this seems a small issue compare to what we can win here. Also we could use __ methods prefix or similar for event's special attributes.

We can push this even further and define the following method in the parent class to have back a data like method we can use in our projections:

def data
  attributes_except :event_id, :event_type, :metadata
end

def attributes_except(*attrs)
  as_json.reject { |k, _| attrs.include?(k) }
end
# from a projection
def fooed(event)
  MyModel.find_by_uid(event.uid).update!(event.data)
end

Would love your thoughts on this :)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.