rails_ops 1.7.7 → 1.8.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 +24 -0
- data/Gemfile.lock +5 -5
- data/README.md +566 -24
- data/VERSION +1 -1
- data/lib/rails_ops/operation.rb +51 -34
- data/lib/rails_ops/railtie.rb +6 -4
- data/lib/rails_ops.rb +1 -0
- data/rails_ops.gemspec +5 -5
- data/test/unit/rails_ops/operation_test.rb +161 -1
- data/test/unit/rails_ops/railtie_test.rb +29 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d983d20a0a0bf3e1b7e967d0dbd16aec64f253c8a7a12e7c6b72f9f6f676b604
|
|
4
|
+
data.tar.gz: 00a7b268b2337ba5b2cf57855b9d611a7c9dbe3492cd9484345ad1f7956b2b27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a50ef1e82a5a82e1adddb76435389babd2f34a6fe3e4f80c06bf3ec061b26235fa80541cfbfdab6fdc43f253245b388b1b376d8032e090c510fe1a4f2edc591
|
|
7
|
+
data.tar.gz: fca76d3c3c65cc564744596dd298e8a52e69320cd0eb5952750e689dc879b21dce5dab13dd6372456108ea367aa536a62e40a58d4c069af5ab78eb214167c3be
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.8.0 (2026-05-07)
|
|
4
|
+
|
|
5
|
+
* `Operation#run` (non-bang) now wraps the call to `run!` in a
|
|
6
|
+
SAVEPOINT whenever a database transaction is already open. This
|
|
7
|
+
ensures that partial writes performed before a validation error are
|
|
8
|
+
rolled back even though `run` swallows the error and returns
|
|
9
|
+
`false`. `run_sub` benefits from the same protection transitively.
|
|
10
|
+
Behavior outside of an open transaction is unchanged, and the
|
|
11
|
+
behavior of `run!` / `run_sub!` is unchanged.
|
|
12
|
+
* Deprecate `with_rollback_on_exception`. The helper now emits an
|
|
13
|
+
`ActiveSupport::Deprecation` warning via `RailsOps.deprecator` and
|
|
14
|
+
will be removed in RailsOps 2.0. The savepoint added to `run` makes
|
|
15
|
+
it obsolete for the common "save then do more work" pattern. For
|
|
16
|
+
non-validation errors that should trigger a rollback, raise
|
|
17
|
+
`RailsOps::Exceptions::RollbackRequired` directly.
|
|
18
|
+
|
|
19
|
+
## 1.7.8 (2026-03-12)
|
|
20
|
+
|
|
21
|
+
* Add practical examples throughout README covering non-model operations,
|
|
22
|
+
find-or-create patterns, state validation policies, sub-operation
|
|
23
|
+
composition, complete CRUD sets, model defaults, custom authorization
|
|
24
|
+
actions, virtual datetime fields, operation inheritance, and load
|
|
25
|
+
operations for show pages.
|
|
26
|
+
|
|
3
27
|
## 1.7.7 (2026-02-24)
|
|
4
28
|
|
|
5
29
|
* Fix `find_model_relation` to merge the returned relation's conditions
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
rails_ops (1.
|
|
4
|
+
rails_ops (1.8.0)
|
|
5
5
|
active_type (>= 1.3.0)
|
|
6
6
|
minitest
|
|
7
7
|
rails (> 4)
|
|
@@ -132,7 +132,7 @@ GEM
|
|
|
132
132
|
mini_mime (1.1.5)
|
|
133
133
|
minitest (5.27.0)
|
|
134
134
|
mutex_m (0.3.0)
|
|
135
|
-
net-imap (0.4.
|
|
135
|
+
net-imap (0.4.24)
|
|
136
136
|
date
|
|
137
137
|
net-protocol
|
|
138
138
|
net-pop (0.1.2)
|
|
@@ -158,8 +158,8 @@ GEM
|
|
|
158
158
|
psych (5.1.2)
|
|
159
159
|
stringio
|
|
160
160
|
racc (1.8.1)
|
|
161
|
-
rack (3.2.
|
|
162
|
-
rack-session (2.1.
|
|
161
|
+
rack (3.2.6)
|
|
162
|
+
rack-session (2.1.2)
|
|
163
163
|
base64 (>= 0.1.0)
|
|
164
164
|
rack (>= 3.0.0)
|
|
165
165
|
rack-test (2.1.0)
|
|
@@ -241,7 +241,7 @@ GEM
|
|
|
241
241
|
sqlite3 (1.6.2-x86_64-linux)
|
|
242
242
|
stringio (3.1.0)
|
|
243
243
|
thor (1.3.1)
|
|
244
|
-
timeout (0.6.
|
|
244
|
+
timeout (0.6.1)
|
|
245
245
|
tzinfo (2.0.6)
|
|
246
246
|
concurrent-ruby (~> 1.0)
|
|
247
247
|
unicode-display_width (3.1.4)
|
data/README.md
CHANGED
|
@@ -245,6 +245,78 @@ end
|
|
|
245
245
|
puts Operations::GenerateHelloWorld.run!(name: 'John Doe').result
|
|
246
246
|
```
|
|
247
247
|
|
|
248
|
+
#### Practical Example: Non-Model Business Logic Operation
|
|
249
|
+
|
|
250
|
+
Operations don't have to involve models. Use the base `RailsOps::Operation`
|
|
251
|
+
class for business logic, background tasks, or service calls:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
module Operations::Cache
|
|
255
|
+
class Rebuild < RailsOps::Operation
|
|
256
|
+
schema3 do
|
|
257
|
+
boo? :include_archived, default: false
|
|
258
|
+
boo? :rebuild_counters, default: true
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Internal use only (called from background jobs)
|
|
262
|
+
without_authorization
|
|
263
|
+
|
|
264
|
+
protected
|
|
265
|
+
|
|
266
|
+
def perform
|
|
267
|
+
rebuild_counters if osparams.rebuild_counters
|
|
268
|
+
rebuild_search_index
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
private
|
|
272
|
+
|
|
273
|
+
def rebuild_counters
|
|
274
|
+
Category.find_each do |category|
|
|
275
|
+
Category.reset_counters(category.id, :articles)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def rebuild_search_index
|
|
280
|
+
Article.where(indexed: false).find_each(&:update_search_index!)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### Practical Example: Find-or-Create Pattern
|
|
287
|
+
|
|
288
|
+
A common pattern for idempotent operations that either find an existing record
|
|
289
|
+
or create a new one. Since this doesn't fit neatly into `Model::Create` or
|
|
290
|
+
`Model::Load`, use the base `RailsOps::Operation` and expose the result via
|
|
291
|
+
`attr_reader`:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
module Operations::Tag
|
|
295
|
+
class FindOrCreate < RailsOps::Operation
|
|
296
|
+
schema3 do
|
|
297
|
+
str! :name
|
|
298
|
+
str? :color
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
without_authorization
|
|
302
|
+
|
|
303
|
+
attr_reader :model
|
|
304
|
+
|
|
305
|
+
protected
|
|
306
|
+
|
|
307
|
+
def perform
|
|
308
|
+
@model = ::Tag.find_or_create_by!(name: osparams.name) do |tag|
|
|
309
|
+
tag.color = osparams.color || '#000000'
|
|
310
|
+
end
|
|
311
|
+
rescue ActiveRecord::RecordNotUnique
|
|
312
|
+
# Race condition: another process created the record between
|
|
313
|
+
# our SELECT and INSERT. Just find the existing one.
|
|
314
|
+
@model = ::Tag.find_by!(name: osparams.name)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
|
|
248
320
|
## Params Handling
|
|
249
321
|
|
|
250
322
|
### Passing Params to Operations
|
|
@@ -526,6 +598,51 @@ In this case the model is not yet set. That will happen later in the `:on_init`
|
|
|
526
598
|
It is also important to note, that this block is
|
|
527
599
|
not guaranteed to be run first in the chain, if multiple blocks have set `:prepend_action` to true.
|
|
528
600
|
|
|
601
|
+
#### Practical Example: State Validation Policies
|
|
602
|
+
|
|
603
|
+
Use policies to validate preconditions based on the model's state. This is
|
|
604
|
+
particularly useful in model operations where you want to reject the operation
|
|
605
|
+
before `perform` runs:
|
|
606
|
+
|
|
607
|
+
```ruby
|
|
608
|
+
module Operations::Article
|
|
609
|
+
class Publish < RailsOps::Operation::Model::Update
|
|
610
|
+
schema3 do
|
|
611
|
+
int! :id
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
model ::Article
|
|
615
|
+
|
|
616
|
+
# Ensure the article is still a draft before publishing.
|
|
617
|
+
# Runs at instantiation time (i.e. when the model is loaded).
|
|
618
|
+
policy :on_init do
|
|
619
|
+
unless model.draft?
|
|
620
|
+
fail 'Only draft articles can be published.'
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Ensure the article has required content.
|
|
625
|
+
# Runs just before perform is called.
|
|
626
|
+
policy do
|
|
627
|
+
if model.body.blank?
|
|
628
|
+
fail RailsOps::Exceptions::ValidationFailed,
|
|
629
|
+
'Article body cannot be empty.'
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
protected
|
|
634
|
+
|
|
635
|
+
def perform
|
|
636
|
+
model.status = 'published'
|
|
637
|
+
model.published_at = Time.current
|
|
638
|
+
super
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Use `:on_init` for checks that should prevent even *displaying* a form, and
|
|
645
|
+
`:before_perform` (the default) for checks that should prevent *submitting* it.
|
|
529
646
|
|
|
530
647
|
## Calling Sub-Operations
|
|
531
648
|
|
|
@@ -579,6 +696,47 @@ catches any validation errors and re-throws them as
|
|
|
579
696
|
{RailsOps::Exceptions::SubOpValidationFailed} which is not caught by the
|
|
580
697
|
surrounding op.
|
|
581
698
|
|
|
699
|
+
#### Practical Example: Composing Operations
|
|
700
|
+
|
|
701
|
+
Here is a realistic example of an operation that uses sub-operations to compose
|
|
702
|
+
a complex workflow:
|
|
703
|
+
|
|
704
|
+
Building on the `Article::Publish` example from the *Policies* section, here
|
|
705
|
+
is a version that also triggers sub-operations after publishing:
|
|
706
|
+
|
|
707
|
+
```ruby
|
|
708
|
+
module Operations::Article
|
|
709
|
+
class PublishWithNotification < RailsOps::Operation::Model::Update
|
|
710
|
+
schema3 do
|
|
711
|
+
int! :id
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
model ::Article
|
|
715
|
+
|
|
716
|
+
protected
|
|
717
|
+
|
|
718
|
+
def perform
|
|
719
|
+
model.status = 'published'
|
|
720
|
+
model.published_at = Time.current
|
|
721
|
+
super # Save the article
|
|
722
|
+
|
|
723
|
+
run_sub! Operations::Cache::Rebuild, rebuild_counters: true
|
|
724
|
+
run_sub! Operations::Notification::Send,
|
|
725
|
+
template: 'article_published',
|
|
726
|
+
record_id: model.id,
|
|
727
|
+
record_type: 'Article'
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
end
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Validation errors raised by either sub-operation propagate up — `run_sub!`
|
|
734
|
+
re-raises any `validation_errors` as
|
|
735
|
+
`RailsOps::Exceptions::SubOpValidationFailed`, which escapes the parent's
|
|
736
|
+
`run` and rolls back the surrounding transaction. If the parent is invoked
|
|
737
|
+
via `run` (non-bang), the savepoint added in 1.8.0 ensures `super`'s save
|
|
738
|
+
is rolled back too. See section *Transactions* for more details.
|
|
739
|
+
|
|
582
740
|
## Contexts
|
|
583
741
|
|
|
584
742
|
Most operations make use of generic parameters like the current user or an
|
|
@@ -1046,7 +1204,6 @@ Rails Ops offers multiple ways of disabling authorization:
|
|
|
1046
1204
|
end
|
|
1047
1205
|
```
|
|
1048
1206
|
|
|
1049
|
-
|
|
1050
1207
|
## Model Operations
|
|
1051
1208
|
|
|
1052
1209
|
One of the key features of RailsOps is model operations. RailsOps provides
|
|
@@ -1360,6 +1517,124 @@ end
|
|
|
1360
1517
|
As this base class is very minimalistic, it is recommended to fully read and
|
|
1361
1518
|
comprehend its source code.
|
|
1362
1519
|
|
|
1520
|
+
### Practical Example: Complete CRUD Operation Set
|
|
1521
|
+
|
|
1522
|
+
Here is a complete, minimal CRUD set for a single model using `schema3`:
|
|
1523
|
+
|
|
1524
|
+
```ruby
|
|
1525
|
+
# app/operations/category/load.rb
|
|
1526
|
+
module Operations::Category
|
|
1527
|
+
class Load < RailsOps::Operation::Model::Load
|
|
1528
|
+
schema3 do
|
|
1529
|
+
int! :id
|
|
1530
|
+
end
|
|
1531
|
+
|
|
1532
|
+
model ::Category
|
|
1533
|
+
end
|
|
1534
|
+
end
|
|
1535
|
+
|
|
1536
|
+
# app/operations/category/create.rb
|
|
1537
|
+
module Operations::Category
|
|
1538
|
+
class Create < RailsOps::Operation::Model::Create
|
|
1539
|
+
schema3 do
|
|
1540
|
+
hsh? :category do
|
|
1541
|
+
str? :name
|
|
1542
|
+
str? :description
|
|
1543
|
+
end
|
|
1544
|
+
end
|
|
1545
|
+
|
|
1546
|
+
model ::Category
|
|
1547
|
+
end
|
|
1548
|
+
end
|
|
1549
|
+
|
|
1550
|
+
# app/operations/category/update.rb
|
|
1551
|
+
module Operations::Category
|
|
1552
|
+
class Update < RailsOps::Operation::Model::Update
|
|
1553
|
+
schema3 do
|
|
1554
|
+
int! :id
|
|
1555
|
+
hsh? :category do
|
|
1556
|
+
str? :name
|
|
1557
|
+
str? :description
|
|
1558
|
+
end
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
model ::Category
|
|
1562
|
+
end
|
|
1563
|
+
end
|
|
1564
|
+
|
|
1565
|
+
# app/operations/category/destroy.rb
|
|
1566
|
+
module Operations::Category
|
|
1567
|
+
class Destroy < RailsOps::Operation::Model::Destroy
|
|
1568
|
+
schema3 do
|
|
1569
|
+
int! :id
|
|
1570
|
+
end
|
|
1571
|
+
|
|
1572
|
+
model ::Category
|
|
1573
|
+
end
|
|
1574
|
+
end
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
For `Create` and `Update`, parameter extraction happens automatically: the
|
|
1578
|
+
params nested under the model's `param_key` (`:category`) are assigned to the
|
|
1579
|
+
model. No `perform` method is needed — the base class handles `save!`.
|
|
1580
|
+
|
|
1581
|
+
### Practical Example: Setting Defaults on Create
|
|
1582
|
+
|
|
1583
|
+
Override `build_model` to set default values or assign associations that aren't
|
|
1584
|
+
part of the user's input:
|
|
1585
|
+
|
|
1586
|
+
```ruby
|
|
1587
|
+
module Operations::Article
|
|
1588
|
+
class Create < RailsOps::Operation::Model::Create
|
|
1589
|
+
schema3 do
|
|
1590
|
+
hsh? :article do
|
|
1591
|
+
str? :title
|
|
1592
|
+
str? :body
|
|
1593
|
+
int? :category_id
|
|
1594
|
+
end
|
|
1595
|
+
end
|
|
1596
|
+
|
|
1597
|
+
model ::Article
|
|
1598
|
+
|
|
1599
|
+
protected
|
|
1600
|
+
|
|
1601
|
+
def build_model
|
|
1602
|
+
super # Builds the model and assigns params from :article key
|
|
1603
|
+
model.author = context.user
|
|
1604
|
+
model.status = 'draft'
|
|
1605
|
+
end
|
|
1606
|
+
end
|
|
1607
|
+
end
|
|
1608
|
+
```
|
|
1609
|
+
|
|
1610
|
+
`super` in `build_model` creates a new model instance and assigns the
|
|
1611
|
+
attributes from params. After `super`, you can set additional attributes.
|
|
1612
|
+
|
|
1613
|
+
### Practical Example: Overriding `build_model` in Update
|
|
1614
|
+
|
|
1615
|
+
In `Update` operations, `build_model` first loads the record (via `Load`), then
|
|
1616
|
+
assigns the params. Override it to modify the model after loading:
|
|
1617
|
+
|
|
1618
|
+
```ruby
|
|
1619
|
+
module Operations::Token
|
|
1620
|
+
class MarkUsed < RailsOps::Operation::Model::Update
|
|
1621
|
+
schema3 do
|
|
1622
|
+
int! :id
|
|
1623
|
+
end
|
|
1624
|
+
|
|
1625
|
+
model ::Token
|
|
1626
|
+
without_authorization
|
|
1627
|
+
|
|
1628
|
+
protected
|
|
1629
|
+
|
|
1630
|
+
def build_model
|
|
1631
|
+
super # Loads the record and assigns params
|
|
1632
|
+
model.used_at = Time.current
|
|
1633
|
+
end
|
|
1634
|
+
end
|
|
1635
|
+
end
|
|
1636
|
+
```
|
|
1637
|
+
|
|
1363
1638
|
### Including Associated Records
|
|
1364
1639
|
|
|
1365
1640
|
Normally, when inheriting from `RailsOps::Operation::Model::Load` (as well as from the
|
|
@@ -1388,6 +1663,46 @@ class Operations::User::Load < RailsOps::Operation::Model::Load
|
|
|
1388
1663
|
end
|
|
1389
1664
|
```
|
|
1390
1665
|
|
|
1666
|
+
#### Practical Example: Load Operation for Show Pages
|
|
1667
|
+
|
|
1668
|
+
A common pattern for "show" pages: a `Load` operation that provides helper
|
|
1669
|
+
methods for loading related data in the view:
|
|
1670
|
+
|
|
1671
|
+
```ruby
|
|
1672
|
+
module Operations::Frontend::Articles
|
|
1673
|
+
class Show < RailsOps::Operation::Model::Load
|
|
1674
|
+
model ::Article
|
|
1675
|
+
model_includes [:tags, :category, { comments: :author }]
|
|
1676
|
+
|
|
1677
|
+
def recent_comments(limit: 10)
|
|
1678
|
+
model.comments.order(created_at: :desc).limit(limit)
|
|
1679
|
+
end
|
|
1680
|
+
|
|
1681
|
+
def related_articles
|
|
1682
|
+
@related_articles ||= ::Article
|
|
1683
|
+
.where(category_id: model.category_id)
|
|
1684
|
+
.where.not(id: model.id)
|
|
1685
|
+
.limit(5)
|
|
1686
|
+
end
|
|
1687
|
+
|
|
1688
|
+
protected
|
|
1689
|
+
|
|
1690
|
+
# Load operations don't need perform logic, but the base class
|
|
1691
|
+
# raises NotImplementedError, so we override with a no-op.
|
|
1692
|
+
def perform; end
|
|
1693
|
+
end
|
|
1694
|
+
end
|
|
1695
|
+
```
|
|
1696
|
+
|
|
1697
|
+
In the controller:
|
|
1698
|
+
|
|
1699
|
+
```ruby
|
|
1700
|
+
def show
|
|
1701
|
+
op Operations::Frontend::Articles::Show
|
|
1702
|
+
# In the view: op.model, op.recent_comments, op.related_articles
|
|
1703
|
+
end
|
|
1704
|
+
```
|
|
1705
|
+
|
|
1391
1706
|
### Parameter Extraction for Create and Update
|
|
1392
1707
|
|
|
1393
1708
|
As mentioned before, the `Create` and `Update` base classes provide an
|
|
@@ -1483,6 +1798,43 @@ class Operations::User::Update < RailsOps::Operation::Model::Update
|
|
|
1483
1798
|
end
|
|
1484
1799
|
```
|
|
1485
1800
|
|
|
1801
|
+
#### Practical Example: Custom Authorization Actions
|
|
1802
|
+
|
|
1803
|
+
For operations beyond standard CRUD (e.g., archiving, classifying, publishing),
|
|
1804
|
+
specify custom authorization actions:
|
|
1805
|
+
|
|
1806
|
+
```ruby
|
|
1807
|
+
module Operations::Article
|
|
1808
|
+
class Archive < RailsOps::Operation::Model::Update
|
|
1809
|
+
schema3 do
|
|
1810
|
+
int! :id
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
load_model_authorization_action :read
|
|
1814
|
+
model_authorization_action :archive
|
|
1815
|
+
|
|
1816
|
+
model ::Article
|
|
1817
|
+
|
|
1818
|
+
protected
|
|
1819
|
+
|
|
1820
|
+
def perform
|
|
1821
|
+
model.archived = true
|
|
1822
|
+
model.archived_at = Time.current
|
|
1823
|
+
model.archived_by = context.user
|
|
1824
|
+
super
|
|
1825
|
+
end
|
|
1826
|
+
end
|
|
1827
|
+
end
|
|
1828
|
+
```
|
|
1829
|
+
|
|
1830
|
+
In your ability file, define the custom action:
|
|
1831
|
+
|
|
1832
|
+
```ruby
|
|
1833
|
+
can :archive, Article do |article|
|
|
1834
|
+
article.author_id == user.id || user.admin?
|
|
1835
|
+
end
|
|
1836
|
+
```
|
|
1837
|
+
|
|
1486
1838
|
### Model Nesting
|
|
1487
1839
|
|
|
1488
1840
|
Using active record, multiple nested models can be saved at once by using
|
|
@@ -1738,6 +2090,51 @@ class Operations::Order::Checkout < RailsOps::Operation::Model::Update
|
|
|
1738
2090
|
end
|
|
1739
2091
|
```
|
|
1740
2092
|
|
|
2093
|
+
#### Practical Example: Virtual Datetime Fields for Forms
|
|
2094
|
+
|
|
2095
|
+
A very common use case is adding virtual datetime attributes for form inputs
|
|
2096
|
+
that need to be transformed before saving:
|
|
2097
|
+
|
|
2098
|
+
```ruby
|
|
2099
|
+
module Operations::Event
|
|
2100
|
+
class Create < RailsOps::Operation::Model::Create
|
|
2101
|
+
schema3 do
|
|
2102
|
+
hsh? :event do
|
|
2103
|
+
str? :title
|
|
2104
|
+
str? :virtual_start_datetime
|
|
2105
|
+
str? :virtual_end_datetime
|
|
2106
|
+
boo? :all_day
|
|
2107
|
+
end
|
|
2108
|
+
end
|
|
2109
|
+
|
|
2110
|
+
model ::Event do
|
|
2111
|
+
attribute :virtual_start_datetime, :datetime
|
|
2112
|
+
attribute :virtual_end_datetime, :datetime
|
|
2113
|
+
|
|
2114
|
+
validates :virtual_start_datetime, presence: true, unless: :all_day?
|
|
2115
|
+
validates :virtual_end_datetime, presence: true, unless: :all_day?
|
|
2116
|
+
validates :virtual_end_datetime,
|
|
2117
|
+
comparison: { greater_than_or_equal_to: :virtual_start_datetime },
|
|
2118
|
+
if: -> { !all_day? && virtual_start_datetime.present? }
|
|
2119
|
+
end
|
|
2120
|
+
|
|
2121
|
+
protected
|
|
2122
|
+
|
|
2123
|
+
def build_model
|
|
2124
|
+
super
|
|
2125
|
+
|
|
2126
|
+
if model.all_day?
|
|
2127
|
+
model.start_date = model.virtual_start_datetime&.beginning_of_day
|
|
2128
|
+
model.end_date = model.virtual_end_datetime&.end_of_day
|
|
2129
|
+
else
|
|
2130
|
+
model.start_date = model.virtual_start_datetime
|
|
2131
|
+
model.end_date = model.virtual_end_datetime
|
|
2132
|
+
end
|
|
2133
|
+
end
|
|
2134
|
+
end
|
|
2135
|
+
end
|
|
2136
|
+
```
|
|
2137
|
+
|
|
1741
2138
|
### Combining Real and Virtual Models
|
|
1742
2139
|
|
|
1743
2140
|
You can create operations that work with both persisted and virtual data:
|
|
@@ -1811,28 +2208,67 @@ end
|
|
|
1811
2208
|
Typically though, transactions are opened on a higher level and outside of
|
|
1812
2209
|
operations, e.g. in controller methods.
|
|
1813
2210
|
|
|
1814
|
-
###
|
|
2211
|
+
### Automatic Savepoint Around `run`
|
|
1815
2212
|
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
2213
|
+
Since version 1.8.0, `RailsOps::Operation#run` (the non-bang variant)
|
|
2214
|
+
automatically wraps the operation in a SAVEPOINT whenever a database
|
|
2215
|
+
transaction is already open. If the operation raises a validation error
|
|
2216
|
+
partway through `perform`, the savepoint is rolled back before `run`
|
|
2217
|
+
catches the error and returns `false`. This eliminates the most common
|
|
2218
|
+
reason to reach for `with_rollback_on_exception`.
|
|
1820
2219
|
|
|
1821
2220
|
```ruby
|
|
1822
|
-
class Operations::User::
|
|
2221
|
+
class Operations::User::Create < RailsOps::Operation::Model::Create
|
|
1823
2222
|
def perform
|
|
1824
|
-
|
|
1825
|
-
|
|
2223
|
+
super # Saves the user
|
|
2224
|
+
something_else! # If this raises ActiveRecord::RecordInvalid, the
|
|
2225
|
+
# save above is rolled back automatically when the
|
|
2226
|
+
# operation is invoked through `run`.
|
|
2227
|
+
end
|
|
2228
|
+
end
|
|
1826
2229
|
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2230
|
+
# Caller (controller, job, etc.):
|
|
2231
|
+
ActiveRecord::Base.transaction do
|
|
2232
|
+
if Operations::User::Create.run(user: { name: 'Alice' })
|
|
2233
|
+
# success path
|
|
2234
|
+
else
|
|
2235
|
+
# validation error — partial writes have already been rolled back
|
|
2236
|
+
end
|
|
2237
|
+
end
|
|
2238
|
+
```
|
|
1832
2239
|
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2240
|
+
A savepoint is only created if a transaction is already open at the time
|
|
2241
|
+
of the call. Without an outer transaction (rake tasks, console sessions,
|
|
2242
|
+
some background jobs), `run` calls `run!` directly and behavior is
|
|
2243
|
+
unchanged: the caller is expected to open a transaction if it wants
|
|
2244
|
+
atomicity. `run!` itself is never wrapped — exceptions propagate naturally
|
|
2245
|
+
and the surrounding transaction (if any) rolls back.
|
|
2246
|
+
|
|
2247
|
+
`run_sub` (non-bang) benefits from the same protection transitively, since
|
|
2248
|
+
it delegates to `.run` on the sub-operation.
|
|
2249
|
+
|
|
2250
|
+
### Rollback on Exception (Non-Validation Errors)
|
|
2251
|
+
|
|
2252
|
+
The savepoint above only protects against **validation errors**
|
|
2253
|
+
(`RailsOps::Exceptions::ValidationFailed` and
|
|
2254
|
+
`ActiveRecord::RecordInvalid`). For other `StandardError` subclasses that
|
|
2255
|
+
should also trigger a rollback when `run` is used, the
|
|
2256
|
+
`with_rollback_on_exception` helper re-raises them as
|
|
2257
|
+
`RailsOps::Exceptions::RollbackRequired`, which is not part of
|
|
2258
|
+
`validation_errors` and therefore propagates up through `run` and rolls
|
|
2259
|
+
back the surrounding transaction:
|
|
2260
|
+
|
|
2261
|
+
```ruby
|
|
2262
|
+
class Operations::User::ComplexUpdate < RailsOps::Operation::Model::Update
|
|
2263
|
+
def perform
|
|
2264
|
+
super # Saves the user — validation errors here are handled by the
|
|
2265
|
+
# automatic savepoint in `run`.
|
|
2266
|
+
|
|
2267
|
+
with_rollback_on_exception do
|
|
2268
|
+
ExternalApi.call!(model) # raises a custom StandardError on failure;
|
|
2269
|
+
# converted into RollbackRequired so the
|
|
2270
|
+
# outer transaction rolls back even when
|
|
2271
|
+
# the operation is invoked via `run`.
|
|
1836
2272
|
end
|
|
1837
2273
|
end
|
|
1838
2274
|
end
|
|
@@ -1851,7 +2287,7 @@ end
|
|
|
1851
2287
|
```
|
|
1852
2288
|
|
|
1853
2289
|
**Important**: `with_rollback_on_exception` only works within an existing
|
|
1854
|
-
transaction. It doesn't create a transaction
|
|
2290
|
+
transaction. It doesn't create a transaction — it just ensures exceptions
|
|
1855
2291
|
cause rollback:
|
|
1856
2292
|
|
|
1857
2293
|
```ruby
|
|
@@ -1861,7 +2297,9 @@ class Operations::Order::Process < RailsOps::Operation::Model::Update
|
|
|
1861
2297
|
super # Order is saved and committed in its own transaction
|
|
1862
2298
|
|
|
1863
2299
|
with_rollback_on_exception do
|
|
1864
|
-
model
|
|
2300
|
+
ExternalApi.charge!(model) # Raises a custom StandardError on failure;
|
|
2301
|
+
# the order has already been committed and
|
|
2302
|
+
# cannot be rolled back.
|
|
1865
2303
|
end
|
|
1866
2304
|
end
|
|
1867
2305
|
end
|
|
@@ -1870,10 +2308,13 @@ end
|
|
|
1870
2308
|
class Operations::Order::Process < RailsOps::Operation::Model::Update
|
|
1871
2309
|
def perform
|
|
1872
2310
|
ActiveRecord::Base.transaction do
|
|
1873
|
-
super # Order is saved
|
|
2311
|
+
super # Order is saved within the surrounding transaction
|
|
1874
2312
|
|
|
1875
2313
|
with_rollback_on_exception do
|
|
1876
|
-
model
|
|
2314
|
+
ExternalApi.charge!(model) # Custom StandardError is converted into
|
|
2315
|
+
# RollbackRequired, which propagates
|
|
2316
|
+
# through `run` and rolls back the
|
|
2317
|
+
# transaction including `super`.
|
|
1877
2318
|
end
|
|
1878
2319
|
end
|
|
1879
2320
|
end
|
|
@@ -1923,9 +2364,13 @@ complete.
|
|
|
1923
2364
|
|
|
1924
2365
|
### Important Notes on Transactions
|
|
1925
2366
|
|
|
1926
|
-
1. **Validation Errors**: When
|
|
1927
|
-
|
|
1928
|
-
|
|
2367
|
+
1. **Validation Errors**: When `run` (without bang) is called inside an
|
|
2368
|
+
open transaction, RailsOps wraps the operation in a SAVEPOINT so that
|
|
2369
|
+
any partial writes are rolled back before the caught validation error
|
|
2370
|
+
is converted into a `false` return value. `run_sub` benefits from the
|
|
2371
|
+
same protection. Use `run!` / `run_sub!` when you want validation
|
|
2372
|
+
errors to propagate and roll back the surrounding transaction
|
|
2373
|
+
directly.
|
|
1929
2374
|
|
|
1930
2375
|
2. **External Services**: Be careful when calling external services within
|
|
1931
2376
|
transactions. Long-running external calls can cause database locks:
|
|
@@ -2127,6 +2572,103 @@ sub-operations, see section *Calling sub-operations* for more information.
|
|
|
2127
2572
|
|
|
2128
2573
|
## Operation Inheritance
|
|
2129
2574
|
|
|
2575
|
+
Operations support standard Ruby class inheritance. This is useful when multiple
|
|
2576
|
+
models share the same operation pattern. Create an abstract base operation and
|
|
2577
|
+
then inherit from it for each model:
|
|
2578
|
+
|
|
2579
|
+
```ruby
|
|
2580
|
+
# app/operations/base/toggle_active.rb
|
|
2581
|
+
module Operations::Base
|
|
2582
|
+
class ToggleActive < RailsOps::Operation::Model::Update
|
|
2583
|
+
schema3 do
|
|
2584
|
+
int! :id
|
|
2585
|
+
end
|
|
2586
|
+
|
|
2587
|
+
protected
|
|
2588
|
+
|
|
2589
|
+
def perform
|
|
2590
|
+
model.active = !model.active
|
|
2591
|
+
super
|
|
2592
|
+
end
|
|
2593
|
+
end
|
|
2594
|
+
end
|
|
2595
|
+
|
|
2596
|
+
# app/operations/category/toggle_active.rb
|
|
2597
|
+
module Operations::Category
|
|
2598
|
+
class ToggleActive < Operations::Base::ToggleActive
|
|
2599
|
+
model ::Category
|
|
2600
|
+
end
|
|
2601
|
+
end
|
|
2602
|
+
|
|
2603
|
+
# app/operations/tag/toggle_active.rb
|
|
2604
|
+
module Operations::Tag
|
|
2605
|
+
class ToggleActive < Operations::Base::ToggleActive
|
|
2606
|
+
model ::Tag
|
|
2607
|
+
end
|
|
2608
|
+
end
|
|
2609
|
+
```
|
|
2610
|
+
|
|
2611
|
+
The base class defines the common schema and behavior. Subclasses only need to
|
|
2612
|
+
specify the `model`. This avoids duplicating logic across many operations.
|
|
2613
|
+
|
|
2614
|
+
Schemas, policies, and authorization settings are all inherited. Subclasses can
|
|
2615
|
+
add additional policies or override methods as needed.
|
|
2616
|
+
|
|
2617
|
+
### Practical Example: Bulk Insert Operation Base
|
|
2618
|
+
|
|
2619
|
+
Another common base class is for bulk insert operations:
|
|
2620
|
+
|
|
2621
|
+
```ruby
|
|
2622
|
+
module Operations::Base
|
|
2623
|
+
class BulkCreate < RailsOps::Operation
|
|
2624
|
+
BATCH_SIZE = 500
|
|
2625
|
+
|
|
2626
|
+
without_authorization
|
|
2627
|
+
|
|
2628
|
+
protected
|
|
2629
|
+
|
|
2630
|
+
def perform
|
|
2631
|
+
unique_ids = ids_to_insert.uniq
|
|
2632
|
+
return if unique_ids.empty?
|
|
2633
|
+
|
|
2634
|
+
now = Time.current
|
|
2635
|
+
|
|
2636
|
+
unique_ids.each_slice(self.class::BATCH_SIZE) do |batch|
|
|
2637
|
+
records = build_records(batch, now)
|
|
2638
|
+
target_class.insert_all!(records)
|
|
2639
|
+
rescue ActiveRecord::RecordNotUnique
|
|
2640
|
+
# Race condition: filter out already-existing records and retry
|
|
2641
|
+
existing = existing_ids_for(batch)
|
|
2642
|
+
new_records = records.reject { |r| existing.include?(r[id_column]) }
|
|
2643
|
+
target_class.insert_all!(new_records) if new_records.any?
|
|
2644
|
+
end
|
|
2645
|
+
end
|
|
2646
|
+
|
|
2647
|
+
private
|
|
2648
|
+
|
|
2649
|
+
def ids_to_insert
|
|
2650
|
+
fail NotImplementedError
|
|
2651
|
+
end
|
|
2652
|
+
|
|
2653
|
+
def build_records(_batch, _now)
|
|
2654
|
+
fail NotImplementedError
|
|
2655
|
+
end
|
|
2656
|
+
|
|
2657
|
+
def target_class
|
|
2658
|
+
fail NotImplementedError
|
|
2659
|
+
end
|
|
2660
|
+
|
|
2661
|
+
def id_column
|
|
2662
|
+
:id
|
|
2663
|
+
end
|
|
2664
|
+
|
|
2665
|
+
def existing_ids_for(_batch)
|
|
2666
|
+
fail NotImplementedError
|
|
2667
|
+
end
|
|
2668
|
+
end
|
|
2669
|
+
end
|
|
2670
|
+
```
|
|
2671
|
+
|
|
2130
2672
|
## Generators
|
|
2131
2673
|
|
|
2132
2674
|
RailsOps features a generator to easily create a structure for common CRUD-style
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.8.0
|
data/lib/rails_ops/operation.rb
CHANGED
|
@@ -14,6 +14,12 @@ class RailsOps::Operation
|
|
|
14
14
|
Symbol
|
|
15
15
|
].freeze
|
|
16
16
|
|
|
17
|
+
# Tracks which call sites have already emitted the deprecation warning
|
|
18
|
+
# for {with_rollback_on_exception}. Keyed by `"<path>:<lineno>"` so the
|
|
19
|
+
# warning fires at most once per call location across the process,
|
|
20
|
+
# avoiding log spam in long-running servers and per-request hot paths.
|
|
21
|
+
WITH_ROLLBACK_DEPRECATION_SEEN = Concurrent::Map.new
|
|
22
|
+
|
|
17
23
|
attr_reader :params
|
|
18
24
|
attr_reader :context
|
|
19
25
|
|
|
@@ -102,8 +108,26 @@ class RailsOps::Operation
|
|
|
102
108
|
|
|
103
109
|
# Runs the operation using {run!} but rescues certain exceptions. Returns
|
|
104
110
|
# `true` on success, otherwise `false`.
|
|
111
|
+
#
|
|
112
|
+
# If a database transaction is already open when {run} is called, the call
|
|
113
|
+
# to {run!} is wrapped in a savepoint via
|
|
114
|
+
# `ActiveRecord::Base.transaction(requires_new: true)`. This ensures that
|
|
115
|
+
# any database writes performed by the operation are rolled back if a
|
|
116
|
+
# validation error is raised, even though that error is then caught here
|
|
117
|
+
# and converted into a `false` return value. This eliminates the most
|
|
118
|
+
# common reason for using {with_rollback_on_exception}.
|
|
119
|
+
#
|
|
120
|
+
# When no transaction is open, behavior is identical to calling {run!}
|
|
121
|
+
# directly: the caller is responsible for atomicity.
|
|
105
122
|
def run
|
|
106
|
-
|
|
123
|
+
if ActiveRecord::Base.connection.transaction_open?
|
|
124
|
+
ActiveRecord::Base.transaction(requires_new: true) do
|
|
125
|
+
run!
|
|
126
|
+
end
|
|
127
|
+
else
|
|
128
|
+
run!
|
|
129
|
+
end
|
|
130
|
+
|
|
107
131
|
return true
|
|
108
132
|
rescue *validation_errors
|
|
109
133
|
return false
|
|
@@ -186,44 +210,37 @@ class RailsOps::Operation
|
|
|
186
210
|
end
|
|
187
211
|
end
|
|
188
212
|
|
|
189
|
-
# Yields the given block and rethrows any possible
|
|
213
|
+
# Yields the given block and rethrows any possible `StandardError` as a
|
|
190
214
|
# {RailsOps::Exceptions::RollbackRequired} exception.
|
|
191
215
|
#
|
|
192
|
-
#
|
|
216
|
+
# @deprecated Since 1.8.0, validation errors raised inside {run} no
|
|
217
|
+
# longer leak partial database writes: {run} wraps the call to
|
|
218
|
+
# {run!} in a SAVEPOINT whenever an outer transaction is open, so
|
|
219
|
+
# any prior `model.save!` is rolled back automatically before
|
|
220
|
+
# {run} returns `false`. This helper is therefore obsolete for
|
|
221
|
+
# the common "save then do more work" pattern and will be
|
|
222
|
+
# removed in RailsOps 2.0.
|
|
193
223
|
#
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
224
|
+
# To convert a non-validation `StandardError` into a rollback
|
|
225
|
+
# signal that escapes {run}'s rescue, raise
|
|
226
|
+
# {RailsOps::Exceptions::RollbackRequired} directly, e.g.
|
|
227
|
+
# `fail RailsOps::Exceptions::RollbackRequired, e, e.backtrace`.
|
|
197
228
|
#
|
|
198
|
-
#
|
|
199
|
-
# model.save! # Throws validation error
|
|
200
|
-
# end
|
|
201
|
-
# end
|
|
202
|
-
#
|
|
203
|
-
# User::Create.run(user: { some: :values })
|
|
204
|
-
#
|
|
205
|
-
# Since this operation is run without the bang method, validation errors are
|
|
206
|
-
# caught and won't result in the transaction beeing rolled back. However, the
|
|
207
|
-
# `super` call already saved the user while the exception happens only at
|
|
208
|
-
# the manual call to `model.save!`. Thus the user will still be in the DB,
|
|
209
|
-
# despite the fact that the second update didn't run.
|
|
210
|
-
#
|
|
211
|
-
# The correct example would therefore be:
|
|
212
|
-
#
|
|
213
|
-
# class User::Create < RailsOps::Operation::Model::Create
|
|
214
|
-
# def perform
|
|
215
|
-
# super # Saves the user
|
|
216
|
-
#
|
|
217
|
-
# with_rollback_on_exception do
|
|
218
|
-
# model.some_field = 'some value'
|
|
219
|
-
# model.save! # Throws validation error
|
|
220
|
-
# end
|
|
221
|
-
# end
|
|
222
|
-
# end
|
|
223
|
-
#
|
|
224
|
-
# This method is one possible solution for issue #28535. There might be a more
|
|
225
|
-
# elegant and transparent approach as explained in the issue.
|
|
229
|
+
# Originally introduced for issue #28535.
|
|
226
230
|
def with_rollback_on_exception(&_block)
|
|
231
|
+
location = caller_locations(1, 1)&.first
|
|
232
|
+
location_key = location && "#{location.path}:#{location.lineno}"
|
|
233
|
+
if location_key.nil? || WITH_ROLLBACK_DEPRECATION_SEEN.put_if_absent(location_key, true).nil?
|
|
234
|
+
RailsOps.deprecator.warn(
|
|
235
|
+
'`with_rollback_on_exception` is deprecated and will be removed ' \
|
|
236
|
+
'in RailsOps 2.0. Validation errors raised inside `run` are now ' \
|
|
237
|
+
'rolled back automatically via a SAVEPOINT, so this helper is no ' \
|
|
238
|
+
'longer required for the common "save then do more work" pattern. ' \
|
|
239
|
+
'For non-validation errors that should trigger a rollback, raise ' \
|
|
240
|
+
'`RailsOps::Exceptions::RollbackRequired` directly.',
|
|
241
|
+
caller_locations(1)
|
|
242
|
+
)
|
|
243
|
+
end
|
|
227
244
|
yield
|
|
228
245
|
rescue StandardError => e
|
|
229
246
|
fail RailsOps::Exceptions::RollbackRequired, e, e.backtrace
|
data/lib/rails_ops/railtie.rb
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
module RailsOps
|
|
2
2
|
# @private
|
|
3
3
|
class Railtie < Rails::Railtie
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
# Register the deprecator early so that app-level deprecation config
|
|
5
|
+
# (`config.active_support.deprecation`,
|
|
6
|
+
# `config.active_support.report_deprecations`, …) is applied to it.
|
|
7
|
+
initializer 'rails_ops.deprecator', before: :load_environment_config do |app|
|
|
8
8
|
if app.respond_to?(:deprecators)
|
|
9
9
|
app.deprecators[:rails_ops] = RailsOps.deprecator
|
|
10
10
|
end
|
|
11
|
+
end
|
|
11
12
|
|
|
13
|
+
initializer 'rails_ops' do
|
|
12
14
|
# ---------------------------------------------------------------
|
|
13
15
|
# Load hookup config eagerly at application startup unless
|
|
14
16
|
# in development mode.
|
data/lib/rails_ops.rb
CHANGED
data/rails_ops.gemspec
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# stub: rails_ops 1.
|
|
2
|
+
# stub: rails_ops 1.8.0 ruby lib
|
|
3
3
|
|
|
4
4
|
Gem::Specification.new do |s|
|
|
5
5
|
s.name = "rails_ops".freeze
|
|
6
|
-
s.version = "1.
|
|
6
|
+
s.version = "1.8.0"
|
|
7
7
|
|
|
8
8
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
9
9
|
s.require_paths = ["lib".freeze]
|
|
10
10
|
s.authors = ["Sitrox".freeze]
|
|
11
|
-
s.date = "2026-
|
|
12
|
-
s.files = [".github/workflows/rubocop.yml".freeze, ".github/workflows/ruby.yml".freeze, ".gitignore".freeze, ".releaser_config".freeze, ".rubocop.yml".freeze, "Appraisals".freeze, "CHANGELOG.md".freeze, "CLAUDE.md".freeze, "Gemfile".freeze, "Gemfile.lock".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "VERSION".freeze, "gemfiles/rails_6.0.gemfile".freeze, "gemfiles/rails_6.1.gemfile".freeze, "gemfiles/rails_7.0.gemfile".freeze, "gemfiles/rails_7.1.gemfile".freeze, "gemfiles/rails_7.2.gemfile".freeze, "gemfiles/rails_8.0.gemfile".freeze, "lib/generators/operation/USAGE".freeze, "lib/generators/operation/operation_generator.rb".freeze, "lib/generators/operation/templates/controller.erb".freeze, "lib/generators/operation/templates/controller_wrapper.erb".freeze, "lib/generators/operation/templates/create.erb".freeze, "lib/generators/operation/templates/destroy.erb".freeze, "lib/generators/operation/templates/load.erb".freeze, "lib/generators/operation/templates/update.erb".freeze, "lib/generators/operation/templates/view.erb".freeze, "lib/rails_ops.rb".freeze, "lib/rails_ops/authorization_backend/abstract.rb".freeze, "lib/rails_ops/authorization_backend/can_can_can.rb".freeze, "lib/rails_ops/configuration.rb".freeze, "lib/rails_ops/context.rb".freeze, "lib/rails_ops/controller_mixin.rb".freeze, "lib/rails_ops/exceptions.rb".freeze, "lib/rails_ops/hooked_job.rb".freeze, "lib/rails_ops/hookup.rb".freeze, "lib/rails_ops/hookup/dsl.rb".freeze, "lib/rails_ops/hookup/dsl_validator.rb".freeze, "lib/rails_ops/hookup/hook.rb".freeze, "lib/rails_ops/log_subscriber.rb".freeze, "lib/rails_ops/mixins.rb".freeze, "lib/rails_ops/mixins/authorization.rb".freeze, "lib/rails_ops/mixins/log_settings.rb".freeze, "lib/rails_ops/mixins/model.rb".freeze, "lib/rails_ops/mixins/model/authorization.rb".freeze, "lib/rails_ops/mixins/model/nesting.rb".freeze, "lib/rails_ops/mixins/param_authorization.rb".freeze, "lib/rails_ops/mixins/policies.rb".freeze, "lib/rails_ops/mixins/require_context.rb".freeze, "lib/rails_ops/mixins/routes.rb".freeze, "lib/rails_ops/mixins/schema_validation.rb".freeze, "lib/rails_ops/mixins/sub_ops.rb".freeze, "lib/rails_ops/model_mixins.rb".freeze, "lib/rails_ops/model_mixins/ar_extension.rb".freeze, "lib/rails_ops/model_mixins/marshalling.rb".freeze, "lib/rails_ops/model_mixins/parent_op.rb".freeze, "lib/rails_ops/model_mixins/sti_fixes.rb".freeze, "lib/rails_ops/model_mixins/virtual_attributes.rb".freeze, "lib/rails_ops/model_mixins/virtual_attributes/virtual_column_wrapper.rb".freeze, "lib/rails_ops/model_mixins/virtual_has_one.rb".freeze, "lib/rails_ops/model_mixins/virtual_model_name.rb".freeze, "lib/rails_ops/operation.rb".freeze, "lib/rails_ops/operation/model.rb".freeze, "lib/rails_ops/operation/model/create.rb".freeze, "lib/rails_ops/operation/model/destroy.rb".freeze, "lib/rails_ops/operation/model/load.rb".freeze, "lib/rails_ops/operation/model/update.rb".freeze, "lib/rails_ops/profiler.rb".freeze, "lib/rails_ops/profiler/node.rb".freeze, "lib/rails_ops/railtie.rb".freeze, "lib/rails_ops/scoped_env.rb".freeze, "lib/rails_ops/virtual_model.rb".freeze, "rails_ops.gemspec".freeze, "test/db/models.rb".freeze, "test/db/schema.rb".freeze, "test/dummy/Rakefile".freeze, "test/dummy/app/assets/config/manifest.js".freeze, "test/dummy/app/assets/images/.keep".freeze, "test/dummy/app/assets/javascripts/application.js".freeze, "test/dummy/app/assets/javascripts/cable.js".freeze, "test/dummy/app/assets/javascripts/channels/.keep".freeze, "test/dummy/app/assets/stylesheets/application.css".freeze, "test/dummy/app/channels/application_cable/channel.rb".freeze, "test/dummy/app/channels/application_cable/connection.rb".freeze, "test/dummy/app/controllers/application_controller.rb".freeze, "test/dummy/app/controllers/concerns/.keep".freeze, "test/dummy/app/controllers/group_controller.rb".freeze, "test/dummy/app/helpers/application_helper.rb".freeze, "test/dummy/app/jobs/application_job.rb".freeze, "test/dummy/app/mailers/application_mailer.rb".freeze, "test/dummy/app/models/ability.rb".freeze, "test/dummy/app/models/animal.rb".freeze, "test/dummy/app/models/application_record.rb".freeze, "test/dummy/app/models/bird.rb".freeze, "test/dummy/app/models/cat.rb".freeze, "test/dummy/app/models/computer.rb".freeze, "test/dummy/app/models/concerns/.keep".freeze, "test/dummy/app/models/cpu.rb".freeze, "test/dummy/app/models/dog.rb".freeze, "test/dummy/app/models/flower.rb".freeze, "test/dummy/app/models/group.rb".freeze, "test/dummy/app/models/mainboard.rb".freeze, "test/dummy/app/models/nightingale.rb".freeze, "test/dummy/app/models/phoenix.rb".freeze, "test/dummy/app/models/user.rb".freeze, "test/dummy/app/views/layouts/application.html.erb".freeze, "test/dummy/app/views/layouts/mailer.html.erb".freeze, "test/dummy/app/views/layouts/mailer.text.erb".freeze, "test/dummy/bin/bundle".freeze, "test/dummy/bin/rails".freeze, "test/dummy/bin/rake".freeze, "test/dummy/bin/setup".freeze, "test/dummy/bin/update".freeze, "test/dummy/bin/yarn".freeze, "test/dummy/config.ru".freeze, "test/dummy/config/application.rb".freeze, "test/dummy/config/boot.rb".freeze, "test/dummy/config/cable.yml".freeze, "test/dummy/config/database.yml".freeze, "test/dummy/config/environment.rb".freeze, "test/dummy/config/environments/development.rb".freeze, "test/dummy/config/environments/production.rb".freeze, "test/dummy/config/environments/test.rb".freeze, "test/dummy/config/hookup.rb".freeze, "test/dummy/config/initializers/application_controller_renderer.rb".freeze, "test/dummy/config/initializers/assets.rb".freeze, "test/dummy/config/initializers/backtrace_silencers.rb".freeze, "test/dummy/config/initializers/cookies_serializer.rb".freeze, "test/dummy/config/initializers/filter_parameter_logging.rb".freeze, "test/dummy/config/initializers/inflections.rb".freeze, "test/dummy/config/initializers/mime_types.rb".freeze, "test/dummy/config/initializers/rails_ops.rb".freeze, "test/dummy/config/initializers/wrap_parameters.rb".freeze, "test/dummy/config/locales/en.yml".freeze, "test/dummy/config/puma.rb".freeze, "test/dummy/config/routes.rb".freeze, "test/dummy/config/secrets.yml".freeze, "test/dummy/config/spring.rb".freeze, "test/dummy/db/schema.rb".freeze, "test/dummy/lib/assets/.keep".freeze, "test/dummy/log/.keep".freeze, "test/dummy/package.json".freeze, "test/dummy/public/404.html".freeze, "test/dummy/public/422.html".freeze, "test/dummy/public/500.html".freeze, "test/dummy/public/apple-touch-icon-precomposed.png".freeze, "test/dummy/public/apple-touch-icon.png".freeze, "test/dummy/public/favicon.ico".freeze, "test/dummy/tmp/.keep".freeze, "test/test_helper.rb".freeze, "test/unit/rails_ops/generators/operation_generator_test.rb".freeze, "test/unit/rails_ops/hookup_test.rb".freeze, "test/unit/rails_ops/mixins/controller_test.rb".freeze, "test/unit/rails_ops/mixins/model/deep_nesting_test.rb".freeze, "test/unit/rails_ops/mixins/model/marshalling_test.rb".freeze, "test/unit/rails_ops/mixins/param_authorization_test.rb".freeze, "test/unit/rails_ops/mixins/policies_test.rb".freeze, "test/unit/rails_ops/operation/auth_test.rb".freeze, "test/unit/rails_ops/operation/model/create_test.rb".freeze, "test/unit/rails_ops/operation/model/destroy_test.rb".freeze, "test/unit/rails_ops/operation/model/load_test.rb".freeze, "test/unit/rails_ops/operation/model/sti_test.rb".freeze, "test/unit/rails_ops/operation/model/update_test.rb".freeze, "test/unit/rails_ops/operation/model_test.rb".freeze, "test/unit/rails_ops/operation/update_lazy_auth_test.rb".freeze, "test/unit/rails_ops/operation_test.rb".freeze, "test/unit/rails_ops/profiler_test.rb".freeze]
|
|
11
|
+
s.date = "2026-05-07"
|
|
12
|
+
s.files = [".github/workflows/rubocop.yml".freeze, ".github/workflows/ruby.yml".freeze, ".gitignore".freeze, ".releaser_config".freeze, ".rubocop.yml".freeze, "Appraisals".freeze, "CHANGELOG.md".freeze, "CLAUDE.md".freeze, "Gemfile".freeze, "Gemfile.lock".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "VERSION".freeze, "gemfiles/rails_6.0.gemfile".freeze, "gemfiles/rails_6.1.gemfile".freeze, "gemfiles/rails_7.0.gemfile".freeze, "gemfiles/rails_7.1.gemfile".freeze, "gemfiles/rails_7.2.gemfile".freeze, "gemfiles/rails_8.0.gemfile".freeze, "lib/generators/operation/USAGE".freeze, "lib/generators/operation/operation_generator.rb".freeze, "lib/generators/operation/templates/controller.erb".freeze, "lib/generators/operation/templates/controller_wrapper.erb".freeze, "lib/generators/operation/templates/create.erb".freeze, "lib/generators/operation/templates/destroy.erb".freeze, "lib/generators/operation/templates/load.erb".freeze, "lib/generators/operation/templates/update.erb".freeze, "lib/generators/operation/templates/view.erb".freeze, "lib/rails_ops.rb".freeze, "lib/rails_ops/authorization_backend/abstract.rb".freeze, "lib/rails_ops/authorization_backend/can_can_can.rb".freeze, "lib/rails_ops/configuration.rb".freeze, "lib/rails_ops/context.rb".freeze, "lib/rails_ops/controller_mixin.rb".freeze, "lib/rails_ops/exceptions.rb".freeze, "lib/rails_ops/hooked_job.rb".freeze, "lib/rails_ops/hookup.rb".freeze, "lib/rails_ops/hookup/dsl.rb".freeze, "lib/rails_ops/hookup/dsl_validator.rb".freeze, "lib/rails_ops/hookup/hook.rb".freeze, "lib/rails_ops/log_subscriber.rb".freeze, "lib/rails_ops/mixins.rb".freeze, "lib/rails_ops/mixins/authorization.rb".freeze, "lib/rails_ops/mixins/log_settings.rb".freeze, "lib/rails_ops/mixins/model.rb".freeze, "lib/rails_ops/mixins/model/authorization.rb".freeze, "lib/rails_ops/mixins/model/nesting.rb".freeze, "lib/rails_ops/mixins/param_authorization.rb".freeze, "lib/rails_ops/mixins/policies.rb".freeze, "lib/rails_ops/mixins/require_context.rb".freeze, "lib/rails_ops/mixins/routes.rb".freeze, "lib/rails_ops/mixins/schema_validation.rb".freeze, "lib/rails_ops/mixins/sub_ops.rb".freeze, "lib/rails_ops/model_mixins.rb".freeze, "lib/rails_ops/model_mixins/ar_extension.rb".freeze, "lib/rails_ops/model_mixins/marshalling.rb".freeze, "lib/rails_ops/model_mixins/parent_op.rb".freeze, "lib/rails_ops/model_mixins/sti_fixes.rb".freeze, "lib/rails_ops/model_mixins/virtual_attributes.rb".freeze, "lib/rails_ops/model_mixins/virtual_attributes/virtual_column_wrapper.rb".freeze, "lib/rails_ops/model_mixins/virtual_has_one.rb".freeze, "lib/rails_ops/model_mixins/virtual_model_name.rb".freeze, "lib/rails_ops/operation.rb".freeze, "lib/rails_ops/operation/model.rb".freeze, "lib/rails_ops/operation/model/create.rb".freeze, "lib/rails_ops/operation/model/destroy.rb".freeze, "lib/rails_ops/operation/model/load.rb".freeze, "lib/rails_ops/operation/model/update.rb".freeze, "lib/rails_ops/profiler.rb".freeze, "lib/rails_ops/profiler/node.rb".freeze, "lib/rails_ops/railtie.rb".freeze, "lib/rails_ops/scoped_env.rb".freeze, "lib/rails_ops/virtual_model.rb".freeze, "rails_ops.gemspec".freeze, "test/db/models.rb".freeze, "test/db/schema.rb".freeze, "test/dummy/Rakefile".freeze, "test/dummy/app/assets/config/manifest.js".freeze, "test/dummy/app/assets/images/.keep".freeze, "test/dummy/app/assets/javascripts/application.js".freeze, "test/dummy/app/assets/javascripts/cable.js".freeze, "test/dummy/app/assets/javascripts/channels/.keep".freeze, "test/dummy/app/assets/stylesheets/application.css".freeze, "test/dummy/app/channels/application_cable/channel.rb".freeze, "test/dummy/app/channels/application_cable/connection.rb".freeze, "test/dummy/app/controllers/application_controller.rb".freeze, "test/dummy/app/controllers/concerns/.keep".freeze, "test/dummy/app/controllers/group_controller.rb".freeze, "test/dummy/app/helpers/application_helper.rb".freeze, "test/dummy/app/jobs/application_job.rb".freeze, "test/dummy/app/mailers/application_mailer.rb".freeze, "test/dummy/app/models/ability.rb".freeze, "test/dummy/app/models/animal.rb".freeze, "test/dummy/app/models/application_record.rb".freeze, "test/dummy/app/models/bird.rb".freeze, "test/dummy/app/models/cat.rb".freeze, "test/dummy/app/models/computer.rb".freeze, "test/dummy/app/models/concerns/.keep".freeze, "test/dummy/app/models/cpu.rb".freeze, "test/dummy/app/models/dog.rb".freeze, "test/dummy/app/models/flower.rb".freeze, "test/dummy/app/models/group.rb".freeze, "test/dummy/app/models/mainboard.rb".freeze, "test/dummy/app/models/nightingale.rb".freeze, "test/dummy/app/models/phoenix.rb".freeze, "test/dummy/app/models/user.rb".freeze, "test/dummy/app/views/layouts/application.html.erb".freeze, "test/dummy/app/views/layouts/mailer.html.erb".freeze, "test/dummy/app/views/layouts/mailer.text.erb".freeze, "test/dummy/bin/bundle".freeze, "test/dummy/bin/rails".freeze, "test/dummy/bin/rake".freeze, "test/dummy/bin/setup".freeze, "test/dummy/bin/update".freeze, "test/dummy/bin/yarn".freeze, "test/dummy/config.ru".freeze, "test/dummy/config/application.rb".freeze, "test/dummy/config/boot.rb".freeze, "test/dummy/config/cable.yml".freeze, "test/dummy/config/database.yml".freeze, "test/dummy/config/environment.rb".freeze, "test/dummy/config/environments/development.rb".freeze, "test/dummy/config/environments/production.rb".freeze, "test/dummy/config/environments/test.rb".freeze, "test/dummy/config/hookup.rb".freeze, "test/dummy/config/initializers/application_controller_renderer.rb".freeze, "test/dummy/config/initializers/assets.rb".freeze, "test/dummy/config/initializers/backtrace_silencers.rb".freeze, "test/dummy/config/initializers/cookies_serializer.rb".freeze, "test/dummy/config/initializers/filter_parameter_logging.rb".freeze, "test/dummy/config/initializers/inflections.rb".freeze, "test/dummy/config/initializers/mime_types.rb".freeze, "test/dummy/config/initializers/rails_ops.rb".freeze, "test/dummy/config/initializers/wrap_parameters.rb".freeze, "test/dummy/config/locales/en.yml".freeze, "test/dummy/config/puma.rb".freeze, "test/dummy/config/routes.rb".freeze, "test/dummy/config/secrets.yml".freeze, "test/dummy/config/spring.rb".freeze, "test/dummy/db/schema.rb".freeze, "test/dummy/lib/assets/.keep".freeze, "test/dummy/log/.keep".freeze, "test/dummy/package.json".freeze, "test/dummy/public/404.html".freeze, "test/dummy/public/422.html".freeze, "test/dummy/public/500.html".freeze, "test/dummy/public/apple-touch-icon-precomposed.png".freeze, "test/dummy/public/apple-touch-icon.png".freeze, "test/dummy/public/favicon.ico".freeze, "test/dummy/tmp/.keep".freeze, "test/test_helper.rb".freeze, "test/unit/rails_ops/generators/operation_generator_test.rb".freeze, "test/unit/rails_ops/hookup_test.rb".freeze, "test/unit/rails_ops/mixins/controller_test.rb".freeze, "test/unit/rails_ops/mixins/model/deep_nesting_test.rb".freeze, "test/unit/rails_ops/mixins/model/marshalling_test.rb".freeze, "test/unit/rails_ops/mixins/param_authorization_test.rb".freeze, "test/unit/rails_ops/mixins/policies_test.rb".freeze, "test/unit/rails_ops/operation/auth_test.rb".freeze, "test/unit/rails_ops/operation/model/create_test.rb".freeze, "test/unit/rails_ops/operation/model/destroy_test.rb".freeze, "test/unit/rails_ops/operation/model/load_test.rb".freeze, "test/unit/rails_ops/operation/model/sti_test.rb".freeze, "test/unit/rails_ops/operation/model/update_test.rb".freeze, "test/unit/rails_ops/operation/model_test.rb".freeze, "test/unit/rails_ops/operation/update_lazy_auth_test.rb".freeze, "test/unit/rails_ops/operation_test.rb".freeze, "test/unit/rails_ops/profiler_test.rb".freeze, "test/unit/rails_ops/railtie_test.rb".freeze]
|
|
13
13
|
s.homepage = "https://github.com/sitrox/rails_ops".freeze
|
|
14
14
|
s.licenses = ["MIT".freeze]
|
|
15
15
|
s.rubygems_version = "3.4.6".freeze
|
|
16
16
|
s.summary = "An operations service layer for rails projects.".freeze
|
|
17
|
-
s.test_files = ["test/db/models.rb".freeze, "test/db/schema.rb".freeze, "test/dummy/Rakefile".freeze, "test/dummy/app/assets/config/manifest.js".freeze, "test/dummy/app/assets/images/.keep".freeze, "test/dummy/app/assets/javascripts/application.js".freeze, "test/dummy/app/assets/javascripts/cable.js".freeze, "test/dummy/app/assets/javascripts/channels/.keep".freeze, "test/dummy/app/assets/stylesheets/application.css".freeze, "test/dummy/app/channels/application_cable/channel.rb".freeze, "test/dummy/app/channels/application_cable/connection.rb".freeze, "test/dummy/app/controllers/application_controller.rb".freeze, "test/dummy/app/controllers/concerns/.keep".freeze, "test/dummy/app/controllers/group_controller.rb".freeze, "test/dummy/app/helpers/application_helper.rb".freeze, "test/dummy/app/jobs/application_job.rb".freeze, "test/dummy/app/mailers/application_mailer.rb".freeze, "test/dummy/app/models/ability.rb".freeze, "test/dummy/app/models/animal.rb".freeze, "test/dummy/app/models/application_record.rb".freeze, "test/dummy/app/models/bird.rb".freeze, "test/dummy/app/models/cat.rb".freeze, "test/dummy/app/models/computer.rb".freeze, "test/dummy/app/models/concerns/.keep".freeze, "test/dummy/app/models/cpu.rb".freeze, "test/dummy/app/models/dog.rb".freeze, "test/dummy/app/models/flower.rb".freeze, "test/dummy/app/models/group.rb".freeze, "test/dummy/app/models/mainboard.rb".freeze, "test/dummy/app/models/nightingale.rb".freeze, "test/dummy/app/models/phoenix.rb".freeze, "test/dummy/app/models/user.rb".freeze, "test/dummy/app/views/layouts/application.html.erb".freeze, "test/dummy/app/views/layouts/mailer.html.erb".freeze, "test/dummy/app/views/layouts/mailer.text.erb".freeze, "test/dummy/bin/bundle".freeze, "test/dummy/bin/rails".freeze, "test/dummy/bin/rake".freeze, "test/dummy/bin/setup".freeze, "test/dummy/bin/update".freeze, "test/dummy/bin/yarn".freeze, "test/dummy/config.ru".freeze, "test/dummy/config/application.rb".freeze, "test/dummy/config/boot.rb".freeze, "test/dummy/config/cable.yml".freeze, "test/dummy/config/database.yml".freeze, "test/dummy/config/environment.rb".freeze, "test/dummy/config/environments/development.rb".freeze, "test/dummy/config/environments/production.rb".freeze, "test/dummy/config/environments/test.rb".freeze, "test/dummy/config/hookup.rb".freeze, "test/dummy/config/initializers/application_controller_renderer.rb".freeze, "test/dummy/config/initializers/assets.rb".freeze, "test/dummy/config/initializers/backtrace_silencers.rb".freeze, "test/dummy/config/initializers/cookies_serializer.rb".freeze, "test/dummy/config/initializers/filter_parameter_logging.rb".freeze, "test/dummy/config/initializers/inflections.rb".freeze, "test/dummy/config/initializers/mime_types.rb".freeze, "test/dummy/config/initializers/rails_ops.rb".freeze, "test/dummy/config/initializers/wrap_parameters.rb".freeze, "test/dummy/config/locales/en.yml".freeze, "test/dummy/config/puma.rb".freeze, "test/dummy/config/routes.rb".freeze, "test/dummy/config/secrets.yml".freeze, "test/dummy/config/spring.rb".freeze, "test/dummy/db/schema.rb".freeze, "test/dummy/lib/assets/.keep".freeze, "test/dummy/log/.keep".freeze, "test/dummy/package.json".freeze, "test/dummy/public/404.html".freeze, "test/dummy/public/422.html".freeze, "test/dummy/public/500.html".freeze, "test/dummy/public/apple-touch-icon-precomposed.png".freeze, "test/dummy/public/apple-touch-icon.png".freeze, "test/dummy/public/favicon.ico".freeze, "test/dummy/tmp/.keep".freeze, "test/test_helper.rb".freeze, "test/unit/rails_ops/generators/operation_generator_test.rb".freeze, "test/unit/rails_ops/hookup_test.rb".freeze, "test/unit/rails_ops/mixins/controller_test.rb".freeze, "test/unit/rails_ops/mixins/model/deep_nesting_test.rb".freeze, "test/unit/rails_ops/mixins/model/marshalling_test.rb".freeze, "test/unit/rails_ops/mixins/param_authorization_test.rb".freeze, "test/unit/rails_ops/mixins/policies_test.rb".freeze, "test/unit/rails_ops/operation/auth_test.rb".freeze, "test/unit/rails_ops/operation/model/create_test.rb".freeze, "test/unit/rails_ops/operation/model/destroy_test.rb".freeze, "test/unit/rails_ops/operation/model/load_test.rb".freeze, "test/unit/rails_ops/operation/model/sti_test.rb".freeze, "test/unit/rails_ops/operation/model/update_test.rb".freeze, "test/unit/rails_ops/operation/model_test.rb".freeze, "test/unit/rails_ops/operation/update_lazy_auth_test.rb".freeze, "test/unit/rails_ops/operation_test.rb".freeze, "test/unit/rails_ops/profiler_test.rb".freeze]
|
|
17
|
+
s.test_files = ["test/db/models.rb".freeze, "test/db/schema.rb".freeze, "test/dummy/Rakefile".freeze, "test/dummy/app/assets/config/manifest.js".freeze, "test/dummy/app/assets/images/.keep".freeze, "test/dummy/app/assets/javascripts/application.js".freeze, "test/dummy/app/assets/javascripts/cable.js".freeze, "test/dummy/app/assets/javascripts/channels/.keep".freeze, "test/dummy/app/assets/stylesheets/application.css".freeze, "test/dummy/app/channels/application_cable/channel.rb".freeze, "test/dummy/app/channels/application_cable/connection.rb".freeze, "test/dummy/app/controllers/application_controller.rb".freeze, "test/dummy/app/controllers/concerns/.keep".freeze, "test/dummy/app/controllers/group_controller.rb".freeze, "test/dummy/app/helpers/application_helper.rb".freeze, "test/dummy/app/jobs/application_job.rb".freeze, "test/dummy/app/mailers/application_mailer.rb".freeze, "test/dummy/app/models/ability.rb".freeze, "test/dummy/app/models/animal.rb".freeze, "test/dummy/app/models/application_record.rb".freeze, "test/dummy/app/models/bird.rb".freeze, "test/dummy/app/models/cat.rb".freeze, "test/dummy/app/models/computer.rb".freeze, "test/dummy/app/models/concerns/.keep".freeze, "test/dummy/app/models/cpu.rb".freeze, "test/dummy/app/models/dog.rb".freeze, "test/dummy/app/models/flower.rb".freeze, "test/dummy/app/models/group.rb".freeze, "test/dummy/app/models/mainboard.rb".freeze, "test/dummy/app/models/nightingale.rb".freeze, "test/dummy/app/models/phoenix.rb".freeze, "test/dummy/app/models/user.rb".freeze, "test/dummy/app/views/layouts/application.html.erb".freeze, "test/dummy/app/views/layouts/mailer.html.erb".freeze, "test/dummy/app/views/layouts/mailer.text.erb".freeze, "test/dummy/bin/bundle".freeze, "test/dummy/bin/rails".freeze, "test/dummy/bin/rake".freeze, "test/dummy/bin/setup".freeze, "test/dummy/bin/update".freeze, "test/dummy/bin/yarn".freeze, "test/dummy/config.ru".freeze, "test/dummy/config/application.rb".freeze, "test/dummy/config/boot.rb".freeze, "test/dummy/config/cable.yml".freeze, "test/dummy/config/database.yml".freeze, "test/dummy/config/environment.rb".freeze, "test/dummy/config/environments/development.rb".freeze, "test/dummy/config/environments/production.rb".freeze, "test/dummy/config/environments/test.rb".freeze, "test/dummy/config/hookup.rb".freeze, "test/dummy/config/initializers/application_controller_renderer.rb".freeze, "test/dummy/config/initializers/assets.rb".freeze, "test/dummy/config/initializers/backtrace_silencers.rb".freeze, "test/dummy/config/initializers/cookies_serializer.rb".freeze, "test/dummy/config/initializers/filter_parameter_logging.rb".freeze, "test/dummy/config/initializers/inflections.rb".freeze, "test/dummy/config/initializers/mime_types.rb".freeze, "test/dummy/config/initializers/rails_ops.rb".freeze, "test/dummy/config/initializers/wrap_parameters.rb".freeze, "test/dummy/config/locales/en.yml".freeze, "test/dummy/config/puma.rb".freeze, "test/dummy/config/routes.rb".freeze, "test/dummy/config/secrets.yml".freeze, "test/dummy/config/spring.rb".freeze, "test/dummy/db/schema.rb".freeze, "test/dummy/lib/assets/.keep".freeze, "test/dummy/log/.keep".freeze, "test/dummy/package.json".freeze, "test/dummy/public/404.html".freeze, "test/dummy/public/422.html".freeze, "test/dummy/public/500.html".freeze, "test/dummy/public/apple-touch-icon-precomposed.png".freeze, "test/dummy/public/apple-touch-icon.png".freeze, "test/dummy/public/favicon.ico".freeze, "test/dummy/tmp/.keep".freeze, "test/test_helper.rb".freeze, "test/unit/rails_ops/generators/operation_generator_test.rb".freeze, "test/unit/rails_ops/hookup_test.rb".freeze, "test/unit/rails_ops/mixins/controller_test.rb".freeze, "test/unit/rails_ops/mixins/model/deep_nesting_test.rb".freeze, "test/unit/rails_ops/mixins/model/marshalling_test.rb".freeze, "test/unit/rails_ops/mixins/param_authorization_test.rb".freeze, "test/unit/rails_ops/mixins/policies_test.rb".freeze, "test/unit/rails_ops/operation/auth_test.rb".freeze, "test/unit/rails_ops/operation/model/create_test.rb".freeze, "test/unit/rails_ops/operation/model/destroy_test.rb".freeze, "test/unit/rails_ops/operation/model/load_test.rb".freeze, "test/unit/rails_ops/operation/model/sti_test.rb".freeze, "test/unit/rails_ops/operation/model/update_test.rb".freeze, "test/unit/rails_ops/operation/model_test.rb".freeze, "test/unit/rails_ops/operation/update_lazy_auth_test.rb".freeze, "test/unit/rails_ops/operation_test.rb".freeze, "test/unit/rails_ops/profiler_test.rb".freeze, "test/unit/rails_ops/railtie_test.rb".freeze]
|
|
18
18
|
|
|
19
19
|
s.specification_version = 4
|
|
20
20
|
|
|
@@ -193,8 +193,168 @@ class RailsOps::OperationTest < ActiveSupport::TestCase
|
|
|
193
193
|
end
|
|
194
194
|
end
|
|
195
195
|
end.new
|
|
196
|
-
|
|
196
|
+
RailsOps.deprecator.silence do
|
|
197
|
+
assert_raises RailsOps::Exceptions::RollbackRequired do
|
|
198
|
+
op.run
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def test_with_rollback_on_exception_emits_deprecation_warning
|
|
204
|
+
# Use a fresh call site to bypass the per-process deduplication cache
|
|
205
|
+
# used by `with_rollback_on_exception`.
|
|
206
|
+
op = Class.new(RailsOps::Operation) do
|
|
207
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
|
208
|
+
def perform
|
|
209
|
+
with_rollback_on_exception do
|
|
210
|
+
# No-op; we only care about the deprecation warning being
|
|
211
|
+
# emitted on entry.
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
RUBY
|
|
215
|
+
end.new
|
|
216
|
+
|
|
217
|
+
messages = []
|
|
218
|
+
previous_behavior = RailsOps.deprecator.behavior
|
|
219
|
+
RailsOps.deprecator.behavior = ->(msg, _callstack, _name, _gem) { messages << msg }
|
|
220
|
+
begin
|
|
197
221
|
op.run
|
|
222
|
+
ensure
|
|
223
|
+
RailsOps.deprecator.behavior = previous_behavior
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
assert_match(/with_rollback_on_exception.*deprecated/, messages.join("\n"))
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def test_with_rollback_on_exception_deduplicates_per_call_site
|
|
230
|
+
op = Class.new(RailsOps::Operation) do
|
|
231
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
|
232
|
+
def perform
|
|
233
|
+
with_rollback_on_exception do
|
|
234
|
+
# No-op
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
RUBY
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
messages = []
|
|
241
|
+
previous_behavior = RailsOps.deprecator.behavior
|
|
242
|
+
RailsOps.deprecator.behavior = ->(msg, _callstack, _name, _gem) { messages << msg }
|
|
243
|
+
begin
|
|
244
|
+
op.new.run
|
|
245
|
+
op.new.run
|
|
246
|
+
op.new.run
|
|
247
|
+
ensure
|
|
248
|
+
RailsOps.deprecator.behavior = previous_behavior
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
assert_equal 1, messages.size, 'expected the warning to fire once per call site'
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# When `run` is called inside an outer transaction and the operation does a
|
|
255
|
+
# save followed by a validation error, the save must be rolled back even
|
|
256
|
+
# though `run` swallows the error and returns `false`. Without the savepoint
|
|
257
|
+
# wrapping inside `run`, the partial write would leak into the outer
|
|
258
|
+
# transaction.
|
|
259
|
+
def test_run_rolls_back_partial_writes_when_inside_outer_transaction
|
|
260
|
+
op_class = Class.new(RailsOps::Operation::Model::Create) do
|
|
261
|
+
model Group
|
|
262
|
+
|
|
263
|
+
def perform
|
|
264
|
+
super
|
|
265
|
+
fail RailsOps::Exceptions::ValidationFailed, 'post-save check failed'
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
ActiveRecord::Base.transaction do
|
|
270
|
+
count_before = Group.count
|
|
271
|
+
refute op_class.run(group: { name: 'partial', color: 'red' })
|
|
272
|
+
assert_equal count_before, Group.count, 'expected save to be rolled back'
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# `run!` keeps its existing behavior: validation errors propagate, the
|
|
277
|
+
# savepoint logic in `run` is not on the call path.
|
|
278
|
+
def test_run_bang_still_raises_inside_transaction
|
|
279
|
+
op_class = Class.new(RailsOps::Operation::Model::Create) do
|
|
280
|
+
model Group
|
|
281
|
+
|
|
282
|
+
def perform
|
|
283
|
+
super
|
|
284
|
+
fail RailsOps::Exceptions::ValidationFailed, 'post-save check failed'
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
assert_raises RailsOps::Exceptions::ValidationFailed do
|
|
289
|
+
ActiveRecord::Base.transaction do
|
|
290
|
+
op_class.run!(group: { name: 'partial', color: 'red' })
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Successful `run` calls inside a transaction still commit the model save.
|
|
296
|
+
def test_run_persists_model_on_success_inside_transaction
|
|
297
|
+
op_class = Class.new(RailsOps::Operation::Model::Create) do
|
|
298
|
+
model Group
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
ActiveRecord::Base.transaction do
|
|
302
|
+
count_before = Group.count
|
|
303
|
+
assert op_class.run(group: { name: 'fine', color: 'green' })
|
|
304
|
+
assert_equal count_before + 1, Group.count
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# `run_sub` (non-bang) delegates to `run`, so a child operation that saves
|
|
309
|
+
# and then fails validation must not leak partial state into the parent
|
|
310
|
+
# transaction.
|
|
311
|
+
def test_run_sub_rolls_back_partial_writes_in_child_op
|
|
312
|
+
child_op = Class.new(RailsOps::Operation::Model::Create) do
|
|
313
|
+
model Group
|
|
314
|
+
|
|
315
|
+
def perform
|
|
316
|
+
super
|
|
317
|
+
fail RailsOps::Exceptions::ValidationFailed, 'post-save check failed'
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
parent_op = Class.new(RailsOps::Operation) do
|
|
322
|
+
attr_reader :sub_result
|
|
323
|
+
|
|
324
|
+
define_method(:perform) do
|
|
325
|
+
@sub_result = run_sub(child_op, group: { name: 'child', color: 'blue' })
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
ActiveRecord::Base.transaction do
|
|
330
|
+
count_before = Group.count
|
|
331
|
+
op = parent_op.new
|
|
332
|
+
op.run!
|
|
333
|
+
refute op.sub_result, 'expected run_sub to return false on child validation error'
|
|
334
|
+
assert_equal count_before, Group.count, 'expected child op save to be rolled back'
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# The same protection applies when the failing exception is
|
|
339
|
+
# `ActiveRecord::RecordInvalid` (raised by `model.save!` on a real
|
|
340
|
+
# model validation failure), which is the more common production case.
|
|
341
|
+
def test_run_rolls_back_partial_writes_on_active_record_record_invalid
|
|
342
|
+
op_class = Class.new(RailsOps::Operation::Model::Create) do
|
|
343
|
+
model Group
|
|
344
|
+
|
|
345
|
+
def perform
|
|
346
|
+
super
|
|
347
|
+
# Trigger a real ActiveRecord::RecordInvalid on a separate record.
|
|
348
|
+
invalid = Group.new
|
|
349
|
+
invalid.errors.add(:base, 'invalid')
|
|
350
|
+
fail ActiveRecord::RecordInvalid, invalid
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
ActiveRecord::Base.transaction do
|
|
355
|
+
count_before = Group.count
|
|
356
|
+
refute op_class.run(group: { name: 'partial', color: 'red' })
|
|
357
|
+
assert_equal count_before, Group.count, 'expected save to be rolled back'
|
|
198
358
|
end
|
|
199
359
|
end
|
|
200
360
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
class RailsOps::RailtieTest < ActiveSupport::TestCase
|
|
4
|
+
include TestHelper
|
|
5
|
+
|
|
6
|
+
def test_deprecator_is_registered_with_rails
|
|
7
|
+
skip 'Rails.application.deprecators is unavailable on this Rails version' \
|
|
8
|
+
unless Rails.application.respond_to?(:deprecators)
|
|
9
|
+
|
|
10
|
+
registered = Rails.application.deprecators[:rails_ops]
|
|
11
|
+
assert_same RailsOps.deprecator, registered,
|
|
12
|
+
'expected RailsOps.deprecator to be registered as Rails.application.deprecators[:rails_ops]'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_deprecator_honors_silenced_flag_set_via_app_config
|
|
16
|
+
skip 'Rails.application.deprecators is unavailable on this Rails version' \
|
|
17
|
+
unless Rails.application.respond_to?(:deprecators)
|
|
18
|
+
|
|
19
|
+
previous_silenced = RailsOps.deprecator.silenced
|
|
20
|
+
begin
|
|
21
|
+
Rails.application.deprecators.silenced = true
|
|
22
|
+
assert RailsOps.deprecator.silenced,
|
|
23
|
+
'expected RailsOps.deprecator to be silenced when ' \
|
|
24
|
+
'Rails.application.deprecators.silenced = true'
|
|
25
|
+
ensure
|
|
26
|
+
Rails.application.deprecators.silenced = previous_silenced
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_ops
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sitrox
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-05-07 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: active_type
|
|
@@ -258,6 +258,7 @@ files:
|
|
|
258
258
|
- test/unit/rails_ops/operation/update_lazy_auth_test.rb
|
|
259
259
|
- test/unit/rails_ops/operation_test.rb
|
|
260
260
|
- test/unit/rails_ops/profiler_test.rb
|
|
261
|
+
- test/unit/rails_ops/railtie_test.rb
|
|
261
262
|
homepage: https://github.com/sitrox/rails_ops
|
|
262
263
|
licenses:
|
|
263
264
|
- MIT
|
|
@@ -374,3 +375,4 @@ test_files:
|
|
|
374
375
|
- test/unit/rails_ops/operation/update_lazy_auth_test.rb
|
|
375
376
|
- test/unit/rails_ops/operation_test.rb
|
|
376
377
|
- test/unit/rails_ops/profiler_test.rb
|
|
378
|
+
- test/unit/rails_ops/railtie_test.rb
|