aef
aef

Reputation: 4698

Cleanly validating event schemas in RailsEventStore

I am building a CQRS and event sourced application with Rails and RailsEventStore. In RailsEventStore it seems you are supposed to inherit classes that represent events from RailsEventStore::Event.

class UserRegistered < RailsEventStore::Event
  …
end

This base class exposes an event_id, a data hash and an optional metadata hash. As far as I understand, this looks more like what Greg Young once called an event envelope and my domain model is supposed to only care for the actual event, which is stored within the envelopes data hash.

Now I would like to enforce a schema on the my event. But it seems like RailsEventStore wants me to model a subclass of RailsEventStore::Event (the envelope) for each event type differentiated by my domain model.

Validating the contents of the data hash in a subclass of the envelope feels hacky and dirty. I always need to override the initialize method and pass on stuff that I really don't care about.

class UserRegistered < RailsEventStore::Event
  def initialize(event_id: SecureRandom.uuid, metadata: nil, data: {})
    ensure_valid!(data)

    super(event_id: event_id, metadata: metadata, data: data)
  end

  def ensure_valid!(data)
    raise ArgumentError unless data[:user_id]
  end
end

Also accessing all the events attributes through event.data[:my_attribute] doesn't feel nice. Adding lots of delegation methods for each envelope subclass seems to be a waste as well.

I would rather like to model a plain Ruby object or use something like Dry::Struct to enforce a schema on the content attributes of my event.

class UserRegistered < Dry::Struct
  attribute :user_id, Dry::Types::String
end

Am I missing something here? How can I validate my events in a clean way?

Upvotes: 1

Views: 539

Answers (1)

Pawel Pacana
Pawel Pacana

Reputation: 281

An example of Dry::Struct schema can be found in sample app repository:

https://github.com/RailsEventStore/cqrs-es-sample-with-res/blob/e4983433bc5d71252da58a23da9374f17e1d5cb3/ordering/lib/ordering/order_submitted.rb + https://github.com/RailsEventStore/cqrs-es-sample-with-res/blob/e4983433bc5d71252da58a23da9374f17e1d5cb3/ordering/lib/ordering/order_submitted.rb

While inheriting from RailsEventStore::Event is the simplest, it is not a requirement. A good example of that can be Protobuf integration: https://railseventstore.org/docs/protobuf/#defining-events. It resembles an event envelope even more.

An event passed to RES is required to respond at least to event_id, data, metadata and type: https://github.com/RailsEventStore/rails_event_store/blob/cfc91c9cb367e514ba1c6de1a711a7610780b520/ruby_event_store/lib/ruby_event_store/mappers/transformation/domain_event.rb#L9-L12. You can also see there how it is instantiated when loaded from the store.

This behaviour can be altered when you provide your own mapper or just replace RubyEventStore::Mappers::Transformation::DomainEvent responsible for that transformation.

Please compare: https://github.com/RailsEventStore/rails_event_store/blob/cfc91c9cb367e514ba1c6de1a711a7610780b520/ruby_event_store/lib/ruby_event_store/mappers/default.rb

vs. https://github.com/RailsEventStore/rails_event_store/blob/cfc91c9cb367e514ba1c6de1a711a7610780b520/ruby_event_store/lib/ruby_event_store/mappers/protobuf.rb

Upvotes: 3

Related Questions