Rails Event Store

Mappers

Mapper is defined as a pipeline of transformations that transforms domain event object into record and back from record to domain event. Mappers are useful when you have to encrypt your events data, or you’d like to access events using both strings nad symbols.

Available mappers

There is set of available mappers that you can use out of the box. They work with certain types of events, described in the table below.

Compatible with \ Mapper Default Protobuf Null Encryption
RubyEventStore::Event
RailsEventStore::Event
RubyEventStore::Proto

RubyEventStore::Mappers::Default

  • Default mapper for RubyEventStore::Client and RailsEventStore::Client
  • Transforms an event into a record (and back). Additionally it symbolizes metadata keys
  • Provide events_class_remapping optional constructor parameter, which is a hash, to map old event names to new ones after you refactored your codebase

RubyEventStore::Mappers::Protobuf

  • Works with Ruby classes generated by google-protobuf gem

RubyEventStore::Mappers::NullMapper

  • Performs no transformations. It's useful in tests

RubyEventStore::Mappers::EncryptionMapper

  • Encrypts event's data
  • Useful for GDPR
  • You can read more about EncryptionMapper in the GDPR section
  • constructor takes following arguments:
    • key_repository - which is responsible for providing encryption keys used to encrypt/descrypt payload by encryption transformation
    • forgotten_data (named argument) - describes how the data will be presented when the encryption key is forgotten. By default it is FORGOTTEN_DATA. You can change the default by passing desired value into constructor. For example ForgottenData.new("Key is forgotten").
    • serializer (named argument) - specifies the serialization/deserialization format of encrypted data. The default is YAML format.

Custom mapper

Mapper is defined as a pipeline of transformations that transforms domain event object into record and back from record to domain event. I.e. Default is implemented as a set of 2 transformations. Transformation works on Record objects. Such Record is given to transformation methods (it is expected that load & dump methods are implemented) and it is result of any transformation performed. Except transformations, each PipelineMapper needs to be given 1 edge transformation:

Extended implementation of Default with explicit default arguments:

module RubyEventStore
  module Mappers
    class Default < PipelineMapper
      def initialize(events_class_remapping: {})
        super(Pipeline.new(
          Transformation::EventClassRemapper.new(events_class_remapping),
          Transformation::SymbolizeMetadataKeys.new,
          to_domain_event: Transformation::DomainEvent.new
        ))
      end
    end
  end
end

When you define new custom mapper you could use mapper PipelineMapper as base class and provide your transformations pipeline.

Each transformation must implement 2 methods: dump(record) & load(record). Both methods take a RubyEventStore::Record and return new instance of it, transformed. Transformation shall not modify given record and shall return new instance as a result.

If you use different class to represent domain event you shall also define edge transformation for your pipeline definition.

Example of custom mapper build based on mappers pipeline:

require 'msgpack'

class MessagePackSerialization
  def dump(record)
    RubyEventStore::Record.new(
      event_id:   record.event_id,
      metadata:   record.metadata.to_msg_pack,
      data:       record.data.to_msg_pack,
      event_type: record.event_type,
      timestamp:  record.timestamp,
      valid_at:   record.valid_at
    )
  end

  def load(record)
    RubyEventStore::Record.new(
      event_id:   record.event_id,
      metadata:   MessagePack.unpack(record.metadata),
      data:       MessagePack.unpack(record.data),
      event_type: record.event_type,
      timestamp:  record.timestamp,
      valid_at:   record.valid_at
    )
  end
end

class MyHashToMessagePackMapper < RubyEventStore::Mappers::PipelineMapper
  def initialize
    super(RubyEventStore::Mappers::Pipeline.new(
      MessagePackSerialization.new
    ))
  end
end

You could build your own transformations or use existing ones combined with the ones build by you. We strongly encourage you to share your transformations as a part of RES-contrib.

Check out the code of our mappers and transformations on github for examples on how to implement mappers & transformations.

You can pass a different mapper as a dependency when instantiating the client.

# config/environments/*.rb

Rails.application.configure do
  config.to_prepare do
    Rails.configuration.event_store = RailsEventStore::Client.new(
      mapper: MyHashToMessagePackMapper.new
    )
  end
end

Now you should be able to publish your events:

class OrderPlaced < RubyEventStore::Event
end

event_store = Rails.configuration.event_store

event_store.publish(OrderPlaced.new(data: {
  'event_id' => SecureRandom.uuid,
  'order_id' => 1,
  'order_amount' => BigDecimal.new('120.55'),
}), stream_name: 'Order$1')