rdux 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|