EctoMiddleware.Engine (ecto_middleware v2.1.0)
View SourceInternal engine for validating and executing middleware chains.
See EctoMiddleware for information on writing middleware. This module is used
internally by EctoMiddleware.Repo to execute middleware chains.
Silencing Deprecation Warnings
During migration from v1 to v2, you may want to silence deprecation warnings. This can be done via application configuration:
# In config/config.exs
config :ecto_middleware, :silence_deprecation_warnings, trueThis will suppress all deprecation warnings from EctoMiddleware. Note that this
should only be used temporarily during migration - the deprecated APIs will be
removed in v3.0.
Middleware Execution
Middleware are executed in a chain. You can think of this as a matroshka doll, where each middleware wraps the next one in the chain.
Each middleware is expected to call yield/2 to continue execution to the next middleware
in the chain, but may also choose to halt execution by not calling yield/2 and returning
a value directly.
For example, given the following middleware chain:
[LoggerMiddleware, AuthMiddleware, VirtualFieldMiddleware]Execution proceeds as follows:
c:Ecto.Repo.insert/2is called.EctoMiddleware.process/2is invoked onLoggerMiddleware.- It logs "Before insert".
- It calls
yield/2to continue execution.
EctoMiddleware.process/2is invoked onAuthMiddleware.- It checks authorization.
- It calls
yield/2to continue execution.
EctoMiddleware.process/2is invoked onVirtualFieldMiddleware.- It immediately calls
yield/2to continue execution.
- It immediately calls
- There aren't any more middleware to execute, so the super function (the actual
c:Ecto.Repo.insert/2call) is invoked.- The database insert occurs, returning
{:ok, user}.
- The database insert occurs, returning
- Control returns to
VirtualFieldMiddleware.- It resolves some virtual fields on the
userstruct. - It returns the user struct w/ virtual fields up the chain.
- It resolves some virtual fields on the
- Control returns to
AuthMiddleware.- It doesn't do anything further, so it returns the user struct w/ virtual fields up the chain.
- Control returns to
LoggerMiddleware.process/2.- It logs "After insert: {:ok, user w/ virtual fields}".
- It returns the user struct up the chain.
- The caller of
c:Ecto.Repo.insert/2sees the function result as{:ok, user_with_virtual_fields}.
For simplicity, middleware authors can either implement the EctoMiddleware.process_before/2 or
EctoMiddleware.process_after/2 callbacks to only operate in the "before" or "after" phases.
Alternatively, you can implement EctoMiddleware.process/2 for full control, but you must
call yield/2 at the appropriate time. Not calling yield/2 will halt execution at that
middleware.
Important: yield/2 returns {result, updated_resolution}. You must destructure this
tuple when calling yield.
See the EctoMiddleware docs for more information.
Backwards Compatibility
Prior versions of EctoMiddleware (v1.x) had a different middleware contract and execution engine.
In those versions of the library, all middleware were expected to implement middleware/2 in a
middleware module (note: this was not a behaviour callback). This function was expected to take some
"resource" and return that "resource" transformed by the middleware.
Middleware executed exactly once, either before or after the super function, depending on their
position relative to EctoMiddleware.Super in the middleware list.
An example of this follows:
defmodule Repo do
use EctoMiddleware
@impl EctoMiddleware
def middleware(:insert, _resource) do
[BeforeMiddleware, EctoMiddleware.Super, AfterMiddleware]
end
end
defmodule BeforeMiddleware do
def middleware(resource, _resolution) do
transform_before(resource)
end
end
defmodule AfterMiddleware do
def middleware(resource, _resolution) do
transform_after(resource)
end
endThe current version of EctoMiddleware (v2.x) supports v1 middleware for backwards compatibility.
This is implemented by dynamically replacing any v1 middleware with a v2 middleware that wraps
the v1 middleware and calls its middleware/2 function either before or after the super function
is executed.
This functionality is temporary and will be removed in v3.0, at which point all middleware must implement the v2 middleware contract.
Using v1 middleware in this way will emit a deprecation warning. We strongly recommend updating
any existing v1 middleware to implement the v2 middleware contract to avoid this warning and
ensure compatibility with future versions of EctoMiddleware.
Summary
Functions
Drops middleware that have not opted into bulk operations when the action is a bulk
action (insert_all, update_all, delete_all).
Runs a v2 middleware's process_before/2 -> yield/2 -> process_after/2 phases.
Validates each middleware implements the required callbacks.
Yields execution to the next middleware in the chain.
Functions
Drops middleware that have not opted into bulk operations when the action is a bulk
action (insert_all, update_all, delete_all).
For non-bulk actions the list is returned unchanged. For bulk actions, only middleware
that declared use EctoMiddleware, bulk_operations: true are kept; everything else
(single-record middleware, v1 middleware) is filtered out so it is never handed a
schema/source or queryable it doesn't expect.
EctoMiddleware.Super is always kept. It is not a middleware but the marker
validate_middleware!/1 uses to split a v1 chain into its :before and :after phases;
dropping it here would leave that reduce stuck in :before, so an opted-in v1 middleware
positioned after Super would be handed the resource instead of the operation's result.
This makes bulk interception opt-in per middleware: a Repo's middleware/2 may keep
returning its usual list (including a catch-all clause) and existing middleware remain
unaffected by bulk operations until they explicitly opt in.
@spec run_phases( resource :: term(), resolution :: EctoMiddleware.Resolution.t(), before_fun :: (term(), EctoMiddleware.Resolution.t() -> term()), after_fun :: (term(), EctoMiddleware.Resolution.t() -> term()), normalize_fun :: (term() -> {:cont, term()} | {:halt, term()}) ) :: {:cont, term(), EctoMiddleware.Resolution.t()} | {:halt, term(), EctoMiddleware.Resolution.t()} | {:halt, term()}
Runs a v2 middleware's process_before/2 -> yield/2 -> process_after/2 phases.
This is the body of the default process/2 generated by use EctoMiddleware. It lives
here (compiled once, with the phase callbacks as opaque function values) rather than being
inlined into each middleware module so that the compiler's type checker cannot narrow the
callbacks' return types to {:cont, _} and flag the {:halt, _} branches as dead clauses
for middleware that never halt.
before_fun/after_fun/normalize_fun are the using module's
process_before/2, process_after/2, and normalize/1.
Returns a {:cont | :halt, value, resolution} 3-tuple on the paths where yield/2 ran,
so that resolution updates made deeper in the chain -- notably before_output, set at the
innermost yield/2 -- propagate back out to middleware wrapping this one. Returning a bare
value here would strand those updates, silently breaking any outer middleware that reads
them (for example ecto_hooks, which dispatches its after_* hooks off before_output).
Validates each middleware implements the required callbacks.
This function is executed prior to execution of any given middleware chain to ensure that the specified middleware pipeline is valid.
Note: This function also wraps any v1 middleware to be compatible with the v2 middleware execution engine. This functionality is temporary and will be removed in v3 when v1 middleware is no longer supported.
@spec yield(resource :: term(), resolution :: EctoMiddleware.Resolution.t()) :: {term(), EctoMiddleware.Resolution.t()}
Yields execution to the next middleware in the chain.
Returns a tuple of {result, updated_resolution} where the resolution may have been
updated during middleware execution (e.g., for V1 compatibility fields like before_output).
- If the result of a middleware's
process/2function is{:halt, value}, execution is halted. - If there are no more middleware to execute, the
superfunction is invoked. - Otherwise, execution continues to the next middleware in the chain.