rdux 0.2.1 → 0.3.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 +19 -5
- data/Rakefile +2 -0
- data/app/models/rdux/action.rb +58 -15
- data/app/models/rdux/actionable.rb +28 -0
- data/app/models/rdux/failed_action.rb +11 -0
- data/db/migrate/20230621215717_create_rdux_failed_actions.rb +18 -0
- data/db/migrate/20230621215718_create_rdux_actions.rb +21 -0
- data/lib/rdux/result.rb +5 -1
- data/lib/rdux/sanitize.rb +27 -0
- data/lib/rdux/version.rb +1 -1
- data/lib/rdux.rb +74 -11
- data/lib/tasks/test.rake +16 -0
- metadata +35 -16
- data/db/migrate/20200823045609_create_rdux_actions.rb +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56611332f7071f2e1b9975e05c8886ff05f6b7107817ee5b71bb36d97e6b720d
|
4
|
+
data.tar.gz: 210f46fd573288d20dbdc38f4fa62c3ecef064ff7cafa48ee0f8549ab631ebcc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 140e8bb81c6ad284a931a3d5565777c76e57bbc20405e8220846e2d4691635939f4fa0069ce2a22756b5f79e4788a9a873895de36a7b5f7ad07cac3b23d28d35
|
7
|
+
data.tar.gz: e61e3931c6ddd9b206ba58d1eb95d2ca375f6343a61c293198bb24be9f119f2020aebee9a35be2aa664ea59b1a8b8f3372b394b2e2e0748fcde61c17025c3e47
|
data/README.md
CHANGED
@@ -1,10 +1,26 @@
|
|
1
1
|
# Rdux
|
2
|
-
|
2
|
+
Minimal take on event sourcing.
|
3
3
|
|
4
4
|
## Usage
|
5
5
|
|
6
6
|
```bash
|
7
7
|
$ bin/rails rdux:install:migrations
|
8
|
+
$ bin/rails db:migrate
|
9
|
+
```
|
10
|
+
|
11
|
+
### Code structure
|
12
|
+
|
13
|
+
### Dispatch action
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
Rdux.perform(
|
17
|
+
Activity::Stop,
|
18
|
+
{ activity_id: current_activity.id },
|
19
|
+
{ activity: current_activity },
|
20
|
+
meta: {
|
21
|
+
stream: { user_id: 123, context: 'foo' }, bar: 'baz'
|
22
|
+
}
|
23
|
+
)
|
8
24
|
```
|
9
25
|
|
10
26
|
## Installation
|
@@ -27,11 +43,9 @@ $ gem install rdux
|
|
27
43
|
## Test
|
28
44
|
|
29
45
|
```bash
|
30
|
-
$ bin/test
|
46
|
+
$ DB=postgres bin/test
|
47
|
+
$ DB=sqlite bin/test
|
31
48
|
```
|
32
49
|
|
33
|
-
## Contributing
|
34
|
-
Contribution directions go here.
|
35
|
-
|
36
50
|
## License
|
37
51
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
data/app/models/rdux/action.rb
CHANGED
@@ -2,32 +2,75 @@
|
|
2
2
|
|
3
3
|
module Rdux
|
4
4
|
class Action < ApplicationRecord
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
include Actionable
|
6
|
+
|
7
|
+
belongs_to :rdux_failed_action, optional: true, class_name: 'Rdux::FailedAction'
|
8
|
+
belongs_to :rdux_action, optional: true, class_name: 'Rdux::Action'
|
9
|
+
has_many :rdux_actions, class_name: 'Rdux::Action', foreign_key: 'rdux_action_id'
|
8
10
|
|
9
|
-
serialize :up_payload, JSON
|
10
11
|
serialize :down_payload, JSON
|
11
12
|
|
12
|
-
|
13
|
-
|
13
|
+
scope :up, -> { where(down_at: nil) }
|
14
|
+
scope :down, -> { where.not(down_at: nil) }
|
14
15
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
def call(opts = {})
|
17
|
+
perform_action(:call, up_payload, opts)
|
18
|
+
end
|
19
|
+
|
20
|
+
def up(opts = {})
|
21
|
+
return false if up_payload_sanitized
|
22
|
+
return false unless down_at.nil?
|
23
|
+
|
24
|
+
perform_action(:up, up_payload, opts)
|
21
25
|
end
|
22
26
|
|
23
27
|
def down
|
24
|
-
|
28
|
+
return false unless down_at.nil?
|
29
|
+
return false unless can_down?
|
30
|
+
|
31
|
+
perform_action(:down, down_payload, build_opts)
|
32
|
+
update(down_at: Time.current)
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_failed_action
|
36
|
+
FailedAction.new(attributes.except('down_payload', 'down_at', 'rdux_action_id'))
|
25
37
|
end
|
26
38
|
|
27
39
|
private
|
28
40
|
|
29
|
-
def
|
30
|
-
|
41
|
+
def can_down?
|
42
|
+
q = self.class.where('created_at > ?', created_at)
|
43
|
+
.where(down_at: nil)
|
44
|
+
.where('id != ?', rdux_action_id.to_i)
|
45
|
+
q = q.where(stream_hash: stream_hash) unless stream_hash.nil?
|
46
|
+
!q.count.positive?
|
47
|
+
end
|
48
|
+
|
49
|
+
def action_creator(meth)
|
50
|
+
name_const = name.to_s.classify.constantize
|
51
|
+
return name_const if name_const.respond_to?(meth)
|
52
|
+
return unless name_const.is_a?(Class)
|
53
|
+
|
54
|
+
obj = name_const.new
|
55
|
+
obj.respond_to?(meth) ? obj : nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def perform_action(meth, payload, opts)
|
59
|
+
responder = action_creator(meth)
|
60
|
+
return if responder.nil?
|
61
|
+
|
62
|
+
if opts.any?
|
63
|
+
responder.public_send(meth, payload, opts)
|
64
|
+
else
|
65
|
+
responder.public_send(meth, payload)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_opts
|
70
|
+
{}.tap do |h|
|
71
|
+
nested = rdux_actions.order(:created_at)
|
72
|
+
h[:nested] = nested if nested.any?
|
73
|
+
end
|
31
74
|
end
|
32
75
|
end
|
33
76
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rdux
|
4
|
+
module Actionable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
serialize :up_payload, JSON
|
9
|
+
serialize :up_result, JSON
|
10
|
+
serialize :meta, JSON
|
11
|
+
|
12
|
+
validates :name, presence: true
|
13
|
+
validates :up_payload, presence: true
|
14
|
+
|
15
|
+
before_save do
|
16
|
+
if meta_changed? && meta['stream'] && (meta_was || {})['stream'] != meta['stream']
|
17
|
+
self.stream_hash = Digest::SHA256.hexdigest(meta['stream'].to_json)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class_methods do
|
23
|
+
def table_name_prefix
|
24
|
+
'rdux_'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rdux
|
4
|
+
class FailedAction < ApplicationRecord
|
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
|
@@ -0,0 +1,18 @@
|
|
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
|
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
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateRduxActions < ActiveRecord::Migration[7.0]
|
4
|
+
def change
|
5
|
+
create_table :rdux_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.column :down_payload, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
|
9
|
+
t.datetime :down_at
|
10
|
+
t.boolean :up_payload_sanitized, default: false
|
11
|
+
t.column :up_result, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
|
12
|
+
t.column :meta, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
|
13
|
+
t.string :stream_hash
|
14
|
+
|
15
|
+
t.belongs_to :rdux_action, index: true, foreign_key: true
|
16
|
+
t.belongs_to :rdux_failed_action, index: true, foreign_key: true
|
17
|
+
|
18
|
+
t.timestamps
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/rdux/result.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Rdux
|
4
|
-
Result = Struct.new(:ok, :down_payload, :resp, :action) do
|
4
|
+
Result = Struct.new(:ok, :down_payload, :resp, :action, :up_result, :nested, :save) do
|
5
5
|
def payload
|
6
6
|
resp || down_payload
|
7
7
|
end
|
8
|
+
|
9
|
+
def save_failed?
|
10
|
+
ok == false && save
|
11
|
+
end
|
8
12
|
end
|
9
13
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rdux
|
4
|
+
module Sanitize
|
5
|
+
class << self
|
6
|
+
def call(payload)
|
7
|
+
filtered_payload = payload.deep_dup # Create a duplicate to avoid modifying the original params
|
8
|
+
Rails.application.config.filter_parameters.each do |filter_param|
|
9
|
+
filter_recursive(filtered_payload, filter_param)
|
10
|
+
end
|
11
|
+
filtered_payload
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def filter_recursive(payload, filter_param)
|
17
|
+
payload.each do |key, value|
|
18
|
+
if value.is_a? Hash # If the value is a nested parameter
|
19
|
+
filter_recursive(value, filter_param)
|
20
|
+
elsif key == filter_param.to_s # to_s is used to ensure that symbol/string difference does not matter
|
21
|
+
payload[key] = '[FILTERED]'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/rdux/version.rb
CHANGED
data/lib/rdux.rb
CHANGED
@@ -1,19 +1,82 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rdux/engine'
|
4
|
-
|
5
4
|
require 'rdux/result'
|
5
|
+
require 'rdux/sanitize'
|
6
|
+
require 'active_support/concern'
|
6
7
|
|
7
8
|
module Rdux
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
9
|
+
class << self
|
10
|
+
def dispatch(action_name, payload, opts = {}, meta: nil)
|
11
|
+
(opts[:ars] || {}).each { |k, v| payload["#{k}_id"] = v.id }
|
12
|
+
action = Action.new(name: action_name, up_payload: payload, meta: meta)
|
13
|
+
call_call_meth_on_action(action, opts)
|
14
|
+
end
|
15
|
+
|
16
|
+
alias perform dispatch
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def call_call_meth_on_action(action, opts)
|
21
|
+
res = action.call(opts)
|
22
|
+
return call_up_meth_on_action(action, opts) if res.nil?
|
23
|
+
|
24
|
+
unless res.down_payload.nil?
|
25
|
+
res.resp = res.down_payload.deep_stringify_keys
|
26
|
+
res.down_payload = nil
|
27
|
+
end
|
28
|
+
assign_and_persist(res, action)
|
29
|
+
end
|
30
|
+
|
31
|
+
def call_up_meth_on_action(action, opts)
|
32
|
+
res = action.up(opts)
|
33
|
+
res.down_payload&.deep_stringify_keys!
|
34
|
+
action.down_payload = res.down_payload
|
35
|
+
assign_and_persist(res, action)
|
36
|
+
end
|
37
|
+
|
38
|
+
def assign_and_persist(res, action)
|
39
|
+
if res.ok
|
40
|
+
assign_and_persist_for_ok(res, action)
|
41
|
+
elsif res.save_failed?
|
42
|
+
assign_and_persist_for_failed(res, action)
|
43
|
+
end
|
44
|
+
res.action ||= action
|
45
|
+
res
|
46
|
+
end
|
47
|
+
|
48
|
+
def assign_and_persist_common(res, action)
|
49
|
+
sanitize(action)
|
50
|
+
action.up_result = res.up_result
|
51
|
+
end
|
52
|
+
|
53
|
+
def assign_and_persist_for_ok(res, action)
|
54
|
+
assign_and_persist_common(res, action)
|
55
|
+
res.nested&.each { |nested_res| action.rdux_actions << nested_res.action }
|
56
|
+
action.save!
|
57
|
+
end
|
58
|
+
|
59
|
+
def assign_and_persist_for_failed(res, action)
|
60
|
+
assign_and_persist_common(res, action)
|
61
|
+
action.up_result ||= res.resp
|
62
|
+
res.action = action.to_failed_action.tap(&:save!)
|
63
|
+
assign_nested_responses_to_failed_action(res.action, res.nested) unless res.nested.nil?
|
64
|
+
end
|
65
|
+
|
66
|
+
def assign_nested_responses_to_failed_action(failed_action, nested)
|
67
|
+
nested.each do |nested_res|
|
68
|
+
if nested_res.action.is_a?(Rdux::Action)
|
69
|
+
failed_action.rdux_actions << nested_res.action
|
70
|
+
else
|
71
|
+
failed_action.rdux_failed_actions << nested_res.action
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def sanitize(action)
|
77
|
+
up_payload_sanitized = Sanitize.call(action.up_payload)
|
78
|
+
action.up_payload_sanitized = action.up_payload != up_payload_sanitized
|
79
|
+
action.up_payload = up_payload_sanitized
|
80
|
+
end
|
18
81
|
end
|
19
82
|
end
|
data/lib/tasks/test.rake
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :test do
|
4
|
+
desc 'Run tests with the SQLite database'
|
5
|
+
task :sqlite do
|
6
|
+
# Set an environment variable to change the database configuration
|
7
|
+
ENV['DB'] = 'sqlite'
|
8
|
+
Rake::Task['test'].invoke
|
9
|
+
end
|
10
|
+
|
11
|
+
desc 'Run tests with the Postgres database'
|
12
|
+
task :postgres do
|
13
|
+
ENV['DB'] = 'postgres'
|
14
|
+
Rake::Task['test'].invoke
|
15
|
+
end
|
16
|
+
end
|
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.
|
4
|
+
version: 0.3.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: 2023-
|
11
|
+
date: 2023-07-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -30,48 +30,62 @@ dependencies:
|
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '8.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: pg
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 1.5.2
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.5.2
|
33
47
|
- !ruby/object:Gem::Dependency
|
34
48
|
name: rubocop
|
35
49
|
requirement: !ruby/object:Gem::Requirement
|
36
50
|
requirements:
|
37
|
-
- - "
|
51
|
+
- - ">="
|
38
52
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
53
|
+
version: 1.54.1
|
40
54
|
type: :development
|
41
55
|
prerelease: false
|
42
56
|
version_requirements: !ruby/object:Gem::Requirement
|
43
57
|
requirements:
|
44
|
-
- - "
|
58
|
+
- - ">="
|
45
59
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
60
|
+
version: 1.54.1
|
47
61
|
- !ruby/object:Gem::Dependency
|
48
62
|
name: rubocop-rails
|
49
63
|
requirement: !ruby/object:Gem::Requirement
|
50
64
|
requirements:
|
51
|
-
- - "
|
65
|
+
- - ">="
|
52
66
|
- !ruby/object:Gem::Version
|
53
|
-
version:
|
67
|
+
version: 2.20.2
|
54
68
|
type: :development
|
55
69
|
prerelease: false
|
56
70
|
version_requirements: !ruby/object:Gem::Requirement
|
57
71
|
requirements:
|
58
|
-
- - "
|
72
|
+
- - ">="
|
59
73
|
- !ruby/object:Gem::Version
|
60
|
-
version:
|
74
|
+
version: 2.20.2
|
61
75
|
- !ruby/object:Gem::Dependency
|
62
76
|
name: sqlite3
|
63
77
|
requirement: !ruby/object:Gem::Requirement
|
64
78
|
requirements:
|
65
|
-
- - "
|
79
|
+
- - ">="
|
66
80
|
- !ruby/object:Gem::Version
|
67
|
-
version: 1.6.
|
81
|
+
version: 1.6.3
|
68
82
|
type: :development
|
69
83
|
prerelease: false
|
70
84
|
version_requirements: !ruby/object:Gem::Requirement
|
71
85
|
requirements:
|
72
|
-
- - "
|
86
|
+
- - ">="
|
73
87
|
- !ruby/object:Gem::Version
|
74
|
-
version: 1.6.
|
88
|
+
version: 1.6.3
|
75
89
|
description: |
|
76
90
|
Write apps that are easy to test.
|
77
91
|
Rdux gives you a possibility to centralize your app's state modification logic (DB changes).
|
@@ -88,11 +102,16 @@ files:
|
|
88
102
|
- README.md
|
89
103
|
- Rakefile
|
90
104
|
- app/models/rdux/action.rb
|
91
|
-
-
|
105
|
+
- app/models/rdux/actionable.rb
|
106
|
+
- app/models/rdux/failed_action.rb
|
107
|
+
- db/migrate/20230621215717_create_rdux_failed_actions.rb
|
108
|
+
- db/migrate/20230621215718_create_rdux_actions.rb
|
92
109
|
- lib/rdux.rb
|
93
110
|
- lib/rdux/engine.rb
|
94
111
|
- lib/rdux/result.rb
|
112
|
+
- lib/rdux/sanitize.rb
|
95
113
|
- lib/rdux/version.rb
|
114
|
+
- lib/tasks/test.rake
|
96
115
|
homepage: https://artofcode.co
|
97
116
|
licenses:
|
98
117
|
- MIT
|
@@ -113,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
132
|
- !ruby/object:Gem::Version
|
114
133
|
version: '0'
|
115
134
|
requirements: []
|
116
|
-
rubygems_version: 3.4.
|
135
|
+
rubygems_version: 3.4.10
|
117
136
|
signing_key:
|
118
137
|
specification_version: 4
|
119
138
|
summary: Rdux adds a new layer to Rails apps - actions.
|