Nexus CQRS

Built by the NexusMods team for providing a universal way of organising CQRS logic (Command and Queries) and permission management. Used by most of NexusMod's rails applications.

The core concept of this gem is to provide a MessageBus that can invoked handlers registered to certain commands. This allows developers to separate business and application logic, whilst also seperating read operations from write operations (Queries and Commands).

Installation

Add this line to your application's Gemfile:

gem 'nexus_cqrs'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install nexus_cqrs

Getting Started

Generators

Generators can be used to aid in the creation of Commands and Queries:

rails g nexus_cqrs:command CommandName
rails g nexus_cqrs:query QueryName

Optionally, a command can be scaffolded with basic authorisation logic with --permission

rails g nexus_cqrs:command CommandName --permission
rails g nexus_cqrs:query QueryName --permission

Once a command has been created, two new files will be created under /app/domain/command or /app/domain/query depending on which generator was used. An initializer will also be created - register_cqrs_handlers.rb

Command Bus and Executor

In register_cqrs_handlers.rb, the command bus and executor will be created and used to register queries and commands:

middleware_stack = Middleware::Builder.new do |b|
  # Configure additional middleware for the CQRS stack here
end

command_bus = NexusCqrs::CommandBus.new(middleware: middleware_stack)
query_bus = NexusCqrs::CommandBus.new(middleware: middleware_stack)

$COMMAND_EXECUTOR = NexusCqrs::CommandExecutor.new(command_bus)
$QUERY_EXECUTOR = NexusCqrs::CommandExecutor.new(query_bus)

NOTE: By default, all classes extending BaseCommand and BaseQuery in the app/domain directory will be registered automatically.

Middleware

Middleware can be created by extending the base middleware and injecting into the middleware stack:

# my_middleware.rb
class MyMiddleware < NexusCqrs::BaseMiddleware
  def call(command)
    @next.call(command)
  end
end

# register_cqrs_handlers.rb
middleware_stack = Middleware::Builder.new do |b|
  b.use MyMiddleware
end

command_bus = Bus.new(middleware: middleware_stack)

The above middleware will pass responsibility for execution to the next responder in the chain and will be ran BEFORE the handler is invoked for every message executed through the CommandExecutor

For more information on writing middleware see: https://github.com/Ibsciss/ruby-middleware

Metadata

Commands/Queries can contain data, but data can also be injected into the message via metadata before the message is executed:

command.(:current_user, user)
execute(command)

Authorisation

There are various tools and helpers to aid with authorisation in this gem. Firstly, the system must be aware of the user that is calling the command, this can be done by providing the current user and global permissions as metadata:

message.(:current_user, current_user)
message.(:global_permissions, @access_token[:global_permissions])
execute(message)

Once the metadata is set, the handler can than access the metadata - which in turn can invoke the authorize method:

  class ModerateHandler < BaseCommandHandler
    include NexusCqrs::Helpers

    # @param [Commands::Moderate] command
    def call(command)
      mod = Mod.kept.find(command.mod_id)

      authorize(command, mod)
    ...

This will look up the correct Policy and automatically pass the metadata from the command, converting it into a UserContext object:

# Pull context variables from command
user = message.[:current_user]
global_permissions = message.[:global_permissions]

# Instantiate new policy class, with context
policy = policy_class.new(UserContext.new(user, global_permissions), record)

This will allow the policy class to access the PermissionProvider and retrieve any permission:

def moderate?
  permissions.has_permission?('mod:moderate', ModPermission, record.id)
end

Development

To contribute to this gem, simple pull the repository, run bundle install and run tests:

```shell script bundle exec rspec bundle exec rubocop


## Releasing

The release process is tied to the git tags. Simply creating a new tag and pushing will trigger a new release to rubygems.