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.set_metadata(: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.set_metadata(:current_user, current_user)
message.set_metadata(: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.metadata[:current_user]
global_permissions = message.metadata[: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:
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.