dexkit 0.8.0 → 0.9.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +6 -2
  4. data/gemfiles/mongoid_no_ar.gemfile +10 -0
  5. data/gemfiles/mongoid_no_ar.gemfile.lock +232 -0
  6. data/guides/llm/EVENT.md +17 -4
  7. data/guides/llm/FORM.md +2 -2
  8. data/guides/llm/OPERATION.md +22 -17
  9. data/guides/llm/QUERY.md +2 -2
  10. data/lib/dex/event/bus.rb +7 -0
  11. data/lib/dex/event/test_helpers.rb +88 -0
  12. data/lib/dex/event_test_helpers.rb +1 -86
  13. data/lib/dex/form/uniqueness_validator.rb +17 -1
  14. data/lib/dex/operation/async_proxy.rb +1 -0
  15. data/lib/dex/operation/explain.rb +11 -7
  16. data/lib/dex/operation/lock_wrapper.rb +15 -2
  17. data/lib/dex/operation/once_wrapper.rb +23 -15
  18. data/lib/dex/operation/record_backend.rb +13 -0
  19. data/lib/dex/operation/record_wrapper.rb +29 -4
  20. data/lib/dex/operation/test_helpers/assertions.rb +335 -0
  21. data/lib/dex/operation/test_helpers/execution.rb +30 -0
  22. data/lib/dex/operation/test_helpers/stubbing.rb +61 -0
  23. data/lib/dex/operation/test_helpers.rb +150 -0
  24. data/lib/dex/operation/transaction_adapter.rb +29 -68
  25. data/lib/dex/operation/transaction_wrapper.rb +10 -16
  26. data/lib/dex/query/backend.rb +13 -0
  27. data/lib/dex/query.rb +9 -5
  28. data/lib/dex/ref_type.rb +4 -0
  29. data/lib/dex/test_helpers.rb +4 -139
  30. data/lib/dex/type_coercion.rb +4 -1
  31. data/lib/dex/version.rb +1 -1
  32. data/lib/dexkit.rb +6 -5
  33. metadata +9 -5
  34. data/lib/dex/test_helpers/assertions.rb +0 -333
  35. data/lib/dex/test_helpers/execution.rb +0 -28
  36. data/lib/dex/test_helpers/stubbing.rb +0 -59
  37. /data/lib/dex/{event_test_helpers → event/test_helpers}/assertions.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1009624f8d508e4d6b61d76c8d54f32fc04a11f445163915c027ebc1bdd30b5e
4
- data.tar.gz: 1b5a0755af0be468d67c3c5447f1322deff584364a493fb7665687d1417189b3
3
+ metadata.gz: 41c8e4455fb4a4cca73b1d12da366b53bdbbada2cbae773917a12d344a150dd4
4
+ data.tar.gz: 86c4e76004df8b968c094b7b252a988eafe2f7aee035505ddb7bbeae1f25e04a
5
5
  SHA512:
6
- metadata.gz: '08552d04b1e9ddf5991f1454f9491fcabe80047d9606f0432be411f09d7b632a797435fabf55d8e9b190ddbc1a70daa5e73c19aa08c2bde88540559c3d35275e'
7
- data.tar.gz: 33d58dd5b421a1e7f41d3ab133c1800787d2d1f6c22327e7db7faf9053222d62d087c4a563105fb39075fbefc1a73f04984b45511595a0e3976eda9833de0cdf
6
+ metadata.gz: 89a9f6075f3955300dbe3944a9adc9b80ab56c3b4dc60d9458ea370f68c5c83ab5b3f2daad26c6f852d6f423a498ccf068914853e9866c05442765539ac6ac91
7
+ data.tar.gz: cd541ee35700cf9aa877b3630d66f0b9580263d0fddc7f381200afe32c638f832d941112387f646fa59500c26bb552461ad5bf0f0ec79dcf7bb97d76ebcbfb86
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.9.0] - 2026-03-09
4
+
5
+ ### Breaking
6
+
7
+ - **Mongoid transaction support removed** — `transaction :mongoid` and `config.transaction_adapter = :mongoid` are no longer valid. Dex no longer ships a Mongoid transaction adapter. Before: Mongoid transactions could be enabled via configuration or per-operation DSL. After: both forms raise `ArgumentError` immediately at declaration/configuration time. Mongoid-only apps continue to work — transactions are automatically disabled (no adapter detected), and `after_commit` fires immediately after success. If you need Mongoid multi-document transactions, call `Mongoid.transaction` directly inside `perform`
8
+ - **Recording backends now validate required attributes before use** — Dex no longer silently drops missing `params`, `result`, `status`, or `once` attributes from `record_class`. Before: partial ActiveRecord/Mongoid recording models could appear to work while losing status transitions, replay data, or async params. After: Dex raises `ArgumentError` naming the missing attributes required by core recording, async record jobs, or `once`. Apps using minimal recording models must add the required columns/fields or explicitly disable the features that need them
9
+
10
+ ### Fixed
11
+
12
+ - **Mongoid-only Rails compatibility** — Dex boots and runs cleanly in Mongoid-only Rails apps without `activerecord` loaded, with prescriptive `LoadError`s for unsupported paths such as `advisory_lock` and async event dispatch without `ActiveJob`
13
+ - **ActiveRecord transaction auto-detection is stricter** — Dex now enables the ActiveRecord transaction adapter only when an ActiveRecord connection pool actually exists. Before: merely loading `activerecord` could make Mongoid-backed operations try to open an ActiveRecord transaction and fail with `ActiveRecord::ConnectionNotDefined`. After: unconfigured ActiveRecord no longer activates transactions implicitly
14
+ - **Mongoid async/recording serialization** — `_Ref(Model)` serializes IDs via `id.as_json`, so `BSON::ObjectId` values round-trip through async operations, async events, and recording without `ActiveJob::SerializationError`. Recording and `once` sanitize untyped Mongoid document results to JSON-safe payloads
15
+ - **Mongoid query and form parity** — query adapter detection and scope merging normalize Mongoid association scopes to `Mongoid::Criteria`, uniqueness validation excludes persisted Mongoid records correctly and uses a case-insensitive regex path for `case_sensitive: false`, and `_Ref(lock: true)` fails fast for model classes that do not support `.lock`
16
+
3
17
  ## [0.8.0] - 2026-03-09
4
18
 
5
19
  ### Added
data/README.md CHANGED
@@ -2,12 +2,16 @@
2
2
 
3
3
  Rails patterns toolbelt. Equip to gain +4 DEX.
4
4
 
5
+ > **Active development.** dexkit is pre-1.0 and evolving rapidly. The public API may change between minor versions as the library matures.
6
+
5
7
  **[Documentation](https://dex.razorjack.net)**
6
8
 
7
9
  ## Operations
8
10
 
9
11
  Service objects with typed properties, transactions, error handling, and more.
10
12
 
13
+ Mongoid-only Rails apps work too – queries, recording, events, and forms all adapt automatically. Transactions are ActiveRecord-only (Mongoid users who need transactions can call `Mongoid.transaction` inside `perform`); `advisory_lock` is also ActiveRecord-only. Operation/event store models can be Mongoid documents; recording models must define the fields required by the enabled recording features.
14
+
11
15
  ```ruby
12
16
  class Order::Place < Dex::Operation
13
17
  prop :customer, _Ref(Customer)
@@ -204,7 +208,7 @@ Order::Placed.publish(order_id: 1, total: 99.99)
204
208
 
205
209
  **Zero-config pub/sub** — define events and handlers, publish. No bus setup needed.
206
210
 
207
- **Async by default** — handlers dispatched via ActiveJob. `sync: true` for inline.
211
+ **Async by default** — handlers dispatched via ActiveJob. `sync: true` for inline. If ActiveJob is not loaded, async publish raises `LoadError`.
208
212
 
209
213
  **Causality tracing** — link events in chains with shared `trace_id`:
210
214
 
@@ -297,7 +301,7 @@ end
297
301
 
298
302
  ## Queries
299
303
 
300
- Declarative query objects for filtering and sorting ActiveRecord relations and Mongoid criteria.
304
+ Declarative query objects for filtering and sorting ActiveRecord and Mongoid scopes.
301
305
 
302
306
  ```ruby
303
307
  class Order::Query < Dex::Query
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "dexkit", path: ".."
4
+
5
+ gem "activejob", ">= 6.1"
6
+ gem "actionpack", ">= 6.1"
7
+ gem "activesupport", ">= 6.1"
8
+ gem "mongoid", ">= 8.0"
9
+ gem "ostruct"
10
+ gem "railties", ">= 6.1"
@@ -0,0 +1,232 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ dexkit (0.8.0)
5
+ activemodel (>= 6.1)
6
+ literal (~> 1.9)
7
+ zeitwerk (~> 2.6)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ actionpack (8.1.2)
13
+ actionview (= 8.1.2)
14
+ activesupport (= 8.1.2)
15
+ nokogiri (>= 1.8.5)
16
+ rack (>= 2.2.4)
17
+ rack-session (>= 1.0.1)
18
+ rack-test (>= 0.6.3)
19
+ rails-dom-testing (~> 2.2)
20
+ rails-html-sanitizer (~> 1.6)
21
+ useragent (~> 0.16)
22
+ actionview (8.1.2)
23
+ activesupport (= 8.1.2)
24
+ builder (~> 3.1)
25
+ erubi (~> 1.11)
26
+ rails-dom-testing (~> 2.2)
27
+ rails-html-sanitizer (~> 1.6)
28
+ activejob (8.1.2)
29
+ activesupport (= 8.1.2)
30
+ globalid (>= 0.3.6)
31
+ activemodel (8.1.2)
32
+ activesupport (= 8.1.2)
33
+ activesupport (8.1.2)
34
+ base64
35
+ bigdecimal
36
+ concurrent-ruby (~> 1.0, >= 1.3.1)
37
+ connection_pool (>= 2.2.5)
38
+ drb
39
+ i18n (>= 1.6, < 2)
40
+ json
41
+ logger (>= 1.4.2)
42
+ minitest (>= 5.1)
43
+ securerandom (>= 0.3)
44
+ tzinfo (~> 2.0, >= 2.0.5)
45
+ uri (>= 0.13.1)
46
+ base64 (0.3.0)
47
+ bigdecimal (4.0.1)
48
+ bson (5.2.0)
49
+ builder (3.3.0)
50
+ concurrent-ruby (1.3.6)
51
+ connection_pool (3.0.2)
52
+ crass (1.0.6)
53
+ date (3.5.1)
54
+ drb (2.2.3)
55
+ erb (6.0.2)
56
+ erubi (1.13.1)
57
+ globalid (1.3.0)
58
+ activesupport (>= 6.1)
59
+ i18n (1.14.8)
60
+ concurrent-ruby (~> 1.0)
61
+ io-console (0.8.2)
62
+ irb (1.17.0)
63
+ pp (>= 0.6.0)
64
+ prism (>= 1.3.0)
65
+ rdoc (>= 4.0.0)
66
+ reline (>= 0.4.2)
67
+ json (2.19.1)
68
+ literal (1.9.0)
69
+ zeitwerk
70
+ logger (1.7.0)
71
+ loofah (2.25.0)
72
+ crass (~> 1.0.2)
73
+ nokogiri (>= 1.12.0)
74
+ minitest (6.0.2)
75
+ drb (~> 2.0)
76
+ prism (~> 1.5)
77
+ mongo (2.23.0)
78
+ base64
79
+ bson (>= 4.14.1, < 6.0.0)
80
+ mongoid (9.0.10)
81
+ activemodel (>= 5.1, < 8.2, != 7.0.0)
82
+ concurrent-ruby (>= 1.0.5, < 2.0)
83
+ mongo (>= 2.18.0, < 3.0.0)
84
+ nokogiri (1.19.1-aarch64-linux-gnu)
85
+ racc (~> 1.4)
86
+ nokogiri (1.19.1-aarch64-linux-musl)
87
+ racc (~> 1.4)
88
+ nokogiri (1.19.1-arm-linux-gnu)
89
+ racc (~> 1.4)
90
+ nokogiri (1.19.1-arm-linux-musl)
91
+ racc (~> 1.4)
92
+ nokogiri (1.19.1-arm64-darwin)
93
+ racc (~> 1.4)
94
+ nokogiri (1.19.1-x86_64-darwin)
95
+ racc (~> 1.4)
96
+ nokogiri (1.19.1-x86_64-linux-gnu)
97
+ racc (~> 1.4)
98
+ nokogiri (1.19.1-x86_64-linux-musl)
99
+ racc (~> 1.4)
100
+ ostruct (0.6.3)
101
+ pp (0.6.3)
102
+ prettyprint
103
+ prettyprint (0.2.0)
104
+ prism (1.9.0)
105
+ psych (5.3.1)
106
+ date
107
+ stringio
108
+ racc (1.8.1)
109
+ rack (3.2.5)
110
+ rack-session (2.1.1)
111
+ base64 (>= 0.1.0)
112
+ rack (>= 3.0.0)
113
+ rack-test (2.2.0)
114
+ rack (>= 1.3)
115
+ rackup (2.3.1)
116
+ rack (>= 3)
117
+ rails-dom-testing (2.3.0)
118
+ activesupport (>= 5.0.0)
119
+ minitest
120
+ nokogiri (>= 1.6)
121
+ rails-html-sanitizer (1.7.0)
122
+ loofah (~> 2.25)
123
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
124
+ railties (8.1.2)
125
+ actionpack (= 8.1.2)
126
+ activesupport (= 8.1.2)
127
+ irb (~> 1.13)
128
+ rackup (>= 1.0.0)
129
+ rake (>= 12.2)
130
+ thor (~> 1.0, >= 1.2.2)
131
+ tsort (>= 0.2)
132
+ zeitwerk (~> 2.6)
133
+ rake (13.3.1)
134
+ rdoc (7.2.0)
135
+ erb
136
+ psych (>= 4.0.0)
137
+ tsort
138
+ reline (0.6.3)
139
+ io-console (~> 0.5)
140
+ securerandom (0.4.1)
141
+ stringio (3.2.0)
142
+ thor (1.5.0)
143
+ tsort (0.2.0)
144
+ tzinfo (2.0.6)
145
+ concurrent-ruby (~> 1.0)
146
+ uri (1.1.1)
147
+ useragent (0.16.11)
148
+ zeitwerk (2.7.5)
149
+
150
+ PLATFORMS
151
+ aarch64-linux-gnu
152
+ aarch64-linux-musl
153
+ arm-linux-gnu
154
+ arm-linux-musl
155
+ arm64-darwin
156
+ x86_64-darwin
157
+ x86_64-linux-gnu
158
+ x86_64-linux-musl
159
+
160
+ DEPENDENCIES
161
+ actionpack (>= 6.1)
162
+ activejob (>= 6.1)
163
+ activesupport (>= 6.1)
164
+ dexkit!
165
+ mongoid (>= 8.0)
166
+ ostruct
167
+ railties (>= 6.1)
168
+
169
+ CHECKSUMS
170
+ actionpack (8.1.2) sha256=ced74147a1f0daafaa4bab7f677513fd4d3add574c7839958f7b4f1de44f8423
171
+ actionview (8.1.2) sha256=80455b2588911c9b72cec22d240edacb7c150e800ef2234821269b2b2c3e2e5b
172
+ activejob (8.1.2) sha256=908dab3713b101859536375819f4156b07bdf4c232cc645e7538adb9e302f825
173
+ activemodel (8.1.2) sha256=e21358c11ce68aed3f9838b7e464977bc007b4446c6e4059781e1d5c03bcf33e
174
+ activesupport (8.1.2) sha256=88842578ccd0d40f658289b0e8c842acfe9af751afee2e0744a7873f50b6fdae
175
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
176
+ bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
177
+ bson (5.2.0) sha256=c468c1e8a3cfa1e80531cc519a890f85586986721d8e305f83465cc36bb82608
178
+ builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
179
+ concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
180
+ connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
181
+ crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
182
+ date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
183
+ dexkit (0.8.0)
184
+ drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
185
+ erb (6.0.2) sha256=9fe6264d44f79422c87490a1558479bd0e7dad4dd0e317656e67ea3077b5242b
186
+ erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
187
+ globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
188
+ i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
189
+ io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
190
+ irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
191
+ json (2.19.1) sha256=dd94fdc59e48bff85913829a32350b3148156bc4fd2a95a2568a78b11344082d
192
+ literal (1.9.0) sha256=b1dfac91931e71e1c4ebfddd4b459306f2973e9f749e077a647fece6ea15414a
193
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
194
+ loofah (2.25.0) sha256=df5ed7ac3bac6a4ec802df3877ee5cc86d027299f8952e6243b3dac446b060e6
195
+ minitest (6.0.2) sha256=db6e57956f6ecc6134683b4c87467d6dd792323c7f0eea7b93f66bd284adbc3d
196
+ mongo (2.23.0) sha256=be2fe4cc6f7119fa6b79e82a1963b2406856b4dc92d0ccfb74db543897be3109
197
+ mongoid (9.0.10) sha256=351192e70027276748f3c946b8926fad9254356e36021dacb5cec08d0740f21d
198
+ nokogiri (1.19.1-aarch64-linux-gnu) sha256=cfdb0eafd9a554a88f12ebcc688d2b9005f9fce42b00b970e3dc199587b27f32
199
+ nokogiri (1.19.1-aarch64-linux-musl) sha256=1e2150ab43c3b373aba76cd1190af7b9e92103564063e48c474f7600923620b5
200
+ nokogiri (1.19.1-arm-linux-gnu) sha256=0a39ed59abe3bf279fab9dd4c6db6fe8af01af0608f6e1f08b8ffa4e5d407fa3
201
+ nokogiri (1.19.1-arm-linux-musl) sha256=3a18e559ee499b064aac6562d98daab3d39ba6cbb4074a1542781b2f556db47d
202
+ nokogiri (1.19.1-arm64-darwin) sha256=dfe2d337e6700eac47290407c289d56bcf85805d128c1b5a6434ddb79731cb9e
203
+ nokogiri (1.19.1-x86_64-darwin) sha256=7093896778cc03efb74b85f915a775862730e887f2e58d6921e3fa3d981e68bf
204
+ nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a
205
+ nokogiri (1.19.1-x86_64-linux-musl) sha256=4267f38ad4fc7e52a2e7ee28ed494e8f9d8eb4f4b3320901d55981c7b995fc23
206
+ ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
207
+ pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
208
+ prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
209
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
210
+ psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
211
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
212
+ rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3
213
+ rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9
214
+ rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
215
+ rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
216
+ rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d
217
+ rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89
218
+ railties (8.1.2) sha256=1289ece76b4f7668fc46d07e55cc992b5b8751f2ad85548b7da351b8c59f8055
219
+ rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
220
+ rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
221
+ reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
222
+ securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
223
+ stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
224
+ thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
225
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
226
+ tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
227
+ uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
228
+ useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
229
+ zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
230
+
231
+ BUNDLED WITH
232
+ 4.0.4
data/guides/llm/EVENT.md CHANGED
@@ -56,7 +56,7 @@ event.publish(sync: true) # sync
56
56
  OrderPlaced.publish(order_id: 1, total: 99.99, caused_by: parent_event)
57
57
  ```
58
58
 
59
- **Async** (default): handlers dispatched via ActiveJob. **Sync**: handlers called inline.
59
+ **Async** (default): handlers dispatched via ActiveJob. If ActiveJob is not loaded, `publish(sync: false)` raises `LoadError`. **Sync**: handlers called inline.
60
60
 
61
61
  ---
62
62
 
@@ -109,7 +109,7 @@ class ProcessPayment < Dex::Event::Handler
109
109
  end
110
110
  ```
111
111
 
112
- When retries exhausted, exception propagates normally.
112
+ When retries exhausted, exception propagates normally. Async handlers and retries require ActiveJob to be loaded.
113
113
 
114
114
  ### Callbacks
115
115
 
@@ -157,7 +157,7 @@ class FulfillOrder < Dex::Event::Handler
157
157
  end
158
158
  ```
159
159
 
160
- Transactions are **disabled by default** on handlers (unlike operations). Opt in with `transaction`. The `after_commit` block defers until the transaction commits; on exception, deferred blocks are discarded.
160
+ Transactions are **disabled by default** on handlers (unlike operations). Opt in with `transaction`. The `after_commit` block defers until the transaction commits; on exception, deferred blocks are discarded. Transactions are ActiveRecord-only – in Mongoid-only apps, `after_commit` fires immediately after handler success.
161
161
 
162
162
  ### Custom Pipeline
163
163
 
@@ -231,6 +231,19 @@ create_table :event_records do |t|
231
231
  end
232
232
  ```
233
233
 
234
+ Mongoid stores work too:
235
+
236
+ ```ruby
237
+ class EventRecord
238
+ include Mongoid::Document
239
+ include Mongoid::Timestamps
240
+
241
+ field :event_type, type: String
242
+ field :payload, type: Hash
243
+ field :metadata, type: Hash
244
+ end
245
+ ```
246
+
234
247
  Persistence failures are silently rescued — they never halt event publishing.
235
248
 
236
249
  ---
@@ -294,7 +307,7 @@ Everything works without configuration. All three settings are optional.
294
307
 
295
308
  ```ruby
296
309
  # test/test_helper.rb
297
- require "dex/event_test_helpers"
310
+ require "dex/event/test_helpers"
298
311
 
299
312
  class Minitest::Test
300
313
  include Dex::Event::TestHelpers
data/guides/llm/FORM.md CHANGED
@@ -224,7 +224,7 @@ validates :email, uniqueness: true
224
224
  | `model:` | Explicit model class | `uniqueness: { model: User }` |
225
225
  | `attribute:` | Column name if different | `uniqueness: { attribute: :email }` |
226
226
  | `scope:` | Scoped uniqueness | `uniqueness: { scope: :tenant_id }` |
227
- | `case_sensitive:` | Case-insensitive check | `uniqueness: { case_sensitive: false }` |
227
+ | `case_sensitive:` | Case-insensitive check (`LOWER()` on ActiveRecord, case-insensitive regex on Mongoid) | `uniqueness: { case_sensitive: false }` |
228
228
  | `conditions:` | Extra query conditions | `uniqueness: { conditions: -> { where(active: true) } }` |
229
229
  | `message:` | Custom error message | `uniqueness: { message: "already registered" }` |
230
230
 
@@ -237,7 +237,7 @@ validates :email, uniqueness: true
237
237
 
238
238
  ### Record exclusion
239
239
 
240
- When `form.record` is persisted, the current record is excluded from the uniqueness check (for updates).
240
+ When `form.record` is persisted, the current record is excluded from the uniqueness check (for updates) on both ActiveRecord and Mongoid-backed forms.
241
241
 
242
242
  ---
243
243
 
@@ -95,7 +95,7 @@ prop? :note, String # optional (nilable, default: nil)
95
95
 
96
96
  ### _Ref(Model)
97
97
 
98
- Accepts model instances or IDs, coerces IDs via `Model.find(id)`. With `lock: true`, uses `Model.lock.find(id)` (SELECT FOR UPDATE). Instances pass through without re-locking. In serialization (recording, async), stores model ID only. IDs are treated as strings in JSON Schema – this supports integer PKs, UUIDs, and Mongoid BSON::ObjectId equally.
98
+ Accepts model instances or IDs, coerces IDs via `Model.find(id)`. With `lock: true`, uses `Model.lock.find(id)` (SELECT FOR UPDATE) – requires a model that responds to `.lock` (ActiveRecord). Mongoid documents do not support row locks and raise `ArgumentError` at declaration time. Instances pass through without re-locking. In serialization (recording, async), stores model ID only via `id.as_json`, so Mongoid BSON::ObjectId values are safe in ActiveJob payloads too. IDs are treated as strings in JSON Schema – this supports integer PKs, UUIDs, and Mongoid BSON::ObjectId equally.
99
99
 
100
100
  Outside the class body (e.g., in tests), use `Dex::RefType.new(Model)` instead of `_Ref(Model)`.
101
101
 
@@ -408,11 +408,11 @@ end
408
408
 
409
409
  ## Transactions
410
410
 
411
- Operations run inside database transactions by default. All changes roll back on error. Nested operations share the outer transaction.
411
+ Operations run inside database transactions when Dex has an active transaction adapter. ActiveRecord is auto-detected. In Mongoid-only apps, no adapter is active, so transactions are automatically disabled – but `after_commit` still works (callbacks fire immediately after success). If you need Mongoid transactions, use `Mongoid.transaction` directly inside `perform`.
412
412
 
413
413
  ```ruby
414
414
  transaction false # disable
415
- transaction :mongoid # adapter override (default: auto-detect AR → Mongoid)
415
+ transaction :active_record # explicit adapter
416
416
  ```
417
417
 
418
418
  Child classes can re-enable: `transaction true`.
@@ -433,7 +433,7 @@ end
433
433
  Callbacks are always deferred — they run after the outermost operation boundary succeeds:
434
434
 
435
435
  - **Transactional operations:** deferred until the DB transaction commits.
436
- - **Non-transactional operations:** queued in memory, flushed after the operation pipeline completes successfully.
436
+ - **Non-transactional operations (including Mongoid-only):** queued in memory, flushed after the operation pipeline completes successfully.
437
437
  - **Nested operations:** callbacks queue up and flush once at the outermost successful boundary.
438
438
  - **On error (`error!` or exception):** queued callbacks are discarded.
439
439
 
@@ -441,13 +441,11 @@ Multiple blocks run in registration order.
441
441
 
442
442
  **ActiveRecord:** requires Rails 7.2+ (`after_all_transactions_commit`).
443
443
 
444
- **Mongoid:** deferred across nested Dex operations. Ambient `Mongoid.transaction` blocks opened outside Dex are not detected — callbacks will fire immediately in that case.
445
-
446
444
  ---
447
445
 
448
446
  ## Advisory Locking
449
447
 
450
- Mutual exclusion via database advisory locks (requires `with_advisory_lock` gem). Wraps **outside** the transaction.
448
+ Mutual exclusion via database advisory locks (requires `with_advisory_lock` gem). Wraps **outside** the transaction. ActiveRecord-only; Mongoid-only apps get a clear `LoadError`.
451
449
 
452
450
  ```ruby
453
451
  advisory_lock { "pay:#{charge_id}" } # dynamic key from props
@@ -510,7 +508,17 @@ record result: false # params only
510
508
  record params: false # result only
511
509
  ```
512
510
 
513
- All outcomes are recorded — success (`completed`), business errors (`error`), and exceptions (`failed`). Recording runs outside the operation's own transaction so error records survive its rollbacks. Records still participate in ambient transactions (e.g., an outer operation's transaction). Missing columns silently skipped.
511
+ All outcomes are recorded — success (`completed`), business errors (`error`), and exceptions (`failed`). Recording runs outside the operation's own transaction so error records survive its rollbacks. Records still participate in ambient transactions (e.g., an outer operation's transaction). Dex validates the configured record model before use and raises if required attributes are missing.
512
+
513
+ Required attributes by feature:
514
+
515
+ - Core recording: `name`, `status`, `error_code`, `error_message`, `error_details`, `performed_at`
516
+ - Params capture: `params` unless `record params: false`
517
+ - Result capture: `result` unless `record result: false`
518
+ - Async record jobs: `params`
519
+ - `once`: `once_key`, plus `once_key_expires_at` when `expires_in:` is used
520
+
521
+ Untyped results are sanitized to JSON-safe values before persistence: Hash keys round-trip as strings, and objects fall back to `as_json`/`to_s` under `"_dex_value"`.
514
522
 
515
523
  Status values: `pending` (async enqueued), `running` (async executing), `completed` (success), `error` (business error via `error!`), `failed` (unhandled exception).
516
524
 
@@ -574,7 +582,7 @@ Clearing is idempotent — clearing a non-existent key is a no-op. After clearin
574
582
  **Requirements:**
575
583
 
576
584
  - Record backend must be configured (`Dex.configure { |c| c.record_class = OperationRecord }`)
577
- - The record table must have `once_key` and `once_key_expires_at` columns (see Recording schema above)
585
+ - The record backend must satisfy the Recording requirements above, and `once` additionally requires `once_key` plus `once_key_expires_at` when `expires_in:` is used
578
586
  - `once` cannot be declared with `record false` — raises `ArgumentError`
579
587
  - Only one `once` declaration per operation
580
588
 
@@ -586,11 +594,10 @@ Clearing is idempotent — clearing a non-existent key is a no-op. After clearin
586
594
  # config/initializers/dexkit.rb
587
595
  Dex.configure do |config|
588
596
  config.record_class = OperationRecord # model for recording (default: nil)
589
- config.transaction_adapter = nil # auto-detect (default); or :active_record / :mongoid
590
597
  end
591
598
  ```
592
599
 
593
- All DSL methods validate arguments at declaration time — typos and wrong types raise `ArgumentError` immediately (e.g., `error "string"`, `async priority: 5`, `transaction :redis`).
600
+ All DSL methods validate arguments at declaration time — typos and wrong types raise `ArgumentError` immediately (e.g., `error "string"`, `async priority: 5`, `transaction :redis`). Only `:active_record` is a valid transaction adapter.
594
601
 
595
602
  ---
596
603
 
@@ -598,22 +605,20 @@ All DSL methods validate arguments at declaration time — typos and wrong types
598
605
 
599
606
  ```ruby
600
607
  # test/test_helper.rb
601
- require "dex/test_helpers"
608
+ require "dex/operation/test_helpers"
602
609
 
603
610
  class Minitest::Test
604
- include Dex::TestHelpers
611
+ include Dex::Operation::TestHelpers
605
612
  end
606
613
  ```
607
614
 
608
615
  Not autoloaded — stays out of production. TestLog and stubs are auto-cleared in `setup`.
609
616
 
610
- For Mongoid-backed operation tests, run against a MongoDB replica set (MongoDB transactions require it).
611
-
612
617
  ### Subject & Execution
613
618
 
614
619
  ```ruby
615
620
  class CreateUserTest < Minitest::Test
616
- include Dex::TestHelpers
621
+ include Dex::Operation::TestHelpers
617
622
 
618
623
  testing CreateUser # default for all helpers
619
624
 
@@ -778,7 +783,7 @@ Each entry has: `name`, `operation_class`, `params`, `result` (Ok/Err), `duratio
778
783
 
779
784
  ```ruby
780
785
  class CreateUserTest < Minitest::Test
781
- include Dex::TestHelpers
786
+ include Dex::Operation::TestHelpers
782
787
 
783
788
  testing CreateUser
784
789
 
data/guides/llm/QUERY.md CHANGED
@@ -91,7 +91,7 @@ Reserved prop names: `scope`, `sort`, `resolve`, `call`, `from_params`, `to_para
91
91
  | `:in` | `IN (values)` | `filter :roles, :in, column: :role` |
92
92
  | `:not_in` | `NOT IN (values)` | `filter :roles, :not_in, column: :role` |
93
93
 
94
- String strategies (`:contains`, `:starts_with`, `:ends_with`) use case-insensitive matching. With ActiveRecord, this uses Arel `matches` (LIKE); with Mongoid, case-insensitive regex. Wildcards in values are auto-sanitized. The adapter is auto-detected from the scope.
94
+ String strategies (`:contains`, `:starts_with`, `:ends_with`) use case-insensitive matching. With ActiveRecord, this uses Arel `matches` (LIKE); with Mongoid, case-insensitive regex. Wildcards in values are auto-sanitized. The adapter is auto-detected from the scope, and Mongoid association scopes/proxies are normalized to `Mongoid::Criteria` automatically.
95
95
 
96
96
  ### Column Mapping
97
97
 
@@ -159,7 +159,7 @@ Only one default per class. Applied when no sort is provided.
159
159
 
160
160
  ### `.call`
161
161
 
162
- Returns a queryable scope (`ActiveRecord::Relation` or `Mongoid::Criteria`):
162
+ Returns a queryable scope (`ActiveRecord::Relation` or `Mongoid::Criteria`). Association scopes like `current_user.posts` work too:
163
163
 
164
164
  ```ruby
165
165
  users = UserSearch.call(name: "ali", role: %w[admin], sort: "-name")
data/lib/dex/event/bus.rb CHANGED
@@ -81,6 +81,7 @@ module Dex
81
81
  end
82
82
 
83
83
  def enqueue(handler_class, event, trace_data)
84
+ ensure_active_job_loaded!
84
85
  ctx = event.context
85
86
 
86
87
  Dex::Event::Processor.perform_later(
@@ -92,6 +93,12 @@ module Dex
92
93
  context: ctx
93
94
  )
94
95
  end
96
+
97
+ def ensure_active_job_loaded!
98
+ return if defined?(ActiveJob::Base)
99
+
100
+ raise LoadError, "ActiveJob is required for async event handlers. Add 'activejob' to your Gemfile."
101
+ end
95
102
  end
96
103
  end
97
104
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dex
4
+ class Event
5
+ module EventTestWrapper
6
+ CAPTURING_KEY = :_dex_event_capturing
7
+ PUBLISHED_KEY = :_dex_event_published
8
+
9
+ @_installed = false
10
+
11
+ class << self
12
+ include ExecutionState
13
+
14
+ def install!
15
+ return if @_installed
16
+
17
+ Dex::Event::Bus.singleton_class.prepend(BusInterceptor)
18
+ @_installed = true
19
+ end
20
+
21
+ def installed?
22
+ @_installed
23
+ end
24
+
25
+ def capturing?
26
+ (_execution_state[CAPTURING_KEY] || 0) > 0
27
+ end
28
+
29
+ def begin_capture!
30
+ _execution_state[CAPTURING_KEY] = (_execution_state[CAPTURING_KEY] || 0) + 1
31
+ end
32
+
33
+ def end_capture!
34
+ depth = (_execution_state[CAPTURING_KEY] || 0) - 1
35
+ _execution_state[CAPTURING_KEY] = [depth, 0].max
36
+ end
37
+
38
+ def published_events
39
+ _execution_state[PUBLISHED_KEY] ||= []
40
+ end
41
+
42
+ def clear_published!
43
+ _execution_state[PUBLISHED_KEY] = []
44
+ end
45
+ end
46
+
47
+ module BusInterceptor
48
+ def publish(event, sync:)
49
+ if Dex::Event::EventTestWrapper.capturing?
50
+ return if Dex::Event::Suppression.suppressed?(event.class)
51
+
52
+ Dex::Event::EventTestWrapper.published_events << event
53
+ else
54
+ super(event, sync: true)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ module TestHelpers
61
+ def self.included(base)
62
+ EventTestWrapper.install!
63
+ end
64
+
65
+ def setup
66
+ super
67
+ EventTestWrapper.clear_published!
68
+ Dex::Event::Trace.clear!
69
+ Dex::Event::Suppression.clear!
70
+ end
71
+
72
+ def capture_events
73
+ EventTestWrapper.begin_capture!
74
+ yield
75
+ ensure
76
+ EventTestWrapper.end_capture!
77
+ end
78
+
79
+ private
80
+
81
+ def _dex_published_events
82
+ EventTestWrapper.published_events
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ require_relative "test_helpers/assertions"