Box.UseCase behaviour (box v0.15.1)

View Source

Use cases are a way to perform mutation in the system through automatic transactioning. They can perform validation and side effects when the results is a success one.

Examples

The following use case is one that handles User creation from an admin panel.

  1. It expects an optiona authenticated_user to make sure that only admin users can actually create users
  2. It's gonna run the insertion in a transaction, if validation succeeded
  3. It's gonna broadcast a pub sub message (maybe to update a table in live) and send an email to the user, if the transaction succeeded
  4. Finally an {:ok, user} will be returned from this execution rather than the multi result.
defmodule Accounts.CreateUser do
  use Box.UseCase

  @impl Box.UseCase
  def validate(params, [authenticated_user: %User{role: "admin"}]) do
    {:ok, params}
  end

  def validate(_, _), do: {:error, :insufficient_permissions}

  @impl Box.UseCase
  def run(multi, params, _options) do
    Ecto.Multi.insert(:user, User.changeset(params))
  end

  @impl Box.UseCase
  def after_run(%{user: %User{id: user_id, email: email}}) do
    PubSub.broadcast("users", {:new_user, user_id})
    Mailer.send_welcome_email(email)
  end

  @impl Box.UseCase
  def return(%{user: %User{} = user}) do
    {:ok, user}
  end
end

Summary

Callbacks

This callback will run some side effects post-transactions. It receives the Ecto.Multi result as-is so a use can can do various thing from the step. Maybe broadcast a message to a post topic when a new comment is created or send an email to a newly created user. Return value is ignored.

This callback is involved right before returning. We'll use this callback to "simplify" the return value. This can be used to return only a main entity from a 5-6 steps multi result and even preload some relations.

This is where the actual transaction body happens. This receives an empty multi for the use to add new transaction step to it. It also receives the same options as the validate/2 callback.

Validates if a given use case can be run or not. This can receive options so you can ensure a user's presence, some sort of roles etc... If it returns an ̀{:ok, _} tuple, we'll move forward to the run/3 callback in order to build an ecto multi.

Types

input_option()

@type input_option() ::
  {:transaction, Keyword.t()}
  | {:run, run_function()}
  | {:after_run?, boolean()}

params()

@type params() :: any()

run_function()

@type run_function() :: (Ecto.Multi.t(), Keyword.t() ->
                     {:ok, any()}
                     | {:error, any()}
                     | {:error, any(), any(), any()})

Callbacks

after_run(params, t)

@callback after_run(params(), Keyword.t()) :: any()

This callback will run some side effects post-transactions. It receives the Ecto.Multi result as-is so a use can can do various thing from the step. Maybe broadcast a message to a post topic when a new comment is created or send an email to a newly created user. Return value is ignored.

return(params, t)

@callback return(params(), Keyword.t()) :: any()

This callback is involved right before returning. We'll use this callback to "simplify" the return value. This can be used to return only a main entity from a 5-6 steps multi result and even preload some relations.

run(t, params, t)

@callback run(Ecto.Multi.t(), params(), Keyword.t()) :: Ecto.Mutli.t()

This is where the actual transaction body happens. This receives an empty multi for the use to add new transaction step to it. It also receives the same options as the validate/2 callback.

validate(params, t)

@callback validate(params(), Keyword.t()) :: {:ok, params()} | :ignore | {:error, any()}

Validates if a given use case can be run or not. This can receive options so you can ensure a user's presence, some sort of roles etc... If it returns an ̀{:ok, _} tuple, we'll move forward to the run/3 callback in order to build an ecto multi.

Functions

execute(module, params, input_options)

@spec execute(module(), params(), [input_option()]) ::
  {:ok, any()} | :ignore | {:error, any()}

execute!(module, params, options)