Skip to content

Commit

Permalink
feat: validate entity and event aggregate_id value and type (#80)
Browse files Browse the repository at this point in the history
#### Why <!-- A short description of why this change is required -->

Eventsimple requires both event and entity to specify entity column used
as `aggregate_id` in the event. This provides flexibility to use
different attribute than primary key to connect event to the entity
class.

It's possible to accidentally set different value for this attribute.
This does not break Eventsimple behaviour, but it introduces
unpredictable behaviour when reading event history.

It's also possible to accidentally set different type on
`event.aggregate_id` column that does not support entity aggregate value
type. This breaks Eventsimple behaviour as events cannot be inserted
into database.

#### What changed <!-- Summary of changes when modifying hundreds of
lines -->

We added validation that `aggregate_id` argument value and column type
matches between event and entity.

<!--
Consider adding the following sections:

#### How I tested [ Bullets for test cases covered ]
#### Next steps [ If your PR is part of a few or a WIP, give context to
reviewers ]
#### Screenshot [ An image is worth a thousand words ]
#### Bug/Ticket tracker [ Unnecessary when prefixing branch with JIRA
ticket, e.g. SECURITY-123-human-readable-thing ]
-->
  • Loading branch information
lukashlavacka authored Jan 2, 2025
1 parent c13dfc5 commit e006a61
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

## 1.5.5 - 2024-12-23
### Changed
- Validate value and type of `aggregate_id` between Event and Entity

## 1.5.4 - 2024-12-05
### Changed
- Rails 8.0 is supported
Expand Down
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
eventsimple (1.5.4)
eventsimple (1.5.5)
concurrent-ruby (>= 1.2.3)
dry-struct (~> 1.6)
dry-types (~> 1.7)
Expand Down Expand Up @@ -185,7 +185,7 @@ GEM
mini_mime (1.1.5)
minitest (5.25.4)
nenv (0.3.0)
net-imap (0.5.1)
net-imap (0.5.4)
date
net-protocol
net-pop (0.1.2)
Expand Down Expand Up @@ -336,7 +336,7 @@ GEM
rubocop-rails (~> 2.26.0)
stringio (3.1.2)
thor (1.3.2)
timeout (0.4.2)
timeout (0.4.3)
treetop (1.6.12)
polyglot (~> 0.3)
tzinfo (2.0.6)
Expand Down
15 changes: 15 additions & 0 deletions lib/eventsimple/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ module Entity
DEFAULT_IGNORE_PROPS = %w[id lock_version].freeze

def event_driven_by(event_klass, aggregate_id:, filter_attributes: [])
if defined?(event_klass._aggregate_id)
raise ArgumentError, "aggregate_id mismatch event:#{event_klass._aggregate_id} entity:#{aggregate_id}" if aggregate_id != event_klass._aggregate_id

begin
aggregate_column_type_in_event = event_klass.column_for_attribute(:aggregate_id).type
aggregate_column_type_in_entity = column_for_attribute(aggregate_id).type

raise ArgumentError, "column type mismatch - event:#{aggregate_column_type_in_event} entity:#{aggregate_column_type_in_entity}" if aggregate_column_type_in_event != aggregate_column_type_in_entity
rescue ActiveRecord::NoDatabaseError
end
end

has_many :events, class_name: event_klass.name.to_s,
foreign_key: :aggregate_id,
primary_key: aggregate_id,
Expand All @@ -18,6 +30,9 @@ def event_driven_by(event_klass, aggregate_id:, filter_attributes: [])
class_attribute :_filter_attributes
self._filter_attributes = [aggregate_id] | Array.wrap(filter_attributes)

class_attribute :_aggregate_id
self._aggregate_id = aggregate_id

# disable automatic timestamp updates
self.record_timestamps = false

Expand Down
12 changes: 12 additions & 0 deletions lib/eventsimple/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ module Event

# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def drives_events_for(aggregate_klass, aggregate_id:, events_namespace: nil)
if defined?(aggregate_klass._aggregate_id)
raise ArgumentError, "aggregate_id mismatch event:#{aggregate_id} entity:#{aggregate_klass._aggregate_id}" if aggregate_id != aggregate_klass._aggregate_id

begin
aggregate_column_type_in_event = aggregate_klass.column_for_attribute(aggregate_klass._aggregate_id).type unless aggregate_klass.attribute_names.blank?
aggregate_column_type_in_entity = column_for_attribute(:aggregate_id).type unless aggregate_klass.attribute_names.blank?

raise ArgumentError, "column type mismatch - event:#{aggregate_column_type_in_event} entity:#{aggregate_column_type_in_entity}" if aggregate_column_type_in_event != aggregate_column_type_in_entity
rescue ActiveRecord::NoDatabaseError
end
end

class_attribute :_events_namespace
self._events_namespace = events_namespace

Expand Down
2 changes: 1 addition & 1 deletion lib/eventsimple/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Eventsimple
VERSION = '1.5.4'
VERSION = '1.5.5'
end
39 changes: 39 additions & 0 deletions spec/lib/eventsimple/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,45 @@ module Eventsimple
)
end

describe '.event_driven_by' do
context 'when aggregate_id value mismatch between entity and event' do
let(:user_class) do
Class.new(ApplicationRecord) do
extend Eventsimple::Entity

event_driven_by UserEvent, aggregate_id: :id
end
end

it 'raises argument error' do
expect { user_class }.to(raise_error(ArgumentError, 'aggregate_id mismatch event:canonical_id entity:id'))
end
end

context 'when aggregate_id column type mismatch between entity and event' do
let(:user_class) do
Class.new(ApplicationRecord) do
def self.name
'User'
end

def self.column_for_attribute(column_name)
return Struct.new(:type).new(:int) if column_name == :canonical_id
super
end

extend Eventsimple::Entity

event_driven_by UserEvent, aggregate_id: :canonical_id
end
end

it 'raises argument error' do
expect { user_class }.to(raise_error(ArgumentError, 'column type mismatch - event:string entity:int'))
end
end
end

describe '#projection_matches_events?' do
it 'returns false if the entity no longer matches state from events' do
expect(user.projection_matches_events?).to be true
Expand Down
39 changes: 39 additions & 0 deletions spec/lib/eventsimple/event_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,43 @@ def self.uses_transaction?(_method) = true
end
end
end

describe '.event_driven_by' do
context 'when aggregate_id mismatch between entity and event' do
let(:event_class) do
Class.new(ApplicationRecord) do
extend Eventsimple::Event

drives_events_for User, aggregate_id: :id, events_namespace: 'UserComponent::Events'
end
end

it 'raises argument error' do
expect { event_class }.to(raise_error(ArgumentError, 'aggregate_id mismatch event:id entity:canonical_id'))
end
end

context 'when aggregate_id column type mismatch between entity and event' do
let(:event_class) do
Class.new(ApplicationRecord) do
def self.name
'UserEvent'
end

def self.column_for_attribute(column_name)
return Struct.new(:type).new(:int) if column_name == :aggregate_id
super
end

extend Eventsimple::Event

drives_events_for User, aggregate_id: :canonical_id, events_namespace: 'UserComponent::Events'
end
end

it 'raises argument error' do
expect { event_class }.to(raise_error(ArgumentError, 'column type mismatch - event:string entity:int'))
end
end
end
end

0 comments on commit e006a61

Please sign in to comment.