EctoMiddleware behaviour (ecto_middleware v1.0.0) View Source

This module provides the EctoMiddleware behaviour, which extends any module that uses Ecto.Repo with the capability to hook into the execution of any Ecto.Repo callback (that reads/writes to said repo).

Setup and initialization

To enable EctoMiddleware, you must use this module in any of your Ecto.Repo modules.

Once done, you will be able to customize the middleware you wish to run either before, or after any given Ecto.Repo callback (again, that reads/writes to said repo).

By default, EctoMiddleware will not run any middleware. You must explicitly define them yourself.

You're also able to customize the middleware you wish to run based on the given "action" or "resource" that an Ecto.Repo callback is being executed on.

The "action" of a given Ecto.Repo callback is the name of the function being executed, without the arity. For example, the "action" of get/3 is get and the "action" of get!/3 is get!.

The "resource" of a given Ecto.Repo is typically the first argument of the function being executed. For example, the "resource" of get/3 is a module that uses Ecto.Schema.

See the below example:

defmodule MyApp.Repo do
  use Ecto.Repo,
    otp_app: :my_app,
    adapter: Ecto.Adapters.Postgres

  use EctoMiddleware

  def middleware(action, _resource) when action in [:delete, :delete!] do
    [MyApp.EctoMiddleware.MaybeSoftDelete, EctoMiddleware.Super, MyApp.EctoMiddleware.Log]
  end

  def middleware(_action, _resource) do
    [EctoMiddleware.Super, MyApp.EctoMiddleware.Log]
  end
end

Any middleware preceding EctoMiddleware.Super will be executed before the given Ecto.Repo callback is executed.

Any middleware following EctoMiddleware.Super will be executed after the given Ecto.Repo callback is executed.

Writing Middleware

To write your own middleware, you must implement the EctoMiddleware behaviour in a module of your choosing.

The EctoMiddleware behaviour requires you to implement the middleware/2 callback, which takes the "resource" of the given Ecto.Repo callback as the first argument, and an EctoMiddleware.Resolution struct as the second argument.

All middleware must return a modified "resource" (or the original "resource" if no modifications were made).

Additionally, any configured middleware is run synchronously, in the order they are defined.

Please see the EctoMiddleware.Resolution struct for more information, but in short, the struct contains various bits of metadata about the given Ecto.Repo callback that is being executed, the inputs to the callback, and the result of the callback (if it has been executed).

Before Middleware

Any middleware preceding EctoMiddleware.Super will be executed before the given Ecto.Repo callback is executed.

Because these middlewares run prior to the Ecto.Repo callback, they are able to modify the inputs to the callback, or even short-circuit the callback entirely, however, they are not able to modify the result of the callback (as it has not been executed yet).

If you wish to modify the result of the callback, you must use an after middleware instead.

After Middleware

Any middleware following EctoMiddleware.Super will be executed after the given Ecto.Repo callback is executed.

Because these middlewares run after the Ecto.Repo callback, they are able to modify the result of the callback, however, they are not able to modify the inputs to the callback (as it has already been executed).

If you wish to modify the inputs to the callback, you must use a before middleware instead.

After middleware are additionally able to reference both the result of the callback, and the result of running any before middleware, making it an ideal place to perform any advanced processing or logging.

Considerations

When writing middleware, you should be aware of the following:

  • If you are modifying the "resource" of the given Ecto.Repo callback, you should ensure that the "resource" is still valid for the given Ecto.Repo callback.

  • Middleware are run synchronously, in the order they are defined, and as such, you should be mindful of the performance implications of your middleware.

  • Any middleware that raises an exception will cause the given Ecto.Repo callback to raise an exception as well, regardless of whether or not the given Ecto.Repo callback has been executed or semantically is expected to raise an exception.

  • Middleware may be a place where dialyzer warnings are suppressed, as it may not be possible for dialyzer to infer the types returned out of any given middleware.

  • It is not possible to modify the "action" of the given Ecto.Repo callback, only the "resource".

  • Ideally, middleware should be written in such a way that they are reusable across multiple Ecto.Repo modules, "actions", or "resources". It is not recommended to to write middleware that is too strongly coupled to the prior or future middleware expected to have/be run.

  • Due to how the given Ecto.Repo callback is executed, it is not at this time possible to provide any transactional guarantees for middleware. If you wish to perform any transactional work, you should do so within your application's business logic, and not within any middleware.

Testing

You may test your middleware modules in a variety of ways:

  • You can test your middleware modules in the context of your application's business logic, by stubbing any Ecto.Repo callbacks that you wish to test.

  • You can either test your middleware modules in isolation, stubbing any "action" or "resource" that you wish to test.

  • You can directly test them by way of executing the given Ecto.Repo callback against a test database, and asserting on the result.

  • You can use the EctoMiddleware.Resolution.execute_before!/1 and EctoMiddleware.Resolution.execute_after!/2 to directly test middleware.

In the future, more work will be done to enable easier testing of individual middleware.

Link to this section Summary

Functions

Enables the ability for a given Ecto.Repo to define and execute middleware.

Returns the configured middleware for the given repo.

Returns the configured middleware for the given repo, partitioning by whether or not the middleware is intended to run before or after the repo callback.

Link to this section Types

Specs

action() ::
  :all
  | :delete!
  | :delete
  | :get!
  | :get
  | :get_by!
  | :get_by
  | :insert!
  | :insert
  | :insert_or_update!
  | :insert_or_update
  | :one!
  | :one
  | :reload!
  | :reload
  | :preload
  | :update!
  | :update

Specs

middleware() :: [module()]

Specs

resource() ::
  %Ecto.Queryable{}
  | %Ecto.Changeset{}
  | %{__meta__: Ecto.Schema.Metadata}
  | {%Ecto.Queryable{}, Keyword.t()}

Link to this section Functions

Link to this macro

__using__(opts)

View Source (macro)

Enables the ability for a given Ecto.Repo to define and execute middleware.

Link to this function

middleware(repo, action, resource)

View Source

Specs

middleware(repo :: module(), action(), resource()) :: [middleware()]

Returns the configured middleware for the given repo.

Link to this function

partition_middleware(repo, action, resource)

View Source

Specs

partition_middleware(repo :: module(), action(), resource()) ::
  {[middleware()], [middleware()]}

Returns the configured middleware for the given repo, partitioning by whether or not the middleware is intended to run before or after the repo callback.

Link to this section Callbacks

Link to this callback

middleware(resource, resolution)

View Source

Specs

middleware(resource :: resource(), resolution :: EctoMiddleware.Resolution.t()) ::
  resource()