EctoMiddleware behaviour (ecto_middleware v2.0.0)
View SourceBehaviour for creating middleware that intercepts and transforms Ecto repository operations.
EctoMiddleware provides a composable way to add cross-cutting concerns to your Ecto operations using a middleware pipeline pattern, similar to Plug or Absinthe middleware.
Quick Example
defmodule NormalizeEmail do
use EctoMiddleware
@impl EctoMiddleware
def process_before(changeset, _resolution) do
case Ecto.Changeset.fetch_change(changeset, :email) do
{:ok, email} ->
{:cont, Ecto.Changeset.put_change(changeset, :email, String.downcase(email))}
:error ->
{:cont, changeset}
end
end
end
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :my_app
use EctoMiddleware.Repo
@impl EctoMiddleware.Repo
def middleware(action, resource) when is_insert(action, resource) do
[NormalizeEmail, HashPassword, AuditLog]
end
def middleware(_action, _resource), do: [AuditLog]
endCore Concepts
The API
Implement process_before/2 to transform data before the database operation:
defmodule AddTimestamp do
use EctoMiddleware
@impl EctoMiddleware
def process_before(changeset, _resolution) do
{:cont, Ecto.Changeset.put_change(changeset, :processed_at, DateTime.utc_now())}
end
endImplement process_after/2 to process data after the database operation:
defmodule NotifyAdmin do
use EctoMiddleware
@impl EctoMiddleware
def process_after({:ok, user} = result, _resolution) do
Task.start(fn -> send_notification(user) end)
{:cont, result}
end
def process_after(result, _resolution), do: {:cont, result}
endImplement process/2 to wrap around the entire operation:
defmodule MeasureLatency do
use EctoMiddleware
@impl EctoMiddleware
def process(resource, resolution) do
start_time = System.monotonic_time()
# Yields control to the next middleware (or Repo operation) in the chain
# before resuming here.
{result, _updated_resolution} = yield(resource, resolution)
duration = System.monotonic_time() - start_time
Logger.info("#{resolution.action} took #{duration}ns")
result
end
endImportant: When using process/2, you must call yield/2 to continue the middleware chain.
Halting Execution
Return {:halt, value} to stop the middleware chain and return a value immediately:
defmodule RequireAuth do
use EctoMiddleware
@impl EctoMiddleware
def process(resource, resolution) do
if authorized?(resolution) do
{result, _} = yield(resource, resolution)
result
else
{:halt, {:error, :unauthorized}}
end
end
defp authorized?(resolution) do
get_private(resolution, :current_user) != nil
end
endGuards for Operation Detection
EctoMiddleware.Utils provides guards to detect operations, especially useful for :insert_or_update:
defmodule ConditionalMiddleware do
use EctoMiddleware
@impl EctoMiddleware
def process_before(changeset, resolution) when is_insert(changeset, resolution) do
{:cont, add_created_metadata(changeset)}
end
def process_before(changeset, resolution) when is_update(changeset, resolution) do
{:cont, add_updated_metadata(changeset)}
end
def process_before(changeset, _resolution) do
{:cont, changeset}
end
endReturn Value Conventions
For process_before/2 and process_after/2
Returning bare values from middleware is supported, but to be explicit, return one of:
{:cont, value}- Continue to next middleware{:halt, value}- Stop execution, returnvalue{:cont, value, updated_resolution}- Continue with updatedEctoMiddleware.Resolution.t/0{:halt, value, updated_resolution}- Stop with updatedEctoMiddleware.Resolution.t/0
Bare values are always treated as {:cont, value}.
Migration from V1
V1 middleware continue to work but emit deprecation warnings.
Key differences:
- Use
EctoMiddleware.Repoinstead ofEctoMiddlewarein Repo modules - Implement
process_before/2,process_after/2, orprocess/2instead of the deprecatedmiddleware/2 - No need for
EctoMiddleware.Supermarker - Return
{:cont, value}or{:halt, value}instead of bare values
See the Migration Guide for details.
Silencing Deprecation Warnings
During migration, you can silence warnings temporarily:
# In config/config.exs
config :ecto_middleware, :silence_deprecation_warnings, trueThis should only be used during migration - deprecated APIs will be removed in v3.0.
Summary
Functions
Stubs out the necessary behaviours and imports for implementing an EctoMiddleware
middleware.
Types
Callbacks
@callback middleware( resource :: EctoMiddleware.Repo.resource(), resolution :: EctoMiddleware.Resolution.t() ) :: EctoMiddleware.Repo.resource()
@callback process(resource :: term(), resolution :: EctoMiddleware.Resolution.t()) :: middleware_result()
@callback process_after(result :: term(), resolution :: EctoMiddleware.Resolution.t()) :: middleware_result()
@callback process_before(resource :: term(), resolution :: EctoMiddleware.Resolution.t()) :: middleware_result()
Functions
Stubs out the necessary behaviours and imports for implementing an EctoMiddleware
middleware.
For backwards compatibility, if used in an Ecto.Repo module, it will emit a deprecation
warning and delegate to use EctoMiddleware.Repo instead. This behaviour will be removed in v3.0.