Getting started
Introduction
A Commanded middleware for checking certain values uniqueness during commands dispatch. Might be useful as a short-term unique values cache before subsequent events persisted and projected.
Based on the Ben Smith's idea described in his "Building Conduit" book.
Installation
Add commanded_uniqueness_middleware to your list of dependencies in mix.exs:
def deps do
[
{:commanded_uniqueness_middleware, "~> 0.6.0"}
]
endConfiguration
Define options in config/config.exs as:
config :commanded_uniqueness_middleware,
adapter: Commanded.Middleware.Uniqueness.Adapter.Cachex,
# ttl: 60 minutes in seconds
ttl: 60 * 60,
use_command_as_partition: falsewhere:
:adapteris an Uniqueness adapter implementedCommanded.Middleware.Uniqueness.Adapterbehavior,:ttlis claimed value time-to-live,:use_command_as_partitionshould be set to true to use each command module name as partition. Use with caution! If neither this nor Unique protocol:partitionoption defined thenCommanded.Middleware.Uniquenessvalue used as a partition name.
Adapters
As of now the only adapter exists is a Cachex based one.
Any adapter implementing Commanded.Middleware.Uniqueness.Adapter behavior can be used.
Usage
Imagine you have an aggregate with a unique field value requirement, for example, it might be a :username field. You've got a new user and issue a RegisterUser command with SomeCoolUsername :name field value. The command successfully went through all checks and spawn a UserRegistered event but this event haven't been projected yet. At this very moment an another user wants to register with the same name, and as the previous event isn't projected you have no information that this user name
has been taken.
You can use Commanded.Middleware.Uniqueness to ensure that your system will not get into a conflict state in between two commands.
It utilizes Elixir Protocol.
You need to put
middleware Commanded.Middleware.Uniquenessinto your Commanded Router as described in Commanded docs.
Then you need to define a Commanded.Middleware.Uniqueness.UniqueFields implementation for the specific command:
defmodule MyApp.RegisterUser do
defstruct [
:id,
:name,
:email
]
end
defimpl Commanded.Middleware.Uniqueness.UniqueFields, for: MyApp.RegisterUser do
def unique(%MyApp.RegisterUser{id: id}),
do: [
{:name, "has already been taken", id, ignore_case: true, is_unique: &is_taken_externally?/4}
]
def is_taken_externally?(_field, value, _owner, _opts), do: !String.starts_with?(value, "ExternallyTaken")
endAt the first command dispatch the Uniqueness Middleware checks the :name field key - value pair is
free and claims it for the given owner id.
If you need to release previously claimed value with existing TTL you should use release/4, release_by_owner/3 or release_by_value/3 adapter methods:
defmodule MyApp.UserNameCacheHandler do
use Commanded.Event.Handler,
application: MyApp.App,
name: "UserNameCacheHandler"
alias MyApp.UserDeleted
def handle(%UserDeleted{id: id}, _metadata) do
:ok = Commanded.Middleware.Uniqueness.release_by_owner(:name, id)
end
end
endTo get to know behavior you can check modules documentation and tests (especially commands described in test/support).