EctoMiddleware.Engine (ecto_middleware v2.0.0)

View Source

Internal 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, true

This 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:

  1. c:Ecto.Repo.insert/2 is called.
  2. EctoMiddleware.process/2 is invoked on LoggerMiddleware.
    • It logs "Before insert".
    • It calls yield/2 to continue execution.
  3. EctoMiddleware.process/2 is invoked on AuthMiddleware.
    • It checks authorization.
    • It calls yield/2 to continue execution.
  4. EctoMiddleware.process/2 is invoked on VirtualFieldMiddleware.
    • It immediately calls yield/2 to continue execution.
  5. There aren't any more middleware to execute, so the super function (the actual c:Ecto.Repo.insert/2 call) is invoked.
    • The database insert occurs, returning {:ok, user}.
  6. Control returns to VirtualFieldMiddleware.
    • It resolves some virtual fields on the user struct.
    • It returns the user struct w/ virtual fields up the chain.
  7. Control returns to AuthMiddleware.
    • It doesn't do anything further, so it returns the user struct w/ virtual fields up the chain.
  8. Control returns to LoggerMiddleware.process/2.
    • It logs "After insert: {:ok, user w/ virtual fields}".
    • It returns the user struct up the chain.
  9. The caller of c:Ecto.Repo.insert/2 sees 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
end

The 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

Validates each middleware implements the required callbacks.

Yields execution to the next middleware in the chain.

Functions

validate_middleware!(middlewares)

@spec validate_middleware!([term()]) :: [term()]

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.

yield(resource, resolution)

@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/2 function is {:halt, value}, execution is halted.
  • If there are no more middleware to execute, the super function is invoked.
  • Otherwise, execution continues to the next middleware in the chain.