nexus_cqrs 0.3.1 → 0.4.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: 22851e4cf72bdb9d4895163003f9ccaef81182f71ba64de7b9729c80f06ea40b
4
- data.tar.gz: a39184dedf35d590ad9b7f3dab51facbd233fd5e9ec7ddeab27c714604c47018
3
+ metadata.gz: 1a8507c881ef6ee7b0d899e87139af7cc7e85d1a96e49aa73294f21f4d2f1421
4
+ data.tar.gz: f02d250460fe4ea0bb5d67a9f458a7f5390301992490db27bd0bc8cce8674919
5
5
  SHA512:
6
- metadata.gz: 5dd0e3cf9cb2014329a289bb94a9659144109988775709566996eb69aeaf663cc504ceb9cc83fb33b9e99fe2fc3e424c2584d0269d0b57564cecb148c1e0006c
7
- data.tar.gz: 92497f3e118eb6bfad46e842fc0713fb64a8785ac0251296215003449593515c445de351c0afe9ff5f30efb9e5df1e0c4cf72c9c5c486ec253293c0e1e3094d4
6
+ metadata.gz: 1017532021ac25aeae7003d8db46dbedda5207231afcc26dab93be66cd1ff2d965a3bb5a5915ccd0c52b420eff4706a483c05ff4075ae784d1295421d93f980b
7
+ data.tar.gz: ba0f68f56bf7e46fba9998cd94ed8f09c6536b2301408df3277a4825f395d5b4f857094043fceee152b0e4a3bffe50ed4186c0c470575487446ea2844e8bbeef
data/.rubocop.yml CHANGED
@@ -3,9 +3,14 @@ inherit_gem:
3
3
 
4
4
  AllCops:
5
5
  Exclude:
6
- - 'lib/generators/nexus_cqrs/templates/**/*.rb'
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.lock CHANGED
@@ -1,9 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nexus_cqrs (0.3.1)
4
+ nexus_cqrs (0.4.0)
5
5
  generator_spec
6
6
  ibsciss-middleware
7
+ pundit
8
+ strings-case
7
9
  thread_safe
8
10
 
9
11
  GEM
@@ -52,6 +54,8 @@ GEM
52
54
  parallel (1.21.0)
53
55
  parser (3.0.2.0)
54
56
  ast (~> 2.4.1)
57
+ pundit (2.1.1)
58
+ activesupport (>= 3.0.0)
55
59
  racc (1.6.0)
56
60
  rack (2.2.3)
57
61
  rack-test (1.1.0)
@@ -98,6 +102,7 @@ GEM
98
102
  rubocop-shopify (1.0.7)
99
103
  rubocop (~> 1.4)
100
104
  ruby-progressbar (1.11.0)
105
+ strings-case (0.3.0)
101
106
  thor (1.1.0)
102
107
  thread_safe (0.3.6)
103
108
  tzinfo (2.0.4)
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
- # Cqrs::Core
1
+ # Nexus CQRS
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/cqrs/core`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Delete this and the text above, and describe your gem
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
- ## Usage
26
+ ## Getting Started
24
27
 
25
- Generators can be used to aide in the creation of Commands and Queries:
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
- Once installed, a CommandBus is required to control the flow of Commands and/or Queries:
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
- When creating a bus, middleware can be attached like so:
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
- middleware_stack = Middleware::Builder.new do |b|
52
- b.use MyMiddleware
53
- end
93
+ command.set_metadata(:current_user, user)
94
+ execute(command)
95
+ ```
54
96
 
55
- command_bus = Bus.new(middleware: middleware_stack)
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 reubygems.
152
+ The release process is tied to the git tags. Simply creating a new tag and pushing will trigger a new release to rubygems.
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  Rails.configuration.to_prepare do
3
3
  middleware_stack = Middleware::Builder.new do |b|
4
- b.use(NexusCqrsAuth::AuthMiddleware)
5
4
  end
6
5
 
7
6
  command_bus = NexusCqrs::CommandBus.new(middleware: middleware_stack)
@@ -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,8 @@
1
1
  # frozen_string_literal: true
2
2
  module NexusCqrs
3
+ # Base class for all commands. Should declare value types for passing to handlers
4
+ #
5
+ # @since 0.1.0
3
6
  class BaseCommand < BaseMessage
4
7
  end
5
8
  end
@@ -1,6 +1,14 @@
1
1
  # frozen_string_literal: true
2
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
3
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
4
12
  def call(command)
5
13
  end
6
14
  end
@@ -1,27 +1,52 @@
1
1
  # frozen_string_literal: true
2
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
3
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
4
16
  def set_metadata(key, value)
5
17
  @metadata = {} unless @metadata
6
18
  @metadata[key.to_sym] = value
7
19
  end
8
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
9
25
  def metadata
10
26
  @metadata || {}
11
27
  end
12
28
 
13
- def to_h
14
- hash = instance_variables.each_with_object({}) do |var, acc|
15
- acc[var.to_s[1..-1].to_sym] = instance_variable_get(var)
16
- end
17
- hash[:metadata] = @metadata || {}
18
- hash
19
- end
20
-
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
21
39
  def policy_class
22
40
  demodularised_class_name + 'Policy'
23
41
  end
24
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
25
50
  def demodularised_class_name
26
51
  self.class.name.split('::').last
27
52
  end
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
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
3
7
  class BaseMiddleware
4
8
  def initialize(next_)
5
9
  @next = next_
6
10
  end
7
11
 
8
- # @param [BaseMessage] message
12
+ # Invoke middleware
13
+ #
14
+ # @param [BaseMessage] message Message object being passed to the bus
9
15
  def call(message)
10
16
  end
11
17
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  module NexusCqrs
3
+ # Base class for all queries. Should declare value types for passing to handlers
4
+ #
5
+ # @since 0.1.0
3
6
  class BaseQuery < BaseMessage
4
7
  end
5
8
  end
@@ -1,6 +1,14 @@
1
1
  # frozen_string_literal: true
2
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
3
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
4
12
  def call(query)
5
13
  end
6
14
  end
@@ -3,6 +3,11 @@ require 'thread_safe'
3
3
  require 'middleware'
4
4
 
5
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
6
11
  class CommandBus
7
12
  UnregisteredHandler = Class.new(StandardError)
8
13
  MultipleHandlers = Class.new(StandardError)
@@ -12,23 +17,44 @@ module NexusCqrs
12
17
  @middleware = middleware || Middleware::Builder.new
13
18
  end
14
19
 
15
- def register(klass, handler)
16
- raise MultipleHandlers, "Multiple handlers not allowed for #{klass}" if handlers[klass]
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]
17
29
 
18
- handlers[klass] = handler
30
+ handlers[message] = handler
19
31
  end
20
32
 
21
- def call(command)
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)
22
39
  runner = Middleware::Builder.new
23
40
  runner.use(@middleware)
24
- runner.use(->(command_) { handler_for_command(command_).call(command_) })
25
- runner.call(command)
41
+ runner.use(->(message_) { handler_for_command(message_).call(message_) })
42
+ runner.call(message)
26
43
  end
27
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
28
50
  def registered_handlers
29
51
  handlers
30
52
  end
31
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
32
58
  def clear_handlers
33
59
  @handlers = ThreadSafe::Cache.new
34
60
  end
@@ -37,6 +63,9 @@ module NexusCqrs
37
63
 
38
64
  private
39
65
 
66
+ # Looks up the handler for a specific command
67
+ #
68
+ # @since 0.1.0
40
69
  def handler_for_command(command)
41
70
  unregistered_handler = proc { raise UnregisteredHandler, "Missing handler for #{command.class}" }
42
71
  handlers.fetch(command.class, &unregistered_handler)
@@ -48,16 +48,24 @@ module NexusCqrs
48
48
  @bus
49
49
  end
50
50
 
51
- # @param [NexusCqrs::BaseCommand] command
52
- def execute(command)
53
- @bus.call(command)
51
+ # Executes a specific handler on the bus, passing in the provided message (Query/Command)
52
+ #
53
+ # @param [NexusCqrs::BaseMessage] message
54
+ def execute(message)
55
+ @bus.call(message)
54
56
  end
55
57
 
58
+ # Helper method for registering a handler on the bus
59
+ #
60
+ # @see CommandBus#register
61
+ # @param [NexusCqrs::BaseMessage] message
56
62
  # @param [NexusCqrs::BaseCommandHandler] handler
57
- def register_command(klass, handler)
58
- @bus.register(klass, handler)
63
+ def register_command(message, handler)
64
+ @bus.register(message, handler)
59
65
  end
60
66
 
67
+ # Clears all handlers from the bus and invokes the block passed to `configure_handlers` to re-register all the
68
+ # handlers again. This can be useful in tests - as we don't want state to persist across handlers
61
69
  def reregister_handlers
62
70
  return if @register_handlers.nil?
63
71
 
@@ -65,6 +73,18 @@ module NexusCqrs
65
73
  @register_handlers.call(self)
66
74
  end
67
75
 
76
+ # Configuration method for allowing handlers to be registered in bulk
77
+ #
78
+ # @see CommandBus#register
79
+ # @param [Proc] block Block to execute to register handlers
80
+ # @example Registering all queries
81
+ # executor.configure_handlers do |executor|
82
+ # executor.autoregister(NexusCqrs::BaseQuery, 'app/domain/queries')
83
+ # end
84
+ # @example Registering a query manually
85
+ # executor.configure_handlers do |executor|
86
+ # executor.register_command(NexusCqrs::BaseQuery, NexusCqrs::BaseQueryHandler)
87
+ # end
68
88
  def configure_handlers(&block)
69
89
  @register_handlers = block
70
90
  @register_handlers.call(self)
@@ -1,21 +1,36 @@
1
1
  # frozen_string_literal: true
2
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.
3
5
  module Helpers
4
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))
5
11
  def execute(command)
6
12
  command_executor.execute(command)
7
13
  end
8
14
 
9
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))
10
20
  def query(query)
11
21
  query_executor.execute(query)
12
22
  end
13
23
 
14
- # 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
15
27
  def command_executor
16
28
  @command_executor ||= $COMMAND_EXECUTOR
17
29
  end
18
30
 
31
+ # Provide access to the CQRS query executor
32
+ #
33
+ # @return [NexusCqrs::CommandExecutor] Returns the executor used for queries
19
34
  def query_executor
20
35
  @query_executor ||= $QUERY_EXECUTOR
21
36
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module NexusCqrs
3
- VERSION = '0.3.1'
3
+ VERSION = '0.4.0'
4
4
  end
data/lib/nexus_cqrs.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
  require 'nexus_cqrs/base_message'
3
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'
4
8
  require 'nexus_cqrs/base_command'
5
9
  require 'nexus_cqrs/base_command_handler'
6
10
  require 'nexus_cqrs/base_query'
data/nexus_cqrs.gemspec CHANGED
@@ -21,4 +21,6 @@ Gem::Specification.new do |spec|
21
21
  spec.add_dependency('generator_spec')
22
22
  spec.add_dependency('thread_safe')
23
23
  spec.add_dependency('ibsciss-middleware')
24
+ spec.add_dependency('pundit')
25
+ spec.add_dependency('strings-case')
24
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.3.1
4
+ version: 0.4.0
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-11 00:00:00.000000000 Z
11
+ date: 2021-11-13 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