rails_ops 1.7.3 → 1.7.5
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/.github/workflows/ruby.yml +0 -4
- data/Appraisals +3 -0
- data/CHANGELOG.md +15 -0
- data/CLAUDE.md +83 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -2
- data/LICENSE +1 -1
- data/README.md +452 -105
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/gemfiles/rails_6.0.gemfile +2 -0
- data/gemfiles/rails_6.1.gemfile +2 -0
- data/gemfiles/rails_7.0.gemfile +2 -0
- data/gemfiles/rails_7.1.gemfile +1 -0
- data/gemfiles/rails_7.2.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/lib/rails_ops/mixins/model/nesting.rb +5 -0
- data/lib/rails_ops/mixins/policies.rb +8 -0
- data/rails_ops.gemspec +5 -4
- data/test/unit/rails_ops/mixins/policies_test.rb +93 -0
- metadata +5 -3
data/README.md
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
[](https://github.com/sitrox/rails_ops/actions/workflows/rubocop.yml)
|
|
3
3
|
[](https://badge.fury.io/rb/rails_ops)
|
|
4
4
|
|
|
5
|
-
rails_ops
|
|
6
|
-
=========
|
|
5
|
+
# rails_ops
|
|
7
6
|
|
|
8
7
|
This Gem introduces an additional service layer for Rails: *Operations*. An
|
|
9
8
|
operation is in most cases a *business action* or *use case* and may or may not
|
|
@@ -18,8 +17,7 @@ To achieve this goal, this Gem provides the following building blocks:
|
|
|
18
17
|
|
|
19
18
|
- A way of abstracting model classes for a specific business action.
|
|
20
19
|
|
|
21
|
-
Requirements & Installation
|
|
22
|
-
---------------------------
|
|
20
|
+
## Requirements & Installation
|
|
23
21
|
|
|
24
22
|
### Requirements
|
|
25
23
|
|
|
@@ -90,10 +88,9 @@ Requirements & Installation
|
|
|
90
88
|
|
|
91
89
|
Taken from [this github issues comment](https://github.com/rails/rails/issues/40126#issuecomment-816275285).
|
|
92
90
|
|
|
93
|
-
Operation Basics
|
|
94
|
-
----------------
|
|
91
|
+
## Operation Basics
|
|
95
92
|
|
|
96
|
-
### Placing and
|
|
93
|
+
### Placing and Naming Operations
|
|
97
94
|
|
|
98
95
|
- Operations generally reside in `app/operations` and can be nested using
|
|
99
96
|
various subdirectories. They're all inside of the `Operations` namespace.
|
|
@@ -110,7 +107,7 @@ Operation Basics
|
|
|
110
107
|
`MoveToPosition` and so on. Do not name an operation something like
|
|
111
108
|
`UserCreator` or `CreateUserOperation`.
|
|
112
109
|
|
|
113
|
-
#### Heads-up: Correct
|
|
110
|
+
#### Heads-up: Correct Namespacing
|
|
114
111
|
|
|
115
112
|
As explained in the previous section, operations should be namespaced properly.
|
|
116
113
|
Operations can either live within a module or within a class. In most cases,
|
|
@@ -160,7 +157,7 @@ Note that, when defining a namespace of which a segment is already known as a
|
|
|
160
157
|
end
|
|
161
158
|
```
|
|
162
159
|
|
|
163
|
-
### Basic
|
|
160
|
+
### Basic Operations
|
|
164
161
|
|
|
165
162
|
Every single operation follows a few basic principles:
|
|
166
163
|
|
|
@@ -186,7 +183,7 @@ class Operations::PrintHelloWorld < RailsOps::Operation
|
|
|
186
183
|
end
|
|
187
184
|
```
|
|
188
185
|
|
|
189
|
-
### Running
|
|
186
|
+
### Running Operations Manually
|
|
190
187
|
|
|
191
188
|
There are various ways of instantiating and running an operation. The most
|
|
192
189
|
basic way is the following:
|
|
@@ -212,7 +209,7 @@ models or ActiveRecord models, `run` does not only catch the
|
|
|
212
209
|
`ActiveRecord::RecordInvalid` exception but also every exception that derives
|
|
213
210
|
from {RailsOps::Exceptions::ValidationFailed}.
|
|
214
211
|
|
|
215
|
-
#### Catching
|
|
212
|
+
#### Catching Custom Exceptions in `run`
|
|
216
213
|
|
|
217
214
|
If you'd like to catch a custom exception if the operation is called using
|
|
218
215
|
`run`, you can either derive this exception from
|
|
@@ -229,7 +226,7 @@ class Operations::PrintHelloWorld < RailsOps::Operation
|
|
|
229
226
|
end
|
|
230
227
|
```
|
|
231
228
|
|
|
232
|
-
### Returning
|
|
229
|
+
### Returning Data from Operations
|
|
233
230
|
|
|
234
231
|
All operations have the same call signatures: `run` always returns `true` or
|
|
235
232
|
`false` while `run!` always returns the operation instance (which allows easy
|
|
@@ -248,10 +245,9 @@ end
|
|
|
248
245
|
puts Operations::GenerateHelloWorld.run!(name: 'John Doe').result
|
|
249
246
|
```
|
|
250
247
|
|
|
251
|
-
Params Handling
|
|
252
|
-
---------------
|
|
248
|
+
## Params Handling
|
|
253
249
|
|
|
254
|
-
|
|
250
|
+
### Passing Params to Operations
|
|
255
251
|
|
|
256
252
|
Each single operation can take a `params` hash. Note that it does not have to be
|
|
257
253
|
in any relation with `ActionController`'s params - it's just a plain ruby hash
|
|
@@ -268,7 +264,7 @@ If no params are given, an empty params hash will be used. If a
|
|
|
268
264
|
`ActionController::Parameters` object is passed, it will be permitted using
|
|
269
265
|
`permit!` and converted into a regular hash.
|
|
270
266
|
|
|
271
|
-
|
|
267
|
+
### Accessing Params
|
|
272
268
|
|
|
273
269
|
For accessing params within an operation, you can use `params` or `osparams`.
|
|
274
270
|
While `params` directly returns the params hash, `osparams` converts them into
|
|
@@ -287,9 +283,9 @@ end
|
|
|
287
283
|
Note that both `params` and `osparams` return independent, deep duplicates of
|
|
288
284
|
the original `params` hash to the operation, so the hashes do not correspond.
|
|
289
285
|
|
|
290
|
-
The hash accessed via `params` is
|
|
286
|
+
The hash accessed via `params` is always an `Object::HashWithIndifferentAccess`.
|
|
291
287
|
|
|
292
|
-
|
|
288
|
+
### Validating Params
|
|
293
289
|
|
|
294
290
|
You're strongly encouraged to perform a validation of the parameters passed to
|
|
295
291
|
an operation, as unvalidated params pose a security threat. This can be done in several ways:
|
|
@@ -310,7 +306,7 @@ an operation, as unvalidated params pose a security threat. This can be done in
|
|
|
310
306
|
|
|
311
307
|
This is the recommended way of performing basic params validation. Please see the next section *Schema best practices* for more information.
|
|
312
308
|
|
|
313
|
-
See documentation of the
|
|
309
|
+
See documentation of the gem `schemacop` for more information on how to
|
|
314
310
|
specify schemata.
|
|
315
311
|
|
|
316
312
|
|
|
@@ -332,7 +328,7 @@ an operation, as unvalidated params pose a security threat. This can be done in
|
|
|
332
328
|
|
|
333
329
|
- Using a business model (see chapter *Model Operations*).
|
|
334
330
|
|
|
335
|
-
### Schema
|
|
331
|
+
### Schema Best Practices
|
|
336
332
|
|
|
337
333
|
As previously mentioned, using schema from the `schemacop` gem is the recommended way to validate params passed in to an operation. In general, it's recommended to use version 3 of schemacop, i.e. either use `schema3` to specify the schema, or set the default schema version to 3:
|
|
338
334
|
|
|
@@ -343,7 +339,7 @@ RailsOps.configure do |config|
|
|
|
343
339
|
end
|
|
344
340
|
```
|
|
345
341
|
|
|
346
|
-
#### Internal
|
|
342
|
+
#### Internal Code
|
|
347
343
|
|
|
348
344
|
When writing a schema for an operation which is only used internally (e.g. called from another operation, or called from a part of the code where you control the params, e.g. a rake task), it's recommended to specify the types of all items, as this will catch any mismatched data. For example:
|
|
349
345
|
|
|
@@ -360,7 +356,7 @@ class Operations::PrintHelloWorldWithId < RailsOps::Operation
|
|
|
360
356
|
end
|
|
361
357
|
```
|
|
362
358
|
|
|
363
|
-
#### Called
|
|
359
|
+
#### Called Within Controllers
|
|
364
360
|
|
|
365
361
|
On the other hand, operations which are called within controllers (e.g. to encapsulate an update operation of a model) should not assume any types, and instead use model validations (if applicable) to validate the correctness of the data. In this case, the schema should only be used to filter the params. As such, it's recommended to use `obj` to specify params which are not strings, as this will allow anything (but only the specified values). An example would be:
|
|
366
362
|
|
|
@@ -381,7 +377,7 @@ end
|
|
|
381
377
|
|
|
382
378
|
Validating that `age` is an integer and `is_active` then should be done with a validation in the `User` model, as this will also populate the model errors, which in turn will display the error in the form. If you were to validate the type of the data here, it would raise a `Schemacop::Exceptions::ValidationError` exception, which you would need to handle seperately.
|
|
383
379
|
|
|
384
|
-
Finally, when additional, obsolete params are supplied, the schema validation would also fail. To have a similar
|
|
380
|
+
Finally, when additional, obsolete params are supplied, the schema validation would also fail. To have a similar behaviour to the strong params from Rails, which drop non-whitelisted params without raising an exception, you can use the `ignore_obsolete_properties` option. This will simply ignore and drop any params which are not explicitly whitelisted:
|
|
385
381
|
|
|
386
382
|
```ruby
|
|
387
383
|
module Operations::User
|
|
@@ -398,9 +394,9 @@ module Operations::User
|
|
|
398
394
|
end
|
|
399
395
|
```
|
|
400
396
|
|
|
401
|
-
### Catching
|
|
397
|
+
### Catching Schema Validation Errors
|
|
402
398
|
|
|
403
|
-
When an operation is called from a controller (via the `run` or `run!` method) and a schema validation
|
|
399
|
+
When an operation is called from a controller (via the `run` or `run!` method) and a schema validation exception occurs, the controller will respond with an empty body and a status code `400` (bad request). This behaviour is enabled by default, but can be disabled with the `rescue_validation_error_in_controller` config option:
|
|
404
400
|
|
|
405
401
|
```ruby
|
|
406
402
|
# config/initializers/rails_ops.rb
|
|
@@ -413,8 +409,7 @@ Generally, this should be left enabled, as sending invalid data to the controlle
|
|
|
413
409
|
|
|
414
410
|
Please note that this behaviour is disabled in development mode, as the full exception messages are useful for debugging purposes.
|
|
415
411
|
|
|
416
|
-
Policies
|
|
417
|
-
--------
|
|
412
|
+
## Policies
|
|
418
413
|
|
|
419
414
|
Policies are nothing more than blocks of code that run either at operation
|
|
420
415
|
instantiation or before / after execution of the `perform` method and can be
|
|
@@ -449,7 +444,7 @@ method. Use policies as much as possible though to keep things separated.
|
|
|
449
444
|
The return value of the policies is discarded. If a policy needs to fail, raise
|
|
450
445
|
an appropriate exception.
|
|
451
446
|
|
|
452
|
-
### Policy
|
|
447
|
+
### Policy Chains
|
|
453
448
|
|
|
454
449
|
As mentioned above, policies can be executed at various points in your
|
|
455
450
|
operation's lifecycle. This is possible using *policy chains*:
|
|
@@ -484,6 +479,17 @@ operation's lifecycle. This is possible using *policy chains*:
|
|
|
484
479
|
its descendants. Policies in this chain run after nested model operations are
|
|
485
480
|
performed before performing any nested model operations.
|
|
486
481
|
|
|
482
|
+
- `:before_model_validation`
|
|
483
|
+
|
|
484
|
+
This only applies to operations deriving from `RailsOps::Operation::Model`
|
|
485
|
+
and its descendants. Policies in this chain run right before
|
|
486
|
+
`model.validate!` is called inside `perform_nested_model_ops!`. This is
|
|
487
|
+
the correct place for attribute cleanup and sanitization — for example,
|
|
488
|
+
nilling out attributes that are irrelevant based on another attribute's
|
|
489
|
+
value (e.g. role-dependent fields after a form reload). At this point
|
|
490
|
+
the model's attributes are already assigned, so you can inspect and
|
|
491
|
+
modify them before validation runs.
|
|
492
|
+
|
|
487
493
|
- `:after_perform`
|
|
488
494
|
|
|
489
495
|
Policies in this chain run immediately after the `perform` method is called.
|
|
@@ -521,8 +527,7 @@ It is also important to note, that this block is
|
|
|
521
527
|
not guaranteed to be run first in the chain, if multiple blocks have set `:prepend_action` to true.
|
|
522
528
|
|
|
523
529
|
|
|
524
|
-
Calling
|
|
525
|
-
----------------------
|
|
530
|
+
## Calling Sub-Operations
|
|
526
531
|
|
|
527
532
|
It is possible and encouraged to call operations within operations if necessary.
|
|
528
533
|
As the basic principle is to create one operation per business action, there are
|
|
@@ -551,7 +556,7 @@ within the context that is automatically adapted and passed to the sub-operation
|
|
|
551
556
|
and enables to maintain the complete call stack and allows to pass on context
|
|
552
557
|
information such as the current user.
|
|
553
558
|
|
|
554
|
-
### A
|
|
559
|
+
### A Note on Validations
|
|
555
560
|
|
|
556
561
|
As always when calling operations, you can decide whether an execution should
|
|
557
562
|
raise an exception on validation errors or else just return `false` by using the
|
|
@@ -574,15 +579,14 @@ catches any validation errors and re-throws them as
|
|
|
574
579
|
{RailsOps::Exceptions::SubOpValidationFailed} which is not caught by the
|
|
575
580
|
surrounding op.
|
|
576
581
|
|
|
577
|
-
Contexts
|
|
578
|
-
--------
|
|
582
|
+
## Contexts
|
|
579
583
|
|
|
580
584
|
Most operations make use of generic parameters like the current user or an
|
|
581
585
|
authorization ability. Sure this could all be passed using the `params` hash,
|
|
582
586
|
but as this would have to be done for every single operation call, it would be
|
|
583
587
|
quite cumbersome.
|
|
584
588
|
|
|
585
|
-
For this reason Rails Ops provides a feature called *Contexts*. Contexts are
|
|
589
|
+
For this reason, Rails Ops provides a feature called *Contexts*. Contexts are
|
|
586
590
|
simple instances of {RailsOps::Context} that may or may not be passed to
|
|
587
591
|
operations. Contexts can include the following data:
|
|
588
592
|
|
|
@@ -625,7 +629,7 @@ operations. Contexts can include the following data:
|
|
|
625
629
|
called by a hook (true) or by a regular method call (false). We will introduce
|
|
626
630
|
hooks below.
|
|
627
631
|
|
|
628
|
-
### Instantiating
|
|
632
|
+
### Instantiating Contexts
|
|
629
633
|
|
|
630
634
|
Contexts behave like a traditional model object and can be instantiated in
|
|
631
635
|
multiple ways:
|
|
@@ -638,7 +642,7 @@ context = Context.new
|
|
|
638
642
|
context.user = current_user
|
|
639
643
|
```
|
|
640
644
|
|
|
641
|
-
### Feeding
|
|
645
|
+
### Feeding Contexts to Operations
|
|
642
646
|
|
|
643
647
|
Contexts are assigned to operations via the operation's constructor:
|
|
644
648
|
|
|
@@ -664,8 +668,7 @@ the parent operation.
|
|
|
664
668
|
This is called *context spawning* and is performed using the
|
|
665
669
|
{RailsOps::Context.spawn} method.
|
|
666
670
|
|
|
667
|
-
Hooks
|
|
668
|
-
-----
|
|
671
|
+
## Hooks
|
|
669
672
|
|
|
670
673
|
In some cases, certain actions must be hooked in after execution of an
|
|
671
674
|
operation. While this can certainly be done with sub-operations, it is not
|
|
@@ -684,7 +687,7 @@ specify which operations should be triggered after which operations. These
|
|
|
684
687
|
operations are then automatically triggered after the original operation's
|
|
685
688
|
`perform` (in the `run` method).
|
|
686
689
|
|
|
687
|
-
### Defining
|
|
690
|
+
### Defining Hooks
|
|
688
691
|
|
|
689
692
|
Hooks are defined in a file named `config/hookup.rb` in your local application.
|
|
690
693
|
In development mode, this file is automatically reloaded on each request so
|
|
@@ -736,7 +739,7 @@ end
|
|
|
736
739
|
In most cases though, situations like these should rather be handled by
|
|
737
740
|
explicitly calling a sub-operation.
|
|
738
741
|
|
|
739
|
-
### Hook
|
|
742
|
+
### Hook Parameters
|
|
740
743
|
|
|
741
744
|
For each hook that is called, at set of parameters is passed to the respective
|
|
742
745
|
operations. When calling events manually (see section *Events*), you can
|
|
@@ -756,7 +759,7 @@ hooks into the source operation and prepares the params specifically for the
|
|
|
756
759
|
target operation, which is then called using a sub-operation or the hooking
|
|
757
760
|
system.
|
|
758
761
|
|
|
759
|
-
### Check if
|
|
762
|
+
### Check if Called via Hook
|
|
760
763
|
|
|
761
764
|
You can determine whether your operation has been (directly) called via a hook
|
|
762
765
|
using the `called_via_hook` context method:
|
|
@@ -774,11 +777,10 @@ sub-operation is set to `false` again.
|
|
|
774
777
|
### Authorization
|
|
775
778
|
|
|
776
779
|
Operations called via hooks perform normal authorization per default. You can
|
|
777
|
-
turn this off by switching off the
|
|
780
|
+
turn this off by switching off the global option
|
|
778
781
|
`config.trigger_hookups_without_authorization`.
|
|
779
782
|
|
|
780
|
-
Authorization
|
|
781
|
-
-------------
|
|
783
|
+
## Authorization
|
|
782
784
|
|
|
783
785
|
Rails Ops offers backend-agnostic authorization using so-called
|
|
784
786
|
*authorization backends*.
|
|
@@ -787,7 +789,7 @@ Authorization basically happens by calling the method `authorize!` (or
|
|
|
787
789
|
`authorize_only!`, more on that later) within an operation. What exactly this
|
|
788
790
|
method does depends on the *authorization backend* specified.
|
|
789
791
|
|
|
790
|
-
### Authorization
|
|
792
|
+
### Authorization Backends
|
|
791
793
|
|
|
792
794
|
Authorization backends are simple classes that supply the method `authorize!`.
|
|
793
795
|
This method, besides the operation instance, can take any number of arguments
|
|
@@ -807,12 +809,12 @@ end
|
|
|
807
809
|
|
|
808
810
|
RailsOps ships with the following backend:
|
|
809
811
|
|
|
810
|
-
- `RailsOps::
|
|
812
|
+
- `RailsOps::AuthorizationBackend::CanCanCan`
|
|
811
813
|
|
|
812
|
-
Offers integration of the `cancancan`
|
|
813
|
-
|
|
814
|
+
Offers integration of the `cancancan` gem (which is a fork of the `cancan`
|
|
815
|
+
gem).
|
|
814
816
|
|
|
815
|
-
### Performing
|
|
817
|
+
### Performing Authorization
|
|
816
818
|
|
|
817
819
|
Authorization is generally performed by calling `authorize!` in an operation.
|
|
818
820
|
The arguments, along with the operation instance, are passed on to the
|
|
@@ -853,7 +855,7 @@ end
|
|
|
853
855
|
|
|
854
856
|
See section *Policy chains* for more information.
|
|
855
857
|
|
|
856
|
-
### Ensure
|
|
858
|
+
### Ensure That Authorization Has Been Performed
|
|
857
859
|
|
|
858
860
|
As it is a very common programming mistake to mistakenly omit calling
|
|
859
861
|
authorization, Rails Ops offers a solution for making sure that authorization
|
|
@@ -887,7 +889,7 @@ end
|
|
|
887
889
|
This method otherwise does exactly the same as `authorize!` (in fact, it's the
|
|
888
890
|
underlying method used by it).
|
|
889
891
|
|
|
890
|
-
### Param
|
|
892
|
+
### Param Authorization
|
|
891
893
|
|
|
892
894
|
Using the static operation method `authorize_param`, you can perform additional
|
|
893
895
|
authorization checks when specific params are passed to the operation. This
|
|
@@ -940,7 +942,7 @@ class Operations::User::Create < RailsOps::Operation::Model::Create
|
|
|
940
942
|
authorize_param %i(user group_id), :assign_group_id
|
|
941
943
|
```
|
|
942
944
|
|
|
943
|
-
### Disabling
|
|
945
|
+
### Disabling Authorization
|
|
944
946
|
|
|
945
947
|
Sometimes you don't want a specific operation to perform authorization, or you
|
|
946
948
|
don't want to perform any authorization at all.
|
|
@@ -1045,8 +1047,7 @@ Rails Ops offers multiple ways of disabling authorization:
|
|
|
1045
1047
|
```
|
|
1046
1048
|
|
|
1047
1049
|
|
|
1048
|
-
Model Operations
|
|
1049
|
-
----------------
|
|
1050
|
+
## Model Operations
|
|
1050
1051
|
|
|
1051
1052
|
One of the key features of RailsOps is model operations. RailsOps provides
|
|
1052
1053
|
multiple operation base classes which allow convenient manipulation of active
|
|
@@ -1059,7 +1060,7 @@ inherit from {RailsOps::Operation::Model} (which in turn inherits from
|
|
|
1059
1060
|
The key principle behind these model classes is to associate *one model class*
|
|
1060
1061
|
and *one model instance* with a particular operation.
|
|
1061
1062
|
|
|
1062
|
-
### Setting a
|
|
1063
|
+
### Setting a Model Class
|
|
1063
1064
|
|
|
1064
1065
|
Using the static method `model`, you can assign a model class that is used in
|
|
1065
1066
|
the scope of this operation.
|
|
@@ -1097,7 +1098,7 @@ class SomeOperation < RailsOps::Operation::Model
|
|
|
1097
1098
|
end
|
|
1098
1099
|
```
|
|
1099
1100
|
|
|
1100
|
-
### Obtaining a
|
|
1101
|
+
### Obtaining a Model Instance
|
|
1101
1102
|
|
|
1102
1103
|
Model instances can be obtained using the *instance* method `model`, which is
|
|
1103
1104
|
not to be confused with the *class* method of the same name. Other than the
|
|
@@ -1126,7 +1127,7 @@ If no cached instance is found, one is built using the instance method
|
|
|
1126
1127
|
but only implemented in its subclasses. You can implement and override this
|
|
1127
1128
|
method to your liking though.
|
|
1128
1129
|
|
|
1129
|
-
### Loading
|
|
1130
|
+
### Loading Models
|
|
1130
1131
|
|
|
1131
1132
|
Using the base operation class {RailsOps::Operation::Model::Load}, a model can
|
|
1132
1133
|
be loaded. This is done by implementing the `build_model` mentioned above. In
|
|
@@ -1150,7 +1151,7 @@ order to load a model (in fact, it cannot be run unless you override the
|
|
|
1150
1151
|
based on a model instance without actually performing any particular action such
|
|
1151
1152
|
as updating a model.
|
|
1152
1153
|
|
|
1153
|
-
#### Specifying ID
|
|
1154
|
+
#### Specifying ID Field
|
|
1154
1155
|
|
|
1155
1156
|
Per default, the model instance is looked up using the field `id` and the ID
|
|
1156
1157
|
obtained from the method params using `params[:id]`. However, you can customize
|
|
@@ -1205,9 +1206,9 @@ class Operations::User::Update < RailsOps::Operation::Model::Load
|
|
|
1205
1206
|
end
|
|
1206
1207
|
```
|
|
1207
1208
|
|
|
1208
|
-
One caveat is
|
|
1209
|
-
|
|
1210
|
-
|
|
1209
|
+
One caveat is that shared locking is only supported for MySQL (MariaDB),
|
|
1210
|
+
PostgreSQL and Oracle DB databases, any other database will always use an
|
|
1211
|
+
exclusive lock.
|
|
1211
1212
|
|
|
1212
1213
|
You can also dynamically enable or disable locking by creating an instance
|
|
1213
1214
|
method `lock_model_at_build?`:
|
|
@@ -1225,7 +1226,7 @@ class Operations::User::Update < RailsOps::Operation::Model::Load
|
|
|
1225
1226
|
end
|
|
1226
1227
|
```
|
|
1227
1228
|
|
|
1228
|
-
### Creating
|
|
1229
|
+
### Creating Models
|
|
1229
1230
|
|
|
1230
1231
|
For creating models, you can use the base class
|
|
1231
1232
|
{RailsOps::Operation::Model::Create}.
|
|
@@ -1255,7 +1256,7 @@ end
|
|
|
1255
1256
|
As this base class is very minimalistic, it is recommended to fully read and
|
|
1256
1257
|
comprehend its source code.
|
|
1257
1258
|
|
|
1258
|
-
#### Overriding the
|
|
1259
|
+
#### Overriding the Perform Method
|
|
1259
1260
|
|
|
1260
1261
|
While in many cases there is no need for overriding the `perform` method, this
|
|
1261
1262
|
can be useful i.e. when assigning or altering properties manually:
|
|
@@ -1269,7 +1270,7 @@ def perform
|
|
|
1269
1270
|
end
|
|
1270
1271
|
```
|
|
1271
1272
|
|
|
1272
|
-
### Updating
|
|
1273
|
+
### Updating Models
|
|
1273
1274
|
|
|
1274
1275
|
For updating models, you can use the base class
|
|
1275
1276
|
{RailsOps::Operation::Model::Update} which is an extension of the `Load` base
|
|
@@ -1304,7 +1305,7 @@ comprehend its source code.
|
|
|
1304
1305
|
As with `Create` operations, the `perform` method can be overwritten at your
|
|
1305
1306
|
liking.
|
|
1306
1307
|
|
|
1307
|
-
### Destroying
|
|
1308
|
+
### Destroying Models
|
|
1308
1309
|
|
|
1309
1310
|
For destroying models, you can use the base class
|
|
1310
1311
|
{RailsOps::Operation::Model::Destroy} which is an extension of the `Load` base
|
|
@@ -1326,9 +1327,9 @@ end
|
|
|
1326
1327
|
As this base class is very minimalistic, it is recommended to fully read and
|
|
1327
1328
|
comprehend its source code.
|
|
1328
1329
|
|
|
1329
|
-
### Including
|
|
1330
|
+
### Including Associated Records
|
|
1330
1331
|
|
|
1331
|
-
|
|
1332
|
+
Normally, when inheriting from `RailsOps::Operation::Model::Load` (as well as from the
|
|
1332
1333
|
`Update` and the `Destroy` operations respectively), RailsOps only loads the instance
|
|
1333
1334
|
of the model specified by the `id` parameter. In some cases, you'd want to eagerly load
|
|
1334
1335
|
associations of the model, e.g. when you need to access associated records.
|
|
@@ -1354,7 +1355,7 @@ class Operations::User::Load < RailsOps::Operation::Model::Load
|
|
|
1354
1355
|
end
|
|
1355
1356
|
```
|
|
1356
1357
|
|
|
1357
|
-
### Parameter
|
|
1358
|
+
### Parameter Extraction for Create and Update
|
|
1358
1359
|
|
|
1359
1360
|
As mentioned before, the `Create` and `Update` base classes provide an
|
|
1360
1361
|
implementation of `build_model` that assigns parameters to a model.
|
|
@@ -1363,12 +1364,12 @@ The attributes are determined by the operation instance method
|
|
|
1363
1364
|
`extract_attributes_from_params` - the name being self-explaining. See its
|
|
1364
1365
|
source code for implementation details.
|
|
1365
1366
|
|
|
1366
|
-
### Model
|
|
1367
|
+
### Model Authorization
|
|
1367
1368
|
|
|
1368
1369
|
While you can use the standard `authorize!` method (see chapter *Authorization*)
|
|
1369
1370
|
for authorizing models, RailsOps provides a more convenient integration.
|
|
1370
1371
|
|
|
1371
|
-
#### Basic
|
|
1372
|
+
#### Basic Authorization
|
|
1372
1373
|
|
|
1373
1374
|
Model authorization can be performed via the operation instance methods
|
|
1374
1375
|
`authorize_model!` and `authorize_model_with_authorize_only!` (see chapter
|
|
@@ -1384,7 +1385,7 @@ methods instead of the basic authorization methods for authorizing models.
|
|
|
1384
1385
|
If no model is given, the model authorization methods automatically obtain the
|
|
1385
1386
|
model from the instance method `model`.
|
|
1386
1387
|
|
|
1387
|
-
#### Automatic
|
|
1388
|
+
#### Automatic Authorization
|
|
1388
1389
|
|
|
1389
1390
|
All model operation classes provide the operation instance method
|
|
1390
1391
|
`model_authorization` which is automatically run at model instantiation (this is
|
|
@@ -1426,7 +1427,7 @@ end
|
|
|
1426
1427
|
Note that using the different model base classes, this is already set to a
|
|
1427
1428
|
sensible default. See the respective class' source code for details.
|
|
1428
1429
|
|
|
1429
|
-
#### Lazy
|
|
1430
|
+
#### Lazy Model Update Authorization
|
|
1430
1431
|
|
|
1431
1432
|
*Please note that using lazy model update authorization is deprecated any may
|
|
1432
1433
|
be removed in a future release. See the changelog for instructions on how to
|
|
@@ -1449,7 +1450,7 @@ class Operations::User::Update < RailsOps::Operation::Model::Update
|
|
|
1449
1450
|
end
|
|
1450
1451
|
```
|
|
1451
1452
|
|
|
1452
|
-
### Model
|
|
1453
|
+
### Model Nesting
|
|
1453
1454
|
|
|
1454
1455
|
Using active record, multiple nested models can be saved at once by using
|
|
1455
1456
|
`accepts_nested_attributes_for`. While this is generally supported by RailsOps,
|
|
@@ -1477,7 +1478,6 @@ class Operations::Group::Create < RailsOps::Operation::Model::Create
|
|
|
1477
1478
|
end
|
|
1478
1479
|
|
|
1479
1480
|
model ::Group
|
|
1480
|
-
nest_model_op :group, Operations::Group::Create
|
|
1481
1481
|
end
|
|
1482
1482
|
```
|
|
1483
1483
|
|
|
@@ -1495,7 +1495,7 @@ class User
|
|
|
1495
1495
|
end
|
|
1496
1496
|
```
|
|
1497
1497
|
|
|
1498
|
-
#### Param
|
|
1498
|
+
#### Param Key
|
|
1499
1499
|
|
|
1500
1500
|
When nesting a model operation, the sub operation is called automatically by
|
|
1501
1501
|
RailsOps. For this purpose, it needs to know which `param_key` to use for
|
|
@@ -1510,7 +1510,7 @@ using the option `param_key`, e.g.:
|
|
|
1510
1510
|
nest_model_op :group, Operations::Group::Create, param_key: :my_custom_key
|
|
1511
1511
|
```
|
|
1512
1512
|
|
|
1513
|
-
#### Custom
|
|
1513
|
+
#### Custom Parameters
|
|
1514
1514
|
|
|
1515
1515
|
In the above examples, all `group_attributes` are automatically passed to the
|
|
1516
1516
|
sub operation. To customize this further, provide a block to the `nest_model_op`
|
|
@@ -1526,10 +1526,10 @@ This block receives the params hash as it would be passed to the sub operation
|
|
|
1526
1526
|
and allows to modify it. The block's return value is then passed to the
|
|
1527
1527
|
sub-operation. Do not change the params inplace but instead return a new hash.
|
|
1528
1528
|
|
|
1529
|
-
### Single-
|
|
1529
|
+
### Single-Table Inheritance
|
|
1530
1530
|
|
|
1531
1531
|
Model operations also support STI models (Single Table Inheritance). However,
|
|
1532
|
-
there is the
|
|
1532
|
+
there is the caveat that if you do extend your model in the operation (e.g.
|
|
1533
1533
|
`model Animal do { ... }`), RailsOps automatically creates an anonymous subclass
|
|
1534
1534
|
of the given class (e.g. `Animal`). Operations will always load / create models
|
|
1535
1535
|
that are instances of this anonymous class.
|
|
@@ -1548,22 +1548,373 @@ class LoadAnimal < RailsOps::Operation::Model::Load
|
|
|
1548
1548
|
end
|
|
1549
1549
|
|
|
1550
1550
|
bird = Bird.create
|
|
1551
|
-
|
|
1551
|
+
op = LoadAnimal.new(id: bird.id)
|
|
1552
|
+
|
|
1553
|
+
bird.class # => Bird (extending Animal)
|
|
1554
|
+
op.model.class # => Anonymous class extending Animal, not Bird
|
|
1555
|
+
```
|
|
1556
|
+
|
|
1557
|
+
## Record Extension and Virtual Records
|
|
1558
|
+
|
|
1559
|
+
RailsOps provides powerful features for extending ActiveRecord models and
|
|
1560
|
+
creating virtual records without affecting your actual model classes. This is
|
|
1561
|
+
achieved through the use of ActiveType and anonymous class generation.
|
|
1562
|
+
|
|
1563
|
+
### Virtual Models
|
|
1564
|
+
|
|
1565
|
+
Virtual models are non-persisted models that behave like ActiveRecord models
|
|
1566
|
+
but exist only in memory. They're useful for:
|
|
1567
|
+
|
|
1568
|
+
- Form objects that don't map directly to database tables
|
|
1569
|
+
- Temporary data structures for complex operations
|
|
1570
|
+
- Aggregating data from multiple sources
|
|
1571
|
+
|
|
1572
|
+
RailsOps provides `RailsOps::VirtualModel` which extends `ActiveType::Object`:
|
|
1573
|
+
|
|
1574
|
+
```ruby
|
|
1575
|
+
class Operations::Contact::Create < RailsOps::Operation::Model::Create
|
|
1576
|
+
model do
|
|
1577
|
+
# Virtual attributes
|
|
1578
|
+
attribute :full_name, :string
|
|
1579
|
+
attribute :email, :string
|
|
1580
|
+
attribute :message, :text
|
|
1581
|
+
attribute :newsletter_opt_in, :boolean, default: false
|
|
1582
|
+
|
|
1583
|
+
# Validations work just like regular models
|
|
1584
|
+
validates :full_name, :email, :message, presence: true
|
|
1585
|
+
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
1586
|
+
end
|
|
1587
|
+
|
|
1588
|
+
def perform
|
|
1589
|
+
# Process the virtual model data
|
|
1590
|
+
ContactMailer.contact_form(
|
|
1591
|
+
name: model.full_name,
|
|
1592
|
+
email: model.email,
|
|
1593
|
+
message: model.message
|
|
1594
|
+
).deliver_later
|
|
1595
|
+
|
|
1596
|
+
# Optionally subscribe to newsletter
|
|
1597
|
+
if model.newsletter_opt_in
|
|
1598
|
+
NewsletterService.subscribe(model.email)
|
|
1599
|
+
end
|
|
1600
|
+
end
|
|
1601
|
+
end
|
|
1602
|
+
```
|
|
1603
|
+
|
|
1604
|
+
### Model Extension
|
|
1605
|
+
|
|
1606
|
+
When you specify a model with a block in an operation, RailsOps creates an
|
|
1607
|
+
anonymous subclass that extends your model without modifying the original:
|
|
1608
|
+
|
|
1609
|
+
```ruby
|
|
1610
|
+
class Operations::User::Import < RailsOps::Operation::Model::Create
|
|
1611
|
+
model User do
|
|
1612
|
+
# These changes only apply within this operation
|
|
1613
|
+
attribute :import_source, :string
|
|
1614
|
+
attribute :skip_notifications, :boolean, default: false
|
|
1615
|
+
|
|
1616
|
+
validates :import_source, presence: true
|
|
1617
|
+
|
|
1618
|
+
# Override methods
|
|
1619
|
+
def name=(value)
|
|
1620
|
+
super(value.strip.titleize)
|
|
1621
|
+
end
|
|
1622
|
+
|
|
1623
|
+
# Add callbacks specific to this operation
|
|
1624
|
+
before_save :normalize_phone_number
|
|
1625
|
+
|
|
1626
|
+
private
|
|
1627
|
+
|
|
1628
|
+
def normalize_phone_number
|
|
1629
|
+
self.phone = PhoneNumberService.normalize(phone) if phone.present?
|
|
1630
|
+
end
|
|
1631
|
+
end
|
|
1632
|
+
|
|
1633
|
+
def perform
|
|
1634
|
+
model.imported_at = Time.current
|
|
1635
|
+
super
|
|
1636
|
+
|
|
1637
|
+
unless model.skip_notifications
|
|
1638
|
+
UserMailer.welcome(model).deliver_later
|
|
1639
|
+
end
|
|
1640
|
+
end
|
|
1641
|
+
end
|
|
1642
|
+
```
|
|
1643
|
+
|
|
1644
|
+
### Virtual Attributes
|
|
1645
|
+
|
|
1646
|
+
Virtual attributes allow you to add non-persisted attributes to your models
|
|
1647
|
+
that behave like regular attributes:
|
|
1648
|
+
|
|
1649
|
+
```ruby
|
|
1650
|
+
class Operations::Order::Checkout < RailsOps::Operation::Model::Update
|
|
1651
|
+
model Order do
|
|
1652
|
+
# Virtual attributes for checkout process
|
|
1653
|
+
attribute :card_number, :string
|
|
1654
|
+
attribute :card_cvv, :string
|
|
1655
|
+
attribute :card_exp_month, :integer
|
|
1656
|
+
attribute :card_exp_year, :integer
|
|
1657
|
+
attribute :save_card, :boolean, default: false
|
|
1658
|
+
|
|
1659
|
+
# Validations for virtual attributes
|
|
1660
|
+
validates :card_number, presence: true, length: { is: 16 }
|
|
1661
|
+
validates :card_cvv, presence: true, length: { in: 3..4 }
|
|
1662
|
+
validates :card_exp_month, inclusion: { in: 1..12 }
|
|
1663
|
+
validates :card_exp_year, numericality: {
|
|
1664
|
+
greater_than_or_equal_to: Date.current.year
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
# Virtual attribute for computed values
|
|
1668
|
+
attribute :total_with_tax, :decimal
|
|
1669
|
+
|
|
1670
|
+
before_validation :calculate_total_with_tax
|
|
1671
|
+
|
|
1672
|
+
private
|
|
1673
|
+
|
|
1674
|
+
def calculate_total_with_tax
|
|
1675
|
+
self.total_with_tax = total * (1 + tax_rate)
|
|
1676
|
+
end
|
|
1677
|
+
end
|
|
1552
1678
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1679
|
+
def perform
|
|
1680
|
+
# Process payment with virtual attributes
|
|
1681
|
+
payment_result = PaymentGateway.charge(
|
|
1682
|
+
amount: model.total_with_tax,
|
|
1683
|
+
card_number: model.card_number,
|
|
1684
|
+
cvv: model.card_cvv,
|
|
1685
|
+
exp_month: model.card_exp_month,
|
|
1686
|
+
exp_year: model.card_exp_year
|
|
1687
|
+
)
|
|
1688
|
+
|
|
1689
|
+
if payment_result.success?
|
|
1690
|
+
model.payment_id = payment_result.transaction_id
|
|
1691
|
+
model.paid_at = Time.current
|
|
1692
|
+
super # Save the order
|
|
1693
|
+
|
|
1694
|
+
# Optionally save card for future use
|
|
1695
|
+
if model.save_card
|
|
1696
|
+
CreatePaymentMethod.run!(
|
|
1697
|
+
user: model.user,
|
|
1698
|
+
token: payment_result.card_token
|
|
1699
|
+
)
|
|
1700
|
+
end
|
|
1701
|
+
else
|
|
1702
|
+
fail PaymentError, payment_result.error_message
|
|
1703
|
+
end
|
|
1704
|
+
end
|
|
1705
|
+
end
|
|
1555
1706
|
```
|
|
1556
1707
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1708
|
+
### Combining Real and Virtual Models
|
|
1709
|
+
|
|
1710
|
+
You can create operations that work with both persisted and virtual data:
|
|
1711
|
+
|
|
1712
|
+
```ruby
|
|
1713
|
+
class Operations::Report::Generate < RailsOps::Operation::Model
|
|
1714
|
+
model do
|
|
1715
|
+
attribute :start_date, :date
|
|
1716
|
+
attribute :end_date, :date
|
|
1717
|
+
attribute :include_archived, :boolean, default: false
|
|
1718
|
+
attribute :format, :string, default: 'pdf'
|
|
1719
|
+
|
|
1720
|
+
validates :start_date, :end_date, presence: true
|
|
1721
|
+
validate :end_date_after_start_date
|
|
1722
|
+
|
|
1723
|
+
private
|
|
1724
|
+
|
|
1725
|
+
def end_date_after_start_date
|
|
1726
|
+
return unless start_date && end_date
|
|
1727
|
+
errors.add(:end_date, 'must be after start date') if end_date < start_date
|
|
1728
|
+
end
|
|
1729
|
+
end
|
|
1730
|
+
|
|
1731
|
+
def perform
|
|
1732
|
+
scope = Order.where(created_at: model.start_date..model.end_date)
|
|
1733
|
+
scope = scope.includes(:archived) if model.include_archived
|
|
1734
|
+
|
|
1735
|
+
report_data = ReportBuilder.new(scope).generate
|
|
1736
|
+
|
|
1737
|
+
case model.format
|
|
1738
|
+
when 'pdf'
|
|
1739
|
+
ReportPdfGenerator.new(report_data).to_pdf
|
|
1740
|
+
when 'csv'
|
|
1741
|
+
ReportCsvGenerator.new(report_data).to_csv
|
|
1742
|
+
else
|
|
1743
|
+
report_data
|
|
1744
|
+
end
|
|
1745
|
+
end
|
|
1746
|
+
end
|
|
1747
|
+
```
|
|
1748
|
+
|
|
1749
|
+
## Transactions
|
|
1750
|
+
|
|
1751
|
+
When working with database operations, it's crucial to ensure data consistency
|
|
1752
|
+
using transactions, especially when multiple models are involved.
|
|
1753
|
+
|
|
1754
|
+
### Transaction Behavior in Model Operations
|
|
1559
1755
|
|
|
1756
|
+
It's important to understand that RailsOps model operations do NOT automatically
|
|
1757
|
+
start any transactions.
|
|
1560
1758
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1759
|
+
To ensure all operations succeed or fail together, you must explicitly wrap them
|
|
1760
|
+
in a transaction:
|
|
1761
|
+
|
|
1762
|
+
```ruby
|
|
1763
|
+
class Operations::Order::Process < RailsOps::Operation::Model::Update
|
|
1764
|
+
def perform
|
|
1765
|
+
ActiveRecord::Base.transaction do
|
|
1766
|
+
model.status = 'processing'
|
|
1767
|
+
model.processed_at = Time.current
|
|
1768
|
+
super # Saves the order
|
|
1769
|
+
|
|
1770
|
+
# Now if this fails, everything is rolled back
|
|
1771
|
+
OrderItem.where(order: model).update_all(status: 'processing')
|
|
1772
|
+
InventoryService.reserve_items(model.items)
|
|
1773
|
+
end
|
|
1774
|
+
end
|
|
1775
|
+
end
|
|
1776
|
+
```
|
|
1777
|
+
|
|
1778
|
+
Typically though, transactions are opened on a higher level and outside of
|
|
1779
|
+
operations, e.g. in controller methods.
|
|
1780
|
+
|
|
1781
|
+
### Rollback on Exception
|
|
1782
|
+
|
|
1783
|
+
When using `run` (without bang), validation errors are caught and may not cause
|
|
1784
|
+
transaction rollback. The `with_rollback_on_exception` helper ensures that
|
|
1785
|
+
exceptions within its block are re-raised as `RollbackRequired`, which will
|
|
1786
|
+
cause a rollback even when using `run`:
|
|
1787
|
+
|
|
1788
|
+
```ruby
|
|
1789
|
+
class Operations::User::ComplexUpdate < RailsOps::Operation::Model::Update
|
|
1790
|
+
def perform
|
|
1791
|
+
ActiveRecord::Base.transaction do
|
|
1792
|
+
super # Saves the user
|
|
1793
|
+
|
|
1794
|
+
# Without with_rollback_on_exception, validation errors here won't
|
|
1795
|
+
# roll back the transaction when the operation is called with run
|
|
1796
|
+
with_rollback_on_exception do
|
|
1797
|
+
model.profile.bio = params[:bio]
|
|
1798
|
+
model.profile.save! # If this fails, transaction is rolled back
|
|
1799
|
+
|
|
1800
|
+
model.settings.notifications = params[:notifications]
|
|
1801
|
+
model.settings.save! # If this fails, transaction is rolled back
|
|
1802
|
+
end
|
|
1803
|
+
end
|
|
1804
|
+
end
|
|
1805
|
+
end
|
|
1806
|
+
|
|
1807
|
+
class UsersController < ApplicationController
|
|
1808
|
+
def update
|
|
1809
|
+
ActiveRecord::Base.transaction do
|
|
1810
|
+
if run Operations::User::ComplexUpdate
|
|
1811
|
+
render json: { status: :success }
|
|
1812
|
+
else
|
|
1813
|
+
render json: { status: :validation_error }
|
|
1814
|
+
end
|
|
1815
|
+
end
|
|
1816
|
+
end
|
|
1817
|
+
end
|
|
1818
|
+
```
|
|
1819
|
+
|
|
1820
|
+
**Important**: `with_rollback_on_exception` only works within an existing
|
|
1821
|
+
transaction. It doesn't create a transaction - it just ensures exceptions
|
|
1822
|
+
cause rollback:
|
|
1823
|
+
|
|
1824
|
+
```ruby
|
|
1825
|
+
class Operations::Order::Process < RailsOps::Operation::Model::Update
|
|
1826
|
+
def perform
|
|
1827
|
+
# PROBLEMATIC: Each save creates its own transaction
|
|
1828
|
+
super # Order is saved and committed in its own transaction
|
|
1829
|
+
|
|
1830
|
+
with_rollback_on_exception do
|
|
1831
|
+
model.line_items.each { |item| item.update!(status: 'processed') }
|
|
1832
|
+
end
|
|
1833
|
+
end
|
|
1834
|
+
end
|
|
1835
|
+
|
|
1836
|
+
# CORRECT: Wrap in a transaction
|
|
1837
|
+
class Operations::Order::Process < RailsOps::Operation::Model::Update
|
|
1838
|
+
def perform
|
|
1839
|
+
ActiveRecord::Base.transaction do
|
|
1840
|
+
super # Order is saved
|
|
1841
|
+
|
|
1842
|
+
with_rollback_on_exception do
|
|
1843
|
+
model.line_items.each { |item| item.update!(status: 'processed') }
|
|
1844
|
+
end
|
|
1845
|
+
end
|
|
1846
|
+
end
|
|
1847
|
+
end
|
|
1848
|
+
```
|
|
1849
|
+
|
|
1850
|
+
### After Commit Callbacks
|
|
1851
|
+
|
|
1852
|
+
When no explicit transaction is used, each `save!` opens and commits its own
|
|
1853
|
+
transaction. You can use Rails' after_commit callbacks in your model
|
|
1854
|
+
extensions for actions that should only run after successful database commits:
|
|
1855
|
+
|
|
1856
|
+
```ruby
|
|
1857
|
+
# Using after_commit callbacks in model extension
|
|
1858
|
+
class Operations::User::Create < RailsOps::Operation::Model::Create
|
|
1859
|
+
model User do
|
|
1860
|
+
after_commit :send_notifications, on: :create
|
|
1861
|
+
|
|
1862
|
+
private
|
|
1863
|
+
|
|
1864
|
+
def send_notifications
|
|
1865
|
+
UserMailer.welcome(self).deliver_later
|
|
1866
|
+
CrmSyncJob.perform_later(self)
|
|
1867
|
+
end
|
|
1868
|
+
end
|
|
1869
|
+
end
|
|
1870
|
+
|
|
1871
|
+
# Or handle it manually after the operation
|
|
1872
|
+
class Operations::Order::Complete < RailsOps::Operation::Model::Update
|
|
1873
|
+
def perform
|
|
1874
|
+
ActiveRecord::Base.transaction do
|
|
1875
|
+
model.status = 'completed'
|
|
1876
|
+
model.completed_at = Time.current
|
|
1877
|
+
super
|
|
1878
|
+
end
|
|
1879
|
+
|
|
1880
|
+
# This runs after the transaction commits successfully
|
|
1881
|
+
# If there was an exception, we never get here
|
|
1882
|
+
OrderMailer.completed(model).deliver_later
|
|
1883
|
+
end
|
|
1884
|
+
end
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
**Note**: Be careful with after_commit callbacks when using transactions.
|
|
1888
|
+
They fire after each transaction commits, not after all nested transactions
|
|
1889
|
+
complete.
|
|
1890
|
+
|
|
1891
|
+
### Important Notes on Transactions
|
|
1892
|
+
|
|
1893
|
+
1. **Validation Errors**: When using `run` (without bang), validation errors
|
|
1894
|
+
are caught and won't roll back the transaction. Use `run!` for
|
|
1895
|
+
sub-operations to ensure transaction rollback on validation errors.
|
|
1896
|
+
|
|
1897
|
+
2. **External Services**: Be careful when calling external services within
|
|
1898
|
+
transactions. Long-running external calls can cause database locks:
|
|
1899
|
+
|
|
1900
|
+
```ruby
|
|
1901
|
+
def perform
|
|
1902
|
+
ActiveRecord::Base.transaction do
|
|
1903
|
+
model.save!
|
|
1904
|
+
|
|
1905
|
+
# DON'T: This could lock the database for a long time
|
|
1906
|
+
# ExternalApi.slow_request(model)
|
|
1907
|
+
end
|
|
1908
|
+
|
|
1909
|
+
# DO: Call external services after the transaction
|
|
1910
|
+
ExternalApi.slow_request(model)
|
|
1911
|
+
end
|
|
1912
|
+
```
|
|
1563
1913
|
|
|
1914
|
+
3. **Nested Transactions**: Rails uses savepoints for nested transactions,
|
|
1915
|
+
which are fully supported by RailsOps operations.
|
|
1564
1916
|
|
|
1565
|
-
Controller Integration
|
|
1566
|
-
----------------------
|
|
1917
|
+
## Controller Integration
|
|
1567
1918
|
|
|
1568
1919
|
While RailsOps certainly does not have to be used from a controller, it
|
|
1569
1920
|
provides a mixin which extends controller classes with functionality that lets
|
|
@@ -1581,7 +1932,7 @@ class ApplicationController
|
|
|
1581
1932
|
end
|
|
1582
1933
|
```
|
|
1583
1934
|
|
|
1584
|
-
### Basic
|
|
1935
|
+
### Basic Usage
|
|
1585
1936
|
|
|
1586
1937
|
The basic concept behind controller integration is to instantiate and
|
|
1587
1938
|
potentially run a single operation per request. Most of this guide refers to
|
|
@@ -1599,7 +1950,7 @@ class SomeController < ApplicationController
|
|
|
1599
1950
|
end
|
|
1600
1951
|
```
|
|
1601
1952
|
|
|
1602
|
-
### Separating
|
|
1953
|
+
### Separating Instantiation and Execution
|
|
1603
1954
|
|
|
1604
1955
|
In the previous example, we instantiated and ran an operation in a single
|
|
1605
1956
|
statement. While this might be feasible for some "fire-and-forget" controller
|
|
@@ -1647,12 +1998,12 @@ def update_username
|
|
|
1647
1998
|
end
|
|
1648
1999
|
```
|
|
1649
2000
|
|
|
1650
|
-
### Checking for
|
|
2001
|
+
### Checking for Operations
|
|
1651
2002
|
|
|
1652
2003
|
Using the method `op?`, you can check whether an operation has already been
|
|
1653
2004
|
instantiated (using `op`).
|
|
1654
2005
|
|
|
1655
|
-
### Model
|
|
2006
|
+
### Model Shortcut
|
|
1656
2007
|
|
|
1657
2008
|
RailsOps conveniently provides you with a `model` instance method, which is a
|
|
1658
2009
|
shortcut for `op.model`. This is particularly useful since this is available as
|
|
@@ -1661,7 +2012,7 @@ a view helper method as well, see next section.
|
|
|
1661
2012
|
You can check whether a model is available by using the `model?` method, which
|
|
1662
2013
|
is available in both controllers and views.
|
|
1663
2014
|
|
|
1664
|
-
### View
|
|
2015
|
+
### View Helper Methods
|
|
1665
2016
|
|
|
1666
2017
|
The following controller methods are automatically provided as helper methods
|
|
1667
2018
|
which can be used in views:
|
|
@@ -1705,7 +2056,7 @@ You can also combine these two approaches:
|
|
|
1705
2056
|
op SomeOperation, some_param: op_params.slice(:some_param, :some_other_param)
|
|
1706
2057
|
```
|
|
1707
2058
|
|
|
1708
|
-
### Authorization
|
|
2059
|
+
### Authorization Ensuring
|
|
1709
2060
|
|
|
1710
2061
|
For security reasons, RailsOps automatically checks after each action whether
|
|
1711
2062
|
authorization has been performed. This is to avoid serving an action's response
|
|
@@ -1733,7 +2084,7 @@ automatically created. The following fields are set automatically:
|
|
|
1733
2084
|
- `session` (uses the `session` controller method)
|
|
1734
2085
|
- `url_options` (uses the `url_options` controller method)
|
|
1735
2086
|
|
|
1736
|
-
### Multiple
|
|
2087
|
+
### Multiple Operations per Request
|
|
1737
2088
|
|
|
1738
2089
|
RailsOps does not currently support calling multiple operations in a single
|
|
1739
2090
|
controller action out-of-the-box. You need to instantiate and run it manually.
|
|
@@ -1741,11 +2092,9 @@ controller action out-of-the-box. You need to instantiate and run it manually.
|
|
|
1741
2092
|
Another approach is to create a parent operation which calls multiple
|
|
1742
2093
|
sub-operations, see section *Calling sub-operations* for more information.
|
|
1743
2094
|
|
|
1744
|
-
Operation Inheritance
|
|
1745
|
-
---------------------
|
|
2095
|
+
## Operation Inheritance
|
|
1746
2096
|
|
|
1747
|
-
Generators
|
|
1748
|
-
----------
|
|
2097
|
+
## Generators
|
|
1749
2098
|
|
|
1750
2099
|
RailsOps features a generator to easily create a structure for common CRUD-style
|
|
1751
2100
|
constructs. The generator creates the CRUD operations, some empty view files, a
|
|
@@ -1833,8 +2182,7 @@ Of course, at this point, the operations will need some adaptions, especially th
|
|
|
1833
2182
|
[parameter schemas](#validating-params), and the controllers need the logic for the
|
|
1834
2183
|
success and failure cases, as this depends on your application.
|
|
1835
2184
|
|
|
1836
|
-
Lazy Load Hooks
|
|
1837
|
-
---------------
|
|
2185
|
+
## Lazy Load Hooks
|
|
1838
2186
|
|
|
1839
2187
|
RailsOps provides the following [Rails Lazy Load
|
|
1840
2188
|
Hooks](https://api.rubyonrails.org/v7.1.3.4/classes/ActiveSupport/LazyLoadHooks.html):
|
|
@@ -1851,10 +2199,9 @@ Example usage:
|
|
|
1851
2199
|
ActiveSupport.on_load(:rails_ops_op_model_create) { include MyMixin }
|
|
1852
2200
|
```
|
|
1853
2201
|
|
|
1854
|
-
Caveats
|
|
1855
|
-
-------
|
|
2202
|
+
## Caveats
|
|
1856
2203
|
|
|
1857
|
-
### Eager
|
|
2204
|
+
### Eager Loading in Development Mode
|
|
1858
2205
|
|
|
1859
2206
|
Eager loading operation classes containing models with nested models or
|
|
1860
2207
|
operations can be very slow in performance. In production mode, the same process
|
|
@@ -1865,11 +2212,11 @@ setting in production mode though.
|
|
|
1865
2212
|
|
|
1866
2213
|
## Contributors
|
|
1867
2214
|
|
|
1868
|
-
This
|
|
2215
|
+
This gem is heavily inspired by the [trailblazer](http://trailblazer.to/) gem
|
|
1869
2216
|
which provides a wonderful, high-level architecture for Rails – beyond just
|
|
1870
2217
|
operations. Be sure to check this out when trying to decide on an alternative
|
|
1871
2218
|
Rails architecture.
|
|
1872
2219
|
|
|
1873
2220
|
## Copyright
|
|
1874
2221
|
|
|
1875
|
-
Copyright © 2017 -
|
|
2222
|
+
Copyright © 2017 - 2026 Sitrox. See `LICENSE` for further details.
|