dexkit 0.4.1 → 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 +60 -57
- data/guides/llm/OPERATION.md +12 -3
- data/guides/llm/QUERY.md +2 -2
- data/lib/dex/operation/async_proxy.rb +1 -1
- data/lib/dex/operation/transaction_adapter.rb +17 -10
- data/lib/dex/operation/transaction_wrapper.rb +60 -9
- 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,26 +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!(:
|
|
22
|
+
error!(:out_of_stock) unless product.in_stock?
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
order = Order.create!(customer: customer, product: product, quantity: quantity, note: note)
|
|
23
25
|
|
|
24
|
-
after_commit {
|
|
26
|
+
after_commit { Order::Placed.publish(order_id: order.id, total: order.total) }
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
order
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
order = Order::Place.call(customer: 42, product: 7, quantity: 2)
|
|
33
|
+
order.id # => 1
|
|
32
34
|
```
|
|
33
35
|
|
|
34
36
|
### What you get out of the box
|
|
@@ -36,35 +38,35 @@ user.name # => "Alice"
|
|
|
36
38
|
**Typed properties** – powered by [literal](https://github.com/joeldrapper/literal). Plain classes, ranges, unions, arrays, nilable, and model references with auto-find:
|
|
37
39
|
|
|
38
40
|
```ruby
|
|
39
|
-
prop :
|
|
41
|
+
prop :quantity, _Integer(1..)
|
|
40
42
|
prop :currency, _Union("USD", "EUR", "GBP")
|
|
41
|
-
prop :
|
|
42
|
-
prop? :note,
|
|
43
|
+
prop :customer, _Ref(Customer) # accepts Customer instance or ID
|
|
44
|
+
prop? :note, String # optional (nil by default)
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
**Structured errors** with `error!`, `assert!`, and `rescue_from`:
|
|
46
48
|
|
|
47
49
|
```ruby
|
|
48
|
-
|
|
50
|
+
product = assert!(:not_found) { Product.find_by(id: product_id) }
|
|
49
51
|
|
|
50
|
-
rescue_from Stripe::CardError, as: :
|
|
52
|
+
rescue_from Stripe::CardError, as: :payment_declined
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
**Ok / Err** – pattern match on operation outcomes with `.safe.call`:
|
|
54
56
|
|
|
55
57
|
```ruby
|
|
56
|
-
case
|
|
57
|
-
in Ok
|
|
58
|
-
|
|
59
|
-
in Err(code: :
|
|
60
|
-
|
|
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"
|
|
61
63
|
end
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
**Async execution** via ActiveJob:
|
|
65
67
|
|
|
66
68
|
```ruby
|
|
67
|
-
|
|
69
|
+
Order::Fulfill.new(order_id: 123).async(queue: "fulfillment").call
|
|
68
70
|
```
|
|
69
71
|
|
|
70
72
|
**Transactions** on by default, **advisory locking**, **recording** to database, **callbacks**, and a customizable **pipeline** – all composable, all optional.
|
|
@@ -74,15 +76,16 @@ SendWelcomeEmail.new(user_id: 123).async(queue: "mailers").call
|
|
|
74
76
|
First-class test helpers for Minitest:
|
|
75
77
|
|
|
76
78
|
```ruby
|
|
77
|
-
class
|
|
78
|
-
testing
|
|
79
|
+
class PlaceOrderTest < Minitest::Test
|
|
80
|
+
testing Order::Place
|
|
79
81
|
|
|
80
|
-
def
|
|
81
|
-
assert_operation(
|
|
82
|
+
def test_places_order
|
|
83
|
+
assert_operation(customer: customer.id, product: product.id, quantity: 2)
|
|
82
84
|
end
|
|
83
85
|
|
|
84
|
-
def
|
|
85
|
-
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)
|
|
86
89
|
end
|
|
87
90
|
end
|
|
88
91
|
```
|
|
@@ -92,14 +95,14 @@ end
|
|
|
92
95
|
Typed, immutable event objects with publish/subscribe, async dispatch, and causality tracing.
|
|
93
96
|
|
|
94
97
|
```ruby
|
|
95
|
-
class
|
|
98
|
+
class Order::Placed < Dex::Event
|
|
96
99
|
prop :order_id, Integer
|
|
97
100
|
prop :total, BigDecimal
|
|
98
101
|
prop? :coupon_code, String
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
class NotifyWarehouse < Dex::Event::Handler
|
|
102
|
-
on
|
|
105
|
+
on Order::Placed
|
|
103
106
|
retries 3
|
|
104
107
|
|
|
105
108
|
def perform
|
|
@@ -107,7 +110,7 @@ class NotifyWarehouse < Dex::Event::Handler
|
|
|
107
110
|
end
|
|
108
111
|
end
|
|
109
112
|
|
|
110
|
-
|
|
113
|
+
Order::Placed.publish(order_id: 1, total: 99.99)
|
|
111
114
|
```
|
|
112
115
|
|
|
113
116
|
### What you get out of the box
|
|
@@ -120,7 +123,7 @@ OrderPlaced.publish(order_id: 1, total: 99.99)
|
|
|
120
123
|
|
|
121
124
|
```ruby
|
|
122
125
|
order_placed.trace do
|
|
123
|
-
|
|
126
|
+
Shipment::Reserved.publish(order_id: 1)
|
|
124
127
|
end
|
|
125
128
|
```
|
|
126
129
|
|
|
@@ -129,13 +132,13 @@ end
|
|
|
129
132
|
### Testing
|
|
130
133
|
|
|
131
134
|
```ruby
|
|
132
|
-
class
|
|
135
|
+
class PlaceOrderTest < Minitest::Test
|
|
133
136
|
include Dex::Event::TestHelpers
|
|
134
137
|
|
|
135
138
|
def test_publishes_order_placed
|
|
136
139
|
capture_events do
|
|
137
|
-
|
|
138
|
-
assert_event_published(
|
|
140
|
+
Order::Place.call(customer: customer.id, product: product.id, quantity: 2)
|
|
141
|
+
assert_event_published(Order::Placed)
|
|
139
142
|
end
|
|
140
143
|
end
|
|
141
144
|
end
|
|
@@ -146,8 +149,8 @@ end
|
|
|
146
149
|
Form objects with typed attributes, normalization, nested forms, and Rails form builder compatibility.
|
|
147
150
|
|
|
148
151
|
```ruby
|
|
149
|
-
class
|
|
150
|
-
model
|
|
152
|
+
class Employee::Form < Dex::Form
|
|
153
|
+
model Employee
|
|
151
154
|
|
|
152
155
|
attribute :first_name, :string
|
|
153
156
|
attribute :last_name, :string
|
|
@@ -165,7 +168,7 @@ class OnboardingForm < Dex::Form
|
|
|
165
168
|
end
|
|
166
169
|
end
|
|
167
170
|
|
|
168
|
-
form =
|
|
171
|
+
form = Employee::Form.new(email: " ALICE@EXAMPLE.COM ", first_name: "Alice", last_name: "Smith")
|
|
169
172
|
form.email # => "alice@example.com"
|
|
170
173
|
form.valid?
|
|
171
174
|
```
|
|
@@ -177,10 +180,10 @@ form.valid?
|
|
|
177
180
|
**Nested forms** — `nested_one` and `nested_many` with automatic Hash coercion, `_destroy` support, and error propagation:
|
|
178
181
|
|
|
179
182
|
```ruby
|
|
180
|
-
nested_many :
|
|
181
|
-
attribute :
|
|
182
|
-
attribute :
|
|
183
|
-
validates :
|
|
183
|
+
nested_many :emergency_contacts do
|
|
184
|
+
attribute :name, :string
|
|
185
|
+
attribute :phone, :string
|
|
186
|
+
validates :name, :phone, presence: true
|
|
184
187
|
end
|
|
185
188
|
```
|
|
186
189
|
|
|
@@ -188,7 +191,7 @@ end
|
|
|
188
191
|
|
|
189
192
|
**Uniqueness validation** against the database, with scope, case-sensitivity, and current-record exclusion.
|
|
190
193
|
|
|
191
|
-
**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`:
|
|
192
195
|
|
|
193
196
|
```ruby
|
|
194
197
|
def save
|
|
@@ -203,24 +206,24 @@ end
|
|
|
203
206
|
|
|
204
207
|
## Queries
|
|
205
208
|
|
|
206
|
-
Declarative query objects for filtering and sorting ActiveRecord relations.
|
|
209
|
+
Declarative query objects for filtering and sorting ActiveRecord relations and Mongoid criteria.
|
|
207
210
|
|
|
208
211
|
```ruby
|
|
209
|
-
class
|
|
210
|
-
scope {
|
|
212
|
+
class Order::Query < Dex::Query
|
|
213
|
+
scope { Order.all }
|
|
211
214
|
|
|
212
|
-
prop? :
|
|
213
|
-
prop? :
|
|
214
|
-
prop? :
|
|
215
|
+
prop? :status, String
|
|
216
|
+
prop? :customer, _Ref(Customer)
|
|
217
|
+
prop? :total_min, Integer
|
|
215
218
|
|
|
216
|
-
filter :
|
|
217
|
-
filter :
|
|
218
|
-
filter :
|
|
219
|
+
filter :status
|
|
220
|
+
filter :customer
|
|
221
|
+
filter :total_min, :gte, column: :total
|
|
219
222
|
|
|
220
|
-
sort :
|
|
223
|
+
sort :created_at, :total, default: "-created_at"
|
|
221
224
|
end
|
|
222
225
|
|
|
223
|
-
|
|
226
|
+
orders = Order::Query.call(status: "pending", sort: "-total")
|
|
224
227
|
```
|
|
225
228
|
|
|
226
229
|
### What you get out of the box
|
|
@@ -232,10 +235,10 @@ users = UserSearch.call(name: "ali", role: %w[admin], sort: "name")
|
|
|
232
235
|
**`from_params`** — HTTP boundary handling with automatic coercion, blank stripping, and invalid value fallback:
|
|
233
236
|
|
|
234
237
|
```ruby
|
|
235
|
-
class
|
|
238
|
+
class OrdersController < ApplicationController
|
|
236
239
|
def index
|
|
237
|
-
query =
|
|
238
|
-
@
|
|
240
|
+
query = Order::Query.from_params(params, scope: policy_scope(Order))
|
|
241
|
+
@orders = pagy(query.resolve)
|
|
239
242
|
end
|
|
240
243
|
end
|
|
241
244
|
```
|
data/guides/llm/OPERATION.md
CHANGED
|
@@ -273,11 +273,18 @@ def perform
|
|
|
273
273
|
end
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
-
|
|
276
|
+
Callbacks are always deferred — they run after the outermost operation boundary succeeds:
|
|
277
277
|
|
|
278
|
-
|
|
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.
|
|
279
282
|
|
|
280
|
-
|
|
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.
|
|
281
288
|
|
|
282
289
|
---
|
|
283
290
|
|
|
@@ -371,6 +378,8 @@ end
|
|
|
371
378
|
|
|
372
379
|
Not autoloaded — stays out of production. TestLog and stubs are auto-cleared in `setup`.
|
|
373
380
|
|
|
381
|
+
For Mongoid-backed operation tests, run against a MongoDB replica set (MongoDB transactions require it).
|
|
382
|
+
|
|
374
383
|
### Subject & Execution
|
|
375
384
|
|
|
376
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
|
|
@@ -53,10 +53,23 @@ module Dex
|
|
|
53
53
|
raise LoadError, "Mongoid is required for transactions"
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
59
71
|
|
|
72
|
+
Thread.current[AFTER_COMMIT_KEY] = []
|
|
60
73
|
block_completed = false
|
|
61
74
|
result = Mongoid.transaction do
|
|
62
75
|
value = block.call
|
|
@@ -64,17 +77,11 @@ module Dex
|
|
|
64
77
|
value
|
|
65
78
|
end
|
|
66
79
|
|
|
67
|
-
if
|
|
80
|
+
if block_completed
|
|
68
81
|
Thread.current[AFTER_COMMIT_KEY].each(&:call)
|
|
69
|
-
elsif !block_completed
|
|
70
|
-
# Mongoid swallowed a Rollback exception — discard callbacks from this level
|
|
71
|
-
Thread.current[AFTER_COMMIT_KEY].slice!(snapshot..)
|
|
72
82
|
end
|
|
73
83
|
|
|
74
84
|
result
|
|
75
|
-
rescue StandardError # rubocop:disable Style/RescueStandardError
|
|
76
|
-
Thread.current[AFTER_COMMIT_KEY]&.slice!(snapshot..) unless outermost
|
|
77
|
-
raise
|
|
78
85
|
ensure
|
|
79
86
|
Thread.current[AFTER_COMMIT_KEY] = nil if outermost
|
|
80
87
|
end
|
|
@@ -4,26 +4,37 @@ 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
|
|
19
30
|
end
|
|
20
31
|
|
|
21
32
|
def after_commit(&block)
|
|
22
33
|
raise ArgumentError, "after_commit requires a block" unless block
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
if
|
|
26
|
-
|
|
35
|
+
deferred = Thread.current[DEFERRED_CALLBACKS_KEY]
|
|
36
|
+
if deferred
|
|
37
|
+
deferred << block
|
|
27
38
|
else
|
|
28
39
|
block.call
|
|
29
40
|
end
|
|
@@ -65,6 +76,33 @@ module Dex
|
|
|
65
76
|
|
|
66
77
|
private
|
|
67
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
|
+
|
|
68
106
|
def _transaction_enabled?
|
|
69
107
|
settings = self.class.settings_for(:transaction)
|
|
70
108
|
return false unless settings.fetch(:enabled, true)
|
|
@@ -78,6 +116,19 @@ module Dex
|
|
|
78
116
|
Operation::TransactionAdapter.for(adapter_name)
|
|
79
117
|
end
|
|
80
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
|
+
|
|
81
132
|
def _transaction_execute(&block)
|
|
82
133
|
_transaction_adapter.wrap(&block)
|
|
83
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
|