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 +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +63 -55
- data/guides/llm/OPERATION.md +34 -1
- data/guides/llm/QUERY.md +2 -2
- data/lib/dex/operation/async_proxy.rb +1 -1
- data/lib/dex/operation/transaction_adapter.rb +54 -1
- data/lib/dex/operation/transaction_wrapper.rb +68 -6
- data/lib/dex/version.rb +1 -1
- metadata +29 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a120052500fd0c1889117f87d0d4260b40eecbc82b31ad50b9e6fc97f99efd9
|
|
4
|
+
data.tar.gz: f140a685dd0825fbd675f5575d1a44d5679598ad7a2d302a65a3f22bae268ce0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
13
|
-
prop :
|
|
14
|
-
prop :
|
|
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(
|
|
17
|
-
error
|
|
18
|
+
success _Ref(Order)
|
|
19
|
+
error :out_of_stock
|
|
18
20
|
|
|
19
21
|
def perform
|
|
20
|
-
error!(:
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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 :
|
|
41
|
+
prop :quantity, _Integer(1..)
|
|
35
42
|
prop :currency, _Union("USD", "EUR", "GBP")
|
|
36
|
-
prop :
|
|
37
|
-
prop? :note,
|
|
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
|
-
|
|
50
|
+
product = assert!(:not_found) { Product.find_by(id: product_id) }
|
|
44
51
|
|
|
45
|
-
rescue_from Stripe::CardError, as: :
|
|
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
|
|
52
|
-
in Ok
|
|
53
|
-
|
|
54
|
-
in Err(code: :
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
73
|
-
testing
|
|
79
|
+
class PlaceOrderTest < Minitest::Test
|
|
80
|
+
testing Order::Place
|
|
74
81
|
|
|
75
|
-
def
|
|
76
|
-
assert_operation(
|
|
82
|
+
def test_places_order
|
|
83
|
+
assert_operation(customer: customer.id, product: product.id, quantity: 2)
|
|
77
84
|
end
|
|
78
85
|
|
|
79
|
-
def
|
|
80
|
-
assert_operation_error(:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
133
|
-
assert_event_published(
|
|
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
|
|
145
|
-
model
|
|
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 =
|
|
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 :
|
|
176
|
-
attribute :
|
|
177
|
-
attribute :
|
|
178
|
-
validates :
|
|
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
|
|
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
|
|
205
|
-
scope {
|
|
212
|
+
class Order::Query < Dex::Query
|
|
213
|
+
scope { Order.all }
|
|
206
214
|
|
|
207
|
-
prop? :
|
|
208
|
-
prop? :
|
|
209
|
-
prop? :
|
|
215
|
+
prop? :status, String
|
|
216
|
+
prop? :customer, _Ref(Customer)
|
|
217
|
+
prop? :total_min, Integer
|
|
210
218
|
|
|
211
|
-
filter :
|
|
212
|
-
filter :
|
|
213
|
-
filter :
|
|
219
|
+
filter :status
|
|
220
|
+
filter :customer
|
|
221
|
+
filter :total_min, :gte, column: :total
|
|
214
222
|
|
|
215
|
-
sort :
|
|
223
|
+
sort :created_at, :total, default: "-created_at"
|
|
216
224
|
end
|
|
217
225
|
|
|
218
|
-
|
|
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
|
|
238
|
+
class OrdersController < ApplicationController
|
|
231
239
|
def index
|
|
232
|
-
query =
|
|
233
|
-
@
|
|
240
|
+
query = Order::Query.from_params(params, scope: policy_scope(Order))
|
|
241
|
+
@orders = pagy(query.resolve)
|
|
234
242
|
end
|
|
235
243
|
end
|
|
236
244
|
```
|
data/guides/llm/OPERATION.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
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
|
+
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
|