Skip to content

vheathen/commanded-uniqueness-middleware

Repository files navigation

Commanded.Middleware.Uniqueness

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.

Please check the latest published CommandedMiddlewareUniqueness release documentation on Hex.

Installation

Add commanded_uniqueness_middleware to your list of dependencies in mix.exs:

def deps do
  [
    {:commanded_uniqueness_middleware, "~> 0.7.0"}
  ]
end

Configuration

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: false

or

  config :commanded_uniqueness_middleware,
    adapter: Commanded.Middleware.Uniqueness.Adapter.Nebulex,
    nebulex_cache: YourApp.Nebulex.Uniqueness.Cache,
    # ttl: 60 minutes in seconds
    ttl: 60 * 60,
    use_command_as_partition: false

where:

  • :adapter is an Uniqueness adapter implemented Commanded.Middleware.Uniqueness.Adapter behavior,
  • :nebulex_cache is a module with Nebulex.Cache behaviour implemented in your application,
  • :ttl is claimed value time-to-live,
  • :use_command_as_partition should be set to true to use each command module name as partition. Use with caution! If neither this nor Unique protocol :partition option defined then Commanded.Middleware.Uniqueness value used as a partition name.

Adapters

Two adapters currently exist:

  • Based on Cachex Commanded.Middleware.Uniqueness.Adapter.Cachex
  • Based on Nebula Commanded.Middleware.Uniqueness.Adapter.Nebulex

Any adapter implementing Commanded.Middleware.Uniqueness.Adapter behavior can be used.

Nebulex

Nebulex adapter requires a custom Nebulex cache module. Please create it inside your application like:

  mix nbx.gen.cache -c YourApp.Nebulex.Uniqueness.Cache

You can configure Nebulex caching strategy in the your application config and/or in the cache module itself. For details please check Nebulex documentation

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 spawned a UserRegistered event but this event hasn'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.Uniqueness

into 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")
end

At 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
end

To get to know behavior you can check modules documentation and tests (especially commands described in test/support).

About

Commanded Uniqueness Middleware

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages