From 85cbf65ce84e024132088661d6fa94def15602c2 Mon Sep 17 00:00:00 2001 From: Lukas Hlavacka Date: Tue, 19 Nov 2024 16:09:42 -0500 Subject: [PATCH] feat: validate entity and event aggregate_id value and type --- CHANGELOG.md | 3 +++ lib/eventsimple/entity.rb | 15 +++++++++++ lib/eventsimple/event.rb | 12 +++++++++ spec/lib/eventsimple/entity_spec.rb | 39 +++++++++++++++++++++++++++++ spec/lib/eventsimple/event_spec.rb | 39 +++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c8c796c5..787a245b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## 1.5.5 - 2024-12-23 +- Validate value and type of `aggregate_id` between Event and Entity + ## 1.5.4 - 2024-12-05 ### Changed - Rails 8.0 is supported diff --git a/lib/eventsimple/entity.rb b/lib/eventsimple/entity.rb index 9710795eb..c825b2384 100644 --- a/lib/eventsimple/entity.rb +++ b/lib/eventsimple/entity.rb @@ -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, @@ -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 diff --git a/lib/eventsimple/event.rb b/lib/eventsimple/event.rb index c4d227d98..a71d74d23 100644 --- a/lib/eventsimple/event.rb +++ b/lib/eventsimple/event.rb @@ -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 diff --git a/spec/lib/eventsimple/entity_spec.rb b/spec/lib/eventsimple/entity_spec.rb index 51ada806c..42743f3d2 100644 --- a/spec/lib/eventsimple/entity_spec.rb +++ b/spec/lib/eventsimple/entity_spec.rb @@ -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 OpenStruct.new(type: :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 diff --git a/spec/lib/eventsimple/event_spec.rb b/spec/lib/eventsimple/event_spec.rb index a5268a054..d69d51c07 100644 --- a/spec/lib/eventsimple/event_spec.rb +++ b/spec/lib/eventsimple/event_spec.rb @@ -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 OpenStruct.new(type: :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