rdux 2.0.1 → 3.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9466995069583a72d2327967aebfa05218c2c224aa091645dbee874e53a491f
4
- data.tar.gz: e1ec237a0de458512951e68f29596b0677a45d56b417d4b139ee4aa6a79bea13
3
+ metadata.gz: 06fb78e021c9f1647140a5f4cb71922f66f1ae7ba6afff88b90e7b9c7652c496
4
+ data.tar.gz: 2982c9a2492d22c4ad7f5d05eb33fe7f9dc5af4e31fdefb79e759901ceaf147a
5
5
  SHA512:
6
- metadata.gz: f1eec1d56443a217a827f9cfc6d30bbcc0d0ab4fb68dc912cbf9ac4986342e91de660e4876ce7d3bbf67493905be10edd5b2a07fa2056d110fe118f2f5e68892
7
- data.tar.gz: 799a77dd6396597790ca62b27cc203fa4b529935a486d6136645bc64d8759801b844c918f863e990a1e06cd973c4b02184da100d496a1439a3c957c34f4a3581
6
+ metadata.gz: e1743dbffc779a50010de16d3eccf6038ddc6ffa5646bc75fc3cabc8e3931f239f9609e8a1523f92df1b83340eb0d131b06818b0b80f774f207f6182ffe81d5b
7
+ data.tar.gz: 11ffa6637ccf91b868ba80800e60d0ddf7e718f0e441b29a09fbb6cb383bc37da77ebb35605aea1e935c610354386656a595889a863041a655a3c25ca32a821e
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 `JSONB` datatype instead of `text` for Postgres.
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 = {}, meta: nil)
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 a class or instance method `call`.
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
- class Create
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
@@ -150,11 +150,7 @@ Definition:
150
150
 
151
151
  ```ruby
152
152
  module Rdux
153
- Result = Struct.new(:ok, :val, :result, :save, :nested, :action) do
154
- def save_failed?
155
- ok == false && save ? true : false
156
- end
157
- end
153
+ Result = Struct.new(:ok, :val, :result, :save, :nested, :action)
158
154
  end
159
155
  ```
160
156
 
@@ -269,7 +265,7 @@ class CreditCard
269
265
  private
270
266
 
271
267
  def create(payload, opts)
272
- res = Rdux.perform(Create, payload, opts)
268
+ res = Rdux.perform(Create, payload, opts:)
273
269
  res.ok ? res : Rdux::Result[ok: false, val: { errors: res.val[:errors] }, save: true]
274
270
  end
275
271
  end
@@ -277,6 +273,191 @@ class CreditCard
277
273
  end
278
274
  ```
279
275
 
276
+ ### 🧹 Development Mode
277
+
278
+ 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.
279
+
280
+ ```bash
281
+ RDUX_DEV=1 bin/rails server
282
+ ```
283
+
284
+ This keeps your development database clean from failed action records caused by exceptions during iterative development.
285
+
286
+ ## 🧩 Process
287
+
288
+ **Process** 👉 a series of actions or steps taken in order to achieve a particular end.
289
+
290
+ `Rdux::Process` is a persisted model that groups multiple `Rdux::Action`s.
291
+ It also stores an ordered list of `steps` (`jsonb`/`json`).
292
+
293
+ When a process starts:
294
+
295
+ * Steps run **sequentially** in the order defined in `STEPS`
296
+ * Process execution continues only when the latest process action returns `ok: true`
297
+ * Execution stops on the first failed action step (`ok == false`)
298
+ * `process.ok` is persisted from the latest non-`nil` step result
299
+
300
+ Key points:
301
+
302
+ * `Rdux::Process` **has many** `Rdux::Action`s (`process.actions`)
303
+ * `Rdux::Action` **belongs to** a process (`action.process`)
304
+ * `Rdux.start(ProcessModuleOrClass, payload)` starts a process performer (a PORO namespace/class with a `STEPS` constant)
305
+ * `STEPS` must be an `Array` (validated on `Rdux::Process`)
306
+ * `steps` is stored as `jsonb` on PostgreSQL and `json` on other adapters (default: `[]`)
307
+ * `STEPS` supports:
308
+ * a step definition hash (`{ name: User::Create, payload: ->(payload, prev_res) { ... } }`)
309
+ * a callable step (`->(payload, process) { ... }`)
310
+ * 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)
311
+ * For callable steps, Rdux calls the step with `(safe_payload, process)` and the step is responsible for dispatching an action (with `Rdux.perform(..., process:)`)
312
+ * ⚠️ 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.
313
+ * Inside an action performer, use `opts[:action]` to access the current persisted action, then traverse `opts[:action].process.actions` (and their `result`)
314
+ * 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
315
+
316
+ Example:
317
+
318
+ ```ruby
319
+ module Processes
320
+ module Subscription
321
+ module Create
322
+ STEPS = [
323
+ lambda { |payload, process|
324
+ payload = payload.slice('plan_id', 'user', 'total_cents')
325
+ Rdux.perform(::Subscription::Preview, payload, process:)
326
+ },
327
+ lambda { |payload, process|
328
+ payload = payload.slice('user')
329
+ Rdux.perform(User::Create, payload, process:)
330
+ },
331
+ { name: CreditCard::Create,
332
+ payload: lambda { |payload, prev_res|
333
+ payload.slice('credit_card').merge(user_id: prev_res.action.result['user_id'])
334
+ } },
335
+ { name: Payment::Create,
336
+ payload: ->(_, prev_res) { { token: prev_res.val[:credit_card].token } } },
337
+ { name: ::Subscription::Create,
338
+ payload: ->(payload, prev_res) { payload.slice('plan_id').merge(ext_charge_id: prev_res.val[:charge_id]) } }
339
+ ].freeze
340
+ end
341
+ end
342
+ end
343
+
344
+ res = Rdux.start(Processes::Subscription::Create, payload)
345
+ process = res.val[:process]
346
+
347
+ # from any action performer:
348
+ def self.call(payload, opts)
349
+ results = opts[:action].process.actions.order(:id).pluck(:result)
350
+ # ...
351
+ end
352
+ ```
353
+
354
+ ## 🛠️ Helpers
355
+
356
+ ### `ActionResult`
357
+
358
+ `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.
359
+
360
+ It sets `action.result` with:
361
+
362
+ * `relations` — a map of `"model_name#id" => id` (or raw hashes) for each resource that was modified or created
363
+ * `db_changes` — `saved_changes` for each resource that was modified or created
364
+ * any extra key/value pairs passed as keyword arguments
365
+
366
+ It also creates an `ActionResource` record for each AR resource, linking it to the action via a polymorphic association.
367
+
368
+ **Usage:**
369
+
370
+ ```ruby
371
+ # inside an action performer
372
+ opts[:action].result = ActionResult.call(
373
+ action: opts[:action],
374
+ resources: [task]
375
+ )
376
+
377
+ # action.result stored in DB:
378
+ # {
379
+ # "relations" => { "task#1" => 1 },
380
+ # "db_changes" => {
381
+ # "task#1" => {
382
+ # "id" => [nil, 1],
383
+ # "name" => [nil, "Foo bar baz"],
384
+ # "user_id" => [nil, 42],
385
+ # "created_at" => [nil, "2024-06-28 21:35:36"],
386
+ # "updated_at" => [nil, "2024-06-28 21:35:36"]
387
+ # }
388
+ # }
389
+ # }
390
+ ```
391
+
392
+ Resources can be ActiveRecord objects or plain hashes (merged directly into `relations`):
393
+
394
+ ```ruby
395
+ ActionResult.call(
396
+ action: opts[:action],
397
+ resources: [task, { user_id: user.id }],
398
+ additional_info: 'Foo Bar Baz'
399
+ )
400
+ ```
401
+
402
+ **`ActionResource` model** (`app/models/action_resource.rb`):
403
+
404
+ ```ruby
405
+ class ActionResource < ApplicationRecord
406
+ belongs_to :action, class_name: 'Rdux::Action'
407
+ belongs_to :resource, polymorphic: true
408
+
409
+ validates :action_id, uniqueness: { scope: %i[resource_type resource_id] }
410
+ validates :resource_type, presence: true
411
+ end
412
+ ```
413
+
414
+ **`ActionResult` service** (`app/services/action_result.rb`):
415
+
416
+ ```ruby
417
+ class ActionResult
418
+ class << self
419
+ def call(action:, resources:, **custom)
420
+ result = { relations: {}, db_changes: {} }
421
+
422
+ resources.each do |resource|
423
+ if resource.is_a?(Hash)
424
+ result[:relations].merge!(resource)
425
+ next
426
+ end
427
+
428
+ key = relation_key(resource)
429
+ result[:relations][key] = resource.id
430
+ result[:db_changes][key] = resource.saved_changes if resource.saved_changes.present?
431
+ end
432
+
433
+ persist_relations(result[:relations], action.id)
434
+ result.merge(custom)
435
+ end
436
+
437
+ private
438
+
439
+ def relation_key(resource)
440
+ "#{resource.class.name.underscore}##{resource.id}"
441
+ end
442
+
443
+ def resource_type_for(name)
444
+ type = name.sub(/_id$/, '').sub(/#\d+$/, '').camelize
445
+ resource_class = type.safe_constantize
446
+ resource_class && resource_class < ApplicationRecord ? type : nil
447
+ end
448
+
449
+ def persist_relations(relations, action_id)
450
+ relations.each do |name, id|
451
+ resource_type = resource_type_for(name)
452
+ next if resource_type.nil? || !id.to_s.match?(/\A\d+\z/)
453
+
454
+ ActionResource.create!(action_id:, resource_type:, resource_id: id)
455
+ end
456
+ end
457
+ end
458
+ end
459
+ ```
460
+
280
461
  ## 👩🏽‍🔬 Testing
281
462
 
282
463
  ### 💉 Setup
data/Rakefile CHANGED
@@ -6,16 +6,6 @@ rescue LoadError
6
6
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
7
  end
8
8
 
9
- require 'rdoc/task'
10
-
11
- RDoc::Task.new(:rdoc) do |rdoc|
12
- rdoc.rdoc_dir = 'rdoc'
13
- rdoc.title = 'Rdux'
14
- rdoc.options << '--line-numbers'
15
- rdoc.rdoc_files.include('README.md')
16
- rdoc.rdoc_files.include('lib/**/*.rb')
17
- end
18
-
19
9
  require 'bundler/gem_tasks'
20
10
 
21
11
  require 'rake/testtask'
@@ -26,6 +16,4 @@ Rake::TestTask.new(:test) do |t|
26
16
  t.verbose = false
27
17
  end
28
18
 
29
- Dir.glob('lib/tasks/**/*.rake').each { |r| import r }
30
-
31
19
  task default: :test
@@ -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
@@ -4,29 +4,31 @@ module Rdux
4
4
  class Action < ActiveRecord::Base
5
5
  self.table_name_prefix = 'rdux_'
6
6
 
7
- attr_accessor :payload_unsanitized
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
+ # has_attribute? keeps Rdux working when the rdux_process_id migration has not been run
20
+ def process_defined?
21
+ has_attribute?(:rdux_process_id) && rdux_process_id
22
+ end
23
+
24
24
  def call(opts = {})
25
25
  return false if performed?
26
- return false if payload_sanitized && payload_unsanitized.nil?
26
+ return false if only_sanitized_payload?
27
+
28
+ performer = name.to_s.constantize
29
+ return performer.call(safe_payload) if performer.method(:call).arity == 1
27
30
 
28
- opts.merge!(action: self)
29
- perform_action(opts)
31
+ performer.call(safe_payload, opts.merge(action: self))
30
32
  end
31
33
 
32
34
  private
@@ -34,25 +36,5 @@ module Rdux
34
36
  def performed?
35
37
  !ok.nil?
36
38
  end
37
-
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
- def perform_action(opts)
48
- performer = action_performer
49
- return if performer.nil?
50
-
51
- if performer.method(:call).arity.abs == 2
52
- performer.call(payload_unsanitized || payload, opts)
53
- else
54
- performer.call(payload_unsanitized || payload)
55
- end
56
- end
57
39
  end
58
40
  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, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text), null: false
9
+ t.column :payload, json_column_type, null: false
8
10
  t.boolean :payload_sanitized, default: false, null: false
9
- t.column :result, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
10
- t.column :meta, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddRduxProcessToRduxActions < ActiveRecord::Migration[7.0]
4
+ def change
5
+ add_reference :rdux_actions, :rdux_process, index: true, foreign_key: { to_table: :rdux_processes }
6
+ end
7
+ 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.save!
17
+ action
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
+ resume_process(action, res)&.tap { return _1 }
27
+
28
+ res
29
+ rescue StandardError => e
30
+ handle_exception(e, action)
31
+ end
32
+
33
+ alias perform dispatch
34
+
35
+ private
36
+
37
+ def destroy_action(res, action)
38
+ return false if res.ok || res.save
39
+ return false if action.process_defined?
40
+
41
+ action.destroy
42
+ end
43
+
44
+ def assign_to_action(res, action)
45
+ action.ok = res.ok
46
+ return unless res.result
47
+
48
+ action.result ||= {}
49
+ action.result.merge!(res.result)
50
+ end
51
+
52
+ def persist(res, action)
53
+ res.action = action.tap(&:save!)
54
+ res.nested&.each { |nested_res| action.rdux_actions << nested_res.action }
55
+ end
56
+
57
+ def resume_process(action, res)
58
+ return unless action.ok
59
+ return unless action.process_defined?
60
+
61
+ action.process.resume(res)
62
+ end
63
+
64
+ def handle_exception(exc, action)
65
+ if ENV['RDUX_DEV']
66
+ action.destroy
67
+ raise exc
68
+ end
69
+
70
+ action.ok = false
71
+ action.result = (action.result || {}).merge('Exception' => { class: exc.class.name, message: exc.message })
72
+ action.save!
73
+ raise exc
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdux
4
+ module MigrationHelpers
5
+ private
6
+
7
+ def json_column_type
8
+ connection.adapter_name == 'PostgreSQL' ? :jsonb : :json
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
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
+ process.save!
9
+ res = process.resume(Rdux::Result[ok: true, val: :start])
10
+ process.update!(ok: res.ok) unless res.ok.nil?
11
+ Result[ok: res.ok, val: { process: }]
12
+ end
13
+ end
14
+ end
data/lib/rdux/result.rb CHANGED
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rdux
4
- Result = Struct.new(:ok, :val, :result, :save, :nested, :action) do
5
- def save_failed?
6
- ok == false && save ? true : false
7
- end
8
- end
4
+ Result = Struct.new(:ok, :val, :result, :save, :nested, :action)
9
5
  end
@@ -0,0 +1,13 @@
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
+ end
12
+ end
13
+ end
data/lib/rdux/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rdux
4
- VERSION = '2.0.1'
4
+ VERSION = '3.1.0'
5
5
  end
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/store'
4
+ require 'rdux/migration_helpers'
5
5
  require 'rdux/result'
6
- require 'active_support/concern'
6
+ require 'rdux/sanitize'
7
+ require 'rdux/dispatching'
8
+ require 'rdux/processing'
7
9
 
8
10
  module Rdux
9
- class << self
10
- def dispatch(name, payload, opts = {}, meta: nil)
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: 2.0.1
4
+ version: 3.1.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.0'
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.0'
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/store.rb
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.1.2
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: 3.6.8
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