eventsimple 1.8.1 → 2.0.1

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.
data/README.md CHANGED
@@ -2,555 +2,66 @@
2
2
  [![Github Actions](https://github.com/wealthsimple/eventsimple/actions/workflows/default.yml/badge.svg)](https://github.com/wealthsimple/eventsimple/actions/workflows/default.yml) [![Gem Version](https://badge.fury.io/rb/eventsimple.svg?v=1)](https://rubygems.org/gems/eventsimple)
3
3
 
4
4
  ## What
5
- Eventsimple implements a simple deterministic event driven system using ActiveRecord and ActiveJob.
5
+ Eventsimple implements a simple deterministic event driven system using ActiveRecord and ActiveJob
6
6
 
7
7
  Use Eventsimple to:
8
8
 
9
9
  * Add Event Sourcing to your ActiveRecord models.
10
- * Implement Pub/Sub.
10
+ * Pub/Sub.
11
11
  * Implement a transactional outbox.
12
- * Store audit logs of changes to your ActiveRecord objects.
12
+ * Store audit logs.
13
13
 
14
- Eventsimple uses standard Rails features like [Single Table Inheritance](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html) and [Optimistic Locking](https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html).
15
- Async workflows are handled using [ActiveJob](https://guides.rubyonrails.org/active_job_basics.html).
16
-
17
- Typical events in Eventsimple are ActiveRecord models that look like this:
18
-
19
- ```ruby
20
- <UserComponent::Events::Created
21
- id: 1,
22
- aggregate_id: 'user-123',
23
- type: "Created",
24
- data: {
25
- name: "John doe",
26
- email: "johndoe@example.com",
27
- },
28
- created_at: 2022-01-01T00:00:00.000000,
29
- updated_at: 2022-01-01T00:00:00.000000,
30
- >
31
-
32
- <UserComponent::Events::Deleted
33
- id: 1,
34
- aggregate_id: 'user-123',
35
- type: "Deleted",
36
- created_at: 2022-01-01T00:30:00.000000,
37
- updated_at: 2022-01-01T00:30:00.000000,
38
- >
39
- ```
40
-
41
- ## Setup
42
-
43
- Add the following line to your Gemfile and run `bundle install`:
44
-
45
- ```
46
- gem 'eventsimple'
47
- ```
48
-
49
- The eventsimple UI allows you to view and navigate event history. Add the following line to your routes.rb:
50
-
51
- ```
52
- mount Eventsimple::Engine => '/eventsimple'
53
- ```
54
-
55
- Setup an initializer in `config/initializers/eventsimple.rb`:
56
-
57
- ```ruby
58
- Eventsimple.configure do |config|
59
- # Optional: Register your dispatch classes here.
60
- # Dispatch classes are used to register reactors to events.
61
- # Reactors are used to implement side effects.
62
- # See the Reactors section below for more details.
63
- config.dispatchers = []
64
-
65
- # Optional: Entity updates use optimistic locking to enforce sequential updates.
66
- # Set the max number of times to retry on concurrency failures.
67
- # Defaults to 2
68
- config.max_concurrency_retries = 2
69
-
70
- # Optional: the metadata column is used to store optional metadata associated with the event.
71
- # The default implemention enforces a typed constraint on the metadata column
72
- # with the following two properties: `actor_id` and `reason`
73
- # Use a custom metadata class to override this behaviour.
74
- # Defaults to `Eventsimple::Metadata`
75
- config.metadata_klass = 'Eventsimple::Metadata'
76
-
77
- # Optional: When using an ActiveJob adapter that writes to a different data store like redis,
78
- # it is possible that the reactor is executed before the transaction persisting the event is committed. This can result in noisy errors when using processors like Sidekiq.
79
- # Enable this option to retry the reactor inline if the event is not found.
80
- # Defaults to false.
81
- config.retry_reactor_on_record_not_found = true
82
- end
83
- ```
84
-
85
- If using `Sidekiq` as a backend to `ActiveJob` for async reactors, please add this setting to
86
- `config/application.rb`:
87
- ```ruby
88
- config.active_job.queue_adapter = :sidekiq
89
- ```
90
- The jobs are pushed into a queue named `eventsimple`, so please add it to your
91
- `sidekiq.yml` as follows:
92
- ```yml
93
- :queues:
94
- - [default, 10]
95
- - [eventsimple, 10]
96
- ```
97
-
98
- Generate a migration and add `Eventsimple` to an existing ActiveRecord model.
99
-
100
- ```ruby
101
- bundle exec rails generate eventsimple:event User
102
- ```
103
-
104
- This will result in the following changes:
105
-
106
- ```ruby
107
- # ActiveRecord Classes
108
- class User < ApplicationRecord
109
- extend Eventsimple::Entity
110
- event_driven_by UserEvent, aggregate_id: :id
111
- end
112
-
113
- class UserEvent < ApplicationRecord
114
- extend Eventsimple::Event
115
- drives_events_for User, events_namespace: 'UserComponent::Events', aggregate_id: :id
116
- end
117
- # Change aggregate_id to the column that represents the unique primary key for your model.
118
-
119
- # Data migration
120
- create_table :user_events do |t|
121
- # Change this to string if your aggregates primary key is a string type
122
- t.bigint :aggregate_id, null: false, index: true
123
- t.string :idempotency_key, null: true
124
- t.string :type, null: false
125
- t.json :data, null: false, default: {}
126
- t.json :metadata, null: false, default: {}
127
-
128
- t.timestamps
129
-
130
- t.index :idempotency_key, unique: true
131
- t.index :created_at
132
- end
133
-
134
- add_column :users, :lock_version, :integer
135
- ```
136
-
137
- Adding lock_version to the model enables [optimistic locking](https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html) and protects against concurrent updates to stale versions of the model. Eventsimple will automatically retry on concurrency failures.
138
-
139
- `events_namespace` is an optional argument pointing to the directory where your events classes are defined. If you do not specify this argument, Eventsimple will store the full namespace of the event classes in the STI column.
140
-
141
- ### Event Table definition
142
-
143
- | Column | Description |
144
- | ------------- | ------------- |
145
- | aggregate_id | Stores the primary key of the entity. |
146
- | idempotency_key | Optional value which can be used to write events that have uniqueness constraints. |
147
- | type | Used by rails to implement Single Table inheritance. Stores the event class name. |
148
- | data | Stores the event payload |
149
- | metadata | Stores optional metadata associated with the event |
150
-
151
- ## Usage
152
-
153
- An example event:
154
-
155
- ```ruby
156
- module UserComponent
157
- module Events
158
- class Created < UserEvent
159
- # Optional: Rails by default will use JSON serialization for the data attribute. Use Eventsimple::DataType to serialize/deserialize the data attribute using the Message subclass below which uses dry-struct.
160
-
161
- class Message < Eventsimple::Message
162
- attribute :canonical_id, DryTypes::Strict::String
163
- attribute :email, DryTypes::Strict::String
164
- end
165
-
166
- # Optional: Context specific validations that can be extended onto the model on event creation.
167
- validates_with UserForm
168
-
169
- # Optional: Implement state machine checks to determine if the event is allowed to be written.
170
- # Will raise Eventsimple::InvalidTransition on failure.
171
- def can_apply?(user)
172
- user.new_record?
173
- end
174
-
175
- # Optional: Update the state of your model based on data in the event payload.
176
- def apply(user)
177
- user.canonical_id = data.canonical_id
178
- user.email = data.email
179
- end
180
- end
181
- end
182
- end
183
- ```
184
-
185
- Write an event:
186
-
187
- ```ruby
188
- user = User.new
189
-
190
- UserComponent::Events::Created.create(
191
- user: user,
192
- data: { canonical_id: 'user-123', email: 'johndoe@example.com' },
193
- metadata: { actor_id: 'user-123' } # optional metadata
194
- )
195
-
196
- if user.errors.any?
197
- # render user errors
198
- else
199
- # render success
200
- end
201
- ```
202
-
203
- ### Using Dry::Struct
204
- The Eventsimple::Message class is a subclass of Dry::Struct. Some common options you can use are:
205
-
206
- ```ruby
207
- class Message < Eventsimple::Message
208
- # attribute key is required and can not be nil
209
- attribute :canonical_id, DryTypes::Strict::String
210
-
211
- # attribute key is required but can be nil
212
- attribute :required_key, DryTypes::Strict::String.optional
213
-
214
- # attribute key is not required and can also be nil
215
- attribute? :optional_key, DryTypes::Strict::String.optional
216
-
217
- # use default value if attribute key is missing or if value is nil
218
- # Note this is not the typical behaviour for dry-struct and is a customization in the Eventsimple::Message class.
219
- attribute :default_key, DryTypes::Strict::String.default('default')
220
- end
221
- ```
222
-
223
- ### Event Reactors
224
-
225
- Callback to events can be defined as reactors in the dispatcher class.
226
- Reactors may be `async` or `sync`, depending on the usecase.
227
-
228
- #### Sync Reactors
229
- Sync reactors are executed within the context of the event transaction block.
230
- They should **only** contain business logic that make additional database writes.
231
-
232
- This is because executing writes to other data stores, e.g API call or writes to kafka/sqs, will result in the transaction being non-deterministic.
233
-
234
- #### Async Reactors
235
- Async reactors are executed via ActiveJob. Eventsimple implements checks to enforce reliable eventually consistent behaviour.
236
-
237
- Use Async reactors to kick off async workflows or writes to external data sources as a side effect of model updates.
238
-
239
- Reactor example:
240
-
241
- ```ruby
242
- # Register your dispatch classes in config/initializers/eventsimple.rb.
243
- Eventsimple.configure do |config|
244
- config.dispatchers = %w[
245
- UserComponent::Dispatcher
246
- ]
247
- end
248
-
249
- # Register reactors in the dispatcher class.
250
- class UserComponent::Dispatcher < Eventsimple::EventDispatcher
251
- # one to one
252
- on UserComponent::Events::Created,
253
- async: UserComponent::Reactors::Created::SendNotification
254
-
255
- # or many to many
256
- on [
257
- UserComponent::Events::Locked,
258
- UserComponent::Events::Unlocked
259
- ], sync: [
260
- UserComponent::Reactors::Locking::UpdateLockCounter,
261
- UserComponent::Reactors::Locking::UpdateLockMetrics
262
- ]
263
- end
264
-
265
- # Reactor classes accept the event as the only argument in the constructor
266
- # and must define a `call` method
267
- module UserComponent::Reactors::Created < Eventsimple::Reactor
268
- class SendNotification
269
- def call(event)
270
- user = event.aggregate
271
- # do something
272
- end
273
- end
274
- end
275
- ```
276
-
277
- ## Configuring an outbox consumer
278
-
279
- For many use cases, async reactors are sufficient to handle workflows like making an API call or publishing to a message broker. However as reactors use ActiveJob, order is not guaranteed. For use cases requiring order, eventsimple provides an simple ordered outbox implementation.
280
-
281
- The current implementation leverages a single advisory lock to guarantee write order. This will impact write throughput on the model. On a db.rg6.large Aurora instance for example, write throughput to the table is ~300 events per second.
282
-
283
- ### Setup an ordered outbox
284
-
285
- Generate migration to setup the outbox cursor table. This table is used to track cursor positions.
286
-
287
- ```ruby
288
- bundle exec rails g eventsimple:outbox:install
289
- ```
290
-
291
- Create a consummer and processor class for the outbox.
14
+ ### Write events to update models
292
15
 
293
16
  ```ruby
294
- require 'eventsimple/outbox/consumer'
295
-
296
- module UserComponent
297
- class Consumer
298
- extend Eventsimple::Outbox::Consumer
299
-
300
- identitfier 'UserComponent::Consumer'
301
- consumes_event UserEvent
302
- processor EventProcessor, concurrency: 5
303
- end
304
- end
305
- ```
306
-
307
- ```ruby
308
- module UserComponent
309
- class EventProcessor
310
- def call(event)
311
- Rails.logger.info("PROCESSING EVENT: #{event.id}")
312
- end
313
- end
314
- end
315
- ```
316
-
317
- ### Usage
318
- Create a rake task to run the consumer
319
-
320
- ```ruby
321
- namespace :consumers do
322
- desc 'Starts the user event outbox consumer'
323
- task :user_events do
324
- UserComponent::Consumer.start
325
- end
17
+ class UserComponent::Events::Created < UserEvent
18
+ class Message < Eventsimple::Message
19
+ attribute :name, Eventsimple::Types::String
20
+ attribute :email, Eventsimple::Types::String
326
21
  end
327
- ```
328
-
329
- To set the cursor position to the latest event:
330
-
331
- ```ruby
332
- Eventsimple::Outbox::Cursor.set('UserComponent::Consumer', UserEvent.last.id)
333
- ```
334
22
 
335
- ## Helper methods
336
- Some convenience methods are provided to help with common use cases.
337
-
338
- **`#enable_writes!`**
339
- Write access on entities is disabled by default outside of writes via events. Use this method to enable writes on an entity.
340
-
341
- ```ruby
342
- user = User.find_by(canonical_id: 'user-123')
343
- user.enable_writes! do
344
- user.reproject
345
- user.save!
23
+ def can_apply?(user)
24
+ user.new_record?
346
25
  end
347
- ```
348
-
349
- If you are using FactoryBot, you can add the following in your rails_helper.rb to enable writes on the entity:
350
- ```ruby
351
- FactoryBot.define do
352
- after(:build) { |model| model.enable_writes! if model.class.ancestors.include?(Eventsimple::Entity::InstanceMethods) }
353
- end
354
- ```
355
-
356
- **`#reproject(at: nil)`**
357
-
358
- Reproject an entity from events (rebuilds in memory but does not persist the entity).
359
-
360
- ```ruby
361
- module UserComponent
362
- module Events
363
- class Created < UserEvent
364
- # ...
365
-
366
- def apply(user)
367
- user.email = data.email
368
26
 
369
- # Changes the projection to start tracking a sign up timestamp.
370
- user.signed_up_at = self.created_at
371
- end
372
- end
27
+ def apply(user)
28
+ user.name = data.name
29
+ user.email = data.email
373
30
  end
374
31
  end
375
32
 
376
- user = User.find_by(canonical_id: 'user-123')
377
- user.reproject
378
- user.changes # => { sign_up_at: [nil, "2022-01-01 00:00:00 UTC"] }
379
- user.save!
380
- ```
381
-
382
- Or reproject the model to inspect what it looked like at a particular point in time.
383
- ```ruby
384
- user = User.find_by(canonical_id: 'user-123')
385
- user.reproject(at: 1.day.ago)
386
- user.changes
387
- ```
388
-
389
- **`#projection_matches_events?`**
390
-
391
- Verify that a reprojection of the model matches it's current state.
392
-
393
- ```ruby
394
- user = User.find_by(canonical_id: 'user-123')
395
- user.update(name: 'something_else')
396
- user.projection_matches_events? => false
397
- ```
398
-
399
- **`.ignored_for_projection`**
400
-
401
- Skip properties on a model that are not managed by the event driven system. This will prevent a reset of the value in case of a reprojection.
402
- Useful if the model that is being event driven has some properties that are managed through other mechanics.
403
-
404
- `id` and `lock_version` columns are always ignored by default.
405
-
406
- ```ruby
407
- class User
408
- self.ignored_for_projection = %i[last_sign_in_at]
409
- end
33
+ UserComponent::Events::Created.create!(
34
+ user: User.new,
35
+ data: { name: "John doe", email: "johndoe@example.com" }
36
+ )
410
37
  ```
411
38
 
412
- ## Common Use cases
413
-
414
- ### I want to add validations to my model.
415
-
416
- You _can_ add conditional validations to the model as usual. For example to verify an email:
39
+ ### Execute side effects using Reactors
417
40
 
418
41
  ```ruby
419
- class User
420
- EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
421
-
422
- validates :email, presence: true, format: {
423
- with: EMAIL_REGEX
424
- }, if: :email_changed?
425
-
426
- validate :allowed_emails, if: :email_changed?
427
-
428
- def allowed_emails
429
- return if EmailBlacklist.allowed?(email)
430
-
431
- errors.add(:email, :invalid, value: email)
432
- end
42
+ class UserComponent::Dispatcher < Eventsimple::Dispatcher
43
+ on(
44
+ UserComponent::Events::Created, async: SendWelcomeEmail
45
+ )
433
46
  end
434
- ```
435
-
436
- However, conditional validations tend to become more complex over time. An alternative approach can be to validate at the point _when_ a handle is being updated.
437
-
438
- Consider extending the model with a mixin, to apply the validation only when the email is actually being set.
439
47
 
440
- ```ruby
441
- module UpdateEmailForm
442
- def self.extended(base)
443
- base.class_eval do
444
- EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
445
-
446
- validates :email, presence: true, format: {
447
- with: EMAIL_REGEX
448
- }
449
-
450
- validate :allowed_emails, if: :email_changed?
451
-
452
- def allowed_emails
453
- return if EmailBlacklist.allowed?(email)
454
-
455
- errors.add(:email, :invalid, value: email)
456
- end
48
+ class UserComponent::Reactors::SendWelcomeEmail < Eventsimple::Reactor
49
+ def call(event)
50
+ EmailService.send_welcome_email(event.aggregate)
457
51
  end
458
52
  end
459
-
460
- user = User.find_by(canonical_id: 'user-123').extend(UpdateEmailForm)
461
-
462
- UserComponent::Events::EmailUpdated.create(user: user, data: { email: 'email' })
463
- ```
464
-
465
- You can configure mixins in the event class itself, so that they are applied automatically at the point of event creating. The following example will extend the user with UpdateEmailForm on user create:
466
-
467
- ```ruby
468
- class UserComponent::Events::Created < UserEvent
469
- ...
470
-
471
- validates_with UpdateEmailForm
472
-
473
- ...
474
- end
475
- ```
476
-
477
- ### I want to modify an existing event by adding a new attribute
478
- New attributes should always be added as being either optional or required with a default value.
479
-
480
- ```ruby
481
- class UserComponent::Events::Created < Eventsimple::Message
482
- attribute :new_attribute_1, DryTypes::Strict::String.default('default')
483
- attribute? :new_attribute_2, DryTypes::Strict::String.optional
484
- end
485
53
  ```
486
54
 
487
- This guarantees compatibility with older events which do not contain this attribute. Old events will be loaded with the attribute being either nil or the new default.
488
-
489
- To ensure old models are also in a consistent state, a data migration may be required to update the new attribute to the new default.
490
-
491
- ```ruby
492
- # migration file
493
- add_column :users, :new_attribute_1, :string, default: 'new_default'
494
-
495
- User.where(new_attribute_1: nil).find_in_batches do |batch|
496
- batch.update_all(new_attribute_1: 'new_default')
497
- end
498
- ```
499
-
500
- ### I want to modify an event by removing a unused attribute
501
- Simply remove the attribute in code and any usage references. Any persisted data in old events will be ignored going forward, so a data migration is not explicitly needed.
502
-
503
- However if this is something that is required, we can follow up code removal with a data migration like:
504
-
505
- ```ruby
506
- UserEvent.where(type: 'MyEventName').in_batches do |batch|
507
- batch.update_all("data = data::jsonb - 'old_attribute_1' - 'old_attribute_2'")
508
- end
509
- ```
510
-
511
- ### I want to remove an event that is not longer required
512
- * If an event and any properties it sets are no longer required, we can delete the Event, any code references and the model columns.
513
- * The persisted events will be ignored going forward, so a data migration is not explicitly needed.
514
-
515
- However if this is something that is required, we can follow up code removal with a data migration like:
516
-
517
- ```ruby
518
- # Remove all code references and then run the following migration:
519
-
520
- UserEvent.where(type: 'MyEventName').in_batches do |batch|
521
- batch.delete_all
522
- end
523
- ```
524
-
525
- ### I want to ignore InvalidTransition errors
526
-
527
- The InvalidTransition error is raised when the `can_apply?` method of an Event returns `false`. In many cases this indicates a bug in the code, but in some cases it is expected behaviour.
528
-
529
- An example scenario for not wanting to raise the error is when the `can_apply?` method is primarily defending against redundant events from being written, perhaps when consuming messages from a message broker.
530
-
531
- You can mute these errors by calling `rescue_invalid_transition` on the event class. This will cause the event to be ignored and the model to remain unchanged. Optionally, you can pass a block to handle the error.
532
-
533
- ```ruby
534
- module FooComponent
535
- module Events
536
- class BarToTrue < FooEvent
537
- rescue_invalid_transition do |error|
538
- logger.info("Receive invalid transition error", error)
539
- end
540
-
541
- def can_apply?(foo)
542
- !foo.bar
543
- end
544
-
545
- def apply(foo)
546
- foo.bar = true
547
-
548
- foo
549
- end
550
- end
551
- end
552
- end
553
- ```
55
+ ## Quick Start
554
56
 
555
- ### Credits
556
- Special credits to [kickstarter](https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224) and [Eventide Project](https://github.com/eventide-project) for much of the inspiration for this gem.
57
+ - **[Home](https://github.com/wealthsimple/eventsimple/wiki/Home)** - Installation, configuration, and getting started
58
+ - **[Usage-Events](https://github.com/wealthsimple/eventsimple/wiki/Usage-Events)** - How to create and use events
59
+ - **[Usage-Reactors](https://github.com/wealthsimple/eventsimple/wiki/Usage-Reactors)** - Handle side effects with sync and async reactors
60
+ - **[Encryption](https://github.com/wealthsimple/eventsimple/wiki/Encryption)** - Encrypt sensitive data in event messages
61
+ - **[Outbox-Pattern](https://github.com/wealthsimple/eventsimple/wiki/Outbox-Pattern)** - Implement ordered event processing
62
+ - **[Best-Practices](https://github.com/wealthsimple/eventsimple/wiki/Best-Practices)** - Development guidelines and best practices
63
+ - **[Testing](https://github.com/wealthsimple/eventsimple/wiki/Testing)** - Testing best practices for events and reactors
64
+ - **[Helper-Methods](https://github.com/wealthsimple/eventsimple/wiki/Helper-Methods)** - Convenience methods for common tasks
65
+ - **[Data-Migrations](https://github.com/wealthsimple/eventsimple/wiki/Data-Migrations)** - Migrating event data
66
+ - **[Existing-Models](https://github.com/wealthsimple/eventsimple/wiki/Existing-Models)** - Adding Eventsimple to existing models
67
+ - **[Factory-Bot-Compatibility](https://github.com/wealthsimple/eventsimple/wiki/Factory-Bot-Compatibility)** - Using Factory Bot with Eventsimple
data/Rakefile CHANGED
@@ -4,9 +4,6 @@ require "bundler/setup"
4
4
 
5
5
  APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
6
6
  Rake.load_rakefile 'spec/dummy/Rakefile'
7
- load 'rails/tasks/engine.rake'
8
-
9
- load 'rails/tasks/statistics.rake'
10
7
 
11
8
  require 'bundler/gem_tasks'
12
9
  require 'rspec/core/rake_task'
@@ -61,7 +61,7 @@
61
61
  <th scope="row" colspan="2">Data</th>
62
62
  </tr>
63
63
  <% if @selected_event.data.present? %>
64
- <% @selected_event.data.to_hash.each do |attr_name, attr_value| %>
64
+ <% @selected_event.data.attributes.each do |attr_name, attr_value| %>
65
65
  <tr>
66
66
  <td>&nbsp;&nbsp;&nbsp;&nbsp;<%= attr_name %></td>
67
67
  <td><code class="entity-property"><%= attr_value %></code></td>
@@ -72,7 +72,7 @@
72
72
  <tr>
73
73
  <th scope="row" colspan="2">Metadata</th>
74
74
  </tr>
75
- <% @selected_event.metadata.to_hash.each do |attr_name, attr_value| %>
75
+ <% @selected_event.metadata.attributes.each do |attr_name, attr_value| %>
76
76
  <tr>
77
77
  <td>&nbsp;&nbsp;&nbsp;&nbsp;<%= attr_name %></td>
78
78
  <td><code class="entity-property">: <%= attr_value %></code></td>
data/eventsimple.gemspec CHANGED
@@ -24,8 +24,8 @@ Gem::Specification.new do |spec|
24
24
  spec.require_paths = ['lib']
25
25
 
26
26
  spec.add_runtime_dependency 'concurrent-ruby', '>= 1.2.3'
27
- spec.add_runtime_dependency 'dry-struct', '~> 1.6'
28
- spec.add_runtime_dependency 'dry-types', '~> 1.7'
27
+ spec.add_runtime_dependency 'dry-struct', '>= 1.8.0'
28
+ spec.add_runtime_dependency 'dry-types', '>= 1.7.0'
29
29
  spec.add_runtime_dependency 'pg', '~> 1.4'
30
30
  spec.add_runtime_dependency 'rails', '>= 7.0', '< 9.0'
31
31
  spec.add_runtime_dependency 'retriable', '~> 3.1'