rdux 0.13.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a4732501f420e77c07dc015b11abfbd2f8c1985229772a2665cde14a99f2752
4
- data.tar.gz: 7f35b68fa9a573ac225ad2f1aeea19139b56ee9d4a56d2618d638c2cf0cda820
3
+ metadata.gz: 8afda82ad3e063c5a07eb7c3e8b2a9f86019bb35ccdaa8fc875a9aebcd463a29
4
+ data.tar.gz: 2d72f680e8cb5014fb55b003b09ea11cbf23107e54c8e83e2e0b5e7bff44b860
5
5
  SHA512:
6
- metadata.gz: b067094df508e07f8165ec5e88c094ec4c6f54bec1ddba8206ecd0bf74b2c6b1f3706c97f1ca56c8b622c68d30c20c272d993da40ce604dfb3cccfaa43084fca
7
- data.tar.gz: 1b72e8f3b2d4cebb015957c996cee3ad10a428d7d23551ae56ccb5daf70b74f6ce556e36968a851e17fea51bb78e4ac55307f2e86c92d5376be38bb91c54d400
6
+ metadata.gz: 982c68f70df4e2c579e88029551a969f54470c0fa328ce5d1cbf89007001ac93fc6f78aec1a2e0096081c852174265c2e3412714d8466b7eb6ee9073273e3d3a
7
+ data.tar.gz: 3e63ef96279b424c87f34a23292c70b3cc95d390b63d7f92d83ad1faf95df34b9f18ada2f969a18bf79bb6846da543f5d6b437322453844ea63fbbe21ba13129
data/README.md CHANGED
@@ -16,11 +16,9 @@ Rdux is a lightweight, minimalistic Rails plugin designed to introduce event sou
16
16
  **Key Features**
17
17
 
18
18
  * **Audit Logging** 👉 Rdux stores sanitized input data, the name of module or class (action performer) responsible for processing them, processing results, and additional metadata in the database.
19
- * **Model Representation** 👉 Before action is executed it gets stored in the database through the `Rdux::Action` model. `Rdux::Action` is converted to the `Rdux::FailedAction` when it fails. These models can be nested, allowing for complex action structures.
20
- * **Revert and Retry** 👉 `Rdux::Action` can be reverted. `Rdux::FailedAction` retains the input data and processing results necessary for implementing custom mechanisms to retry failed actions.
21
- * **Exception Handling and Recovery** 👉 Rdux automatically creates a `Rdux::FailedAction` when an exception occurs during action execution. It retains the `up_payload` and allows you to capture additional data using `opts[:up_result]`, ensuring all necessary information is available for retrying the action.
19
+ * **Model Representation** 👉 Before action is executed it gets stored in the database through the `Rdux::Action` model. This model can be nested, allowing for complex action structures.
20
+ * **Exception Handling and Recovery** 👉 Rdux automatically creates a `Rdux::Action` record when an exception occurs during action execution. It retains the `payload` and allows you to capture additional data using `opts[:result]`, ensuring all necessary information is available for retrying the action.
22
21
  * **Metadata** 👉 Metadata can include the ID of the authenticated resource responsible for performing a given action, as well as resource IDs from external systems related to the action. This creates a clear audit trail of who executed each action and on whose behalf.
23
- * **Streams** 👉 Rdux enables the identification of action chains (streams) by utilizing resource IDs stored in metadata. This makes it easy to query and track related actions.
24
22
 
25
23
  Rdux is designed to integrate seamlessly with your existing Rails application, offering a straightforward and powerful solution for managing and auditing key actions.
26
24
 
@@ -69,10 +67,10 @@ alias perform dispatch
69
67
 
70
68
  Arguments:
71
69
 
72
- * `action`: The name of the module or class (action performer) that processes the action. This is stored in the database as an instance of `Rdux::Action`, with its `name` attribute set to `action` (e.g., `Task::Create`).
73
- * `payload` (Hash): The input data passed as the first argument to the `call` or `up` method of the action performer. The data is sanitized and stored in the database before being processed by the action performer. During deserialization, the keys in the `payload` are converted to strings.
74
- * `opts` (Hash): Optional parameters passed as the second argument to the `call` or `up` method, if defined. This can help avoid redundant database queries (e.g., if you already have an ActiveRecord object available before calling `Rdux.perform`). A helper is available to facilitate this use case: `(opts[:ars] || {}).each { |k, v| payload["#{k}_id"] = v.id }`, where `:ars` represents ActiveRecord objects. Note that `opts` is not stored in the database, and the `payload` should be fully sufficient to perform an **action**. `opts` provides an optimization.
75
- * `meta` (Hash): Additional metadata stored in the database alongside the `action` and `payload`. The `stream` key is particularly useful for specifying the stream of actions used during reversions. For example, a `stream` can be constructed based on the owner of the action.
70
+ * `action`: The name of the module or class (action performer) that processes the action. `action` is stored in the database as the `name` attribute of the `Rdux::Action` instance (e.g., `Task::Create`).
71
+ * `payload` (Hash): The input data passed as the first argument to the `call` method of the action performer. The data is sanitized and stored in the database before being processed by the action performer. During deserialization, the keys in the `payload` are converted to strings.
72
+ * `opts` (Hash): Optional parameters passed as the second argument to the `call` method, if defined. This can help avoid redundant database queries (e.g., if you already have an ActiveRecord object available before calling `Rdux.perform`). A helper is available to facilitate this use case: `(opts[:ars] || {}).each { |k, v| payload["#{k}_id"] = v.id }`, where `:ars` represents ActiveRecord objects. Note that `opts` is not stored in the database, and the `payload` should be fully sufficient to perform an **action**. `opts` provides an optimization.
73
+ * `meta` (Hash): Additional metadata stored in the database alongside the `action` and `payload`.
76
74
 
77
75
  Example:
78
76
 
@@ -81,10 +79,7 @@ Rdux.perform(
81
79
  Task::Create,
82
80
  { task: { name: 'Foo bar baz' } },
83
81
  { ars: { user: current_user } },
84
- meta: {
85
- stream: { user_id: current_user.id, context: 'foo' },
86
- bar: 'baz'
87
- }
82
+ meta: { bar: 'baz' }
88
83
  )
89
84
  ```
90
85
 
@@ -94,62 +89,39 @@ Rdux.perform(
94
89
 
95
90
  ### 🕵️‍♀️ Processing an action
96
91
 
97
- 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` or `up`.
98
- This method must return a `Rdux::Result` `struct`.
99
- Optionally, an action can implement a class or instance method `down` to specify how to revert it.
100
-
101
- #### Action Structure:
102
-
103
- * `call` or `up` method: Accepts a required `payload` and an optional `opts` argument. This method processes the action and returns a `Rdux::Result`.
104
- * `down` method: Accepts the deserialized `down_payload` which is one of arguments of the `Rdux::Result` `struct` returned by the `up` method on success and saved in DB. `down` method can optionally accept the 2nd argument (Hash) which `:nested` key contains nested `Rdux::Action`s
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`.
93
+ This method accepts a required `payload` and an optional `opts` argument.
94
+ `opts[:action]` stores the Active Record object.
95
+ `call` method processes the action and must return a `Rdux::Result` struct.
105
96
 
106
97
  See [🚛 Dispatching an action](#-dispatching-an-action) section.
107
98
 
108
- Examples:
99
+ Example:
109
100
 
110
101
  ```ruby
111
102
  # app/actions/task/create.rb
112
103
 
113
104
  class Task
114
105
  class Create
115
- def up(payload, opts)
106
+ def call(payload, opts)
116
107
  user = opts.dig(:ars, :user) || User.find(payload['user_id'])
117
108
  task = user.tasks.new(payload['task'])
118
109
  if task.save
119
- Rdux::Result[ok: true, down_payload: { user_id: user.id, task_id: task.id }, val: { task: }]
110
+ Rdux::Result[ok: true, val: { task: }]
120
111
  else
121
112
  Rdux::Result[false, { errors: task.errors }]
122
113
  end
123
114
  end
124
-
125
- def down(payload)
126
- Delete.up(payload)
127
- end
128
115
  end
129
116
  end
130
117
  ```
131
118
 
132
- ```ruby
133
- # app/actions/task/delete.rb
134
-
135
- class Task
136
- module Delete
137
- def self.up(payload)
138
- user = User.find(payload['user_id'])
139
- task = user.tasks.find(payload['task_id'])
140
- task.destroy
141
- Rdux::Result[true, { task: task.attributes }]
142
- end
143
- end
144
- end
145
- ```
119
+ #### Suggested Directory Structure
146
120
 
147
- #### Suggested Directory Structure:
148
-
149
- The location that is often used for entities like actions accross code bases is `app/services`.
150
- This directory is de facto the bag of random objects.
151
- I'd recomment to place actions inside `app/actions` for better organization and consistency.
152
- Actions are consistent in terms of structure, input and output data.
121
+ The location that is often used for entities like actions accross code bases is `app/services`.
122
+ This directory is de facto the bag of random objects.
123
+ I'd recomment to place actions inside `app/actions` for better organization and consistency.
124
+ Actions are consistent in terms of structure, input and output data.
153
125
  They are good canditates to create a new layer in Rails apps.
154
126
 
155
127
  Structure:
@@ -178,13 +150,9 @@ Definition:
178
150
 
179
151
  ```ruby
180
152
  module Rdux
181
- Result = Struct.new(:ok, :down_payload, :val, :up_result, :save, :after_save, :nested, :action) do
182
- def val
183
- self[:val] || down_payload
184
- end
185
-
153
+ Result = Struct.new(:ok, :val, :result, :save, :nested, :action) do
186
154
  def save_failed?
187
- ok == false && save
155
+ ok == false && save ? true : false
188
156
  end
189
157
  end
190
158
  end
@@ -193,22 +161,11 @@ end
193
161
  Arguments:
194
162
 
195
163
  * `ok` (Boolean): Indicates whether the action was successful. If `true`, the `Rdux::Action` is persisted in the database.
196
- * `down_payload` (Hash): Passed to the action performer’s `down` method during reversion (`down` method is called on `Rdux::Action`). It does not have to be defined if an action performer does not implement the `down` method. `down_payload` is saved in the DB.
197
- * `val` (Hash): Contains different returned data than `down_payload`.
198
- * `up_result` (Hash): Stores data related to the action’s execution, such as created record IDs, DB changes, responses from 3rd parties, etc.
199
- * `save` (Boolean): If `true` and `ok` is `false`, the action is saved as a `Rdux::FailedAction`.
200
- * `after_save` (Proc): Called just before the `dispatch` method returns the `Rdux::Result` with `Rdux::Action` or `Rdux::FailedAction` as an argument.
201
- * `nested` (Array of `Rdux::Result`): `Rdux::Action` can be connected with other `rdux_actions`. `Rdux::FailedAction` can be connected with other `rdux_actions` and `rdux_failed_actions`. To establish an association, a given action must `Rdux.dispatch` other actions in the `up` or `call` method and add the returned by the `dispatch` value (`Rdux::Result`) to the `:nested` array
202
- * `action`: Rdux assigns `Rdux::Action` or `Rdux::FailedAction` to this argument
203
-
204
- ### ⏮️ Reverting an Action
205
-
206
- To revert an action, call the `down` method on the persisted in DB `Rdux::Action` instance.
207
- The `Rdux::Action` must have a `down_payload` defined and the action (action performer) must have the `down` method implemented.
208
-
209
- ![Revert action](docs/down.png)
210
-
211
- The `down_at` attribute is set upon successful reversion. Actions cannot be reverted if there are newer, unreverted actions in the same stream (if defined) or in general. See `meta` in [🚛 Dispatching an action](#-dispatching-an-action) section.
164
+ * `val` (Hash): returned data.
165
+ * `result` (Hash): Stores data related to the action’s execution, such as created record IDs, DB changes, responses from 3rd parties, etc. that will be persisted as `Rdux::Action#result`.
166
+ * `save` (Boolean): If `true` and `ok` is `false`, the action is still persisted in the database.
167
+ * `nested` (Array of `Rdux::Result`): `Rdux::Action` can be connected with other `rdux_actions`. To establish an association, a given action must `Rdux.dispatch` other actions in the `call` method and add the returned by the `dispatch` value (`Rdux::Result`) to the `:nested` array
168
+ * `action`: Rdux assigns persisted `Rdux::Action` to this argument
212
169
 
213
170
  ### 🗿 Data model
214
171
 
@@ -224,35 +181,28 @@ res.action
224
181
  # #<Rdux::Action:0x000000011c4d8e98
225
182
  # id: 1,
226
183
  # name: "Task::Create",
227
- # up_payload: {"task"=>{"name"=>"Foo bar baz"}, "user_id"=>159163583},
228
- # down_payload: {"task_id"=>207620945},
229
- # down_at: nil,
230
- # up_payload_sanitized: false,
231
- # up_result: nil,
184
+ # payload: {"task"=>{"name"=>"Foo bar baz"}, "user_id"=>159163583},
185
+ # payload_sanitized: false,
186
+ # result: nil,
232
187
  # meta: {},
233
- # stream_hash: nil,
234
188
  # rdux_action_id: nil,
235
- # rdux_failed_action_id: nil,
236
189
  # created_at: Fri, 28 Jun 2024 21:35:36.838898000 UTC +00:00,
237
190
  # updated_at: Fri, 28 Jun 2024 21:35:36.839728000 UTC +00:00>>
238
-
239
- res.action.down
240
191
  ```
241
192
 
242
193
  ### 😷 Sanitization
243
194
 
244
- When `Rdux.perform` is called, the `up_payload` is sanitized using `Rails.application.config.filter_parameters` before being saved to the database.
245
- The action performer’s `up` or `call` method receives the unsanitized version.
246
- Note that once the `up_payload` is sanitized, the `Rdux::Action` cannot be retried by calling the `#up` method.
195
+ When `Rdux.perform` is called, the `payload` is sanitized using `Rails.application.config.filter_parameters` before being saved to the database.
196
+ The action performer’s `call` method receives the unsanitized version.
247
197
 
248
198
  ### 🗣️ Queries
249
199
 
250
- Most likely, it won't be necessary to save a `Rdux::Action` for every request a Rails app receives.
251
- The suggested approach is to save `Rdux::Action`s for Create, Update, and Delete (CUD) operations.
252
- This approach organically creates a new layer - queries in addition to actions.
200
+ Most likely, it won't be necessary to save a `Rdux::Action` for every request a Rails app receives.
201
+ The suggested approach is to save `Rdux::Action`s for Create, Update, and Delete (CUD) operations.
202
+ This approach organically creates a new layer - queries in addition to actions.
253
203
  Thus, it is required to call `Rdux.perform` only for actions.
254
204
 
255
- One approach is to create a `perform` method that invokes either `Rdux.perform` or a query, depending on the presence of `action` or `query` keywords.
205
+ One approach is to create a `perform` method that invokes either `Rdux.perform` or a query, depending on the presence of `action` or `query` keywords.
256
206
  This method can also handle setting `meta` attributes, performing parameter validation, and more.
257
207
 
258
208
  Example:
@@ -277,11 +227,12 @@ end
277
227
 
278
228
  ### 🕵️ Indexing
279
229
 
280
- Depending on your use case, it’s recommended to create indices, especially when using PostgreSQL and querying JSONB columns.
281
- Both `Rdux::Action` and `Rdux::FailedAction` are standard ActiveRecord models.
282
- You can inherit from them and extend.
230
+ Depending on your use case, it’s recommended to create indices, especially when using PostgreSQL and querying JSONB columns.\
231
+ `Rdux::Action` is a standard ActiveRecord model.
232
+ You can inherit from it and extend.
283
233
 
284
234
  Example:
235
+
285
236
  ```ruby
286
237
  class Action < Rdux::Action
287
238
  include Actionable
@@ -290,13 +241,14 @@ end
290
241
 
291
242
  ### 🚑 Recovering from Exceptions
292
243
 
293
- Rdux creates a `Rdux::FailedAction` when an exception is raised during the execution of an action.
294
- The `up_payload` is retained, but having only the input data is often not enough to retry an action.
295
- It is crucial to capture data obtained during the action’s execution, up until the exception occurred.
296
- This can be done by using `opts[:up_result]` to store all necessary data incrementally.
297
- The assigned data will then be available as the `up_result` argument in the `Rdux::FailedAction`.
244
+ Rdux captures exceptions raised during the execution of an action and sets the `Rdux::Action#ok` attribute to `false`.
245
+ The `payload` is retained, but having only the input data is often not enough to retry an action.
246
+ It is crucial to capture data obtained during the action’s execution, up until the exception occurred.
247
+ This can be done by using `opts[:result]` to store all necessary data incrementally.
248
+ The assigned data will then be available as the `Rdux::Action#result` attribute.
298
249
 
299
250
  Example:
251
+
300
252
  ```ruby
301
253
  class CreditCard
302
254
  class Charge
@@ -305,7 +257,7 @@ class CreditCard
305
257
  create_res = create(payload.slice('user_id', 'credit_card'), opts.slice(:user))
306
258
  return create_res unless create_res.ok
307
259
 
308
- opts[:up_result] = { credit_card_create_action_id: create_res.action.id }
260
+ opts[:result] = { credit_card_create_action_id: create_res.action.id }
309
261
  charge_id = PaymentGateway.charge(create_res.val[:credit_card].token, payload['amount'])[:id]
310
262
  if charge_id.nil?
311
263
  Rdux::Result[ok: false, val: { errors: { base: 'Invalid credit card' } }, save: true,
@@ -319,9 +271,7 @@ class CreditCard
319
271
 
320
272
  def create(payload, opts)
321
273
  res = Rdux.perform(Create, payload, opts)
322
- return res if res.ok
323
-
324
- Rdux::Result[ok: false, val: { errors: res.val[:errors] }, save: true, nested: [res]]
274
+ res.ok ? res : Rdux::Result[ok: false, val: { errors: res.val[:errors] }, save: true]
325
275
  end
326
276
  end
327
277
  end
@@ -2,77 +2,56 @@
2
2
 
3
3
  module Rdux
4
4
  class Action < ActiveRecord::Base
5
- include Actionable
5
+ self.table_name_prefix = 'rdux_'
6
6
 
7
- attr_accessor :up_payload_unsanitized
7
+ attr_accessor :payload_unsanitized
8
8
 
9
- belongs_to :rdux_failed_action, optional: true, class_name: 'Rdux::FailedAction'
10
9
  belongs_to :rdux_action, optional: true, class_name: 'Rdux::Action'
11
10
  has_many :rdux_actions, class_name: 'Rdux::Action', foreign_key: 'rdux_action_id'
12
11
 
13
- serialize :down_payload, coder: JSON if ActiveRecord::Base.connection.adapter_name != 'PostgreSQL'
14
-
15
- scope :up, -> { where(down_at: nil) }
16
- scope :down, -> { where.not(down_at: nil) }
17
-
18
- def call(opts = {})
19
- perform_action(:call, up_payload_unsanitized || up_payload, opts)
12
+ if ActiveRecord::Base.connection.adapter_name != 'PostgreSQL'
13
+ serialize :payload, coder: JSON
14
+ serialize :result, coder: JSON
15
+ serialize :meta, coder: JSON
20
16
  end
21
17
 
22
- def up(opts = {})
23
- return false if up_payload_sanitized && up_payload_unsanitized.nil?
24
- return false unless down_at.nil?
25
-
26
- perform_action(:up, up_payload_unsanitized || up_payload, opts)
27
- end
18
+ validates :name, presence: true
19
+ validates :payload, presence: true
28
20
 
29
- def down
30
- return false unless down_at.nil?
31
- return false unless can_down?
21
+ scope :ok, ->(val = true) { where(ok: val) }
22
+ scope :failed, -> { where(ok: false) }
32
23
 
33
- res = perform_action(:down, down_payload, build_opts)
34
- update(down_at: Time.current)
35
- res
36
- end
24
+ def call(opts = {})
25
+ return false if performed?
26
+ return false if payload_sanitized && payload_unsanitized.nil?
37
27
 
38
- def to_failed_action
39
- FailedAction.new(attributes.except('down_payload', 'down_at', 'rdux_action_id'))
28
+ opts.merge!(action: self)
29
+ perform_action(opts)
40
30
  end
41
31
 
42
32
  private
43
33
 
44
- def can_down?
45
- q = self.class.where('created_at > ?', created_at)
46
- .where(down_at: nil)
47
- .where('rdux_action_id IS NULL OR rdux_action_id != ?', id)
48
- q = q.where(stream_hash:) unless stream_hash.nil?
49
- !q.count.positive?
34
+ def performed?
35
+ !ok.nil?
50
36
  end
51
37
 
52
- def action_performer(meth)
38
+ def action_performer
53
39
  name_const = name.to_s.constantize
54
- return name_const if name_const.respond_to?(meth)
40
+ return name_const if name_const.respond_to?(:call)
55
41
  return unless name_const.is_a?(Class)
56
42
 
57
43
  obj = name_const.new
58
- obj.respond_to?(meth) ? obj : nil
44
+ obj.respond_to?(:call) ? obj : nil
59
45
  end
60
46
 
61
- def perform_action(meth, payload, opts)
62
- performer = action_performer(meth)
47
+ def perform_action(opts)
48
+ performer = action_performer
63
49
  return if performer.nil?
64
50
 
65
- if opts.any? || performer.method(meth).arity.abs == 2
66
- performer.public_send(meth, payload, opts.merge!(action: self))
51
+ if performer.method(:call).arity.abs == 2
52
+ performer.call(payload_unsanitized || payload, opts)
67
53
  else
68
- performer.public_send(meth, payload)
69
- end
70
- end
71
-
72
- def build_opts
73
- nested = rdux_actions.order(:created_at)
74
- {}.tap do |h|
75
- h[:nested] = nested if nested.any?
54
+ performer.call(payload_unsanitized || payload)
76
55
  end
77
56
  end
78
57
  end
@@ -4,16 +4,13 @@ class CreateRduxActions < ActiveRecord::Migration[7.0]
4
4
  def change
5
5
  create_table :rdux_actions do |t|
6
6
  t.string :name, null: false
7
- t.column :up_payload, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text), null: false
8
- t.column :down_payload, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
9
- t.datetime :down_at
10
- t.boolean :up_payload_sanitized, default: false, null: false
11
- t.column :up_result, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
7
+ t.column :payload, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text), null: false
8
+ t.boolean :payload_sanitized, default: false, null: false
9
+ t.column :result, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
12
10
  t.column :meta, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
13
- t.string :stream_hash
11
+ t.column :ok, :boolean
14
12
 
15
13
  t.belongs_to :rdux_action, index: true, foreign_key: true
16
- t.belongs_to :rdux_failed_action, index: true, foreign_key: true
17
14
 
18
15
  t.timestamps
19
16
  end
data/lib/rdux/result.rb CHANGED
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rdux
4
- Result = Struct.new(:ok, :down_payload, :val, :up_result, :save, :after_save, :nested, :action) do
5
- def val
6
- self[:val] || down_payload
7
- end
8
-
4
+ Result = Struct.new(:ok, :val, :result, :save, :nested, :action) do
9
5
  def save_failed?
10
- ok == false && save
6
+ ok == false && save ? true : false
11
7
  end
12
8
  end
13
9
  end
data/lib/rdux/store.rb ADDED
@@ -0,0 +1,24 @@
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
data/lib/rdux/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rdux
4
- VERSION = '0.13.0'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/rdux.rb CHANGED
@@ -1,100 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rdux/engine'
4
+ require 'rdux/store'
4
5
  require 'rdux/result'
5
6
  require 'active_support/concern'
6
7
 
7
8
  module Rdux
8
9
  class << self
9
- def dispatch(action_name, payload, opts = {}, meta: nil)
10
- action = create_action(action_name, payload, opts, meta)
11
- res = call_call_or_up_on_action(action, opts)
12
- res.up_result ||= opts[:up_result]
13
- assign_and_persist(res, action)
14
- res.after_save.call(res.action) if res.after_save && res.action
15
- res
10
+ def dispatch(name, payload, opts = {}, meta: nil)
11
+ action = store(name, payload, ars: opts[:ars], meta:)
12
+ process(action, opts)
16
13
  end
17
14
 
18
- alias perform dispatch
19
-
20
- private
21
-
22
- def create_action(action_name, payload, opts, meta)
23
- (opts[:ars] || {}).each { |k, v| payload["#{k}_id"] = v.id }
24
- action = Action.new(name: action_name, up_payload: payload, meta:)
25
- sanitize(action)
26
- action.save!
27
- action
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)
28
18
  end
29
19
 
30
- def call_call_or_up_on_action(action, opts)
20
+ def process(action, opts = {})
31
21
  res = action.call(opts)
32
- if res
33
- no_down(res)
34
- return res
35
- end
22
+ res.result ||= opts[:result]
23
+ return res if destroy_action(res, action)
36
24
 
37
- action.up(opts)
25
+ assign_to_action(res, action)
26
+ persist(res, action)
27
+ res
38
28
  rescue StandardError => e
39
- handle_exception(e, action, opts[:up_result])
29
+ handle_exception(e, action, opts[:result])
40
30
  end
41
31
 
42
- def no_down(res)
43
- res[:val] ||= res.down_payload
44
- res.down_payload = nil
45
- end
32
+ alias perform dispatch
46
33
 
47
- def assign_and_persist(res, action)
48
- action.down_payload = res.down_payload&.deep_stringify_keys!
49
- if res.ok
50
- assign_and_persist_for_ok(res, action)
51
- elsif res.save_failed?
52
- assign_and_persist_for_failed(res, action)
53
- else
54
- action.destroy
55
- end
56
- end
34
+ private
57
35
 
58
- def assign_and_persist_for_ok(res, action)
59
- action.up_result = res.up_result
60
- res.action = action.tap(&:save!)
61
- res.nested&.each { |nested_res| action.rdux_actions << nested_res.action }
62
- end
36
+ def destroy_action(res, action)
37
+ return false if res.ok || res.save
63
38
 
64
- def assign_and_persist_for_failed(res, action)
65
- action.up_result = res.up_result
66
- res.action = action.to_failed_action.tap(&:save!)
67
39
  action.destroy
68
- assign_nested_responses_to_failed_action(res.action, res.nested) if res.nested
69
40
  end
70
41
 
71
- def assign_nested_responses_to_failed_action(failed_action, nested)
72
- nested.each do |nested_res|
73
- if nested_res.action.is_a?(Rdux::Action)
74
- failed_action.rdux_actions << nested_res.action
75
- else
76
- failed_action.rdux_failed_actions << nested_res.action
77
- end
78
- end
42
+ def assign_to_action(res, action)
43
+ action.ok = res.ok
44
+ action.result = res.result
79
45
  end
80
46
 
81
- def sanitize(action)
82
- param_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
83
- up_payload_sanitized = param_filter.filter(action.up_payload)
84
- action.up_payload_sanitized = action.up_payload != up_payload_sanitized
85
- action.up_payload_unsanitized = action.up_payload if action.up_payload_sanitized
86
- action.up_payload = up_payload_sanitized
47
+ def persist(res, action)
48
+ res.action = action.tap(&:save!)
49
+ res.nested&.each { |nested_res| action.rdux_actions << nested_res.action }
87
50
  end
88
51
 
89
- def handle_exception(exc, action, up_result)
90
- failed_action = action.to_failed_action
91
- failed_action.up_result ||= up_result || {}
92
- failed_action.up_result.merge!({ 'Exception' => {
93
- class: exc.class.name,
94
- message: exc.message
95
- } })
96
- failed_action.save!
97
- action.destroy
52
+ def handle_exception(exc, action, result)
53
+ action.ok = false
54
+ action.result ||= result || {}
55
+ action.result.merge!({ 'Exception' => {
56
+ class: exc.class.name,
57
+ message: exc.message
58
+ } })
59
+ action.save!
98
60
  raise exc
99
61
  end
100
62
  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: 0.13.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zbigniew Humeniuk
@@ -99,13 +99,11 @@ files:
99
99
  - README.md
100
100
  - Rakefile
101
101
  - app/models/rdux/action.rb
102
- - app/models/rdux/actionable.rb
103
- - app/models/rdux/failed_action.rb
104
- - db/migrate/20230621215717_create_rdux_failed_actions.rb
105
102
  - db/migrate/20230621215718_create_rdux_actions.rb
106
103
  - lib/rdux.rb
107
104
  - lib/rdux/engine.rb
108
105
  - lib/rdux/result.rb
106
+ - lib/rdux/store.rb
109
107
  - lib/rdux/version.rb
110
108
  homepage: https://artofcode.co
111
109
  licenses:
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rdux
4
- module Actionable
5
- extend ActiveSupport::Concern
6
-
7
- included do
8
- if ActiveRecord::Base.connection.adapter_name != 'PostgreSQL'
9
- serialize :up_payload, coder: JSON
10
- serialize :up_result, coder: JSON
11
- serialize :meta, coder: JSON
12
- end
13
-
14
- validates :name, presence: true
15
- validates :up_payload, presence: true
16
-
17
- before_save do
18
- if meta_changed? && meta['stream'] && (meta_was || {})['stream'] != meta['stream']
19
- self.stream_hash = Digest::SHA256.hexdigest(meta['stream'].to_json)
20
- end
21
- end
22
- end
23
-
24
- class_methods do
25
- def table_name_prefix
26
- 'rdux_'
27
- end
28
- end
29
- end
30
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rdux
4
- class FailedAction < ActiveRecord::Base
5
- include Actionable
6
-
7
- belongs_to :rdux_failed_action, optional: true, class_name: 'Rdux::FailedAction'
8
- has_many :rdux_failed_actions, class_name: 'Rdux::FailedAction', foreign_key: 'rdux_failed_action_id'
9
- has_many :rdux_actions, class_name: 'Rdux::Action', foreign_key: 'rdux_failed_action_id'
10
- end
11
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateRduxFailedActions < ActiveRecord::Migration[7.0]
4
- def change
5
- create_table :rdux_failed_actions do |t|
6
- t.string :name, null: false
7
- t.column :up_payload, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text), null: false
8
- t.boolean :up_payload_sanitized, default: false, null: false
9
- t.column :up_result, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
10
- t.column :meta, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
11
- t.string :stream_hash
12
-
13
- t.belongs_to :rdux_failed_action, index: true, foreign_key: true
14
-
15
- t.timestamps
16
- end
17
- end
18
- end