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 +4 -4
- data/README.md +47 -97
- data/app/models/rdux/action.rb +25 -46
- data/db/migrate/20230621215718_create_rdux_actions.rb +4 -7
- data/lib/rdux/result.rb +2 -6
- data/lib/rdux/store.rb +24 -0
- data/lib/rdux/version.rb +1 -1
- data/lib/rdux.rb +32 -70
- metadata +2 -4
- data/app/models/rdux/actionable.rb +0 -30
- data/app/models/rdux/failed_action.rb +0 -11
- data/db/migrate/20230621215717_create_rdux_failed_actions.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8afda82ad3e063c5a07eb7c3e8b2a9f86019bb35ccdaa8fc875a9aebcd463a29
|
4
|
+
data.tar.gz: 2d72f680e8cb5014fb55b003b09ea11cbf23107e54c8e83e2e0b5e7bff44b860
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
20
|
-
* **
|
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.
|
73
|
-
* `payload` (Hash): The input data passed as the first argument to the `call`
|
74
|
-
* `opts` (Hash): Optional parameters passed as the second argument to the `call`
|
75
|
-
* `meta` (Hash): Additional metadata stored in the database alongside the `action` and `payload`.
|
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
|
98
|
-
This method
|
99
|
-
|
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
|
-
|
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
|
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,
|
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
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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, :
|
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
|
-
* `
|
197
|
-
* `
|
198
|
-
* `
|
199
|
-
* `
|
200
|
-
* `
|
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
|
-

|
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
|
-
#
|
228
|
-
#
|
229
|
-
#
|
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 `
|
245
|
-
The action performer’s `
|
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
|
-
|
282
|
-
You can inherit from
|
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
|
294
|
-
The `
|
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[:
|
297
|
-
The assigned data will then be available as the `
|
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[:
|
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
|
-
|
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
|
data/app/models/rdux/action.rb
CHANGED
@@ -2,77 +2,56 @@
|
|
2
2
|
|
3
3
|
module Rdux
|
4
4
|
class Action < ActiveRecord::Base
|
5
|
-
|
5
|
+
self.table_name_prefix = 'rdux_'
|
6
6
|
|
7
|
-
attr_accessor :
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
30
|
-
|
31
|
-
return false unless can_down?
|
21
|
+
scope :ok, ->(val = true) { where(ok: val) }
|
22
|
+
scope :failed, -> { where(ok: false) }
|
32
23
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
24
|
+
def call(opts = {})
|
25
|
+
return false if performed?
|
26
|
+
return false if payload_sanitized && payload_unsanitized.nil?
|
37
27
|
|
38
|
-
|
39
|
-
|
28
|
+
opts.merge!(action: self)
|
29
|
+
perform_action(opts)
|
40
30
|
end
|
41
31
|
|
42
32
|
private
|
43
33
|
|
44
|
-
def
|
45
|
-
|
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
|
38
|
+
def action_performer
|
53
39
|
name_const = name.to_s.constantize
|
54
|
-
return name_const if name_const.respond_to?(
|
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?(
|
44
|
+
obj.respond_to?(:call) ? obj : nil
|
59
45
|
end
|
60
46
|
|
61
|
-
def perform_action(
|
62
|
-
performer = action_performer
|
47
|
+
def perform_action(opts)
|
48
|
+
performer = action_performer
|
63
49
|
return if performer.nil?
|
64
50
|
|
65
|
-
if
|
66
|
-
performer.
|
51
|
+
if performer.method(:call).arity.abs == 2
|
52
|
+
performer.call(payload_unsanitized || payload, opts)
|
67
53
|
else
|
68
|
-
performer.
|
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 :
|
8
|
-
t.
|
9
|
-
t.
|
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.
|
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, :
|
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
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(
|
10
|
-
action =
|
11
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
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
|
20
|
+
def process(action, opts = {})
|
31
21
|
res = action.call(opts)
|
32
|
-
|
33
|
-
|
34
|
-
return res
|
35
|
-
end
|
22
|
+
res.result ||= opts[:result]
|
23
|
+
return res if destroy_action(res, action)
|
36
24
|
|
37
|
-
action
|
25
|
+
assign_to_action(res, action)
|
26
|
+
persist(res, action)
|
27
|
+
res
|
38
28
|
rescue StandardError => e
|
39
|
-
handle_exception(e, action, opts[:
|
29
|
+
handle_exception(e, action, opts[:result])
|
40
30
|
end
|
41
31
|
|
42
|
-
|
43
|
-
res[:val] ||= res.down_payload
|
44
|
-
res.down_payload = nil
|
45
|
-
end
|
32
|
+
alias perform dispatch
|
46
33
|
|
47
|
-
|
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
|
59
|
-
|
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
|
72
|
-
|
73
|
-
|
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
|
82
|
-
|
83
|
-
|
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,
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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.
|
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
|