EctoMiddleware.Utils (ecto_middleware v2.1.0)

View Source

Utility functions and guards for working with middleware operations.

Guards for Operation Detection

Use guards in middleware to detect operation types, especially useful for insert_or_update which can be either an insert or update:

defmodule SmartTimestamp do
  use EctoMiddleware

  def process_before(changeset, resolution) do
    changeset = cond do
      is_insert(changeset, resolution.action) ->
        changeset
        |> put_change(:created_at, DateTime.utc_now())
        |> put_change(:updated_at, DateTime.utc_now())

      is_update(changeset, resolution.action) ->
        put_change(changeset, :updated_at, DateTime.utc_now())

      true ->
        changeset
    end

    {:cont, changeset}
  end
end

Available Guards

  • is_read/2 - Matches read operations (get, get!, all, etc.)
  • is_write/2 - Matches write operations (insert, update, delete, bulk, etc.)
  • is_insert/2 - Matches single-record inserts (including insert_or_update with new records)
  • is_update/2 - Matches single-record updates (including insert_or_update with existing records)
  • is_delete/2 - Matches single-record delete operations
  • is_preload/2 - Matches preload operations
  • is_bulk_action/2 - Matches bulk operations (insert_all, update_all, delete_all)

How insert_or_update Detection Works

For insert_or_update actions, the guards inspect the changeset's __meta__.state:

# New record (insert)
changeset.data.__meta__.state == :built

# Existing record (update)
changeset.data.__meta__.state == :loaded

Result Transformation

Use apply/3 to transform Ecto results while preserving their structure:

defmodule EnrichUsers do
  use EctoMiddleware

  def process_after(result, resolution) do
    enriched = Utils.apply(result, resolution, fn user ->
      %{user | full_name: "#{user.first_name} #{user.last_name}"}
    end)

    {:cont, enriched}
  end
end

apply/3 handles all common Ecto return types:

  • {:ok, value} → {:ok, transformed}
  • {:error, reason} → unchanged
  • [values] → [transformed values]
  • nil → unchanged
  • {count, list} → {count, [transformed values]}
  • Bare values → transformed

See apply/3 documentation for complete examples.

Summary

Functions

Applies a function to the "inner" value of an Ecto result, handling all common return types.

Guard that matches bulk (batch) actions.

Guard that matches delete actions.

Guard that matches insert actions.

Guard that matches the preload action.

Guard that matches read actions.

Guard that matches update actions.

Guard that matches write actions (anything not a read).

Functions

apply(result, context, fun)

@spec apply(result :: term(), context :: struct(), fun :: (term() -> term())) ::
  term()

Applies a function to the "inner" value of an Ecto result, handling all common return types.

The second argument is reserved for future use and currently ignored. You can pass the resolution struct or any placeholder value.

Return Type Handling

InputOutput
{:ok, value}{:ok, fun.(value)}
{:error, reason}{:error, reason} (unchanged)
[values]Enum.map(values, fun)
nilnil (unchanged)
{count, list}{count, Enum.map(list, fun)}
{count, nil}{count, nil} (unchanged)
bare valuefun.(value)

Examples

# In process_after/2
def process_after(result, resolution) do
  {:cont, Utils.apply(result, resolution, &Map.put(&1, :enriched, true))}
end

# {:ok, struct} -> {:ok, enriched_struct}
Utils.apply({:ok, user}, resolution, &Map.put(&1, :enriched, true))
#=> {:ok, %User{enriched: true, ...}}

# {:error, changeset} -> {:error, changeset} (unchanged)
Utils.apply({:error, changeset}, resolution, &Map.put(&1, :enriched, true))
#=> {:error, changeset}

# [structs] -> [enriched_structs]
Utils.apply([user1, user2], resolution, &Map.put(&1, :enriched, true))
#=> [%User{enriched: true}, %User{enriched: true}]

# nil -> nil
Utils.apply(nil, resolution, &Map.put(&1, :enriched, true))
#=> nil

# {count, list} from update_all/delete_all
Utils.apply({3, [user1, user2, user3]}, resolution, &Map.put(&1, :processed, true))
#=> {3, [%User{processed: true}, ...]}

is_bulk_action(resource, action)

(macro)

Guard that matches bulk (batch) actions.

Matches the action atoms for c:Ecto.Repo.insert_all/3, c:Ecto.Repo.update_all/3, and c:Ecto.Repo.delete_all/2.

Bulk actions are not matched by is_insert/2, is_update/2, or is_delete/2 — those guards are scoped to single-record changeset operations. Use this guard inside a middleware that opted into bulk operations (via use EctoMiddleware, bulk_operations: true) to branch on the differing resource shape: insert_all receives the schema/source (first argument), while update_all/delete_all receive an Ecto.Queryable (rows/updates are available in resolution.args).

def process_before(resource, %{action: action}) when is_bulk_action(resource, action) do
  {:cont, handle_bulk(resource)}
end

def process_before(changeset, _resolution) do
  {:cont, handle_single(changeset)}
end

is_delete(changeset, action)

(macro)

Guard that matches delete actions.

Matches the action atoms for c:Ecto.Repo.delete/2 and c:Ecto.Repo.delete!/2.

is_insert(changeset, action)

(macro)

Guard that matches insert actions.

Matches the action atoms for c:Ecto.Repo.insert/2 and c:Ecto.Repo.insert!/2.

Also matches c:Ecto.Repo.insert_or_update/2 and c:Ecto.Repo.insert_or_update!/2 when the record is new (changeset's __meta__.state is :built).

is_preload(changeset, action)

(macro)

Guard that matches the preload action.

Matches the action atom for c:Ecto.Repo.preload/3.

is_read(changeset, action)

(macro)

Guard that matches read actions.

Matches the action atoms for the following Ecto.Repo callbacks: c:Ecto.Repo.get/3, c:Ecto.Repo.get!/3, c:Ecto.Repo.get_by/3, c:Ecto.Repo.get_by!/3, c:Ecto.Repo.one/2, c:Ecto.Repo.one!/2, c:Ecto.Repo.all/2, c:Ecto.Repo.reload/2, c:Ecto.Repo.reload!/2, c:Ecto.Repo.preload/3

is_update(changeset, action)

(macro)

Guard that matches update actions.

Matches the action atoms for c:Ecto.Repo.update/2 and c:Ecto.Repo.update!/2.

Also matches c:Ecto.Repo.insert_or_update/2 and c:Ecto.Repo.insert_or_update!/2 when the record already exists (changeset's __meta__.state is :loaded).

is_write(changeset, action)

(macro)

Guard that matches write actions (anything not a read).

Matches the action atoms for the following Ecto.Repo callbacks: c:Ecto.Repo.insert/2, c:Ecto.Repo.insert!/2, c:Ecto.Repo.update/2, c:Ecto.Repo.update!/2, c:Ecto.Repo.delete/2, c:Ecto.Repo.delete!/2, c:Ecto.Repo.insert_or_update/2, c:Ecto.Repo.insert_or_update!/2, c:Ecto.Repo.insert_all/3, c:Ecto.Repo.update_all/3, c:Ecto.Repo.delete_all/2