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 givenEcto.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 givenEcto.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
andEctoMiddleware.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
Enables the ability for a given Ecto.Repo
to define and execute middleware.
Specs
middleware(repo :: module(), action(), resource()) :: [middleware()]
Returns the configured middleware for the given repo.
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
Specs
middleware(resource :: resource(), resolution :: EctoMiddleware.Resolution.t()) :: resource()