ExOperation v0.5.0 ExOperation View Source

A library for making domain operations wrapped in a single database transaction.

Example

An operation definition:

defmodule MyApp.Book.Update do
  use ExOperation, params: %{
    id!: :integer,
    title!: :string,
    author_id: :integer
  }

  def validate_params(changeset) do
    changeset
    |> Ecto.Changeset.validate_length(:title, min: 5)
  end

  def call(operation) do
    operation
    |> find(:book, schema: MyApp.Book, preload: [:author])
    |> find(:author, schema: MyApp.Author, id_path: [:author_id], optional: true)
    |> step(:result, &do_update(operation.context, &1, operation.params))
    |> after_commit(&send_notifcation(&1))
  end

  defp do_update(context, txn, params) do
    txn.book
    |> Ecto.Changeset.cast(params, [:title])
    |> Ecto.Changeset.put_assoc(:author, txn.author)
    |> Ecto.Changeset.put_assoc(:updated_by, context.current_user)
    |> MyApp.Repo.update()
  end

  defp send_notification(txn) do
    # …
    {:ok, txn}
  end
end

The call:

context = %{current_user: current_user}

with {:ok, %{result: book}} <- MyApp.Book.Update |> ExOperation.run(context, params) do
  # …
end

Features

  • Railway oriented domain logic pipeline.
  • Running all steps in a single database transaction. It uses Ecto.Multi inside.
  • Params casting & validation with Ecto.Changeset. Thanks to params library.
  • Convenient fetching of entitites from the database.
  • Context passing. Useful for passing current user, current locale etc.
  • Composable operations: one operation can call another through suboperation/3 function.
  • After commit hooks for scheduling asynchronous things.

Installation

Add the following to your mix.exs and then run mix deps.get:

def deps do
  [
    {:ex_operation, "~> 0.1.0"}
  ]
end

Add to your config/config.exs:

config :ex_operation,
  repo: MyApp.Repo

where MyApp.Repo is the name of your Ecto.Repo module.

Link to this section Summary

Functions

Call an operation from module. module must implement ExOperation.Operation behaviour

Link to this section Functions

Link to this function run(module, context \\ %{}, raw_params \\ %{}) View Source
run(module :: atom(), context :: map(), raw_params :: map()) ::
  {:ok, map()}
  | {:error, Ecto.Changeset.t()}
  | {:error, step_name :: any(), reason :: any(), txn :: map()}

Call an operation from module. module must implement ExOperation.Operation behaviour.

context is an arbitrary map for passing application-specific data such as current user.

raw_params is a map of parameters that will be casted and validated according to the operation’s params specification. Keys may be strings either atoms. Nesting is supported.

In case when all steps return {:ok, result} tuple and DB transaction successfully commited it returns {:ok, txn} tuple where txn is a map where keys are step names and values are their results.

In case of invalid raw_params it returns {:error, changeset} where changeset is an Ecto.Chageset struct containing validation errors.

In case of error in one of the steps or database error it returns {:error, name, reason, txn} tuple where name is the failed step name, reason is the error and txn is changes so far.

If any after commit callbacks are scheduled they get called after database transaction commit and before the return.