From 503d116974e6c53deffe2e6a46667a7e2b1a806f 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 | 4 +++ Gemfile.lock | 14 ++++++----- lib/eventsimple/entity.rb | 15 +++++++++++ lib/eventsimple/event.rb | 12 +++++++++ lib/eventsimple/version.rb | 2 +- spec/lib/eventsimple/entity_spec.rb | 39 +++++++++++++++++++++++++++++ spec/lib/eventsimple/event_spec.rb | 39 +++++++++++++++++++++++++++++ 7 files changed, 118 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a3a66f16..651a73196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## 1.5.4 - 2024-09-09 +### Changed +- Validate value and type of `aggregate_id` between Event and Entity + ## 1.5.3 - 2024-09-09 ### Changed - Pass self to `enable_writes!` block diff --git a/Gemfile.lock b/Gemfile.lock index 14b8af336..14557e30d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - eventsimple (1.5.3) + eventsimple (1.5.4) concurrent-ruby (>= 1.2.3) dry-struct (~> 1.6) dry-types (~> 1.7) @@ -102,12 +102,13 @@ GEM concurrent-ruby (1.2.3) connection_pool (2.4.1) crass (1.0.6) - date (3.3.4) + date (3.4.0) diff-lcs (1.5.1) drb (2.2.0) ruby2_keywords - dry-core (1.0.1) + dry-core (1.0.2) concurrent-ruby (~> 1.0) + logger zeitwerk (~> 2.6) dry-inflector (1.1.0) dry-logic (1.5.0) @@ -169,6 +170,7 @@ GEM listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.1) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -184,7 +186,7 @@ GEM minitest (5.22.2) mutex_m (0.2.0) nenv (0.3.0) - net-imap (0.4.16) + net-imap (0.5.1) date net-protocol net-pop (0.1.2) @@ -207,7 +209,7 @@ GEM parser (3.3.0.5) ast (~> 2.4.1) racc - pg (1.5.8) + pg (1.5.9) polyglot (0.3.5) pry (0.14.2) coderay (~> 1.1) @@ -342,7 +344,7 @@ GEM stringio (3.1.0) strscan (3.1.0) thor (1.3.1) - timeout (0.4.1) + timeout (0.4.2) treetop (1.6.12) polyglot (~> 0.3) tzinfo (2.0.6) 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 4c47fb9c4..9b172fbf7 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/lib/eventsimple/version.rb b/lib/eventsimple/version.rb index 02fad3d13..6054cabcb 100644 --- a/lib/eventsimple/version.rb +++ b/lib/eventsimple/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Eventsimple - VERSION = '1.5.3' + VERSION = '1.5.4' end 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