rdux 2.0.1 → 3.0.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/README.md +192 -7
- data/app/models/concerns/rdux/safe_payload.rb +19 -0
- data/app/models/rdux/action.rb +10 -20
- data/app/models/rdux/process.rb +61 -0
- data/db/migrate/20230621215718_create_rdux_actions.rb +5 -3
- data/db/migrate/20260109000000_create_rdux_processes.rb +19 -0
- data/db/migrate/20260109000001_add_rdux_process_to_rdux_actions.rb +7 -0
- data/lib/rdux/dispatching.rb +76 -0
- data/lib/rdux/migration_helpers.rb +11 -0
- data/lib/rdux/processing.rb +13 -0
- data/lib/rdux/sanitize.rb +14 -0
- data/lib/rdux/version.rb +1 -1
- data/lib/rdux.rb +6 -58
- metadata +13 -6
- data/lib/rdux/store.rb +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d08bba313e7358c9ab679919bffa752086205e61c3884285da3cca786b1c63e
|
|
4
|
+
data.tar.gz: 6aa632159bdc20f99fb82686374bc0cf72359a08219485168d5be3fe2e4441bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6420dd96aa5f4c5f51e8bd4ef1d5b689892a7693405b5f27045bbaa5af57a8c09237a9213eb30a85907d7fd7bd4af926c85494843ff32d0330f36dc39c8e375e
|
|
7
|
+
data.tar.gz: 873fe11556fff66df5290cb3f9aaa9414c39d28299b2f8a1a207a5351853de70e5801fd8d654d9022d1864a243eab4b12f2727be6d866ce7731877d96727a712
|
data/README.md
CHANGED
|
@@ -49,7 +49,7 @@ $ bin/rails rdux:install:migrations
|
|
|
49
49
|
$ bin/rails db:migrate
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
⚠️ Note: Rdux uses `
|
|
52
|
+
⚠️ Note: Rdux requires Rails 7.1+. It uses `jsonb` columns on PostgreSQL and `json` on other adapters.
|
|
53
53
|
|
|
54
54
|
## 🎮 Usage
|
|
55
55
|
|
|
@@ -60,7 +60,7 @@ To dispatch an action using Rdux, use the `dispatch` method (aliased as `perform
|
|
|
60
60
|
Definition:
|
|
61
61
|
|
|
62
62
|
```ruby
|
|
63
|
-
def dispatch(action, payload, opts
|
|
63
|
+
def dispatch(action, payload, opts: {}, meta: nil)
|
|
64
64
|
|
|
65
65
|
alias perform dispatch
|
|
66
66
|
```
|
|
@@ -78,7 +78,7 @@ Example:
|
|
|
78
78
|
Rdux.perform(
|
|
79
79
|
Task::Create,
|
|
80
80
|
{ task: { name: 'Foo bar baz' } },
|
|
81
|
-
{ ars: { user: current_user } },
|
|
81
|
+
opts: { ars: { user: current_user } },
|
|
82
82
|
meta: { bar: 'baz' }
|
|
83
83
|
)
|
|
84
84
|
```
|
|
@@ -89,7 +89,7 @@ Rdux.perform(
|
|
|
89
89
|
|
|
90
90
|
### 🕵️♀️ Processing an action
|
|
91
91
|
|
|
92
|
-
Action in Rdux is processed by an action performer which is a Plain Old Ruby Object (PORO) that implements
|
|
92
|
+
Action in Rdux is processed by an action performer which is a Plain Old Ruby Object (PORO) that implements the `self.call` method.
|
|
93
93
|
This method accepts a required `payload` and an optional `opts` argument.
|
|
94
94
|
`opts[:action]` stores the Active Record object.
|
|
95
95
|
`call` method processes the action and must return a `Rdux::Result` struct.
|
|
@@ -102,8 +102,8 @@ Example:
|
|
|
102
102
|
# app/actions/task/create.rb
|
|
103
103
|
|
|
104
104
|
class Task
|
|
105
|
-
|
|
106
|
-
def call(payload, opts)
|
|
105
|
+
module Create
|
|
106
|
+
def self.call(payload, opts)
|
|
107
107
|
user = opts.dig(:ars, :user) || User.find(payload['user_id'])
|
|
108
108
|
task = user.tasks.new(payload['task'])
|
|
109
109
|
if task.save
|
|
@@ -269,7 +269,7 @@ class CreditCard
|
|
|
269
269
|
private
|
|
270
270
|
|
|
271
271
|
def create(payload, opts)
|
|
272
|
-
res = Rdux.perform(Create, payload, opts)
|
|
272
|
+
res = Rdux.perform(Create, payload, opts:)
|
|
273
273
|
res.ok ? res : Rdux::Result[ok: false, val: { errors: res.val[:errors] }, save: true]
|
|
274
274
|
end
|
|
275
275
|
end
|
|
@@ -277,6 +277,191 @@ class CreditCard
|
|
|
277
277
|
end
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
+
### 🧹 Development Mode
|
|
281
|
+
|
|
282
|
+
Set the `RDUX_DEV` environment variable to prevent Rdux from persisting failed actions on exceptions. When `RDUX_DEV` is set, the action record is destroyed and the exception is re-raised without storing error details in the database.
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
RDUX_DEV=1 bin/rails server
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
This keeps your development database clean from failed action records caused by exceptions during iterative development.
|
|
289
|
+
|
|
290
|
+
## 🧩 Process
|
|
291
|
+
|
|
292
|
+
**Process** 👉 a series of actions or steps taken in order to achieve a particular end.
|
|
293
|
+
|
|
294
|
+
`Rdux::Process` is a persisted model that groups multiple `Rdux::Action`s.
|
|
295
|
+
It also stores an ordered list of `steps` (`jsonb`/`json`).
|
|
296
|
+
|
|
297
|
+
When a process starts:
|
|
298
|
+
|
|
299
|
+
* Steps run **sequentially** in the order defined in `STEPS`
|
|
300
|
+
* Process execution continues only when the latest process action returns `ok: true`
|
|
301
|
+
* Execution stops on the first failed action step (`ok == false`)
|
|
302
|
+
* `process.ok` is persisted from the latest non-`nil` step result
|
|
303
|
+
|
|
304
|
+
Key points:
|
|
305
|
+
|
|
306
|
+
* `Rdux::Process` **has many** `Rdux::Action`s (`process.actions`)
|
|
307
|
+
* `Rdux::Action` **belongs to** a process (`action.process`)
|
|
308
|
+
* `Rdux.start(ProcessModuleOrClass, payload)` starts a process performer (a PORO namespace/class with a `STEPS` constant)
|
|
309
|
+
* `STEPS` must be an `Array` (validated on `Rdux::Process`)
|
|
310
|
+
* `steps` is stored as `jsonb` on PostgreSQL and `json` on other adapters (default: `[]`)
|
|
311
|
+
* `STEPS` supports:
|
|
312
|
+
* a step definition hash (`{ name: User::Create, payload: ->(payload, prev_res) { ... } }`)
|
|
313
|
+
* a callable step (`->(payload, process) { ... }`)
|
|
314
|
+
* For hash steps, Rdux dispatches `Rdux.perform(step_name, step_payload, process: process)` (`step_payload` is the full process payload unless `payload:` proc is provided)
|
|
315
|
+
* For callable steps, Rdux calls the step with `(safe_payload, process)` and the step is responsible for dispatching an action (with `Rdux.perform(..., process:)`)
|
|
316
|
+
* ⚠️ If a step returns `ok: false`, that step action is persisted (and can be assigned to the process) **only** when it also returns `save: true`. This is required.
|
|
317
|
+
* Inside an action performer, use `opts[:action]` to access the current persisted action, then traverse `opts[:action].process.actions` (and their `result`)
|
|
318
|
+
* Actions dispatched *inside* an action performer (via `Rdux.perform`) are linked via `rdux_action_id` (`action.rdux_actions`) and are not automatically assigned to the process
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
module Processes
|
|
324
|
+
module Subscription
|
|
325
|
+
module Create
|
|
326
|
+
STEPS = [
|
|
327
|
+
lambda { |payload, process|
|
|
328
|
+
payload = payload.slice('plan_id', 'user', 'total_cents')
|
|
329
|
+
Rdux.perform(::Subscription::Preview, payload, process:)
|
|
330
|
+
},
|
|
331
|
+
lambda { |payload, process|
|
|
332
|
+
payload = payload.slice('user')
|
|
333
|
+
Rdux.perform(User::Create, payload, process:)
|
|
334
|
+
},
|
|
335
|
+
{ name: CreditCard::Create,
|
|
336
|
+
payload: lambda { |payload, prev_res|
|
|
337
|
+
payload.slice('credit_card').merge(user_id: prev_res.action.result['user_id'])
|
|
338
|
+
} },
|
|
339
|
+
{ name: Payment::Create,
|
|
340
|
+
payload: ->(_, prev_res) { { token: prev_res.val[:credit_card].token } } },
|
|
341
|
+
{ name: ::Subscription::Create,
|
|
342
|
+
payload: ->(payload, prev_res) { payload.slice('plan_id').merge(ext_charge_id: prev_res.val[:charge_id]) } }
|
|
343
|
+
].freeze
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
res = Rdux.start(Processes::Subscription::Create, payload)
|
|
349
|
+
process = res.val[:process]
|
|
350
|
+
|
|
351
|
+
# from any action performer:
|
|
352
|
+
def self.call(payload, opts)
|
|
353
|
+
results = opts[:action].process.actions.order(:id).pluck(:result)
|
|
354
|
+
# ...
|
|
355
|
+
end
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## 🛠️ Helpers
|
|
359
|
+
|
|
360
|
+
### `ActionResult`
|
|
361
|
+
|
|
362
|
+
`ActionResult` is not part of Rdux itself, but a useful helper you can copy into your app to persist DB changes and resource relations alongside an action.
|
|
363
|
+
|
|
364
|
+
It sets `action.result` with:
|
|
365
|
+
|
|
366
|
+
* `relations` — a map of `"model_name#id" => id` (or raw hashes) for each resource that was modified or created
|
|
367
|
+
* `db_changes` — `saved_changes` for each resource that was modified or created
|
|
368
|
+
* any extra key/value pairs passed as keyword arguments
|
|
369
|
+
|
|
370
|
+
It also creates an `ActionResource` record for each AR resource, linking it to the action via a polymorphic association.
|
|
371
|
+
|
|
372
|
+
**Usage:**
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
# inside an action performer
|
|
376
|
+
opts[:action].result = ActionResult.call(
|
|
377
|
+
action: opts[:action],
|
|
378
|
+
resources: [task]
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# action.result stored in DB:
|
|
382
|
+
# {
|
|
383
|
+
# "relations" => { "task#1" => 1 },
|
|
384
|
+
# "db_changes" => {
|
|
385
|
+
# "task#1" => {
|
|
386
|
+
# "id" => [nil, 1],
|
|
387
|
+
# "name" => [nil, "Foo bar baz"],
|
|
388
|
+
# "user_id" => [nil, 42],
|
|
389
|
+
# "created_at" => [nil, "2024-06-28 21:35:36"],
|
|
390
|
+
# "updated_at" => [nil, "2024-06-28 21:35:36"]
|
|
391
|
+
# }
|
|
392
|
+
# }
|
|
393
|
+
# }
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Resources can be ActiveRecord objects or plain hashes (merged directly into `relations`):
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
ActionResult.call(
|
|
400
|
+
action: opts[:action],
|
|
401
|
+
resources: [task, { user_id: user.id }],
|
|
402
|
+
additional_info: 'Foo Bar Baz'
|
|
403
|
+
)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**`ActionResource` model** (`app/models/action_resource.rb`):
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
class ActionResource < ApplicationRecord
|
|
410
|
+
belongs_to :action, class_name: 'Rdux::Action'
|
|
411
|
+
belongs_to :resource, polymorphic: true
|
|
412
|
+
|
|
413
|
+
validates :action_id, uniqueness: { scope: %i[resource_type resource_id] }
|
|
414
|
+
validates :resource_type, presence: true
|
|
415
|
+
end
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**`ActionResult` service** (`app/services/action_result.rb`):
|
|
419
|
+
|
|
420
|
+
```ruby
|
|
421
|
+
class ActionResult
|
|
422
|
+
class << self
|
|
423
|
+
def call(action:, resources:, **custom)
|
|
424
|
+
result = { relations: {}, db_changes: {} }
|
|
425
|
+
|
|
426
|
+
resources.each do |resource|
|
|
427
|
+
if resource.is_a?(Hash)
|
|
428
|
+
result[:relations].merge!(resource)
|
|
429
|
+
next
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
key = relation_key(resource)
|
|
433
|
+
result[:relations][key] = resource.id
|
|
434
|
+
result[:db_changes][key] = resource.saved_changes if resource.saved_changes.present?
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
persist_relations(result[:relations], action.id)
|
|
438
|
+
result.merge(custom)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
private
|
|
442
|
+
|
|
443
|
+
def relation_key(resource)
|
|
444
|
+
"#{resource.class.name.underscore}##{resource.id}"
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def resource_type_for(name)
|
|
448
|
+
type = name.sub(/_id$/, '').sub(/#\d+$/, '').camelize
|
|
449
|
+
resource_class = type.safe_constantize
|
|
450
|
+
resource_class && resource_class < ApplicationRecord ? type : nil
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def persist_relations(relations, action_id)
|
|
454
|
+
relations.each do |name, id|
|
|
455
|
+
resource_type = resource_type_for(name)
|
|
456
|
+
next if resource_type.nil? || !id.to_s.match?(/\A\d+\z/)
|
|
457
|
+
|
|
458
|
+
ActionResource.create!(action_id:, resource_type:, resource_id: id)
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
```
|
|
464
|
+
|
|
280
465
|
## 👩🏽🔬 Testing
|
|
281
466
|
|
|
282
467
|
### 💉 Setup
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdux
|
|
4
|
+
module SafePayload
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
attr_accessor :payload_unsanitized
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def only_sanitized_payload?
|
|
12
|
+
payload_sanitized && payload_unsanitized.nil?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def safe_payload
|
|
16
|
+
payload_unsanitized || payload
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/app/models/rdux/action.rb
CHANGED
|
@@ -4,26 +4,25 @@ module Rdux
|
|
|
4
4
|
class Action < ActiveRecord::Base
|
|
5
5
|
self.table_name_prefix = 'rdux_'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
include SafePayload
|
|
8
8
|
|
|
9
9
|
belongs_to :rdux_action, optional: true, class_name: 'Rdux::Action'
|
|
10
|
+
belongs_to :process, optional: true, class_name: 'Rdux::Process', foreign_key: 'rdux_process_id'
|
|
10
11
|
has_many :rdux_actions, class_name: 'Rdux::Action', foreign_key: 'rdux_action_id'
|
|
11
12
|
|
|
12
|
-
if ActiveRecord::Base.connection.adapter_name != 'PostgreSQL'
|
|
13
|
-
serialize :payload, coder: JSON
|
|
14
|
-
serialize :result, coder: JSON
|
|
15
|
-
serialize :meta, coder: JSON
|
|
16
|
-
end
|
|
17
|
-
|
|
18
13
|
validates :name, presence: true
|
|
19
14
|
validates :payload, presence: true
|
|
20
15
|
|
|
21
16
|
scope :ok, ->(val = true) { where(ok: val) }
|
|
22
17
|
scope :failed, -> { where(ok: false) }
|
|
23
18
|
|
|
19
|
+
def process_defined?
|
|
20
|
+
has_attribute?(:rdux_process_id) && rdux_process_id
|
|
21
|
+
end
|
|
22
|
+
|
|
24
23
|
def call(opts = {})
|
|
25
24
|
return false if performed?
|
|
26
|
-
return false if
|
|
25
|
+
return false if only_sanitized_payload?
|
|
27
26
|
|
|
28
27
|
opts.merge!(action: self)
|
|
29
28
|
perform_action(opts)
|
|
@@ -35,23 +34,14 @@ module Rdux
|
|
|
35
34
|
!ok.nil?
|
|
36
35
|
end
|
|
37
36
|
|
|
38
|
-
def action_performer
|
|
39
|
-
name_const = name.to_s.constantize
|
|
40
|
-
return name_const if name_const.respond_to?(:call)
|
|
41
|
-
return unless name_const.is_a?(Class)
|
|
42
|
-
|
|
43
|
-
obj = name_const.new
|
|
44
|
-
obj.respond_to?(:call) ? obj : nil
|
|
45
|
-
end
|
|
46
|
-
|
|
47
37
|
def perform_action(opts)
|
|
48
|
-
performer =
|
|
38
|
+
performer = name.to_s.constantize
|
|
49
39
|
return if performer.nil?
|
|
50
40
|
|
|
51
41
|
if performer.method(:call).arity.abs == 2
|
|
52
|
-
performer.call(
|
|
42
|
+
performer.call(safe_payload, opts)
|
|
53
43
|
else
|
|
54
|
-
performer.call(
|
|
44
|
+
performer.call(safe_payload)
|
|
55
45
|
end
|
|
56
46
|
end
|
|
57
47
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdux
|
|
4
|
+
class Process < ActiveRecord::Base
|
|
5
|
+
self.table_name_prefix = 'rdux_'
|
|
6
|
+
|
|
7
|
+
include SafePayload
|
|
8
|
+
|
|
9
|
+
has_many :actions, class_name: 'Rdux::Action', foreign_key: 'rdux_process_id', inverse_of: :process,
|
|
10
|
+
dependent: :nullify
|
|
11
|
+
|
|
12
|
+
validates :name, presence: true
|
|
13
|
+
validates :payload, presence: true
|
|
14
|
+
validate :steps_must_be_array
|
|
15
|
+
|
|
16
|
+
before_validation on: :create do
|
|
17
|
+
self.steps = performer::STEPS.map { _1.is_a?(Hash) ? _1[:name] : _1 }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def resume(res)
|
|
21
|
+
return res unless res.ok
|
|
22
|
+
|
|
23
|
+
ok_actions_count = 0
|
|
24
|
+
if res.val != :start
|
|
25
|
+
ok_actions_count = actions.ok.count
|
|
26
|
+
update!(ok: true) && return if ok_actions_count == steps.size
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
call_step(index: ok_actions_count, prev_res: res)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def steps_must_be_array
|
|
35
|
+
if !steps.is_a?(Array)
|
|
36
|
+
errors.add(:steps, 'must be an Array')
|
|
37
|
+
elsif steps.empty?
|
|
38
|
+
errors.add(:steps, 'must include at least 1 step')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def performer
|
|
43
|
+
name.constantize
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def action_payload(step_def:, prev_res:)
|
|
47
|
+
step_def[:payload].is_a?(Proc) ? step_def[:payload].call(safe_payload, prev_res) : safe_payload
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def call_step(index:, prev_res: nil)
|
|
51
|
+
step_def = performer::STEPS[index]
|
|
52
|
+
if steps[index].is_a?(Hash)
|
|
53
|
+
step_def.call(safe_payload, self)
|
|
54
|
+
return Rdux::Result[ok: nil]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
action_payload = action_payload(step_def:, prev_res:)
|
|
58
|
+
Rdux.perform(steps[index], action_payload, process: self)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class CreateRduxActions < ActiveRecord::Migration[7.0]
|
|
4
|
+
include Rdux::MigrationHelpers
|
|
5
|
+
|
|
4
6
|
def change
|
|
5
7
|
create_table :rdux_actions do |t|
|
|
6
8
|
t.string :name, null: false
|
|
7
|
-
t.column :payload,
|
|
9
|
+
t.column :payload, json_column_type, null: false
|
|
8
10
|
t.boolean :payload_sanitized, default: false, null: false
|
|
9
|
-
t.column :result,
|
|
10
|
-
t.column :meta,
|
|
11
|
+
t.column :result, json_column_type
|
|
12
|
+
t.column :meta, json_column_type
|
|
11
13
|
t.column :ok, :boolean
|
|
12
14
|
|
|
13
15
|
t.belongs_to :rdux_action, index: true, foreign_key: true
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRduxProcesses < ActiveRecord::Migration[7.0]
|
|
4
|
+
include Rdux::MigrationHelpers
|
|
5
|
+
|
|
6
|
+
def change
|
|
7
|
+
create_table :rdux_processes do |t|
|
|
8
|
+
t.string :name, null: false
|
|
9
|
+
t.boolean :ok
|
|
10
|
+
t.column :steps, json_column_type,
|
|
11
|
+
null: false,
|
|
12
|
+
default: []
|
|
13
|
+
t.column :payload, json_column_type, null: false
|
|
14
|
+
t.boolean :payload_sanitized, default: false, null: false
|
|
15
|
+
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdux
|
|
4
|
+
module Dispatching
|
|
5
|
+
def dispatch(name, payload, opts_arg = {}, opts: nil, meta: nil, process: nil) # rubocop:disable Metrics/ParameterLists
|
|
6
|
+
opts ||= opts_arg
|
|
7
|
+
action = store(name, payload, ars: opts.delete(:ars), meta:, process:)
|
|
8
|
+
process(action, opts.merge(process:))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def store(name, payload, ars: nil, meta: nil, process: nil)
|
|
12
|
+
(ars || {}).each { |k, v| payload["#{k}_id"] = v.id }
|
|
13
|
+
action = Action.new(name:, payload:, meta:)
|
|
14
|
+
action.process = process if process
|
|
15
|
+
Sanitize.call(action)
|
|
16
|
+
action
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def process(action, opts = {})
|
|
20
|
+
res = action.call(opts)
|
|
21
|
+
return res if destroy_action(res, action)
|
|
22
|
+
|
|
23
|
+
assign_to_action(res, action)
|
|
24
|
+
persist(res, action)
|
|
25
|
+
resume_process(action, res)&.tap { return _1 }
|
|
26
|
+
|
|
27
|
+
res
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
handle_exception(e, action)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
alias perform dispatch
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def destroy_action(res, action)
|
|
37
|
+
return false if res.ok || res.save
|
|
38
|
+
return false if action.process_defined?
|
|
39
|
+
|
|
40
|
+
action.destroy
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def assign_to_action(res, action)
|
|
44
|
+
action.ok = res.ok
|
|
45
|
+
return unless res.result
|
|
46
|
+
|
|
47
|
+
action.result ||= {}
|
|
48
|
+
action.result.merge!(res.result)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def persist(res, action)
|
|
52
|
+
res.action = action.tap(&:save!)
|
|
53
|
+
res.nested&.each { |nested_res| action.rdux_actions << nested_res.action }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def resume_process(action, res)
|
|
57
|
+
return unless action.ok
|
|
58
|
+
return unless action.process_defined?
|
|
59
|
+
|
|
60
|
+
action.process.resume(res)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def handle_exception(exc, action)
|
|
64
|
+
raise(action.destroy && exc) if ENV['RDUX_DEV']
|
|
65
|
+
|
|
66
|
+
action.ok = false
|
|
67
|
+
action.result ||= {}
|
|
68
|
+
action.result.merge!({ 'Exception' => {
|
|
69
|
+
class: exc.class.name,
|
|
70
|
+
message: exc.message
|
|
71
|
+
} })
|
|
72
|
+
action.save!
|
|
73
|
+
raise exc
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdux
|
|
4
|
+
module Processing
|
|
5
|
+
def start(performer, payload)
|
|
6
|
+
process = Process.new(name: performer, payload:)
|
|
7
|
+
Sanitize.call(process)
|
|
8
|
+
res = process.resume(Rdux::Result[ok: true, val: :start])
|
|
9
|
+
process.update!(ok: res.ok) unless res.ok.nil?
|
|
10
|
+
Result[ok: res.ok, val: { process: }]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdux
|
|
4
|
+
module Sanitize
|
|
5
|
+
def self.call(aro)
|
|
6
|
+
param_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
|
|
7
|
+
payload_sanitized = param_filter.filter(aro.payload)
|
|
8
|
+
aro.payload_sanitized = aro.payload != payload_sanitized
|
|
9
|
+
aro.payload_unsanitized = aro.payload if aro.payload_sanitized
|
|
10
|
+
aro.payload = payload_sanitized
|
|
11
|
+
aro.save! if aro.changed?
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/rdux/version.rb
CHANGED
data/lib/rdux.rb
CHANGED
|
@@ -1,65 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rdux/engine'
|
|
4
|
-
require 'rdux/
|
|
4
|
+
require 'rdux/migration_helpers'
|
|
5
5
|
require 'rdux/result'
|
|
6
|
-
require '
|
|
6
|
+
require 'rdux/sanitize'
|
|
7
|
+
require 'rdux/dispatching'
|
|
8
|
+
require 'rdux/processing'
|
|
7
9
|
|
|
8
10
|
module Rdux
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
action = store(name, payload, ars: opts[:ars], meta:)
|
|
12
|
-
process(action, opts)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def store(name, payload, ars: nil, meta: nil)
|
|
16
|
-
(ars || {}).each { |k, v| payload["#{k}_id"] = v.id }
|
|
17
|
-
Store.call(name, payload, meta)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def process(action, opts = {})
|
|
21
|
-
res = action.call(opts)
|
|
22
|
-
return res if destroy_action(res, action)
|
|
23
|
-
|
|
24
|
-
assign_to_action(res, action)
|
|
25
|
-
persist(res, action)
|
|
26
|
-
res
|
|
27
|
-
rescue StandardError => e
|
|
28
|
-
handle_exception(e, action)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
alias perform dispatch
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def destroy_action(res, action)
|
|
36
|
-
return false if res.ok || res.save
|
|
37
|
-
|
|
38
|
-
action.destroy
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def assign_to_action(res, action)
|
|
42
|
-
action.ok = res.ok
|
|
43
|
-
return unless res.result
|
|
44
|
-
|
|
45
|
-
action.result ||= {}
|
|
46
|
-
action.result.merge!(res.result)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def persist(res, action)
|
|
50
|
-
res.action = action.tap(&:save!)
|
|
51
|
-
res.nested&.each { |nested_res| action.rdux_actions << nested_res.action }
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def handle_exception(exc, action)
|
|
55
|
-
action.ok = false
|
|
56
|
-
action.result ||= {}
|
|
57
|
-
action.result.merge!({ 'Exception' => {
|
|
58
|
-
class: exc.class.name,
|
|
59
|
-
message: exc.message
|
|
60
|
-
} })
|
|
61
|
-
action.save!
|
|
62
|
-
raise exc
|
|
63
|
-
end
|
|
64
|
-
end
|
|
11
|
+
extend Dispatching
|
|
12
|
+
extend Processing
|
|
65
13
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rdux
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zbigniew Humeniuk
|
|
@@ -15,7 +15,7 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '7.
|
|
18
|
+
version: '7.1'
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
21
|
version: '9.0'
|
|
@@ -25,7 +25,7 @@ dependencies:
|
|
|
25
25
|
requirements:
|
|
26
26
|
- - ">="
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: '7.
|
|
28
|
+
version: '7.1'
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '9.0'
|
|
@@ -98,12 +98,19 @@ files:
|
|
|
98
98
|
- MIT-LICENSE
|
|
99
99
|
- README.md
|
|
100
100
|
- Rakefile
|
|
101
|
+
- app/models/concerns/rdux/safe_payload.rb
|
|
101
102
|
- app/models/rdux/action.rb
|
|
103
|
+
- app/models/rdux/process.rb
|
|
102
104
|
- db/migrate/20230621215718_create_rdux_actions.rb
|
|
105
|
+
- db/migrate/20260109000000_create_rdux_processes.rb
|
|
106
|
+
- db/migrate/20260109000001_add_rdux_process_to_rdux_actions.rb
|
|
103
107
|
- lib/rdux.rb
|
|
108
|
+
- lib/rdux/dispatching.rb
|
|
104
109
|
- lib/rdux/engine.rb
|
|
110
|
+
- lib/rdux/migration_helpers.rb
|
|
111
|
+
- lib/rdux/processing.rb
|
|
105
112
|
- lib/rdux/result.rb
|
|
106
|
-
- lib/rdux/
|
|
113
|
+
- lib/rdux/sanitize.rb
|
|
107
114
|
- lib/rdux/version.rb
|
|
108
115
|
homepage: https://github.com/artofcodelabs/rdux
|
|
109
116
|
licenses:
|
|
@@ -117,14 +124,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
117
124
|
requirements:
|
|
118
125
|
- - ">="
|
|
119
126
|
- !ruby/object:Gem::Version
|
|
120
|
-
version: 3.
|
|
127
|
+
version: 3.3.8
|
|
121
128
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
129
|
requirements:
|
|
123
130
|
- - ">="
|
|
124
131
|
- !ruby/object:Gem::Version
|
|
125
132
|
version: '0'
|
|
126
133
|
requirements: []
|
|
127
|
-
rubygems_version:
|
|
134
|
+
rubygems_version: 4.0.6
|
|
128
135
|
specification_version: 4
|
|
129
136
|
summary: A Minimal Event Sourcing Plugin for Rails
|
|
130
137
|
test_files: []
|
data/lib/rdux/store.rb
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Rdux
|
|
4
|
-
module Store
|
|
5
|
-
class << self
|
|
6
|
-
def call(name, payload, meta)
|
|
7
|
-
action = Action.new(name:, payload:, meta:)
|
|
8
|
-
sanitize(action)
|
|
9
|
-
action.save!
|
|
10
|
-
action
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
|
|
15
|
-
def sanitize(action)
|
|
16
|
-
param_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
|
|
17
|
-
payload_sanitized = param_filter.filter(action.payload)
|
|
18
|
-
action.payload_sanitized = action.payload != payload_sanitized
|
|
19
|
-
action.payload_unsanitized = action.payload if action.payload_sanitized
|
|
20
|
-
action.payload = payload_sanitized
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|