EctoMiddleware.Utils (ecto_middleware v2.1.0)
View SourceUtility 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
endAvailable 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 operationsis_preload/2- Matches preload operationsis_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 == :loadedResult 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
endapply/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
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
| Input | Output |
|---|---|
{:ok, value} | {:ok, fun.(value)} |
{:error, reason} | {:error, reason} (unchanged) |
[values] | Enum.map(values, fun) |
nil | nil (unchanged) |
{count, list} | {count, Enum.map(list, fun)} |
{count, nil} | {count, nil} (unchanged) |
| bare value | fun.(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}, ...]}
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
Guard that matches delete actions.
Matches the action atoms for c:Ecto.Repo.delete/2 and c:Ecto.Repo.delete!/2.
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).
Guard that matches the preload action.
Matches the action atom for c:Ecto.Repo.preload/3.
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
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).
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