rails_ops 1.7.7 → 1.7.8
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 +8 -0
- data/Gemfile.lock +1 -1
- data/README.md +497 -1
- data/VERSION +1 -1
- data/rails_ops.gemspec +3 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d40d2ec6a8a45a3725040a4f9fd0837ca7e42d36625827d1253e1722589e2e21
|
|
4
|
+
data.tar.gz: 53c235fe982863f62f0e6a756f6d7be9f127ad7b7693b1f46f4100b7b914bec8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d226ffc592c1c46ed0c536d6e33e3f54342afc59ef6e162670437b7d2847df8d05d213b62ff128de3cb1084e53470a8bd5c7fa37ac875c9e65610ab54585633d
|
|
7
|
+
data.tar.gz: 611fbd338f724d311d32d300677bf378d031891543bced3f074134793a45e106c337ea8a07958a36d6696f355965d0f23f004f14e2b881605cb68c1de58ac257
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.8 (2026-03-12)
|
|
4
|
+
|
|
5
|
+
* Add practical examples throughout README covering non-model operations,
|
|
6
|
+
find-or-create patterns, state validation policies, sub-operation
|
|
7
|
+
composition, complete CRUD sets, model defaults, custom authorization
|
|
8
|
+
actions, virtual datetime fields, operation inheritance, and load
|
|
9
|
+
operations for show pages.
|
|
10
|
+
|
|
3
11
|
## 1.7.7 (2026-02-24)
|
|
4
12
|
|
|
5
13
|
* Fix `find_model_relation` to merge the returned relation's conditions
|
data/Gemfile.lock
CHANGED
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,49 @@ 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
|
+
with_rollback_on_exception do
|
|
724
|
+
run_sub! Operations::Cache::Rebuild, rebuild_counters: true
|
|
725
|
+
run_sub! Operations::Notification::Send,
|
|
726
|
+
template: 'article_published',
|
|
727
|
+
record_id: model.id,
|
|
728
|
+
record_type: 'Article'
|
|
729
|
+
end
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
Note the use of `with_rollback_on_exception`: if any sub-operation fails after
|
|
736
|
+
`super` has already saved the article, the exception is re-raised as
|
|
737
|
+
`RailsOps::Exceptions::RollbackRequired`, which is not caught by `run` and
|
|
738
|
+
therefore causes a surrounding transaction to roll back. Without it, the article
|
|
739
|
+
could remain saved even if the notification fails (since `run` catches standard
|
|
740
|
+
validation errors). See section *Transactions* for more details.
|
|
741
|
+
|
|
582
742
|
## Contexts
|
|
583
743
|
|
|
584
744
|
Most operations make use of generic parameters like the current user or an
|
|
@@ -1046,7 +1206,6 @@ Rails Ops offers multiple ways of disabling authorization:
|
|
|
1046
1206
|
end
|
|
1047
1207
|
```
|
|
1048
1208
|
|
|
1049
|
-
|
|
1050
1209
|
## Model Operations
|
|
1051
1210
|
|
|
1052
1211
|
One of the key features of RailsOps is model operations. RailsOps provides
|
|
@@ -1360,6 +1519,124 @@ end
|
|
|
1360
1519
|
As this base class is very minimalistic, it is recommended to fully read and
|
|
1361
1520
|
comprehend its source code.
|
|
1362
1521
|
|
|
1522
|
+
### Practical Example: Complete CRUD Operation Set
|
|
1523
|
+
|
|
1524
|
+
Here is a complete, minimal CRUD set for a single model using `schema3`:
|
|
1525
|
+
|
|
1526
|
+
```ruby
|
|
1527
|
+
# app/operations/category/load.rb
|
|
1528
|
+
module Operations::Category
|
|
1529
|
+
class Load < RailsOps::Operation::Model::Load
|
|
1530
|
+
schema3 do
|
|
1531
|
+
int! :id
|
|
1532
|
+
end
|
|
1533
|
+
|
|
1534
|
+
model ::Category
|
|
1535
|
+
end
|
|
1536
|
+
end
|
|
1537
|
+
|
|
1538
|
+
# app/operations/category/create.rb
|
|
1539
|
+
module Operations::Category
|
|
1540
|
+
class Create < RailsOps::Operation::Model::Create
|
|
1541
|
+
schema3 do
|
|
1542
|
+
hsh? :category do
|
|
1543
|
+
str? :name
|
|
1544
|
+
str? :description
|
|
1545
|
+
end
|
|
1546
|
+
end
|
|
1547
|
+
|
|
1548
|
+
model ::Category
|
|
1549
|
+
end
|
|
1550
|
+
end
|
|
1551
|
+
|
|
1552
|
+
# app/operations/category/update.rb
|
|
1553
|
+
module Operations::Category
|
|
1554
|
+
class Update < RailsOps::Operation::Model::Update
|
|
1555
|
+
schema3 do
|
|
1556
|
+
int! :id
|
|
1557
|
+
hsh? :category do
|
|
1558
|
+
str? :name
|
|
1559
|
+
str? :description
|
|
1560
|
+
end
|
|
1561
|
+
end
|
|
1562
|
+
|
|
1563
|
+
model ::Category
|
|
1564
|
+
end
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
# app/operations/category/destroy.rb
|
|
1568
|
+
module Operations::Category
|
|
1569
|
+
class Destroy < RailsOps::Operation::Model::Destroy
|
|
1570
|
+
schema3 do
|
|
1571
|
+
int! :id
|
|
1572
|
+
end
|
|
1573
|
+
|
|
1574
|
+
model ::Category
|
|
1575
|
+
end
|
|
1576
|
+
end
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
For `Create` and `Update`, parameter extraction happens automatically: the
|
|
1580
|
+
params nested under the model's `param_key` (`:category`) are assigned to the
|
|
1581
|
+
model. No `perform` method is needed — the base class handles `save!`.
|
|
1582
|
+
|
|
1583
|
+
### Practical Example: Setting Defaults on Create
|
|
1584
|
+
|
|
1585
|
+
Override `build_model` to set default values or assign associations that aren't
|
|
1586
|
+
part of the user's input:
|
|
1587
|
+
|
|
1588
|
+
```ruby
|
|
1589
|
+
module Operations::Article
|
|
1590
|
+
class Create < RailsOps::Operation::Model::Create
|
|
1591
|
+
schema3 do
|
|
1592
|
+
hsh? :article do
|
|
1593
|
+
str? :title
|
|
1594
|
+
str? :body
|
|
1595
|
+
int? :category_id
|
|
1596
|
+
end
|
|
1597
|
+
end
|
|
1598
|
+
|
|
1599
|
+
model ::Article
|
|
1600
|
+
|
|
1601
|
+
protected
|
|
1602
|
+
|
|
1603
|
+
def build_model
|
|
1604
|
+
super # Builds the model and assigns params from :article key
|
|
1605
|
+
model.author = context.user
|
|
1606
|
+
model.status = 'draft'
|
|
1607
|
+
end
|
|
1608
|
+
end
|
|
1609
|
+
end
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
`super` in `build_model` creates a new model instance and assigns the
|
|
1613
|
+
attributes from params. After `super`, you can set additional attributes.
|
|
1614
|
+
|
|
1615
|
+
### Practical Example: Overriding `build_model` in Update
|
|
1616
|
+
|
|
1617
|
+
In `Update` operations, `build_model` first loads the record (via `Load`), then
|
|
1618
|
+
assigns the params. Override it to modify the model after loading:
|
|
1619
|
+
|
|
1620
|
+
```ruby
|
|
1621
|
+
module Operations::Token
|
|
1622
|
+
class MarkUsed < RailsOps::Operation::Model::Update
|
|
1623
|
+
schema3 do
|
|
1624
|
+
int! :id
|
|
1625
|
+
end
|
|
1626
|
+
|
|
1627
|
+
model ::Token
|
|
1628
|
+
without_authorization
|
|
1629
|
+
|
|
1630
|
+
protected
|
|
1631
|
+
|
|
1632
|
+
def build_model
|
|
1633
|
+
super # Loads the record and assigns params
|
|
1634
|
+
model.used_at = Time.current
|
|
1635
|
+
end
|
|
1636
|
+
end
|
|
1637
|
+
end
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1363
1640
|
### Including Associated Records
|
|
1364
1641
|
|
|
1365
1642
|
Normally, when inheriting from `RailsOps::Operation::Model::Load` (as well as from the
|
|
@@ -1388,6 +1665,46 @@ class Operations::User::Load < RailsOps::Operation::Model::Load
|
|
|
1388
1665
|
end
|
|
1389
1666
|
```
|
|
1390
1667
|
|
|
1668
|
+
#### Practical Example: Load Operation for Show Pages
|
|
1669
|
+
|
|
1670
|
+
A common pattern for "show" pages: a `Load` operation that provides helper
|
|
1671
|
+
methods for loading related data in the view:
|
|
1672
|
+
|
|
1673
|
+
```ruby
|
|
1674
|
+
module Operations::Frontend::Articles
|
|
1675
|
+
class Show < RailsOps::Operation::Model::Load
|
|
1676
|
+
model ::Article
|
|
1677
|
+
model_includes [:tags, :category, { comments: :author }]
|
|
1678
|
+
|
|
1679
|
+
def recent_comments(limit: 10)
|
|
1680
|
+
model.comments.order(created_at: :desc).limit(limit)
|
|
1681
|
+
end
|
|
1682
|
+
|
|
1683
|
+
def related_articles
|
|
1684
|
+
@related_articles ||= ::Article
|
|
1685
|
+
.where(category_id: model.category_id)
|
|
1686
|
+
.where.not(id: model.id)
|
|
1687
|
+
.limit(5)
|
|
1688
|
+
end
|
|
1689
|
+
|
|
1690
|
+
protected
|
|
1691
|
+
|
|
1692
|
+
# Load operations don't need perform logic, but the base class
|
|
1693
|
+
# raises NotImplementedError, so we override with a no-op.
|
|
1694
|
+
def perform; end
|
|
1695
|
+
end
|
|
1696
|
+
end
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
In the controller:
|
|
1700
|
+
|
|
1701
|
+
```ruby
|
|
1702
|
+
def show
|
|
1703
|
+
op Operations::Frontend::Articles::Show
|
|
1704
|
+
# In the view: op.model, op.recent_comments, op.related_articles
|
|
1705
|
+
end
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1391
1708
|
### Parameter Extraction for Create and Update
|
|
1392
1709
|
|
|
1393
1710
|
As mentioned before, the `Create` and `Update` base classes provide an
|
|
@@ -1483,6 +1800,43 @@ class Operations::User::Update < RailsOps::Operation::Model::Update
|
|
|
1483
1800
|
end
|
|
1484
1801
|
```
|
|
1485
1802
|
|
|
1803
|
+
#### Practical Example: Custom Authorization Actions
|
|
1804
|
+
|
|
1805
|
+
For operations beyond standard CRUD (e.g., archiving, classifying, publishing),
|
|
1806
|
+
specify custom authorization actions:
|
|
1807
|
+
|
|
1808
|
+
```ruby
|
|
1809
|
+
module Operations::Article
|
|
1810
|
+
class Archive < RailsOps::Operation::Model::Update
|
|
1811
|
+
schema3 do
|
|
1812
|
+
int! :id
|
|
1813
|
+
end
|
|
1814
|
+
|
|
1815
|
+
load_model_authorization_action :read
|
|
1816
|
+
model_authorization_action :archive
|
|
1817
|
+
|
|
1818
|
+
model ::Article
|
|
1819
|
+
|
|
1820
|
+
protected
|
|
1821
|
+
|
|
1822
|
+
def perform
|
|
1823
|
+
model.archived = true
|
|
1824
|
+
model.archived_at = Time.current
|
|
1825
|
+
model.archived_by = context.user
|
|
1826
|
+
super
|
|
1827
|
+
end
|
|
1828
|
+
end
|
|
1829
|
+
end
|
|
1830
|
+
```
|
|
1831
|
+
|
|
1832
|
+
In your ability file, define the custom action:
|
|
1833
|
+
|
|
1834
|
+
```ruby
|
|
1835
|
+
can :archive, Article do |article|
|
|
1836
|
+
article.author_id == user.id || user.admin?
|
|
1837
|
+
end
|
|
1838
|
+
```
|
|
1839
|
+
|
|
1486
1840
|
### Model Nesting
|
|
1487
1841
|
|
|
1488
1842
|
Using active record, multiple nested models can be saved at once by using
|
|
@@ -1738,6 +2092,51 @@ class Operations::Order::Checkout < RailsOps::Operation::Model::Update
|
|
|
1738
2092
|
end
|
|
1739
2093
|
```
|
|
1740
2094
|
|
|
2095
|
+
#### Practical Example: Virtual Datetime Fields for Forms
|
|
2096
|
+
|
|
2097
|
+
A very common use case is adding virtual datetime attributes for form inputs
|
|
2098
|
+
that need to be transformed before saving:
|
|
2099
|
+
|
|
2100
|
+
```ruby
|
|
2101
|
+
module Operations::Event
|
|
2102
|
+
class Create < RailsOps::Operation::Model::Create
|
|
2103
|
+
schema3 do
|
|
2104
|
+
hsh? :event do
|
|
2105
|
+
str? :title
|
|
2106
|
+
str? :virtual_start_datetime
|
|
2107
|
+
str? :virtual_end_datetime
|
|
2108
|
+
boo? :all_day
|
|
2109
|
+
end
|
|
2110
|
+
end
|
|
2111
|
+
|
|
2112
|
+
model ::Event do
|
|
2113
|
+
attribute :virtual_start_datetime, :datetime
|
|
2114
|
+
attribute :virtual_end_datetime, :datetime
|
|
2115
|
+
|
|
2116
|
+
validates :virtual_start_datetime, presence: true, unless: :all_day?
|
|
2117
|
+
validates :virtual_end_datetime, presence: true, unless: :all_day?
|
|
2118
|
+
validates :virtual_end_datetime,
|
|
2119
|
+
comparison: { greater_than_or_equal_to: :virtual_start_datetime },
|
|
2120
|
+
if: -> { !all_day? && virtual_start_datetime.present? }
|
|
2121
|
+
end
|
|
2122
|
+
|
|
2123
|
+
protected
|
|
2124
|
+
|
|
2125
|
+
def build_model
|
|
2126
|
+
super
|
|
2127
|
+
|
|
2128
|
+
if model.all_day?
|
|
2129
|
+
model.start_date = model.virtual_start_datetime&.beginning_of_day
|
|
2130
|
+
model.end_date = model.virtual_end_datetime&.end_of_day
|
|
2131
|
+
else
|
|
2132
|
+
model.start_date = model.virtual_start_datetime
|
|
2133
|
+
model.end_date = model.virtual_end_datetime
|
|
2134
|
+
end
|
|
2135
|
+
end
|
|
2136
|
+
end
|
|
2137
|
+
end
|
|
2138
|
+
```
|
|
2139
|
+
|
|
1741
2140
|
### Combining Real and Virtual Models
|
|
1742
2141
|
|
|
1743
2142
|
You can create operations that work with both persisted and virtual data:
|
|
@@ -2127,6 +2526,103 @@ sub-operations, see section *Calling sub-operations* for more information.
|
|
|
2127
2526
|
|
|
2128
2527
|
## Operation Inheritance
|
|
2129
2528
|
|
|
2529
|
+
Operations support standard Ruby class inheritance. This is useful when multiple
|
|
2530
|
+
models share the same operation pattern. Create an abstract base operation and
|
|
2531
|
+
then inherit from it for each model:
|
|
2532
|
+
|
|
2533
|
+
```ruby
|
|
2534
|
+
# app/operations/base/toggle_active.rb
|
|
2535
|
+
module Operations::Base
|
|
2536
|
+
class ToggleActive < RailsOps::Operation::Model::Update
|
|
2537
|
+
schema3 do
|
|
2538
|
+
int! :id
|
|
2539
|
+
end
|
|
2540
|
+
|
|
2541
|
+
protected
|
|
2542
|
+
|
|
2543
|
+
def perform
|
|
2544
|
+
model.active = !model.active
|
|
2545
|
+
super
|
|
2546
|
+
end
|
|
2547
|
+
end
|
|
2548
|
+
end
|
|
2549
|
+
|
|
2550
|
+
# app/operations/category/toggle_active.rb
|
|
2551
|
+
module Operations::Category
|
|
2552
|
+
class ToggleActive < Operations::Base::ToggleActive
|
|
2553
|
+
model ::Category
|
|
2554
|
+
end
|
|
2555
|
+
end
|
|
2556
|
+
|
|
2557
|
+
# app/operations/tag/toggle_active.rb
|
|
2558
|
+
module Operations::Tag
|
|
2559
|
+
class ToggleActive < Operations::Base::ToggleActive
|
|
2560
|
+
model ::Tag
|
|
2561
|
+
end
|
|
2562
|
+
end
|
|
2563
|
+
```
|
|
2564
|
+
|
|
2565
|
+
The base class defines the common schema and behavior. Subclasses only need to
|
|
2566
|
+
specify the `model`. This avoids duplicating logic across many operations.
|
|
2567
|
+
|
|
2568
|
+
Schemas, policies, and authorization settings are all inherited. Subclasses can
|
|
2569
|
+
add additional policies or override methods as needed.
|
|
2570
|
+
|
|
2571
|
+
### Practical Example: Bulk Insert Operation Base
|
|
2572
|
+
|
|
2573
|
+
Another common base class is for bulk insert operations:
|
|
2574
|
+
|
|
2575
|
+
```ruby
|
|
2576
|
+
module Operations::Base
|
|
2577
|
+
class BulkCreate < RailsOps::Operation
|
|
2578
|
+
BATCH_SIZE = 500
|
|
2579
|
+
|
|
2580
|
+
without_authorization
|
|
2581
|
+
|
|
2582
|
+
protected
|
|
2583
|
+
|
|
2584
|
+
def perform
|
|
2585
|
+
unique_ids = ids_to_insert.uniq
|
|
2586
|
+
return if unique_ids.empty?
|
|
2587
|
+
|
|
2588
|
+
now = Time.current
|
|
2589
|
+
|
|
2590
|
+
unique_ids.each_slice(self.class::BATCH_SIZE) do |batch|
|
|
2591
|
+
records = build_records(batch, now)
|
|
2592
|
+
target_class.insert_all!(records)
|
|
2593
|
+
rescue ActiveRecord::RecordNotUnique
|
|
2594
|
+
# Race condition: filter out already-existing records and retry
|
|
2595
|
+
existing = existing_ids_for(batch)
|
|
2596
|
+
new_records = records.reject { |r| existing.include?(r[id_column]) }
|
|
2597
|
+
target_class.insert_all!(new_records) if new_records.any?
|
|
2598
|
+
end
|
|
2599
|
+
end
|
|
2600
|
+
|
|
2601
|
+
private
|
|
2602
|
+
|
|
2603
|
+
def ids_to_insert
|
|
2604
|
+
fail NotImplementedError
|
|
2605
|
+
end
|
|
2606
|
+
|
|
2607
|
+
def build_records(_batch, _now)
|
|
2608
|
+
fail NotImplementedError
|
|
2609
|
+
end
|
|
2610
|
+
|
|
2611
|
+
def target_class
|
|
2612
|
+
fail NotImplementedError
|
|
2613
|
+
end
|
|
2614
|
+
|
|
2615
|
+
def id_column
|
|
2616
|
+
:id
|
|
2617
|
+
end
|
|
2618
|
+
|
|
2619
|
+
def existing_ids_for(_batch)
|
|
2620
|
+
fail NotImplementedError
|
|
2621
|
+
end
|
|
2622
|
+
end
|
|
2623
|
+
end
|
|
2624
|
+
```
|
|
2625
|
+
|
|
2130
2626
|
## Generators
|
|
2131
2627
|
|
|
2132
2628
|
RailsOps features a generator to easily create a structure for common CRUD-style
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.7.
|
|
1
|
+
1.7.8
|
data/rails_ops.gemspec
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# stub: rails_ops 1.7.
|
|
2
|
+
# stub: rails_ops 1.7.8 ruby lib
|
|
3
3
|
|
|
4
4
|
Gem::Specification.new do |s|
|
|
5
5
|
s.name = "rails_ops".freeze
|
|
6
|
-
s.version = "1.7.
|
|
6
|
+
s.version = "1.7.8"
|
|
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-
|
|
11
|
+
s.date = "2026-03-12"
|
|
12
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]
|
|
13
13
|
s.homepage = "https://github.com/sitrox/rails_ops".freeze
|
|
14
14
|
s.licenses = ["MIT".freeze]
|
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.7.
|
|
4
|
+
version: 1.7.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sitrox
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-03-12 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: active_type
|