eventsimple 2.3.0 → 2.5.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: 394c4820e133907b2e63fe826b7891ab2cd0fbc1f436683b667ac9cfee73e57a
4
- data.tar.gz: 70508717fa8b8a1b2528a38044648c3748b63ffd721c606ec66fdf959e54b53a
3
+ metadata.gz: c49589e4a0e45a9149f99568927f76b0d17b1e6b22f73bb079edd1b27ea856cd
4
+ data.tar.gz: 95794663749d9759b1ddbb6867c6774ffc8df60a30449c408958fb68f7160dc3
5
5
  SHA512:
6
- metadata.gz: 1150de14bcfc4580f7b2be46a406d602a29bcecb7b75aabd136215e8893fc8b4cc98fbfc29082cc1a7ac30cb7235e7ac81206e05532cdf260c74d8e68bf10176
7
- data.tar.gz: 4d47af195c91593c5424c0f9a5316cd35e3c4aa9b135cbdb894fa86bcef72d899b1e6f1187afe063c627dbeb787d9bb0dfc310fc8de60ad0bbbf7d48cbbf4a60
6
+ metadata.gz: c134a1dfdf5e16e888b2c9018756819417205f924609b095001cbd2802e1c97e27f0268b93c6eada4c036162cf4086b33c72127c2539d9fcd085cca7ca11b24f
7
+ data.tar.gz: d8edcf526493068375fc706d04180f206c73fcb4fc57848e1fc6c5cabfcb09690235f3550a5f7b6ac67f230e94deb56f6a5a91097a43476660f8e5b4aabfb84f
data/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 2.5.0 - 2026-04-26
10
+ ### Added
11
+ - 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.
12
+ - `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.
13
+
14
+ ### Removed
15
+ - Model instance `#enable_writes!` — calling it raises `Eventsimple::DeprecatedInstanceEnableWrites`. Use `Eventsimple.enable_writes! { ... }` instead.
16
+
17
+ ## 2.4.0 - 2026-04-25
18
+ ### Added
19
+ - Collapsible JSON viewer for Hash and Array values in the admin UI.
20
+
9
21
  ## 2.3.0 - 2026-02-24
10
22
  ### Added
11
23
  - Per-event metadata class: pass `metadata_class:` to `drives_events_for` to use a custom metadata class for that event (Class or String).
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- eventsimple (2.3.0)
4
+ eventsimple (2.5.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.3.0)
461
+ eventsimple (2.5.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
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eventsimple
4
+ module EntitiesHelper
5
+ def render_value(value)
6
+ case value
7
+ when nil
8
+ content_tag(:code, '-', class: 'entity-property')
9
+ when Hash, Array
10
+ content_tag(:div, '', class: 'json-viewer-target', data: { json: value.to_json })
11
+ else
12
+ content_tag(:code, value.to_s, class: 'entity-property')
13
+ end
14
+ end
15
+ end
16
+ end
@@ -32,8 +32,8 @@
32
32
  <% @entity_changes.each do |property| %>
33
33
  <tr class="<%= property[:is_changed] ? 'table-info' : '' %>">
34
34
  <th scope="row"><%= property[:label] %></th>
35
- <td><code class="entity-property"><%= property[:historical_value].nil? ? '-' : property[:historical_value] %></code></td>
36
- <td><code class="entity-property"><%= property[:current_value].nil? ? '-' : property[:current_value] %></code></td>
35
+ <td><%= render_value(property[:historical_value]) %></td>
36
+ <td><%= render_value(property[:current_value]) %></td>
37
37
  </tr>
38
38
  <% end %>
39
39
  </tbody>
@@ -64,7 +64,7 @@
64
64
  <% @selected_event.data.to_h.each do |attr_name, attr_value| %>
65
65
  <tr>
66
66
  <td>&nbsp;&nbsp;&nbsp;&nbsp;<%= attr_name %></td>
67
- <td><code class="entity-property"><%= attr_value %></code></td>
67
+ <td><%= render_value(attr_value) %></td>
68
68
  </tr>
69
69
  <% end %>
70
70
  <% end %>
@@ -75,7 +75,7 @@
75
75
  <% @selected_event.metadata.to_h.each do |attr_name, attr_value| %>
76
76
  <tr>
77
77
  <td>&nbsp;&nbsp;&nbsp;&nbsp;<%= attr_name %></td>
78
- <td><code class="entity-property">: <%= attr_value %></code></td>
78
+ <td><%= render_value(attr_value) %></td>
79
79
  </tr>
80
80
  <% end %>
81
81
  </tbody>
@@ -25,6 +25,15 @@
25
25
  integrity: "sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=",
26
26
  crossorigin: "anonymous"
27
27
  %>
28
+ <%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/jquery.json-viewer@1.5.0/json-viewer/jquery.json-viewer.css",
29
+ media: "all",
30
+ integrity: "sha256-rXfxviikI1RGZM3px6piq9ZL0YZuO5ETcO8+toY+DDY=",
31
+ crossorigin: "anonymous"
32
+ %>
33
+ <%= javascript_include_tag "https://cdn.jsdelivr.net/npm/jquery.json-viewer@1.5.0/json-viewer/jquery.json-viewer.js",
34
+ integrity: "sha256-Tbq6mI1/ZeudUckwCGhWMlV8S/Ae9cQRIf/yBDVg0Ek=",
35
+ crossorigin: "anonymous"
36
+ %>
28
37
 
29
38
  <%= render partial: 'eventsimple/shared/style' %>
30
39
  </head>
@@ -63,6 +72,10 @@
63
72
  query.set(param, value);
64
73
  link.attr("href",window.location.pathname + '?' + query.toString());
65
74
  });
75
+
76
+ $('.json-viewer-target').each(function () {
77
+ $(this).jsonViewer($(this).data('json'), { collapsed: true, withQuotes: true, withLinks: false });
78
+ });
66
79
  });
67
80
  </script>
68
81
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Eventsimple
4
- VERSION = '2.3.0'
4
+ VERSION = '2.5.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.3.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zulfiqar Ali
@@ -330,6 +330,7 @@ files:
330
330
  - app/controllers/eventsimple/entities_controller.rb
331
331
  - app/controllers/eventsimple/home_controller.rb
332
332
  - app/controllers/eventsimple/models_controller.rb
333
+ - app/helpers/eventsimple/entities_helper.rb
333
334
  - app/views/eventsimple/entities/show.html.erb
334
335
  - app/views/eventsimple/home/index.html.erb
335
336
  - app/views/eventsimple/models/show.html.erb
@@ -362,6 +363,7 @@ files:
362
363
  - lib/eventsimple/outbox/models/cursor.rb
363
364
  - lib/eventsimple/reactor.rb
364
365
  - lib/eventsimple/reactor_worker.rb
366
+ - lib/eventsimple/readonly.rb
365
367
  - lib/eventsimple/support/spec_helpers.rb
366
368
  - lib/eventsimple/types.rb
367
369
  - lib/eventsimple/types/encrypted_type.rb