eventsimple 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 552ecc441120517eee35f12c6ebef012611c7565730b4c0cdf180f2e58ab1d05
4
- data.tar.gz: c5e55ce426acbf14f25dddcbc861a3eb8a3fac2635ce6d706dd97e90eb4151db
3
+ metadata.gz: 64f15c989c426a973f8d1689e098c0ea7947af521742bb8e70badf2b705ed162
4
+ data.tar.gz: 48de4621966b60aa7328e4eb51c383d2cb65a5862ce8301d9d754eca6cbc0d3e
5
5
  SHA512:
6
- metadata.gz: dbeb3130ffe536969366bdfe22cb193310d045eac757eee98c894d7944f7ff0b31d6be889a068030432ff51d4adc6a50ff56c026c4b78f48f65e1e9356ff6967
7
- data.tar.gz: a35d64be339e546d3dcbb48ff5f27d0385bca49d119d3b054ab970ae91458e76a42160871b252deab2ebaf2d65475052085fb9f9487132f4381f5c8139ad254f
6
+ metadata.gz: 6c0a3945fd3e5dc70798933b0058d91c79cea258ecbb895917e6f93a9d6e3cf404bd356f36332d8e072b2dcb7a4bdbb32b560696af4266e592bf34e115200904
7
+ data.tar.gz: d1f53a2cbdbe9f3c951368d52300db2e3fc692f2b9e76d39c179d8f37675e5ff6ff7735643e480b1b68c748bdc2cd8a6c0a2cbbd9e330827f2078b3b253ab5d4
data/CHANGELOG.md CHANGED
@@ -6,6 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 2.6.0 - 2026-05-01
10
+ ### Added
11
+ - `Eventsimple::Types::*.encrypted` now accepts an optional
12
+ `key_provider:` keyword. When supplied, encryption/decryption goes
13
+ through that `ActiveRecord::Encryption::KeyProvider` instead of the
14
+ default keyed off `config.active_record.encryption.primary_key`.
15
+ Existing call sites (no `key_provider:`) keep using the global
16
+ config. Lets a single Rails app encrypt different event attributes
17
+ against different `KeyProvider`s — e.g. when collapsing a
18
+ microservice that used its own primary key (and possibly its own
19
+ derivation salt) into a host app whose globals are already used for
20
+ its own encrypted events.
21
+
22
+ ## 2.5.0 - 2026-04-26
23
+ ### Added
24
+ - Entities and events now raise `ActiveRecord::ReadOnlyRecord` when persistence methods that bypass ActiveRecord's built-in readonly check are invoked (`delete`, `update_column(s)`, `update_attribute(!)`, `touch`, `toggle!`, `increment!`, `decrement!`). Relation-level bulk SQL (`insert` / `insert_all` / `upsert` / `upsert_all` / `update_all` / `delete_all` / `touch_all`) is guarded the same way.
25
+ - `Eventsimple.enable_writes! { }` is the single API for opting in to writes (instance saves/updates and relation bulk SQL). While active, `readonly?` is bypassed for event-sourced models so `save`/`update`/`destroy` work inside the block.
26
+
27
+ ### Removed
28
+ - Model instance `#enable_writes!` — calling it raises `Eventsimple::DeprecatedInstanceEnableWrites`. Use `Eventsimple.enable_writes! { ... }` instead.
29
+
9
30
  ## 2.4.0 - 2026-04-25
10
31
  ### Added
11
32
  - Collapsible JSON viewer for Hash and Array values in the admin UI.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- eventsimple (2.4.0)
4
+ eventsimple (2.6.0)
5
5
  concurrent-ruby (>= 1.2.3)
6
6
  dry-struct (>= 1.8.0)
7
7
  dry-types (>= 1.7.0)
@@ -458,7 +458,7 @@ CHECKSUMS
458
458
  dry-types (1.9.0) sha256=7b656fe0a78d2432500ae1f29fefd6762f5a032ca7000e4f36bc111453d45d4d
459
459
  erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
460
460
  erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
461
- eventsimple (2.4.0)
461
+ eventsimple (2.6.0)
462
462
  factory_bot (6.5.6) sha256=12beb373214dccc086a7a63763d6718c49769d5606f0501e0a4442676917e077
463
463
  factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68
464
464
  ffi (1.17.3-aarch64-linux-gnu) sha256=28ad573df26560f0aedd8a90c3371279a0b2bd0b4e834b16a2baa10bd7a97068
@@ -25,7 +25,7 @@ module Eventsimple
25
25
  has_many :events, class_name: event_klass.name.to_s,
26
26
  foreign_key: :aggregate_id,
27
27
  primary_key: aggregate_id,
28
- dependent: :delete_all,
28
+ dependent: :restrict_with_exception,
29
29
  inverse_of: model_name.element.to_sym,
30
30
  autosave: false,
31
31
  validate: false
@@ -45,8 +45,11 @@ module Eventsimple
45
45
 
46
46
  Eventsimple.configuration.ui_visible_model_names |= [name]
47
47
 
48
+ include Readonly
48
49
  include InstanceMethods
49
50
  extend ClassMethods
51
+
52
+ Readonly.install_relation_guards!(self)
50
53
  end
51
54
 
52
55
  module InstanceMethods
@@ -56,16 +59,6 @@ module Eventsimple
56
59
  attributes == reprojected.attributes
57
60
  end
58
61
 
59
- def enable_writes!(&block)
60
- was_readonly = @readonly
61
- @readonly = false
62
-
63
- return unless block
64
-
65
- yield self
66
- @readonly = was_readonly
67
- end
68
-
69
62
  def reproject(at: nil, skip_deleted: false)
70
63
  event_history = at ? events.where('created_at <= ?', at).load : events.load
71
64
 
@@ -75,8 +75,11 @@ module Eventsimple
75
75
  after_create :dispatch
76
76
  after_create :readonly!
77
77
 
78
+ include Readonly
78
79
  include InstanceMethods
79
80
  extend ClassMethods
81
+
82
+ Readonly.install_relation_guards!(self)
80
83
  end
81
84
 
82
85
  module InstanceMethods
@@ -123,8 +126,9 @@ module Eventsimple
123
126
  apply_timestamps(aggregate)
124
127
  apply(aggregate)
125
128
 
126
- aggregate.enable_writes!
127
- aggregate.save!
129
+ Eventsimple.enable_writes! do
130
+ aggregate.save!
131
+ end
128
132
  aggregate.readonly!
129
133
 
130
134
  self.aggregate = aggregate
@@ -141,16 +145,6 @@ module Eventsimple
141
145
  def aggregate=(aggregate)
142
146
  public_send(:"#{_aggregate_klass.model_name.element}=", aggregate)
143
147
  end
144
-
145
- def enable_writes!(&block)
146
- was_readonly = @readonly
147
- @readonly = false
148
-
149
- return unless block
150
-
151
- yield self
152
- @readonly = was_readonly
153
- end
154
148
  end
155
149
 
156
150
  module ClassMethods
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eventsimple
4
+ # Raised when +#enable_writes!+ is called on a model instance. Use
5
+ # {Eventsimple.enable_writes!} instead.
6
+ DeprecatedInstanceEnableWrites = Class.new(StandardError)
7
+
8
+ # Closes gaps where writes bypass +readonly!+ (instance helpers like +delete+,
9
+ # +update_column+, relation SQL like +upsert+ / +delete_all+ / +update_all+).
10
+ # Raises +ActiveRecord::ReadOnlyRecord+ unless {Eventsimple.enable_writes!} is active.
11
+ module Readonly
12
+ THREAD_KEY = :eventsimple_class_writes_depth
13
+
14
+ module RelationMethods
15
+ RELATION_WRITE_METHODS = %i[
16
+ insert insert!
17
+ insert_all insert_all!
18
+ upsert upsert_all
19
+ update_all delete_all touch_all
20
+ ].freeze
21
+
22
+ RELATION_WRITE_METHODS.each do |method_name|
23
+ define_method(method_name) do |*args, **kwargs, &block|
24
+ _raise_relation_readonly! unless Readonly.class_writes_enabled?
25
+ super(*args, **kwargs, &block)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def _raise_relation_readonly!
32
+ raise ActiveRecord::ReadOnlyRecord, "#{model.name} is marked as readonly"
33
+ end
34
+ end
35
+
36
+ class << self
37
+ def install_relation_guards!(model_class)
38
+ ActiveRecord::Delegation.delegated_classes.each do |relation_base|
39
+ delegate_class = model_class.relation_delegate_class(relation_base)
40
+ next if delegate_class.ancestors.include?(RelationMethods)
41
+
42
+ delegate_class.prepend(RelationMethods)
43
+ end
44
+ end
45
+
46
+ def enable_writes!
47
+ Thread.current[THREAD_KEY] = Thread.current[THREAD_KEY].to_i + 1
48
+ yield
49
+ ensure
50
+ d = Thread.current[THREAD_KEY].to_i - 1
51
+ Thread.current[THREAD_KEY] = d.positive? ? d : nil
52
+ end
53
+
54
+ def class_writes_enabled?
55
+ Thread.current[THREAD_KEY].to_i.positive?
56
+ end
57
+ end
58
+
59
+ def readonly?
60
+ return false if Readonly.class_writes_enabled?
61
+
62
+ super
63
+ end
64
+
65
+ def enable_writes!(&_block)
66
+ raise DeprecatedInstanceEnableWrites, <<~MSG.squish
67
+ Use Eventsimple.enable_writes! { ... } instead of #{self.class.name}#enable_writes!
68
+ MSG
69
+ end
70
+
71
+ def delete
72
+ _raise_readonly_record_error if readonly?
73
+ super
74
+ end
75
+
76
+ def update_column(...)
77
+ _raise_readonly_record_error if readonly?
78
+ super
79
+ end
80
+
81
+ def update_columns(...)
82
+ _raise_readonly_record_error if readonly?
83
+ super
84
+ end
85
+
86
+ def update_attribute(...)
87
+ _raise_readonly_record_error if readonly?
88
+ super
89
+ end
90
+
91
+ def update_attribute!(...)
92
+ _raise_readonly_record_error if readonly?
93
+ super
94
+ end
95
+
96
+ def increment!(...)
97
+ _raise_readonly_record_error if readonly?
98
+ super
99
+ end
100
+
101
+ def decrement!(...)
102
+ _raise_readonly_record_error if readonly?
103
+ super
104
+ end
105
+
106
+ def toggle!(...)
107
+ _raise_readonly_record_error if readonly?
108
+ super
109
+ end
110
+
111
+ def touch(...)
112
+ _raise_readonly_record_error if readonly?
113
+ super
114
+ end
115
+ end
116
+ end
@@ -9,16 +9,17 @@ module Eventsimple
9
9
  include Dry::Types::Decorator
10
10
  include Dry::Types::Builder
11
11
 
12
- def initialize(type, **options)
12
+ def initialize(type, key_provider: nil, **options)
13
13
  super
14
14
  @type = type
15
+ @key_provider = key_provider
15
16
  freeze
16
17
  end
17
18
 
18
19
  def meta(data = nil)
19
20
  return { eventsimple: true } if data.nil?
20
21
 
21
- self.class.new(@type.meta(data))
22
+ self.class.new(@type.meta(data), key_provider: @key_provider)
22
23
  end
23
24
 
24
25
  def encrypt(value)
@@ -26,9 +27,13 @@ module Eventsimple
26
27
  return value if value == '' || (value.is_a?(::String) && value.blank?)
27
28
 
28
29
  string_value = serialize_value(value)
29
- return string_value if ActiveRecord::Encryption::Encryptor.new.encrypted?(string_value)
30
+ return string_value if encryptor.encrypted?(string_value)
30
31
 
31
- ActiveRecord::Encryption::Encryptor.new.encrypt(string_value)
32
+ if @key_provider
33
+ encryptor.encrypt(string_value, key_provider: @key_provider)
34
+ else
35
+ encryptor.encrypt(string_value)
36
+ end
32
37
  end
33
38
 
34
39
  def decrypt(value)
@@ -36,7 +41,11 @@ module Eventsimple
36
41
  return value if value == '' || (value.is_a?(::String) && value.blank?)
37
42
 
38
43
  decrypted = begin
39
- ActiveRecord::Encryption::Encryptor.new.decrypt(value)
44
+ if @key_provider
45
+ encryptor.decrypt(value, key_provider: @key_provider)
46
+ else
47
+ encryptor.decrypt(value)
48
+ end
40
49
  rescue StandardError
41
50
  return value # Return original if decryption fails
42
51
  end
@@ -46,6 +55,10 @@ module Eventsimple
46
55
 
47
56
  private
48
57
 
58
+ def encryptor
59
+ ActiveRecord::Encryption::Encryptor.new
60
+ end
61
+
49
62
  def serialize_value(value)
50
63
  case value
51
64
  when Array, Hash
@@ -26,12 +26,12 @@ module Eventsimple
26
26
  end
27
27
  end
28
28
 
29
- def encrypted
29
+ def encrypted(key_provider: nil)
30
30
  if respond_to?(:name) && name.to_s.include?('TrueClass') && name.to_s.include?('FalseClass')
31
31
  raise ArgumentError, 'Bool type does not support encryption'
32
32
  end
33
33
 
34
- EncryptedType.new(self)
34
+ EncryptedType.new(self, key_provider: key_provider)
35
35
  end
36
36
  end
37
37
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Eventsimple
4
- VERSION = '2.4.0'
4
+ VERSION = '2.6.0'
5
5
  end
data/lib/eventsimple.rb CHANGED
@@ -21,6 +21,7 @@ require 'eventsimple/event_dispatcher'
21
21
  require 'eventsimple/reactor'
22
22
  require 'eventsimple/invalid_transition'
23
23
 
24
+ require 'eventsimple/readonly'
24
25
  require 'eventsimple/entity'
25
26
  require 'eventsimple/event'
26
27
 
@@ -36,5 +37,11 @@ module Eventsimple
36
37
  def configure
37
38
  yield(configuration)
38
39
  end
40
+
41
+ # Unlocks writes for event-sourced models for the duration of the block (instance +save+,
42
+ # relation +update_all+, +upsert+, +delete_all+, etc.).
43
+ def enable_writes!(&block)
44
+ Readonly.enable_writes!(&block)
45
+ end
39
46
  end
40
47
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eventsimple
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zulfiqar Ali
@@ -363,6 +363,7 @@ files:
363
363
  - lib/eventsimple/outbox/models/cursor.rb
364
364
  - lib/eventsimple/reactor.rb
365
365
  - lib/eventsimple/reactor_worker.rb
366
+ - lib/eventsimple/readonly.rb
366
367
  - lib/eventsimple/support/spec_helpers.rb
367
368
  - lib/eventsimple/types.rb
368
369
  - lib/eventsimple/types/encrypted_type.rb