dexkit 0.4.0 → 0.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: bc994e44218cf9063af69ad1d01929d86b91fe35b6f30769dd068832fb04fc37
4
- data.tar.gz: b4a078fb25780824cd2a876f5d668196ba2f3a2094ce63a1b9731dfeab1c7aa6
3
+ metadata.gz: 4a120052500fd0c1889117f87d0d4260b40eecbc82b31ad50b9e6fc97f99efd9
4
+ data.tar.gz: f140a685dd0825fbd675f5575d1a44d5679598ad7a2d302a65a3f22bae268ce0
5
5
  SHA512:
6
- metadata.gz: b4bb8dbbe3c9acd66e5c3ed6a6b203408f66c7455544c36de4e60e755a9cfa9a2fa3405a39648e77e5bd42b2593a053d5d517e16ce1cd5236225083bd24891b3
7
- data.tar.gz: c050bd2dc477e19efcebbe6bfa6fe7d46dbfa4dea5367f38c098b6c82750439211d1b4a28224c21fd66e98c8971b9532f341f4e17b4d2e836383470ccf778913
6
+ metadata.gz: f2186829d67bb0c19d71e9a81021a94efc56cb90dc943d11ad62a9306833ed08183691eb3196fc8dec950dcd3e7a624be5e67eda00abb3e74a444404b7947267
7
+ data.tar.gz: 5314488039b706abbbf68bc3ca7fb3e2445718286a2b21ce7346dfae60f8cdb5ca68f89369b3b54fe1a756826e95a76f22bac973d75494a10c4605e8210e1d94
data/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## 0.5.0 - 2026-03-05
4
+
5
+ ### Added
6
+
7
+ - **Verified Mongoid support** — operations (transactions, recording, async) and queries now have dedicated Mongoid test coverage running against a MongoDB replica set
8
+ - **CI workflow for Mongoid** — GitHub Actions matrix includes a MongoDB replica-set job that runs Mongoid-specific tests
9
+
10
+ ### Breaking
11
+
12
+ - **`after_commit` now always defers** — non-transactional operations queue callbacks in memory and flush them after the operation pipeline succeeds, matching the behavior of transactional operations. Previously, `after_commit` fired immediately inline when no transaction was active — code that relied on immediate execution (e.g., reading side effects of the callback later in `perform`) must account for the new deferred timing. Callbacks are discarded on `error!` or exception. Nested operations flush once at the outermost successful boundary. Ambient database transactions (e.g., `ActiveRecord::Base.transaction { op.call }`) are still respected via the adapter.
13
+
14
+ ### Fixed
15
+
16
+ - **Mongoid async recording** — `record_id` is now serialized with `.to_s` so BSON::ObjectId values pass through ActiveJob correctly
17
+ - **Mongoid transaction adapter** — simplified nesting logic; the outermost `Mongoid.transaction` block now reliably owns callback flushing, fixing edge cases where nested rollbacks could leak callbacks
18
+
19
+ ## [0.4.1] - 2026-03-04
20
+
21
+ ### Added
22
+
23
+ - **`after_commit` in operations** — `after_commit { ... }` defers a block until the surrounding database transaction commits
24
+ - ActiveRecord adapter uses `ActiveRecord.after_all_transactions_commit` (requires Rails 7.2+)
25
+ - Mongoid adapter manually tracks callbacks and fires them after the outermost `Mongoid.transaction` commits
26
+ - If called outside a transaction, the block executes immediately
27
+
28
+ ## [0.4.0] - 2026-03-04
29
+
30
+ ### Added
31
+
32
+ - **Query objects** — `Dex::Query` base class for encapsulating database queries with filtering, sorting, and parameter binding
33
+ - Typed properties via `prop`/`prop?` DSL (same as Operation and Event)
34
+ - `scope { Model.all }` DSL for declaring the base scope
35
+ - **Filter DSL** — `filter :name, :strategy` with built-in strategies: `eq`, `not_eq`, `contains`, `starts_with`, `ends_with`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`
36
+ - Custom filter blocks: `filter(:name) { |scope, value| ... }`
37
+ - Optional filters (nilable props) are automatically skipped when `nil`
38
+ - **Sort DSL** — `sort :col1, :col2` for column sorts with automatic `asc`/`desc` via `"-col"` prefix convention
39
+ - Custom sort blocks: `sort(:name) { |scope| ... }`
40
+ - Default sort: `sort :name, default: "-created_at"`
41
+ - **Backend adapters** for both ActiveRecord and Mongoid (auto-detected from scope)
42
+ - `scope:` injection for pre-scoping (e.g., `current_user.posts`)
43
+ - `from_params` for binding from controller params with automatic type coercion and sort validation
44
+ - `to_params` for round-tripping query state back to URL params
45
+ - `param_key` DSL for customizing the params namespace
46
+ - ActiveModel::Naming / ActiveModel::Conversion for Rails form compatibility
47
+ - Convenience class methods: `.call`, `.count`, `.exists?`, `.any?`
48
+ - Inheritance support — filters, sorts, and scope are inherited by subclasses
49
+ - `Dex::Match` is now included in `Dex::Form` — `Ok`/`Err` are available without prefix inside forms
50
+
3
51
  ## [0.3.0] - 2026-03-03
4
52
 
5
53
  ### Added
data/README.md CHANGED
@@ -9,21 +9,28 @@ Rails patterns toolbelt. Equip to gain +4 DEX.
9
9
  Service objects with typed properties, transactions, error handling, and more.
10
10
 
11
11
  ```ruby
12
- class CreateUser < Dex::Operation
13
- prop :name, String
14
- prop :email, String
12
+ class Order::Place < Dex::Operation
13
+ prop :customer, _Ref(Customer)
14
+ prop :product, _Ref(Product)
15
+ prop :quantity, _Integer(1..)
16
+ prop? :note, String
15
17
 
16
- success _Ref(User)
17
- error :email_taken
18
+ success _Ref(Order)
19
+ error :out_of_stock
18
20
 
19
21
  def perform
20
- error!(:email_taken) if User.exists?(email: email)
21
- User.create!(name: name, email: email)
22
+ error!(:out_of_stock) unless product.in_stock?
23
+
24
+ order = Order.create!(customer: customer, product: product, quantity: quantity, note: note)
25
+
26
+ after_commit { Order::Placed.publish(order_id: order.id, total: order.total) }
27
+
28
+ order
22
29
  end
23
30
  end
24
31
 
25
- user = CreateUser.call(name: "Alice", email: "alice@example.com")
26
- user.name # => "Alice"
32
+ order = Order::Place.call(customer: 42, product: 7, quantity: 2)
33
+ order.id # => 1
27
34
  ```
28
35
 
29
36
  ### What you get out of the box
@@ -31,35 +38,35 @@ user.name # => "Alice"
31
38
  **Typed properties** – powered by [literal](https://github.com/joeldrapper/literal). Plain classes, ranges, unions, arrays, nilable, and model references with auto-find:
32
39
 
33
40
  ```ruby
34
- prop :amount, _Integer(1..)
41
+ prop :quantity, _Integer(1..)
35
42
  prop :currency, _Union("USD", "EUR", "GBP")
36
- prop :user, _Ref(User) # accepts User instance or ID
37
- prop? :note, String # optional (nil by default)
43
+ prop :customer, _Ref(Customer) # accepts Customer instance or ID
44
+ prop? :note, String # optional (nil by default)
38
45
  ```
39
46
 
40
47
  **Structured errors** with `error!`, `assert!`, and `rescue_from`:
41
48
 
42
49
  ```ruby
43
- user = assert!(:not_found) { User.find_by(id: user_id) }
50
+ product = assert!(:not_found) { Product.find_by(id: product_id) }
44
51
 
45
- rescue_from Stripe::CardError, as: :card_declined
52
+ rescue_from Stripe::CardError, as: :payment_declined
46
53
  ```
47
54
 
48
55
  **Ok / Err** – pattern match on operation outcomes with `.safe.call`:
49
56
 
50
57
  ```ruby
51
- case CreateUser.new(email: email).safe.call
52
- in Ok(name:)
53
- puts "Welcome, #{name}!"
54
- in Err(code: :email_taken)
55
- puts "Already registered"
58
+ case Order::Place.new(customer: 42, product: 7, quantity: 2).safe.call
59
+ in Ok => result
60
+ redirect_to order_path(result.id)
61
+ in Err(code: :out_of_stock)
62
+ flash[:error] = "Product is out of stock"
56
63
  end
57
64
  ```
58
65
 
59
66
  **Async execution** via ActiveJob:
60
67
 
61
68
  ```ruby
62
- SendWelcomeEmail.new(user_id: 123).async(queue: "mailers").call
69
+ Order::Fulfill.new(order_id: 123).async(queue: "fulfillment").call
63
70
  ```
64
71
 
65
72
  **Transactions** on by default, **advisory locking**, **recording** to database, **callbacks**, and a customizable **pipeline** – all composable, all optional.
@@ -69,15 +76,16 @@ SendWelcomeEmail.new(user_id: 123).async(queue: "mailers").call
69
76
  First-class test helpers for Minitest:
70
77
 
71
78
  ```ruby
72
- class CreateUserTest < Minitest::Test
73
- testing CreateUser
79
+ class PlaceOrderTest < Minitest::Test
80
+ testing Order::Place
74
81
 
75
- def test_creates_user
76
- assert_operation(name: "Alice", email: "alice@example.com")
82
+ def test_places_order
83
+ assert_operation(customer: customer.id, product: product.id, quantity: 2)
77
84
  end
78
85
 
79
- def test_rejects_duplicate_email
80
- assert_operation_error(:email_taken, name: "Alice", email: "taken@example.com")
86
+ def test_rejects_out_of_stock
87
+ assert_operation_error(:out_of_stock, customer: customer.id,
88
+ product: out_of_stock_product.id, quantity: 1)
81
89
  end
82
90
  end
83
91
  ```
@@ -87,14 +95,14 @@ end
87
95
  Typed, immutable event objects with publish/subscribe, async dispatch, and causality tracing.
88
96
 
89
97
  ```ruby
90
- class OrderPlaced < Dex::Event
98
+ class Order::Placed < Dex::Event
91
99
  prop :order_id, Integer
92
100
  prop :total, BigDecimal
93
101
  prop? :coupon_code, String
94
102
  end
95
103
 
96
104
  class NotifyWarehouse < Dex::Event::Handler
97
- on OrderPlaced
105
+ on Order::Placed
98
106
  retries 3
99
107
 
100
108
  def perform
@@ -102,7 +110,7 @@ class NotifyWarehouse < Dex::Event::Handler
102
110
  end
103
111
  end
104
112
 
105
- OrderPlaced.publish(order_id: 1, total: 99.99)
113
+ Order::Placed.publish(order_id: 1, total: 99.99)
106
114
  ```
107
115
 
108
116
  ### What you get out of the box
@@ -115,7 +123,7 @@ OrderPlaced.publish(order_id: 1, total: 99.99)
115
123
 
116
124
  ```ruby
117
125
  order_placed.trace do
118
- InventoryReserved.publish(order_id: 1)
126
+ Shipment::Reserved.publish(order_id: 1)
119
127
  end
120
128
  ```
121
129
 
@@ -124,13 +132,13 @@ end
124
132
  ### Testing
125
133
 
126
134
  ```ruby
127
- class CreateOrderTest < Minitest::Test
135
+ class PlaceOrderTest < Minitest::Test
128
136
  include Dex::Event::TestHelpers
129
137
 
130
138
  def test_publishes_order_placed
131
139
  capture_events do
132
- CreateOrder.call(item_id: 1)
133
- assert_event_published(OrderPlaced, order_id: 1)
140
+ Order::Place.call(customer: customer.id, product: product.id, quantity: 2)
141
+ assert_event_published(Order::Placed)
134
142
  end
135
143
  end
136
144
  end
@@ -141,8 +149,8 @@ end
141
149
  Form objects with typed attributes, normalization, nested forms, and Rails form builder compatibility.
142
150
 
143
151
  ```ruby
144
- class OnboardingForm < Dex::Form
145
- model User
152
+ class Employee::Form < Dex::Form
153
+ model Employee
146
154
 
147
155
  attribute :first_name, :string
148
156
  attribute :last_name, :string
@@ -160,7 +168,7 @@ class OnboardingForm < Dex::Form
160
168
  end
161
169
  end
162
170
 
163
- form = OnboardingForm.new(email: " ALICE@EXAMPLE.COM ", first_name: "Alice", last_name: "Smith")
171
+ form = Employee::Form.new(email: " ALICE@EXAMPLE.COM ", first_name: "Alice", last_name: "Smith")
164
172
  form.email # => "alice@example.com"
165
173
  form.valid?
166
174
  ```
@@ -172,10 +180,10 @@ form.valid?
172
180
  **Nested forms** — `nested_one` and `nested_many` with automatic Hash coercion, `_destroy` support, and error propagation:
173
181
 
174
182
  ```ruby
175
- nested_many :documents do
176
- attribute :document_type, :string
177
- attribute :document_number, :string
178
- validates :document_type, :document_number, presence: true
183
+ nested_many :emergency_contacts do
184
+ attribute :name, :string
185
+ attribute :phone, :string
186
+ validates :name, :phone, presence: true
179
187
  end
180
188
  ```
181
189
 
@@ -183,7 +191,7 @@ end
183
191
 
184
192
  **Uniqueness validation** against the database, with scope, case-sensitivity, and current-record exclusion.
185
193
 
186
- **Multi-model forms** — when a form spans User, Employee, and Address, define a `.for` convention method to map records and a `#save` method that delegates to a `Dex::Operation`:
194
+ **Multi-model forms** — when a form spans Employee, Department, and Address, define a `.for` convention method to map records and a `#save` method that delegates to a `Dex::Operation`:
187
195
 
188
196
  ```ruby
189
197
  def save
@@ -198,24 +206,24 @@ end
198
206
 
199
207
  ## Queries
200
208
 
201
- Declarative query objects for filtering and sorting ActiveRecord relations.
209
+ Declarative query objects for filtering and sorting ActiveRecord relations and Mongoid criteria.
202
210
 
203
211
  ```ruby
204
- class UserSearch < Dex::Query
205
- scope { User.all }
212
+ class Order::Query < Dex::Query
213
+ scope { Order.all }
206
214
 
207
- prop? :name, String
208
- prop? :role, _Array(String)
209
- prop? :age_min, Integer
215
+ prop? :status, String
216
+ prop? :customer, _Ref(Customer)
217
+ prop? :total_min, Integer
210
218
 
211
- filter :name, :contains
212
- filter :role, :in
213
- filter :age_min, :gte, column: :age
219
+ filter :status
220
+ filter :customer
221
+ filter :total_min, :gte, column: :total
214
222
 
215
- sort :name, :created_at, default: "-created_at"
223
+ sort :created_at, :total, default: "-created_at"
216
224
  end
217
225
 
218
- users = UserSearch.call(name: "ali", role: %w[admin], sort: "name")
226
+ orders = Order::Query.call(status: "pending", sort: "-total")
219
227
  ```
220
228
 
221
229
  ### What you get out of the box
@@ -227,10 +235,10 @@ users = UserSearch.call(name: "ali", role: %w[admin], sort: "name")
227
235
  **`from_params`** — HTTP boundary handling with automatic coercion, blank stripping, and invalid value fallback:
228
236
 
229
237
  ```ruby
230
- class UsersController < ApplicationController
238
+ class OrdersController < ApplicationController
231
239
  def index
232
- query = UserSearch.from_params(params, scope: policy_scope(User))
233
- @users = pagy(query.resolve)
240
+ query = Order::Query.from_params(params, scope: policy_scope(Order))
241
+ @orders = pagy(query.resolve)
234
242
  end
235
243
  end
236
244
  ```
@@ -20,7 +20,12 @@ class CreateUser < Dex::Operation
20
20
  def perform
21
21
  error!(:invalid_email) unless email.include?("@")
22
22
  error!(:email_taken) if User.exists?(email: email)
23
- User.create!(email: email, name: name, role: role)
23
+
24
+ user = User.create!(email: email, name: name, role: role)
25
+
26
+ after_commit { WelcomeMailer.with(user: user).deliver_later }
27
+
28
+ user
24
29
  end
25
30
  end
26
31
  ```
@@ -255,6 +260,32 @@ transaction :mongoid # adapter override (default: auto-detect AR → Mongoid
255
260
 
256
261
  Child classes can re-enable: `transaction true`.
257
262
 
263
+ ### after_commit
264
+
265
+ Register blocks to run after the transaction commits. Use for side effects that should only happen on success (emails, webhooks, cache invalidation):
266
+
267
+ ```ruby
268
+ def perform
269
+ user = User.create!(name: name, email: email)
270
+ after_commit { WelcomeMailer.with(user: user).deliver_later }
271
+ after_commit { Analytics.track(:user_created, user_id: user.id) }
272
+ user
273
+ end
274
+ ```
275
+
276
+ Callbacks are always deferred — they run after the outermost operation boundary succeeds:
277
+
278
+ - **Transactional operations:** deferred until the DB transaction commits.
279
+ - **Non-transactional operations:** queued in memory, flushed after the operation pipeline completes successfully.
280
+ - **Nested operations:** callbacks queue up and flush once at the outermost successful boundary.
281
+ - **On error (`error!` or exception):** queued callbacks are discarded.
282
+
283
+ Multiple blocks run in registration order.
284
+
285
+ **ActiveRecord:** requires Rails 7.2+ (`after_all_transactions_commit`).
286
+
287
+ **Mongoid:** deferred across nested Dex operations. Ambient `Mongoid.transaction` blocks opened outside Dex are not detected — callbacks will fire immediately in that case.
288
+
258
289
  ---
259
290
 
260
291
  ## Advisory Locking
@@ -347,6 +378,8 @@ end
347
378
 
348
379
  Not autoloaded — stays out of production. TestLog and stubs are auto-cleared in `setup`.
349
380
 
381
+ For Mongoid-backed operation tests, run against a MongoDB replica set (MongoDB transactions require it).
382
+
350
383
  ### Subject & Execution
351
384
 
352
385
  ```ruby
data/guides/llm/QUERY.md CHANGED
@@ -159,7 +159,7 @@ Only one default per class. Applied when no sort is provided.
159
159
 
160
160
  ### `.call`
161
161
 
162
- Returns an ActiveRecord relation:
162
+ Returns a queryable scope (`ActiveRecord::Relation` or `Mongoid::Criteria`):
163
163
 
164
164
  ```ruby
165
165
  users = UserSearch.call(name: "ali", role: %w[admin], sort: "-name")
@@ -295,7 +295,7 @@ end
295
295
 
296
296
  ## Testing
297
297
 
298
- Queries return standard ActiveRecord relations. Test them with plain Minitest:
298
+ Queries return standard scopes (`ActiveRecord::Relation` or `Mongoid::Criteria`). Test them with plain Minitest:
299
299
 
300
300
  ```ruby
301
301
  class UserSearchTest < Minitest::Test
@@ -32,7 +32,7 @@ module Dex
32
32
  )
33
33
  begin
34
34
  job = apply_options(Operation::RecordJob)
35
- job.perform_later(class_name: operation_class_name, record_id: record.id)
35
+ job.perform_later(class_name: operation_class_name, record_id: record.id.to_s)
36
36
  rescue => e
37
37
  begin
38
38
  record.destroy
@@ -32,17 +32,70 @@ module Dex
32
32
  ActiveRecord::Base.transaction(&block)
33
33
  end
34
34
 
35
+ def self.after_commit(&block)
36
+ unless defined?(ActiveRecord) && ActiveRecord.respond_to?(:after_all_transactions_commit)
37
+ raise LoadError, "after_commit requires Rails 7.2+"
38
+ end
39
+
40
+ ActiveRecord.after_all_transactions_commit(&block)
41
+ end
42
+
35
43
  def self.rollback_exception_class
36
44
  ActiveRecord::Rollback
37
45
  end
38
46
  end
39
47
 
40
48
  module MongoidAdapter
49
+ AFTER_COMMIT_KEY = :_dex_mongoid_after_commit
50
+
41
51
  def self.wrap(&block)
42
52
  unless defined?(Mongoid)
43
53
  raise LoadError, "Mongoid is required for transactions"
44
54
  end
45
- Mongoid.transaction(&block)
55
+
56
+ callbacks = Thread.current[AFTER_COMMIT_KEY]
57
+ outermost = callbacks.nil?
58
+
59
+ unless outermost
60
+ snapshot = callbacks.length
61
+ begin
62
+ return block.call
63
+ rescue rollback_exception_class
64
+ callbacks.slice!(snapshot..)
65
+ return nil
66
+ rescue StandardError # rubocop:disable Style/RescueStandardError
67
+ callbacks.slice!(snapshot..)
68
+ raise
69
+ end
70
+ end
71
+
72
+ Thread.current[AFTER_COMMIT_KEY] = []
73
+ block_completed = false
74
+ result = Mongoid.transaction do
75
+ value = block.call
76
+ block_completed = true
77
+ value
78
+ end
79
+
80
+ if block_completed
81
+ Thread.current[AFTER_COMMIT_KEY].each(&:call)
82
+ end
83
+
84
+ result
85
+ ensure
86
+ Thread.current[AFTER_COMMIT_KEY] = nil if outermost
87
+ end
88
+
89
+ # NOTE: Only detects transactions opened via MongoidAdapter.wrap (i.e. Dex operations).
90
+ # Ambient Mongoid.transaction blocks opened outside Dex are invisible here —
91
+ # the callback will fire immediately instead of deferring to the outer commit.
92
+ def self.after_commit(&block)
93
+ callbacks = Thread.current[AFTER_COMMIT_KEY]
94
+ if callbacks
95
+ callbacks << block
96
+ else
97
+ block.call
98
+ end
46
99
  end
47
100
 
48
101
  def self.rollback_exception_class
@@ -4,18 +4,40 @@ module Dex
4
4
  module TransactionWrapper
5
5
  extend Dex::Concern
6
6
 
7
+ DEFERRED_CALLBACKS_KEY = :_dex_after_commit_queue
8
+
7
9
  def _transaction_wrap
8
- return yield unless _transaction_enabled?
10
+ deferred = Thread.current[DEFERRED_CALLBACKS_KEY]
11
+ outermost = deferred.nil?
12
+ Thread.current[DEFERRED_CALLBACKS_KEY] = [] if outermost
13
+ snapshot = Thread.current[DEFERRED_CALLBACKS_KEY].length
9
14
 
10
- interceptor = nil
11
- result = _transaction_execute do
12
- interceptor = Operation::HaltInterceptor.new { yield }
13
- raise _transaction_adapter.rollback_exception_class if interceptor.error?
14
- interceptor.result
15
+ result, interceptor = if _transaction_enabled?
16
+ _transaction_run_adapter(snapshot) { yield }
17
+ else
18
+ _transaction_run_deferred(snapshot) { yield }
15
19
  end
16
20
 
21
+ _transaction_flush_deferred if outermost
22
+
17
23
  interceptor&.rethrow!
18
24
  result
25
+ rescue # rubocop:disable Style/RescueStandardError -- explicit for clarity
26
+ Thread.current[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
27
+ raise
28
+ ensure
29
+ Thread.current[DEFERRED_CALLBACKS_KEY] = nil if outermost
30
+ end
31
+
32
+ def after_commit(&block)
33
+ raise ArgumentError, "after_commit requires a block" unless block
34
+
35
+ deferred = Thread.current[DEFERRED_CALLBACKS_KEY]
36
+ if deferred
37
+ deferred << block
38
+ else
39
+ block.call
40
+ end
19
41
  end
20
42
 
21
43
  TRANSACTION_KNOWN_ADAPTERS = %i[active_record mongoid].freeze
@@ -54,6 +76,33 @@ module Dex
54
76
 
55
77
  private
56
78
 
79
+ def _transaction_run_adapter(snapshot)
80
+ interceptor = nil
81
+ result = _transaction_execute do
82
+ interceptor = Operation::HaltInterceptor.new { yield }
83
+ raise _transaction_adapter.rollback_exception_class if interceptor.error?
84
+ interceptor.result
85
+ end
86
+
87
+ if interceptor&.error?
88
+ Thread.current[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
89
+ interceptor.rethrow!
90
+ end
91
+
92
+ [result, interceptor]
93
+ end
94
+
95
+ def _transaction_run_deferred(snapshot)
96
+ interceptor = Operation::HaltInterceptor.new { yield }
97
+
98
+ if interceptor.error?
99
+ Thread.current[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
100
+ interceptor.rethrow!
101
+ end
102
+
103
+ [interceptor.result, interceptor]
104
+ end
105
+
57
106
  def _transaction_enabled?
58
107
  settings = self.class.settings_for(:transaction)
59
108
  return false unless settings.fetch(:enabled, true)
@@ -67,6 +116,19 @@ module Dex
67
116
  Operation::TransactionAdapter.for(adapter_name)
68
117
  end
69
118
 
119
+ def _transaction_flush_deferred
120
+ callbacks = Thread.current[DEFERRED_CALLBACKS_KEY]
121
+ return if callbacks.empty?
122
+
123
+ flush = -> { callbacks.each(&:call) }
124
+ adapter = _transaction_adapter
125
+ if adapter
126
+ adapter.after_commit(&flush)
127
+ else
128
+ flush.call
129
+ end
130
+ end
131
+
70
132
  def _transaction_execute(&block)
71
133
  _transaction_adapter.wrap(&block)
72
134
  end
data/lib/dex/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dex
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dexkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jacek Galanciak
@@ -79,6 +79,20 @@ dependencies:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '6.1'
82
+ - !ruby/object:Gem::Dependency
83
+ name: ostruct
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
82
96
  - !ruby/object:Gem::Dependency
83
97
  name: actionpack
84
98
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +121,20 @@ dependencies:
107
121
  - - ">="
108
122
  - !ruby/object:Gem::Version
109
123
  version: '6.1'
124
+ - !ruby/object:Gem::Dependency
125
+ name: mongoid
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '8.0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '8.0'
110
138
  - !ruby/object:Gem::Dependency
111
139
  name: sqlite3
112
140
  requirement: !ruby/object:Gem::Requirement