nexus_cqrs 0.2.2 → 0.4.2
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/.gitignore +5 -0
- data/.rubocop.yml +8 -3
- data/Gemfile +1 -0
- data/Gemfile.lock +64 -57
- data/README.md +97 -14
- data/Rakefile +1 -0
- data/bin/console +1 -0
- data/lib/generators/nexus_cqrs/USAGE +1 -1
- data/lib/generators/nexus_cqrs/command_generator.rb +6 -6
- data/lib/generators/nexus_cqrs/query_generator.rb +6 -6
- data/lib/generators/nexus_cqrs/templates/command_handler.rb +19 -2
- data/lib/generators/nexus_cqrs/templates/query_handler.rb +3 -2
- data/lib/generators/nexus_cqrs/templates/register_cqrs_handlers.rb +21 -6
- data/lib/nexus_cqrs/auth/auth.rb +52 -0
- data/lib/nexus_cqrs/auth/ownable.rb +56 -0
- data/lib/nexus_cqrs/auth/permission_provider.rb +76 -0
- data/lib/nexus_cqrs/auth/user_context.rb +15 -0
- data/lib/nexus_cqrs/base_command.rb +4 -0
- data/lib/nexus_cqrs/base_command_handler.rb +9 -0
- data/lib/nexus_cqrs/base_message.rb +34 -8
- data/lib/nexus_cqrs/base_middleware.rb +8 -1
- data/lib/nexus_cqrs/base_query.rb +4 -0
- data/lib/nexus_cqrs/base_query_handler.rb +9 -0
- data/lib/nexus_cqrs/command_bus.rb +36 -6
- data/lib/nexus_cqrs/command_executor.rb +75 -5
- data/lib/nexus_cqrs/helpers.rb +17 -1
- data/lib/nexus_cqrs/version.rb +3 -1
- data/lib/nexus_cqrs.rb +5 -0
- data/nexus_cqrs.gemspec +3 -0
- metadata +35 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 967dfb16241304d1d0f2233b23e3410b87abff47ec4a916569101a53e4b963c5
|
|
4
|
+
data.tar.gz: d528d7da45c96cae5fe8a403116e3f9c8abc83c2842dbd165dfa5c3267a81e1d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f407d669371be6e3fd4f9ad4e55f92678b6fc82825b1dba6f5bf4a1dabb070c79005bdf37a114dd2461f6b99d7213daf777894d04f3337daf4a8255a74fa2f6
|
|
7
|
+
data.tar.gz: 6d31c90bff990bd92f7aeb9ffcf289d1fe47bb5fd987d47f14fb12e7a7d9baa0571f731a466dff569999976a370c7028a6380c8f4423a5798ecc1063f8847029
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -3,9 +3,14 @@ inherit_gem:
|
|
|
3
3
|
|
|
4
4
|
AllCops:
|
|
5
5
|
Exclude:
|
|
6
|
-
-
|
|
6
|
+
- db/schema.rb
|
|
7
|
+
- db/seeds.rb
|
|
8
|
+
- db/migrate/*.rb
|
|
9
|
+
- config/initializers/devise.rb
|
|
10
|
+
- spec/**/*
|
|
11
|
+
|
|
12
|
+
Style/Documentation:
|
|
13
|
+
Enabled: false
|
|
7
14
|
|
|
8
15
|
Style/GlobalVars:
|
|
9
16
|
Enabled: false
|
|
10
|
-
Style/FrozenStringLiteralComment:
|
|
11
|
-
Enabled: false
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -4,104 +4,111 @@ PATH
|
|
|
4
4
|
nexus_cqrs (0.1.0)
|
|
5
5
|
generator_spec
|
|
6
6
|
ibsciss-middleware
|
|
7
|
+
pundit
|
|
8
|
+
strings-case
|
|
7
9
|
thread_safe
|
|
8
10
|
|
|
9
11
|
GEM
|
|
10
12
|
remote: https://rubygems.org/
|
|
11
13
|
specs:
|
|
12
|
-
actionpack (6.
|
|
13
|
-
actionview (= 6.
|
|
14
|
-
activesupport (= 6.
|
|
15
|
-
rack (~> 2.0, >= 2.0.
|
|
14
|
+
actionpack (6.1.4.1)
|
|
15
|
+
actionview (= 6.1.4.1)
|
|
16
|
+
activesupport (= 6.1.4.1)
|
|
17
|
+
rack (~> 2.0, >= 2.0.9)
|
|
16
18
|
rack-test (>= 0.6.3)
|
|
17
19
|
rails-dom-testing (~> 2.0)
|
|
18
20
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
|
19
|
-
actionview (6.
|
|
20
|
-
activesupport (= 6.
|
|
21
|
+
actionview (6.1.4.1)
|
|
22
|
+
activesupport (= 6.1.4.1)
|
|
21
23
|
builder (~> 3.1)
|
|
22
24
|
erubi (~> 1.4)
|
|
23
25
|
rails-dom-testing (~> 2.0)
|
|
24
26
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
|
25
|
-
activesupport (6.
|
|
27
|
+
activesupport (6.1.4.1)
|
|
26
28
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
27
|
-
i18n (>=
|
|
28
|
-
minitest (
|
|
29
|
-
tzinfo (~>
|
|
30
|
-
zeitwerk (~> 2.
|
|
31
|
-
ast (2.4.
|
|
29
|
+
i18n (>= 1.6, < 2)
|
|
30
|
+
minitest (>= 5.1)
|
|
31
|
+
tzinfo (~> 2.0)
|
|
32
|
+
zeitwerk (~> 2.3)
|
|
33
|
+
ast (2.4.2)
|
|
32
34
|
builder (3.2.4)
|
|
33
|
-
concurrent-ruby (1.1.
|
|
35
|
+
concurrent-ruby (1.1.9)
|
|
34
36
|
crass (1.0.6)
|
|
35
37
|
diff-lcs (1.4.4)
|
|
36
|
-
erubi (1.
|
|
38
|
+
erubi (1.10.0)
|
|
37
39
|
generator_spec (0.9.4)
|
|
38
40
|
activesupport (>= 3.0.0)
|
|
39
41
|
railties (>= 3.0.0)
|
|
40
|
-
i18n (1.8.
|
|
42
|
+
i18n (1.8.11)
|
|
41
43
|
concurrent-ruby (~> 1.0)
|
|
42
44
|
ibsciss-middleware (0.4.2)
|
|
43
|
-
loofah (2.
|
|
45
|
+
loofah (2.12.0)
|
|
44
46
|
crass (~> 1.0.2)
|
|
45
47
|
nokogiri (>= 1.5.9)
|
|
46
48
|
method_source (1.0.0)
|
|
47
|
-
mini_portile2 (2.
|
|
48
|
-
minitest (5.14.
|
|
49
|
-
nokogiri (1.
|
|
50
|
-
mini_portile2 (~> 2.
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
mini_portile2 (2.6.1)
|
|
50
|
+
minitest (5.14.4)
|
|
51
|
+
nokogiri (1.12.5)
|
|
52
|
+
mini_portile2 (~> 2.6.1)
|
|
53
|
+
racc (~> 1.4)
|
|
54
|
+
parallel (1.21.0)
|
|
55
|
+
parser (3.0.2.0)
|
|
53
56
|
ast (~> 2.4.1)
|
|
57
|
+
pundit (2.1.1)
|
|
58
|
+
activesupport (>= 3.0.0)
|
|
59
|
+
racc (1.6.0)
|
|
54
60
|
rack (2.2.3)
|
|
55
61
|
rack-test (1.1.0)
|
|
56
62
|
rack (>= 1.0, < 3)
|
|
57
63
|
rails-dom-testing (2.0.3)
|
|
58
64
|
activesupport (>= 4.2.0)
|
|
59
65
|
nokogiri (>= 1.6)
|
|
60
|
-
rails-html-sanitizer (1.
|
|
66
|
+
rails-html-sanitizer (1.4.2)
|
|
61
67
|
loofah (~> 2.3)
|
|
62
|
-
railties (6.
|
|
63
|
-
actionpack (= 6.
|
|
64
|
-
activesupport (= 6.
|
|
68
|
+
railties (6.1.4.1)
|
|
69
|
+
actionpack (= 6.1.4.1)
|
|
70
|
+
activesupport (= 6.1.4.1)
|
|
65
71
|
method_source
|
|
66
|
-
rake (>= 0.
|
|
67
|
-
thor (
|
|
72
|
+
rake (>= 0.13)
|
|
73
|
+
thor (~> 1.0)
|
|
68
74
|
rainbow (3.0.0)
|
|
69
|
-
rake (13.0.
|
|
70
|
-
regexp_parser (1.
|
|
71
|
-
rexml (3.2.
|
|
72
|
-
rspec (3.
|
|
73
|
-
rspec-core (~> 3.
|
|
74
|
-
rspec-expectations (~> 3.
|
|
75
|
-
rspec-mocks (~> 3.
|
|
76
|
-
rspec-core (3.
|
|
77
|
-
rspec-support (~> 3.
|
|
78
|
-
rspec-expectations (3.
|
|
75
|
+
rake (13.0.6)
|
|
76
|
+
regexp_parser (2.1.1)
|
|
77
|
+
rexml (3.2.5)
|
|
78
|
+
rspec (3.10.0)
|
|
79
|
+
rspec-core (~> 3.10.0)
|
|
80
|
+
rspec-expectations (~> 3.10.0)
|
|
81
|
+
rspec-mocks (~> 3.10.0)
|
|
82
|
+
rspec-core (3.10.1)
|
|
83
|
+
rspec-support (~> 3.10.0)
|
|
84
|
+
rspec-expectations (3.10.1)
|
|
79
85
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
80
|
-
rspec-support (~> 3.
|
|
81
|
-
rspec-mocks (3.
|
|
86
|
+
rspec-support (~> 3.10.0)
|
|
87
|
+
rspec-mocks (3.10.2)
|
|
82
88
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
83
|
-
rspec-support (~> 3.
|
|
84
|
-
rspec-support (3.
|
|
85
|
-
rubocop (
|
|
89
|
+
rspec-support (~> 3.10.0)
|
|
90
|
+
rspec-support (3.10.3)
|
|
91
|
+
rubocop (1.22.3)
|
|
86
92
|
parallel (~> 1.10)
|
|
87
|
-
parser (>=
|
|
93
|
+
parser (>= 3.0.0.0)
|
|
88
94
|
rainbow (>= 2.2.2, < 4.0)
|
|
89
|
-
regexp_parser (>= 1.
|
|
95
|
+
regexp_parser (>= 1.8, < 3.0)
|
|
90
96
|
rexml
|
|
91
|
-
rubocop-ast (>=
|
|
97
|
+
rubocop-ast (>= 1.12.0, < 2.0)
|
|
92
98
|
ruby-progressbar (~> 1.7)
|
|
93
|
-
unicode-display_width (>= 1.4.0, <
|
|
94
|
-
rubocop-ast (
|
|
95
|
-
parser (>=
|
|
96
|
-
rubocop-shopify (1.0.
|
|
97
|
-
rubocop (
|
|
98
|
-
ruby-progressbar (1.
|
|
99
|
-
|
|
99
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
|
100
|
+
rubocop-ast (1.13.0)
|
|
101
|
+
parser (>= 3.0.1.1)
|
|
102
|
+
rubocop-shopify (1.0.7)
|
|
103
|
+
rubocop (~> 1.4)
|
|
104
|
+
ruby-progressbar (1.11.0)
|
|
105
|
+
strings-case (0.3.0)
|
|
106
|
+
thor (1.1.0)
|
|
100
107
|
thread_safe (0.3.6)
|
|
101
|
-
tzinfo (
|
|
102
|
-
|
|
103
|
-
unicode-display_width (1.
|
|
104
|
-
zeitwerk (2.
|
|
108
|
+
tzinfo (2.0.4)
|
|
109
|
+
concurrent-ruby (~> 1.0)
|
|
110
|
+
unicode-display_width (2.1.0)
|
|
111
|
+
zeitwerk (2.5.1)
|
|
105
112
|
|
|
106
113
|
PLATFORMS
|
|
107
114
|
ruby
|
data/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Nexus CQRS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Built by the NexusMods team for providing a universal way of organising CQRS logic (Command and Queries) and permission
|
|
4
|
+
management. Used by most of NexusMod's rails applications.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
The core concept of this gem is to provide a MessageBus that can invoked handlers registered to certain commands.
|
|
7
|
+
This allows developers to separate business and application logic, whilst also seperating read operations from write
|
|
8
|
+
operations (Queries and Commands).
|
|
6
9
|
|
|
7
10
|
## Installation
|
|
8
11
|
|
|
@@ -20,39 +23,119 @@ Or install it yourself as:
|
|
|
20
23
|
|
|
21
24
|
$ gem install nexus_cqrs
|
|
22
25
|
|
|
23
|
-
##
|
|
26
|
+
## Getting Started
|
|
24
27
|
|
|
25
|
-
Generators
|
|
28
|
+
### Generators
|
|
29
|
+
|
|
30
|
+
Generators can be used to aid in the creation of Commands and Queries:
|
|
26
31
|
|
|
27
32
|
rails g nexus_cqrs:command CommandName
|
|
28
33
|
rails g nexus_cqrs:query QueryName
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
Optionally, a command can be scaffolded with basic authorisation logic with `--permission`
|
|
36
|
+
|
|
37
|
+
rails g nexus_cqrs:command CommandName --permission
|
|
38
|
+
rails g nexus_cqrs:query QueryName --permission
|
|
39
|
+
|
|
40
|
+
Once a command has been created, two new files will be created under `/app/domain/command` or `/app/domain/query`
|
|
41
|
+
depending on which generator was used. An initializer will also be created - `register_cqrs_handlers.rb`
|
|
42
|
+
|
|
43
|
+
### Command Bus and Executor
|
|
44
|
+
|
|
45
|
+
In `register_cqrs_handlers.rb`, the command bus and executor will be created and used to register queries and commands:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
middleware_stack = Middleware::Builder.new do |b|
|
|
49
|
+
# Configure additional middleware for the CQRS stack here
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
command_bus = NexusCqrs::CommandBus.new(middleware: middleware_stack)
|
|
53
|
+
query_bus = NexusCqrs::CommandBus.new(middleware: middleware_stack)
|
|
54
|
+
|
|
55
|
+
$COMMAND_EXECUTOR = NexusCqrs::CommandExecutor.new(command_bus)
|
|
56
|
+
$QUERY_EXECUTOR = NexusCqrs::CommandExecutor.new(query_bus)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
*NOTE: By default, all classes extending `BaseCommand` and `BaseQuery` in the `app/domain` directory will be
|
|
60
|
+
registered automatically.*
|
|
31
61
|
|
|
32
62
|
### Middleware
|
|
33
63
|
|
|
34
|
-
Middleware can be created by extending the base middleware:
|
|
64
|
+
Middleware can be created by extending the base middleware and injecting into the middleware stack:
|
|
35
65
|
|
|
36
66
|
```ruby
|
|
67
|
+
# my_middleware.rb
|
|
37
68
|
class MyMiddleware < NexusCqrs::BaseMiddleware
|
|
38
69
|
def call(command)
|
|
39
70
|
@next.call(command)
|
|
40
71
|
end
|
|
41
72
|
end
|
|
73
|
+
|
|
74
|
+
# register_cqrs_handlers.rb
|
|
75
|
+
middleware_stack = Middleware::Builder.new do |b|
|
|
76
|
+
b.use MyMiddleware
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
command_bus = Bus.new(middleware: middleware_stack)
|
|
42
80
|
```
|
|
43
81
|
|
|
44
|
-
The above middleware will pass responsibility for execution to the next responder in the chain
|
|
82
|
+
The above middleware will pass responsibility for execution to the next responder in the chain and will be ran BEFORE
|
|
83
|
+
the handler is invoked for every message executed through the `CommandExecutor`
|
|
45
84
|
|
|
46
85
|
For more information on writing middleware see: https://github.com/Ibsciss/ruby-middleware
|
|
47
86
|
|
|
48
|
-
|
|
87
|
+
### Metadata
|
|
88
|
+
|
|
89
|
+
Commands/Queries can contain data, but data can also be injected into the message via metadata before the message is
|
|
90
|
+
executed:
|
|
49
91
|
|
|
50
92
|
```ruby
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
93
|
+
command.set_metadata(:current_user, user)
|
|
94
|
+
execute(command)
|
|
95
|
+
```
|
|
54
96
|
|
|
55
|
-
|
|
97
|
+
### Authorisation
|
|
98
|
+
|
|
99
|
+
There are various tools and helpers to aid with authorisation in this gem. Firstly, the system must be aware of the
|
|
100
|
+
user that is calling the command, this can be done by providing the current user and global permissions as metadata:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
message.set_metadata(:current_user, current_user)
|
|
104
|
+
message.set_metadata(:global_permissions, @access_token[:global_permissions])
|
|
105
|
+
execute(message)
|
|
106
|
+
```
|
|
107
|
+
Once the metadata is set, the handler can than access the metadata - which in turn can invoke the `authorize` method:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
class ModerateHandler < BaseCommandHandler
|
|
111
|
+
include NexusCqrs::Helpers
|
|
112
|
+
|
|
113
|
+
# @param [Commands::Moderate] command
|
|
114
|
+
def call(command)
|
|
115
|
+
mod = Mod.kept.find(command.mod_id)
|
|
116
|
+
|
|
117
|
+
authorize(command, mod)
|
|
118
|
+
...
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
This will look up the correct Policy and automatically pass the metadata from the command, converting it into a
|
|
122
|
+
`UserContext` object:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# Pull context variables from command
|
|
126
|
+
user = message.metadata[:current_user]
|
|
127
|
+
global_permissions = message.metadata[:global_permissions]
|
|
128
|
+
|
|
129
|
+
# Instantiate new policy class, with context
|
|
130
|
+
policy = policy_class.new(UserContext.new(user, global_permissions), record)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This will allow the policy class to access the `PermissionProvider` and retrieve any permission:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
def moderate?
|
|
137
|
+
permissions.has_permission?('mod:moderate', ModPermission, record.id)
|
|
138
|
+
end
|
|
56
139
|
```
|
|
57
140
|
|
|
58
141
|
## Development
|
|
@@ -66,4 +149,4 @@ To contribute to this gem, simple pull the repository, run `bundle install` and
|
|
|
66
149
|
|
|
67
150
|
## Releasing
|
|
68
151
|
|
|
69
|
-
The release process is tied to the git tags. Simply creating a new tag and pushing will trigger a new release to
|
|
152
|
+
The release process is tied to the git tags. Simply creating a new tag and pushing will trigger a new release to rubygems.
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'rails/generators/base'
|
|
2
3
|
|
|
3
4
|
module NexusCqrs
|
|
4
5
|
class CommandGenerator < Rails::Generators::NamedBase
|
|
5
6
|
source_root File.expand_path('templates', __dir__)
|
|
7
|
+
class_option :permission, type: :string, default: nil
|
|
6
8
|
|
|
7
9
|
def copy_command_file
|
|
8
10
|
file_path = class_name.underscore
|
|
9
11
|
|
|
12
|
+
@permission = options['permission']
|
|
13
|
+
|
|
10
14
|
template('command.rb', "app/domain/commands/#{file_path}.rb")
|
|
11
15
|
template('command_handler.rb', "app/domain/commands/#{file_path}_handler.rb")
|
|
12
16
|
|
|
13
|
-
register_command
|
|
17
|
+
register_command
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
private
|
|
17
21
|
|
|
18
|
-
def register_command
|
|
22
|
+
def register_command
|
|
19
23
|
handler_config = 'config/initializers/register_cqrs_handlers.rb'
|
|
20
24
|
|
|
21
25
|
unless File.exist?('config/initializers/register_cqrs_handlers.rb')
|
|
22
26
|
template('register_cqrs_handlers.rb', handler_config)
|
|
23
27
|
end
|
|
24
|
-
|
|
25
|
-
code_to_inject = "$COMMAND_EXECUTOR.register_command(#{full_name}, #{full_name}Handler.new)\n"
|
|
26
|
-
|
|
27
|
-
inject_into_file(handler_config, code_to_inject, after: "# Register Commands\n")
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'rails/generators/base'
|
|
2
3
|
|
|
3
4
|
module NexusCqrs
|
|
4
5
|
class QueryGenerator < Rails::Generators::NamedBase
|
|
5
6
|
source_root File.expand_path('templates', __dir__)
|
|
7
|
+
class_option :permission, type: :string, default: nil
|
|
6
8
|
|
|
7
9
|
def copy_query_file
|
|
8
10
|
file_path = class_name.underscore
|
|
9
11
|
|
|
12
|
+
@permission = options['permission']
|
|
13
|
+
|
|
10
14
|
template('query.rb', "app/domain/queries/#{file_path}.rb")
|
|
11
15
|
template('query_handler.rb', "app/domain/queries/#{file_path}_handler.rb")
|
|
12
16
|
|
|
13
|
-
register_query
|
|
17
|
+
register_query
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
private
|
|
17
21
|
|
|
18
|
-
def register_query
|
|
22
|
+
def register_query
|
|
19
23
|
handler_config = 'config/initializers/register_cqrs_handlers.rb'
|
|
20
24
|
|
|
21
25
|
unless File.exist?('config/initializers/register_cqrs_handlers.rb')
|
|
22
26
|
template('register_cqrs_handlers.rb', handler_config)
|
|
23
27
|
end
|
|
24
|
-
|
|
25
|
-
code_to_inject = "$QUERY_EXECUTOR.register_command(#{full_name}, #{full_name}Handler.new)\n"
|
|
26
|
-
|
|
27
|
-
inject_into_file(handler_config, code_to_inject, after: "# Register Queries\n")
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -1,9 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module Commands
|
|
2
|
-
|
|
3
|
+
# Command handler
|
|
4
|
+
class <%= class_name %>Handler < BaseCommandHandler
|
|
5
|
+
<% if @permission %>include NexusCqrs::Helpers<% end %>
|
|
3
6
|
|
|
4
|
-
#
|
|
7
|
+
# @param [<%= class_name %>] command
|
|
5
8
|
def call(command)
|
|
9
|
+
<% if @permission %>authorize(command, Model)<% end %>
|
|
6
10
|
|
|
11
|
+
domain_event(command)
|
|
7
12
|
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def domain_event(command)
|
|
17
|
+
NexusDomainEvents::Event.new(
|
|
18
|
+
message_name: :ENTITY_WAS_ACTIONED,
|
|
19
|
+
actor: command.metadata[:current_user].member_id,
|
|
20
|
+
payload: {
|
|
21
|
+
object_id: command.id,
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
end
|
|
8
25
|
end
|
|
9
26
|
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
module Queries
|
|
2
2
|
class <%= class_name %>Handler < NexusCqrs::BaseQueryHandler
|
|
3
|
+
<% if @permission %>include NexusCqrs::Helpers<% end %>
|
|
3
4
|
|
|
4
|
-
#
|
|
5
|
+
# @param [<%= class_name %>] command
|
|
5
6
|
def call(command)
|
|
6
|
-
|
|
7
|
+
<% if @permission %>authorize(command, Model)<% end %>
|
|
7
8
|
end
|
|
8
9
|
end
|
|
9
10
|
end
|
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
Rails.configuration.to_prepare do
|
|
3
|
+
middleware_stack = Middleware::Builder.new do |b|
|
|
4
|
+
end
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
command_bus = NexusCqrs::CommandBus.new(middleware: middleware_stack)
|
|
7
|
+
query_bus = NexusCqrs::CommandBus.new(middleware: middleware_stack)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
$COMMAND_EXECUTOR = NexusCqrs::CommandExecutor.new(command_bus)
|
|
10
|
+
$QUERY_EXECUTOR = NexusCqrs::CommandExecutor.new(query_bus)
|
|
8
11
|
|
|
9
|
-
# Register Commands
|
|
12
|
+
# Register Commands
|
|
13
|
+
$QUERY_EXECUTOR.configure_handlers do |executor|
|
|
14
|
+
# Manually registered queries should always go above the autoregister
|
|
15
|
+
|
|
16
|
+
executor.autoregister(NexusCqrs::BaseQuery, 'app/domain/queries')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
$COMMAND_EXECUTOR.configure_handlers do |executor|
|
|
20
|
+
# Manually registered commands should always go above the autoregister
|
|
21
|
+
|
|
22
|
+
executor.autoregister(NexusCqrs::BaseCommand, 'app/domain/commands')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'pundit'
|
|
3
|
+
require 'strings-case'
|
|
4
|
+
|
|
5
|
+
module NexusCqrs
|
|
6
|
+
# Concern used to provide authorisation abilities to handlers and other classes. Overrides pundit's `authorize` method
|
|
7
|
+
# and creates helpers for the permission_provider
|
|
8
|
+
module Auth
|
|
9
|
+
include Pundit
|
|
10
|
+
|
|
11
|
+
# Overrides pundit's `authorize` method, allowing the message to be passed
|
|
12
|
+
#
|
|
13
|
+
# @see Pundit#authorize
|
|
14
|
+
# @param [NexusCqrs::BaseMessage] message Message to authorise against
|
|
15
|
+
def authorize(message, record, query = nil, policy_class: nil)
|
|
16
|
+
# Populate the query from the command, or the params if it's being overriden
|
|
17
|
+
query ||= Strings::Case.snakecase(message.demodularised_class_name) + '?'
|
|
18
|
+
|
|
19
|
+
# Retreive the policy class object from the type of record we are passing in
|
|
20
|
+
policy_class ||= PolicyFinder.new(record).policy
|
|
21
|
+
|
|
22
|
+
# Pull context variables from command
|
|
23
|
+
user = message.metadata[:current_user]
|
|
24
|
+
global_permissions = message.metadata[:global_permissions]
|
|
25
|
+
|
|
26
|
+
# Instantiate new policy class, with context
|
|
27
|
+
policy = policy_class.new(UserContext.new(user, global_permissions), record)
|
|
28
|
+
raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
|
|
29
|
+
|
|
30
|
+
record.is_a?(Array) ? record.last : record
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Helper method for creating a permissions provider object from a query object. This allows certain permissions
|
|
34
|
+
# to be checked inside the command handler, as opposed to inside the policy
|
|
35
|
+
#
|
|
36
|
+
# @param [NexusCqrs::BaseMessage] message Create the PermissionProvider using the message metadata
|
|
37
|
+
# @return [PermissionProvider] A new instance of the permission provider
|
|
38
|
+
def permission_provider(message)
|
|
39
|
+
PermissionProvider.new(message.metadata[:current_user], message.metadata[:global_permissions])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def pundit_user
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def current_user
|
|
47
|
+
return super if defined?(super)
|
|
48
|
+
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module NexusCqrs
|
|
3
|
+
module Auth
|
|
4
|
+
# Concern used to integrate models to the permissions system. Including this module to a model will assume the
|
|
5
|
+
# model can be "owned" by a user. When the model is created, permissions will automatically be assigned to the user
|
|
6
|
+
# and permissions can be validated and "repaired" retroactively.
|
|
7
|
+
module Ownable
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
class_attribute :granted_permissions, default: {}
|
|
12
|
+
|
|
13
|
+
# Default relationship to <class>_permissions
|
|
14
|
+
class_attribute :permissions_relation, default: top_ancestor_class.name.downcase + "_permissions"
|
|
15
|
+
class_attribute :owner_column, default: :user_id
|
|
16
|
+
|
|
17
|
+
before_create :assign_permissions
|
|
18
|
+
|
|
19
|
+
define_model_callbacks :create_permissions
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module ClassMethods
|
|
23
|
+
# Gets the class that this module is included in
|
|
24
|
+
def top_ancestor_class
|
|
25
|
+
ancestors.first
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def assign_permissions
|
|
30
|
+
# As we are doing this on before_create, we must "build" this association, as opposed to creating it. This
|
|
31
|
+
# ensures validation is passed before saving this parent Collection.
|
|
32
|
+
granted_permissions.each do |p|
|
|
33
|
+
unless permissions.where(permission: p, entity_id: id, user_id: owner_id).exists?
|
|
34
|
+
permissions.build(user_id: owner_id, permission: p)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def owner_id
|
|
40
|
+
send(owner_column)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def permissions
|
|
44
|
+
if respond_to?(permissions_relation)
|
|
45
|
+
return send(permissions_relation)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
raise OwnableRelationshipNotSet, "Permissions relation not set.
|
|
49
|
+
Set it on your model with `self.permissions_relation = :xxx_permissions`, and ensure relationship has been created"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class OwnableRelationshipNotSet < StandardError
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module NexusCqrs
|
|
3
|
+
module Auth
|
|
4
|
+
class PermissionProvider
|
|
5
|
+
def initialize(user_id, global_permissions)
|
|
6
|
+
@user_id = user_id
|
|
7
|
+
@global_permissions = parse_permissions_array(global_permissions)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Returns true if the current user has the requested permission on the requested entity (if passed), or globally
|
|
11
|
+
#
|
|
12
|
+
# @param [String] permission_key Permission key to check against
|
|
13
|
+
# @param [ApplicationRecord] permission_model Permission model
|
|
14
|
+
# @param [Integer] entity_id ID of the entity
|
|
15
|
+
# @return [Boolean] Returns true if the current user has this permission on this entity
|
|
16
|
+
# @example Check for permission
|
|
17
|
+
# permissions.has_permission?('collection:publish', CollectionPermissions, collection.id) #=> true
|
|
18
|
+
def has_permission?(permission_key, permission_model = nil, entity_id = nil)
|
|
19
|
+
return false if @user_id.nil?
|
|
20
|
+
|
|
21
|
+
return true if @global_permissions.include?(permission_key)
|
|
22
|
+
|
|
23
|
+
# check entity-specific permissions
|
|
24
|
+
unless permission_model.nil?
|
|
25
|
+
return true if permission_model.where(permission: permission_key, entity_id: entity_id,
|
|
26
|
+
user_id: @user_id.id).exists?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Retrieves a list of permissions assigned to a user for a specific entity
|
|
33
|
+
#
|
|
34
|
+
# @param [ApplicationRecord] permission_model Permission model
|
|
35
|
+
# @param [Integer] entity_id ID of the entity
|
|
36
|
+
# @return [Array] Returns an array of hashes representing permission keys, along with their global status
|
|
37
|
+
# @example Get a list of permissions
|
|
38
|
+
# permissions.for_user(CollectionPermissions, collection.id) #=>
|
|
39
|
+
# [
|
|
40
|
+
# {:global=>false, :key=>"collection:discard"},
|
|
41
|
+
# {:global=>false, :key=>"collection:publish"},
|
|
42
|
+
# {:global=>false, :key=>"collection:view_under_moderation"},
|
|
43
|
+
# {:global=>false, :key=>"collection:set_status"}
|
|
44
|
+
# ]
|
|
45
|
+
def for_user_on_entity(permission_model, entity_id)
|
|
46
|
+
return [] if @user_id.nil?
|
|
47
|
+
|
|
48
|
+
# retrieve entity-specific permissions from DB and map to hash
|
|
49
|
+
entity_permissions = permission_model.where(user_id: @user_id, entity_id: entity_id)
|
|
50
|
+
.map { |p| { global: false, key: p.permission } }
|
|
51
|
+
|
|
52
|
+
# Map global permissions to hash
|
|
53
|
+
global_permissions = @global_permissions.map { |p| { global: true, key: p } }
|
|
54
|
+
|
|
55
|
+
# Combine hashes and ensure global permissions take priority
|
|
56
|
+
(global_permissions + entity_permissions).uniq { |p| p[:key] }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def parse_permissions_array(permissions_array)
|
|
62
|
+
return [] if permissions_array.nil?
|
|
63
|
+
|
|
64
|
+
permissions = []
|
|
65
|
+
|
|
66
|
+
permissions_array.each do |entity, action_array|
|
|
67
|
+
action_array.each do |action|
|
|
68
|
+
permissions << entity + ":" + action
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
permissions
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module NexusCqrs
|
|
3
|
+
module Auth
|
|
4
|
+
# Class used to provide additional context into pundit. This enables us to not only pass the user model, but also
|
|
5
|
+
# the global permissions for that user - as those are pulled from the user's request, not the model.
|
|
6
|
+
class UserContext
|
|
7
|
+
attr_reader :user, :global_permissions
|
|
8
|
+
|
|
9
|
+
def initialize(user, global_permissions)
|
|
10
|
+
@user = user
|
|
11
|
+
@global_permissions = global_permissions
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module NexusCqrs
|
|
3
|
+
# Base class for all command handlers. Should always declare a `call` method for invoking the handler.
|
|
4
|
+
#
|
|
5
|
+
# @since 0.1.0
|
|
2
6
|
class BaseCommandHandler
|
|
7
|
+
include NexusCqrs::Auth
|
|
8
|
+
|
|
9
|
+
# Method for invoking this handler - all command logic should be contained in this method.
|
|
10
|
+
#
|
|
11
|
+
# @param [NexusCqrs::BaseCommand] command Command to be executed by this handler
|
|
3
12
|
def call(command)
|
|
4
13
|
end
|
|
5
14
|
end
|
|
@@ -1,26 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module NexusCqrs
|
|
3
|
+
# All messages passed through the message bus will extend this class. Commands and Queries are both extended types of
|
|
4
|
+
# `BaseMessage`. This is mainly used to store metadata for Commands/Queries (such as the user context), as well
|
|
5
|
+
# as helper methods for retrieving the policy method for this Command/Query/Message
|
|
6
|
+
#
|
|
7
|
+
# @since 0.1.0
|
|
2
8
|
class BaseMessage
|
|
9
|
+
# Sets metadata on this message.
|
|
10
|
+
#
|
|
11
|
+
# @param [Symbol] key Metadata key
|
|
12
|
+
# @param [Object] value Metadata value
|
|
13
|
+
# @example Set the current user on a command/query
|
|
14
|
+
# command.set_metadata(:current_user, user)
|
|
15
|
+
# @since 0.1.0
|
|
3
16
|
def set_metadata(key, value)
|
|
4
17
|
@metadata = {} unless @metadata
|
|
5
18
|
@metadata[key.to_sym] = value
|
|
6
19
|
end
|
|
7
20
|
|
|
21
|
+
# Getter for retrieving the metadata on this message
|
|
22
|
+
#
|
|
23
|
+
# @return [Hash] Any metadata attached to this message
|
|
24
|
+
# @since 0.1.0
|
|
8
25
|
def metadata
|
|
9
26
|
@metadata || {}
|
|
10
27
|
end
|
|
11
28
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
# Helper method for retrieving the name of the class used to define the auth policy for this message
|
|
30
|
+
#
|
|
31
|
+
# @example Get the policy_class on a `Commands::Mods::DeleteMod` command
|
|
32
|
+
# command.policy_class #=> "DeleteModPolicy"
|
|
33
|
+
#
|
|
34
|
+
# @return [String] Name of the policy class
|
|
35
|
+
# @deprecated This used to be used to authorise policies before they hit the command bus - requiring a new policy
|
|
36
|
+
# for every message. E.g. `DeleteModPolicy`, `UpdateModPolicy`, `CreateModPolicy` etc. It is now recommended to
|
|
37
|
+
# simple call `authorise` within the handler as it is far more flexible
|
|
38
|
+
# @since 0.1.0
|
|
20
39
|
def policy_class
|
|
21
40
|
demodularised_class_name + 'Policy'
|
|
22
41
|
end
|
|
23
42
|
|
|
43
|
+
# Helper method for retrieving the demodularised name of the class used to define the auth policy for this message
|
|
44
|
+
#
|
|
45
|
+
# @example Get the demodularised_class_name on a `Commands::Mods::DeleteMod` command
|
|
46
|
+
# command.policy_class #=> "DeleteMod"
|
|
47
|
+
#
|
|
48
|
+
# @return [String] Name of the policy class
|
|
49
|
+
# @since 0.1.0
|
|
24
50
|
def demodularised_class_name
|
|
25
51
|
self.class.name.split('::').last
|
|
26
52
|
end
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module NexusCqrs
|
|
3
|
+
# Base middleware class to allow custom middleware to be injected into the command bus
|
|
4
|
+
#
|
|
5
|
+
# @abstract
|
|
6
|
+
# @since 0.1.0
|
|
2
7
|
class BaseMiddleware
|
|
3
8
|
def initialize(next_)
|
|
4
9
|
@next = next_
|
|
5
10
|
end
|
|
6
11
|
|
|
7
|
-
#
|
|
12
|
+
# Invoke middleware
|
|
13
|
+
#
|
|
14
|
+
# @param [BaseMessage] message Message object being passed to the bus
|
|
8
15
|
def call(message)
|
|
9
16
|
end
|
|
10
17
|
end
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module NexusCqrs
|
|
3
|
+
# Base class for all query handlers. Should always declare a `call` method for invoking the handler.
|
|
4
|
+
#
|
|
5
|
+
# @since 0.1.0
|
|
2
6
|
class BaseQueryHandler
|
|
7
|
+
include NexusCqrs::Auth
|
|
8
|
+
|
|
9
|
+
# Method for invoking this handler - all query logic should be contained in this method.
|
|
10
|
+
#
|
|
11
|
+
# @param [NexusCqrs::BaseQuery] query Query to be executed by this handler
|
|
3
12
|
def call(query)
|
|
4
13
|
end
|
|
5
14
|
end
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'thread_safe'
|
|
2
3
|
require 'middleware'
|
|
3
4
|
|
|
4
5
|
module NexusCqrs
|
|
6
|
+
# The command bus is responsible for registering and invoking the handlers. It stores a simple list of all registered
|
|
7
|
+
# commands, along with their handlers. When a message is passed to `call`, the middleware is triggered for the message
|
|
8
|
+
# then the handler is invoked.
|
|
9
|
+
#
|
|
10
|
+
# @since 0.1.0
|
|
5
11
|
class CommandBus
|
|
6
12
|
UnregisteredHandler = Class.new(StandardError)
|
|
7
13
|
MultipleHandlers = Class.new(StandardError)
|
|
@@ -11,23 +17,44 @@ module NexusCqrs
|
|
|
11
17
|
@middleware = middleware || Middleware::Builder.new
|
|
12
18
|
end
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
# Registers a handler for a command.
|
|
21
|
+
#
|
|
22
|
+
# @param [NexusCqrs::BaseMessage] message Message to register a handler for
|
|
23
|
+
# @param [NexusCqrs::BaseCommandHandler] handler CommandHandler/QueryHandler to be invoked for this message
|
|
24
|
+
# @raise [MultipleHandlers] If the message already has a handler registered, an error will be raised
|
|
25
|
+
#
|
|
26
|
+
# @since 0.1.0
|
|
27
|
+
def register(message, handler)
|
|
28
|
+
raise MultipleHandlers, "Multiple handlers not allowed for #{message}" if handlers[message]
|
|
16
29
|
|
|
17
|
-
handlers[
|
|
30
|
+
handlers[message] = handler
|
|
18
31
|
end
|
|
19
32
|
|
|
20
|
-
|
|
33
|
+
# Invokes a handler from a message
|
|
34
|
+
#
|
|
35
|
+
# @param [NexusCqrs::BaseMessage] message Message used to determine which handler to invoke
|
|
36
|
+
#
|
|
37
|
+
# @since 0.1.0
|
|
38
|
+
def call(message)
|
|
21
39
|
runner = Middleware::Builder.new
|
|
22
40
|
runner.use(@middleware)
|
|
23
|
-
runner.use(->(
|
|
24
|
-
runner.call(
|
|
41
|
+
runner.use(->(message_) { handler_for_command(message_).call(message_) })
|
|
42
|
+
runner.call(message)
|
|
25
43
|
end
|
|
26
44
|
|
|
45
|
+
# Return a list of registered handlers
|
|
46
|
+
#
|
|
47
|
+
# @return [ThreadSafe::Cache] ThreadSafe::Cache object containing a list of all the registered handlers
|
|
48
|
+
#
|
|
49
|
+
# @since 0.1.0
|
|
27
50
|
def registered_handlers
|
|
28
51
|
handlers
|
|
29
52
|
end
|
|
30
53
|
|
|
54
|
+
# Removes all registered handlers from the bus. This can be useful in tests - as we don't want state to persist
|
|
55
|
+
# across handlers
|
|
56
|
+
#
|
|
57
|
+
# @since 0.1.0
|
|
31
58
|
def clear_handlers
|
|
32
59
|
@handlers = ThreadSafe::Cache.new
|
|
33
60
|
end
|
|
@@ -36,6 +63,9 @@ module NexusCqrs
|
|
|
36
63
|
|
|
37
64
|
private
|
|
38
65
|
|
|
66
|
+
# Looks up the handler for a specific command
|
|
67
|
+
#
|
|
68
|
+
# @since 0.1.0
|
|
39
69
|
def handler_for_command(command)
|
|
40
70
|
unregistered_handler = proc { raise UnregisteredHandler, "Missing handler for #{command.class}" }
|
|
41
71
|
handlers.fetch(command.class, &unregistered_handler)
|
|
@@ -1,20 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module NexusCqrs
|
|
2
4
|
class CommandExecutor
|
|
3
5
|
# @param [NexusCqrs::CommandBus] command_bus
|
|
4
6
|
def initialize(command_bus)
|
|
5
7
|
@bus = command_bus
|
|
8
|
+
@logger = logger
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Get logger instance depending on runtime environment.
|
|
12
|
+
def logger
|
|
13
|
+
defined?(Rails) && defined?(Rails.logger) ? Rails.logger : Logger.new(STDOUT)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def autoregister(base_class, dir = 'app/domain/commands', ignore_strings = ['.rb', 'app/domain/'])
|
|
17
|
+
if defined?(Rails)
|
|
18
|
+
# Iterate over the directory passed and find all ruby files.
|
|
19
|
+
Dir["#{dir}/**/*.rb"].each do |file|
|
|
20
|
+
# Constantize class name to constant to force rails to autoload this class.
|
|
21
|
+
# Note that autoloading won't work when testing this gem standalone, but this does trigger the necessary
|
|
22
|
+
# loading when in a rails environment.
|
|
23
|
+
ignore_strings.each do |i|
|
|
24
|
+
file.slice!(i)
|
|
25
|
+
end
|
|
26
|
+
@logger.debug { "Attempting constantize of #{file}" }
|
|
27
|
+
file.camelize.constantize
|
|
28
|
+
rescue NameError => e
|
|
29
|
+
@logger.warn { "Failed autoregister #{file.camelize}, received NameError: #{e}" }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
ObjectSpace.each_object(Class).select { |klass| klass < base_class }.each do |c|
|
|
34
|
+
@logger.debug { "Attempting auto registration of #{c}" }
|
|
35
|
+
handler_name = (c.to_s + "Handler")
|
|
36
|
+
begin
|
|
37
|
+
handler = handler_name.constantize.new
|
|
38
|
+
rescue NameError
|
|
39
|
+
@logger.warn { "Command `#{c}` tried to autoregister `#{handler_name}` but the class was not found" }
|
|
40
|
+
next
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if handler.respond_to?(:call)
|
|
44
|
+
register_command(c, handler)
|
|
45
|
+
else
|
|
46
|
+
@logger.warn { "Command `#{c}` tried to autoregister `#{handler_name}` but `call` method did not exist" }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns the bus used by this executor
|
|
52
|
+
#
|
|
53
|
+
# @return [NexusCqrs::CommandBus] command_bus
|
|
54
|
+
def command_bus
|
|
55
|
+
@bus
|
|
6
56
|
end
|
|
7
57
|
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
58
|
+
# Executes a specific handler on the bus, passing in the provided message (Query/Command)
|
|
59
|
+
#
|
|
60
|
+
# @param [NexusCqrs::BaseMessage] message
|
|
61
|
+
def execute(message)
|
|
62
|
+
@bus.call(message)
|
|
11
63
|
end
|
|
12
64
|
|
|
65
|
+
# Helper method for registering a handler on the bus
|
|
66
|
+
#
|
|
67
|
+
# @see CommandBus#register
|
|
68
|
+
# @param [NexusCqrs::BaseMessage] message
|
|
13
69
|
# @param [NexusCqrs::BaseCommandHandler] handler
|
|
14
|
-
def register_command(
|
|
15
|
-
@bus.register(
|
|
70
|
+
def register_command(message, handler)
|
|
71
|
+
@bus.register(message, handler)
|
|
16
72
|
end
|
|
17
73
|
|
|
74
|
+
# Clears all handlers from the bus and invokes the block passed to `configure_handlers` to re-register all the
|
|
75
|
+
# handlers again. This can be useful in tests - as we don't want state to persist across handlers
|
|
18
76
|
def reregister_handlers
|
|
19
77
|
return if @register_handlers.nil?
|
|
20
78
|
|
|
@@ -22,6 +80,18 @@ module NexusCqrs
|
|
|
22
80
|
@register_handlers.call(self)
|
|
23
81
|
end
|
|
24
82
|
|
|
83
|
+
# Configuration method for allowing handlers to be registered in bulk
|
|
84
|
+
#
|
|
85
|
+
# @see CommandBus#register
|
|
86
|
+
# @param [Proc] block Block to execute to register handlers
|
|
87
|
+
# @example Registering all queries
|
|
88
|
+
# executor.configure_handlers do |executor|
|
|
89
|
+
# executor.autoregister(NexusCqrs::BaseQuery, 'app/domain/queries')
|
|
90
|
+
# end
|
|
91
|
+
# @example Registering a query manually
|
|
92
|
+
# executor.configure_handlers do |executor|
|
|
93
|
+
# executor.register_command(NexusCqrs::BaseQuery, NexusCqrs::BaseQueryHandler)
|
|
94
|
+
# end
|
|
25
95
|
def configure_handlers(&block)
|
|
26
96
|
@register_handlers = block
|
|
27
97
|
@register_handlers.call(self)
|
data/lib/nexus_cqrs/helpers.rb
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module NexusCqrs
|
|
3
|
+
# Simple module to inject execution methods into a class. Used to execute commands and queries from controllers and/or
|
|
4
|
+
# graphql resolvers.
|
|
2
5
|
module Helpers
|
|
3
6
|
# Executes a CQRS Command
|
|
7
|
+
#
|
|
8
|
+
# @param [NexusCqrs::BaseCommand] command Command to execute
|
|
9
|
+
# @example Execute a command
|
|
10
|
+
# execute(DeleteMod.new(mod.id))
|
|
4
11
|
def execute(command)
|
|
5
12
|
command_executor.execute(command)
|
|
6
13
|
end
|
|
7
14
|
|
|
8
15
|
# Executes a CQRS Query
|
|
16
|
+
#
|
|
17
|
+
# @param [NexusCqrs::BaseQuery] query Query to execute
|
|
18
|
+
# @example Execute a query
|
|
19
|
+
# execute(GetMod.new(mod.id))
|
|
9
20
|
def query(query)
|
|
10
21
|
query_executor.execute(query)
|
|
11
22
|
end
|
|
12
23
|
|
|
13
|
-
# Provide access to the CQRS executor
|
|
24
|
+
# Provide access to the CQRS command executor
|
|
25
|
+
#
|
|
26
|
+
# @return [NexusCqrs::CommandExecutor] Returns the executor used for commands
|
|
14
27
|
def command_executor
|
|
15
28
|
@command_executor ||= $COMMAND_EXECUTOR
|
|
16
29
|
end
|
|
17
30
|
|
|
31
|
+
# Provide access to the CQRS query executor
|
|
32
|
+
#
|
|
33
|
+
# @return [NexusCqrs::CommandExecutor] Returns the executor used for queries
|
|
18
34
|
def query_executor
|
|
19
35
|
@query_executor ||= $QUERY_EXECUTOR
|
|
20
36
|
end
|
data/lib/nexus_cqrs/version.rb
CHANGED
data/lib/nexus_cqrs.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'nexus_cqrs/base_message'
|
|
2
3
|
require 'nexus_cqrs/base_middleware'
|
|
4
|
+
require 'nexus_cqrs/auth/auth'
|
|
5
|
+
require 'nexus_cqrs/auth/user_context'
|
|
6
|
+
require 'nexus_cqrs/auth/permission_provider'
|
|
7
|
+
require 'nexus_cqrs/auth/ownable'
|
|
3
8
|
require 'nexus_cqrs/base_command'
|
|
4
9
|
require 'nexus_cqrs/base_command_handler'
|
|
5
10
|
require 'nexus_cqrs/base_query'
|
data/nexus_cqrs.gemspec
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require_relative 'lib/nexus_cqrs/version'
|
|
2
3
|
|
|
3
4
|
Gem::Specification.new do |spec|
|
|
@@ -20,4 +21,6 @@ Gem::Specification.new do |spec|
|
|
|
20
21
|
spec.add_dependency('generator_spec')
|
|
21
22
|
spec.add_dependency('thread_safe')
|
|
22
23
|
spec.add_dependency('ibsciss-middleware')
|
|
24
|
+
spec.add_dependency('pundit')
|
|
25
|
+
spec.add_dependency('strings-case')
|
|
23
26
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nexus_cqrs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dean Lovett
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2021-
|
|
11
|
+
date: 2021-11-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: generator_spec
|
|
@@ -52,6 +52,34 @@ dependencies:
|
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: pundit
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: strings-case
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
55
83
|
description:
|
|
56
84
|
email:
|
|
57
85
|
- dean.lovett@nexusmods.com
|
|
@@ -79,6 +107,10 @@ files:
|
|
|
79
107
|
- lib/generators/nexus_cqrs/templates/query_handler.rb
|
|
80
108
|
- lib/generators/nexus_cqrs/templates/register_cqrs_handlers.rb
|
|
81
109
|
- lib/nexus_cqrs.rb
|
|
110
|
+
- lib/nexus_cqrs/auth/auth.rb
|
|
111
|
+
- lib/nexus_cqrs/auth/ownable.rb
|
|
112
|
+
- lib/nexus_cqrs/auth/permission_provider.rb
|
|
113
|
+
- lib/nexus_cqrs/auth/user_context.rb
|
|
82
114
|
- lib/nexus_cqrs/base_command.rb
|
|
83
115
|
- lib/nexus_cqrs/base_command_handler.rb
|
|
84
116
|
- lib/nexus_cqrs/base_message.rb
|
|
@@ -108,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
108
140
|
- !ruby/object:Gem::Version
|
|
109
141
|
version: '0'
|
|
110
142
|
requirements: []
|
|
111
|
-
rubygems_version: 3.2.
|
|
143
|
+
rubygems_version: 3.2.31
|
|
112
144
|
signing_key:
|
|
113
145
|
specification_version: 4
|
|
114
146
|
summary: Core package for the nexus cqrs gem
|