rdux 0.9.1 โ†’ 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb70d8633e88f42179223f60bfe5634273591875f0e3d104fb076128af8811ba
4
- data.tar.gz: 375487c3f51b78803b9d610877edb5e5fcec70918e86e376cf2601188a9e4358
3
+ metadata.gz: dbb84ff27b098d28ad7395b590852b257851825cef51837d01a48c353e2b528e
4
+ data.tar.gz: b6bab2fc5e8685bf8ec774e1b3c76e8b533fe0f5e3cbe85d44564f4f04067202
5
5
  SHA512:
6
- metadata.gz: 4fbd330495ba4b140f828dc2e5184e27f4abd653c9e8d314d8a576c4612294c8baca8aceeeeccdab1e8be184a566544daaf1a75bc2dab2f1bba1801aab7f11cd
7
- data.tar.gz: da3820133a5d277e1b5a20eee3876efb13e7cd434105585a3b2da5a014062072e17a77a93b35f35bb601d1f7d6c30dc4b930adb9e0f56e99aaeb358f1b9629e8
6
+ metadata.gz: bc9bd6c320bbbfe0f058234dee3c4729d64aa2a56d0568f26e896fc1fd9a63116a43d8e2634dc68995cb5f4fadb1cc6b30fdeabeee486e67aeedc3eb6e3c9294
7
+ data.tar.gz: 86eabdd326c7d46a3654b43f7e06faf79c4af7e00122ce25dda9b49ae1307df7436e536199c7ed31b4d6bdd08b9652abc3e7430eb18b72e936d5845b232b3467
data/README.md CHANGED
@@ -1,54 +1,285 @@
1
- # Rdux
2
- Minimal take on event sourcing.
1
+ # Rdux - A Minimal Event Sourcing Plugin for Rails
3
2
 
4
- ## Usage
3
+ ![Logo](docs/logo.webp)
4
+
5
+ Rdux is a lightweight, minimalistic Rails plugin designed to introduce event sourcing and audit logging capabilities to your Rails application. With Rdux, you can efficiently track and store the history of actions performed within your app, offering transparency and traceability for key processes.
6
+
7
+ **Key Features**
8
+
9
+ * **Audit Logging** ๐Ÿ‘‰ Rdux stores sanitized input data, the name of module or class (action) responsible for processing them, processing results, and additional metadata in the database.
10
+ * **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.
11
+ * **Revert and Retry** ๐Ÿ‘‰ `Rdux::Action` can be reverted or retried.
12
+
13
+ Rdux is designed to integrate seamlessly with your existing Rails application, offering a straightforward and powerful solution for managing and auditing key actions.
14
+
15
+ ## ๐Ÿ“ฒ Instalation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'rdux'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ ```bash
26
+ $ bundle
27
+ ```
28
+
29
+ Or install it yourself as:
30
+
31
+ ```bash
32
+ $ gem install rdux
33
+ ```
34
+
35
+ Then install and run migrations:
5
36
 
6
37
  ```bash
7
38
  $ bin/rails rdux:install:migrations
8
39
  $ bin/rails db:migrate
9
40
  ```
10
41
 
11
- ### Code structure
42
+ โš ๏ธ Note: Rdux uses `JSONB` datatype instead of `text` for Postgres.
43
+
44
+ ## ๐ŸŽฎ Usage
45
+
46
+ ### ๐Ÿš› Dispatching an action
47
+
48
+ To dispatch an action using Rdux, use the `dispatch` method (aliased as `perform`).
49
+
50
+ Definition:
51
+
52
+ ```ruby
53
+ def dispatch(action_name, payload, opts = {}, meta: nil)
54
+
55
+ alias perform dispatch
56
+ ```
57
+
58
+ Arguments:
59
+
60
+ * `action_name`: The name of the service, class, or module that will process the action. This is persisted as an instance of `Rdux::Action` in the database, with its `name` attribute set to `action_name`. The `action_name` should correspond to the class or module that implements the `call` or `up` method, referred to as "action" or โ€œaction performer.โ€
61
+ * `payload` (Hash): The input data passed as the first argument to the `call` or `up` method of the action performer. This is sanitized and stored in the database before being processed. The keys in the `payload` are stringified during deserialization.
62
+ * `opts` (Hash): Optional parameters passed as the second argument to the `call` or `up` method, if defined. This is useful when you want to avoid redundant database queries (e.g., if you already have an ActiveRecord object available). There is a helper that facitilates this use case. The implementation is clear enough IMO `(opts[:ars] || {}).each { |k, v| payload["#{k}_id"] = v.id }`. `:ars` means ActiveRecords. Note that `opts` is not stored in the database and `payload` should be fully sufficient to perform an **action**. `opts` provides an optimization.
63
+ * `meta` (Hash): Additional metadata stored in the database alongside the `action_name` and `payload`. The `stream` key is particularly useful for scoping actions during reversions. For example, you can construct a `stream` based on the owner of action.
12
64
 
13
- #### Dispatch action
65
+ Example:
14
66
 
15
67
  ```ruby
16
68
  Rdux.perform(
17
- Activity::Stop,
18
- { activity_id: current_activity.id },
19
- { activity: current_activity },
69
+ Task::Create,
70
+ { task: { name: 'Foo bar baz' } },
71
+ { ars: { user: current_user } },
20
72
  meta: {
21
- stream: { user_id: 123, context: 'foo' }, bar: 'baz'
73
+ stream: { user_id: current_user.id, context: 'foo' },
74
+ bar: 'baz'
22
75
  }
23
76
  )
24
77
  ```
25
78
 
26
- #### Return
79
+ ### ๐Ÿ“ˆ Flow diagram
80
+
81
+ ![Flow Diagram](docs/flow.png)
82
+
83
+ ### ๐Ÿ’ช Action
84
+
85
+ An action in Rdux is a Plain Old Ruby Object (PORO) that implements a class or instance method `call` or `up`.
86
+ This method must return an `Rdux::Result` `struct`.
87
+ Optionally, an action can implement a class or instance method `down` to specify how to revert it.
88
+
89
+ #### Action Structure:
90
+
91
+ * `call` or `up` method: Accepts a required `payload` and an optional `opts` argument. This method processes the action and returns an `Rdux::Result`.
92
+ * `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
93
+
94
+ See [๐Ÿš› Dispatching an action](#-dispatching-an-action) section.
95
+
96
+ Examples:
27
97
 
28
98
  ```ruby
29
- Rdux::Result[true, { activity: activity }]
99
+ # app/actions/task/create.rb
100
+
101
+ class Task
102
+ class Create
103
+ def up(payload, opts)
104
+ user = opts.dig(:ars, :user) || User.find(payload['user_id'])
105
+ task = user.tasks.new(payload['task'])
106
+ if task.save
107
+ Rdux::Result[ok: true, down_payload: { user_id: user.id, task_id: task.id }, val: { task: }]
108
+ else
109
+ Rdux::Result[false, { errors: task.errors }]
110
+ end
111
+ end
112
+
113
+ def down(payload)
114
+ Delete.up(payload)
115
+ end
116
+ end
117
+ end
30
118
  ```
31
119
 
32
- ## Installation
33
- Add this line to your application's Gemfile:
120
+ ```ruby
121
+ # app/actions/task/delete.rb
122
+
123
+ class Task
124
+ module Delete
125
+ def self.up(payload)
126
+ user = User.find(payload['user_id'])
127
+ task = user.tasks.find(payload['task_id'])
128
+ task.destroy
129
+ Rdux::Result[true, { task: task.attributes }]
130
+ end
131
+ end
132
+ end
133
+ ```
134
+
135
+ #### Suggested Directory Structure:
136
+
137
+ The location that is often used for entities like actions accross code bases is `app/services`.
138
+ This directory is de facto the bag of random objects.
139
+ I'd recomment to place actions inside `app/actions` for better organization and consistency.
140
+ Actions are consistent in terms of structure, input and output data.
141
+ They are good canditates to create a new layer in Rails apps.
142
+
143
+ Structure:
144
+
145
+ ```
146
+ .
147
+ โ””โ”€โ”€ app/actions/
148
+ โ”œโ”€โ”€ activity/
149
+ โ”‚ โ”œโ”€โ”€ common/
150
+ โ”‚ โ”‚ โ””โ”€โ”€ fetch.rb
151
+ โ”‚ โ”œโ”€โ”€ create.rb
152
+ โ”‚ โ”œโ”€โ”€ stop.rb
153
+ โ”‚ โ””โ”€โ”€ switch.rb
154
+ โ”œโ”€โ”€ task/
155
+ โ”‚ โ”œโ”€โ”€ create.rb
156
+ โ”‚ โ””โ”€โ”€ delete.rb
157
+ โ””โ”€โ”€ misc/
158
+ โ””โ”€โ”€ create_attachment.rb
159
+ ```
160
+
161
+ The [dedicated page about actions](docs/ACTIONS.md) contains more arguments in favor of actions.
162
+
163
+ ### โ›ฉ๏ธ Returned `struct` `Rdux::Result`
164
+
165
+ Definition:
34
166
 
35
167
  ```ruby
36
- gem 'rdux'
168
+ module Rdux
169
+ Result = Struct.new(:ok, :down_payload, :val, :up_result, :save, :after_save, :nested, :action) do
170
+ def val
171
+ self[:val] || down_payload
172
+ end
173
+
174
+ def save_failed?
175
+ ok == false && save
176
+ end
177
+ end
178
+ end
37
179
  ```
38
180
 
39
- And then execute:
40
- ```bash
41
- $ bundle
181
+ Arguments:
182
+
183
+ * `ok` (Boolean): Indicates whether the action was successful. If `true`, the `Rdux::Action` is persisted in the database.
184
+ * `down_payload` (Hash): Passed to the actionโ€™s `down` method during reversion (`down` method is called on `Rdux::Action`). It does not have to be defined if an action does not implement the `down` method. `down_payload` is saved in the DB.
185
+ * `val` (Hash): Contains any additional data to return besides down_payload.
186
+ * `up_result` (Hash): Stores data related to the actionโ€™s execution, such as created record IDs, DB changes, responses from 3rd parties, etc.
187
+ * `save` (Boolean): If `true` and `ok` is `false`, the action is saved as a `Rdux::FailedAction`.
188
+ * `after_save` (Proc): Called just before the `dispatch` method returns the `Rdux::Result` with `Rdux::Action` or `Rdux::FailedAction` as an argument.
189
+ * `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
190
+ * `action`: Rdux assigns `Rdux::Action` or `Rdux::FailedAction` to this argument
191
+
192
+ ### โฎ๏ธ Reverting an Action
193
+
194
+ To revert an action, call the `down` method on the persisted in DB `Rdux::Action` instance.
195
+ The `Rdux::Action` must have a `down_payload` defined and the action (action performer) must have the `down` method implemented.
196
+
197
+ ![Revert action](docs/down.png)
198
+
199
+ 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.
200
+
201
+ ### ๐Ÿ—ฟ Data model
202
+
203
+ ```ruby
204
+ payload = {
205
+ task: { 'name' => 'Foo bar baz' },
206
+ user_id: 159163583
207
+ }
208
+
209
+ res = Rdux.dispatch(Task::Create, payload)
210
+
211
+ res.action
212
+ # #<Rdux::Action:0x000000011c4d8e98
213
+ # id: 1,
214
+ # name: "Task::Create",
215
+ # up_payload: {"task"=>{"name"=>"Foo bar baz"}, "user_id"=>159163583},
216
+ # down_payload: {"task_id"=>207620945},
217
+ # down_at: nil,
218
+ # up_payload_sanitized: false,
219
+ # up_result: nil,
220
+ # meta: {},
221
+ # stream_hash: nil,
222
+ # rdux_action_id: nil,
223
+ # rdux_failed_action_id: nil,
224
+ # created_at: Fri, 28 Jun 2024 21:35:36.838898000 UTC +00:00,
225
+ # updated_at: Fri, 28 Jun 2024 21:35:36.839728000 UTC +00:00>>
226
+
227
+ res.action.down
42
228
  ```
43
229
 
44
- Or install it yourself as:
45
- ```bash
46
- $ gem install rdux
230
+ ### ๐Ÿ˜ท Sanitization
231
+
232
+ When calling `Rdux.perform`, the `up_payload` is sanitized using `Rails.application.config.filter_parameters` before saving to the database.
233
+ The actionโ€™s `up` or `call` method receives the unsanitized version.
234
+ Note that if the `up_payload` is sanitized, the `Rdux::Action` cannot be retried via calling the `#up` method.
235
+
236
+ ### ๐Ÿ—ฃ๏ธ Queries
237
+
238
+ Most likely, it won't be needed to save a `Rdux::Action` for every request a Rails app receives.
239
+ The suggested approach is to save `Rdux::Action`s for Create, Update, and Delete (CUD) operations.
240
+ This approach organically creates a new layer - queries in addition to actions.
241
+ Thus, it is required to call `Rdux.perform` only for actions.
242
+
243
+ An example approach is to create the `perform` method that calls `Rdux.perform` or a query depending on the presence of `action` or `query` keywords.
244
+ This method can set `meta` attributes, fulfill params validation, etc.
245
+
246
+ Example:
247
+
248
+ ```ruby
249
+ class TasksController < ApiController
250
+ def show
251
+ perform(
252
+ query: Task::Show,
253
+ payload: { id: params[:id] }
254
+ )
255
+ end
256
+
257
+ def create
258
+ perform(
259
+ action: Task::Create,
260
+ payload: create_task_params
261
+ )
262
+ end
263
+ end
47
264
  ```
48
265
 
49
- ## Test
266
+ ### ๐Ÿ•ต๏ธ Indexing
50
267
 
51
- ### Setup
268
+ Depending on your use case, create indices, especially when using PostgreSQL and querying based on JSONB columns.
269
+ Both `Rdux::Action` and `Rdux::FailedAction` are standard ActiveRecord models.
270
+ You can inherit from them and extend.
271
+ Depending on your use case, create indices, especially when using PostgreSQL and querying based on `JSONB` columns.
272
+
273
+ Example:
274
+ ```ruby
275
+ class Action < Rdux::Action
276
+ include Actionable
277
+ end
278
+ ```
279
+
280
+ ## ๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ”ฌ Testing
281
+
282
+ ### ๐Ÿ’‰ Setup
52
283
 
53
284
  ```bash
54
285
  $ cd test/dummy
@@ -57,12 +288,17 @@ $ DB=all bin/rails db:prepare
57
288
  $ cd ../..
58
289
  ```
59
290
 
60
- ### Run tests
291
+ ### ๐Ÿงช Run tests
61
292
 
62
293
  ```bash
63
294
  $ DB=postgres bin/rails test
64
295
  $ DB=sqlite bin/rails test
65
296
  ```
66
297
 
67
- ## License
298
+ ## ๐Ÿ“„ License
299
+
68
300
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
301
+
302
+ ## ๐Ÿ‘จโ€๐Ÿญ Author
303
+
304
+ Zbigniew Humeniuk from [Art of Code](https://artofcode.co)
@@ -63,15 +63,15 @@ module Rdux
63
63
  return if performer.nil?
64
64
 
65
65
  if opts.any? || performer.method(meth).arity.abs == 2
66
- performer.public_send(meth, payload, opts.merge(action: self))
66
+ performer.public_send(meth, payload, opts.merge!(action: self))
67
67
  else
68
68
  performer.public_send(meth, payload)
69
69
  end
70
70
  end
71
71
 
72
72
  def build_opts
73
+ nested = rdux_actions.order(:created_at)
73
74
  {}.tap do |h|
74
- nested = rdux_actions.order(:created_at)
75
75
  h[:nested] = nested if nested.any?
76
76
  end
77
77
  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.9.1'
4
+ VERSION = '0.10.0'
5
5
  end
data/lib/rdux.rb CHANGED
@@ -13,6 +13,7 @@ module Rdux
13
13
  sanitize(action)
14
14
  action.save!
15
15
  res = call_call_or_up_on_action(action, opts)
16
+ res.up_result ||= opts[:up_result]
16
17
  assign_and_persist(res, action)
17
18
  res.after_save&.call(res.action)
18
19
  res
@@ -31,7 +32,7 @@ module Rdux
31
32
 
32
33
  action.up(opts)
33
34
  rescue StandardError => e
34
- handle_exception(e, action)
35
+ handle_exception(e, action, opts[:up_result])
35
36
  end
36
37
 
37
38
  def no_down(res)
@@ -80,14 +81,13 @@ module Rdux
80
81
  action.up_payload = up_payload_sanitized
81
82
  end
82
83
 
83
- def handle_exception(exc, action)
84
+ def handle_exception(exc, action, up_result)
84
85
  failed_action = action.to_failed_action
85
- failed_action.up_result = {
86
- 'Exception' => {
87
- class: exc.class.name,
88
- message: exc.message
89
- }
90
- }
86
+ failed_action.up_result ||= up_result || {}
87
+ failed_action.up_result.merge!({ 'Exception' => {
88
+ class: exc.class.name,
89
+ message: exc.message
90
+ } })
91
91
  failed_action.save!
92
92
  action.destroy
93
93
  raise exc
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rdux
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zbigniew Humeniuk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-08 00:00:00.000000000 Z
11
+ date: 2024-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -36,59 +36,60 @@ dependencies:
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 1.5.4
39
+ version: 1.5.8
40
40
  type: :development
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 1.5.4
46
+ version: 1.5.8
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rubocop
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 1.59.0
53
+ version: 1.66.1
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: 1.59.0
60
+ version: 1.66.1
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: rubocop-rails
63
63
  requirement: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
- version: 2.23.1
67
+ version: 2.26.0
68
68
  type: :development
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: 2.23.1
74
+ version: 2.26.0
75
75
  - !ruby/object:Gem::Dependency
76
76
  name: sqlite3
77
77
  requirement: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: 1.7.0
81
+ version: 2.1.0
82
82
  type: :development
83
83
  prerelease: false
84
84
  version_requirements: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
- version: 1.7.0
89
- description: |
90
- Write apps that are easy to test.
91
- It makes it easy to trace when, where, why, and how your application's state changed.
88
+ version: 2.1.0
89
+ description: "Rdux is a lightweight, minimalistic Rails plugin designed to introduce
90
+ event sourcing and audit logging capabilities to your Rails application. \nWith
91
+ Rdux, you can efficiently track and store the history of actions performed within
92
+ your app, offering transparency and traceability for key processes.\n"
92
93
  email:
93
94
  - hello@artofcode.co
94
95
  executables: []
@@ -128,8 +129,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
129
  - !ruby/object:Gem::Version
129
130
  version: '0'
130
131
  requirements: []
131
- rubygems_version: 3.5.6
132
+ rubygems_version: 3.5.16
132
133
  signing_key:
133
134
  specification_version: 4
134
- summary: Rdux adds a new layer to Rails apps - actions.
135
+ summary: A Minimal Event Sourcing Plugin for Rails
135
136
  test_files: []