Packages

Adds callbacks/hooks to Ecto: `after_insert`, `after_update`, `after_delete`, `after_get`, `before_insert`, `before_update`, `before_delete`. Useful for setting virtual fields and centralising logic.

Current section

Files

Jump to
ecto_hooks lib ecto_hooks.ex
Raw

lib/ecto_hooks.ex

defmodule EctoHooks do
@moduledoc """
Module exposing various utility functions for interacting with, and introspecting
the state of EctoHooks.
For more extensive usage instructions, please see associated documentation for
`EctoHooks.Repo`.
"""
alias EctoHooks.Delta
alias EctoHooks.State
@hooks [
:before_delete,
:before_insert,
:before_update,
:after_delete,
:after_get,
:after_insert,
:after_update
]
# Hack: I don't want to add any dependencies I don't need to this library, so
# we're just hackily generating a struct via this literal. Otherwise we'll
# need to bring in Ecto as a dep, or ignore Dialyzer warnings.
@callback before_insert(queryable :: %{__struct__: Ecto.Queryable}) :: %{
__struct__: Ecto.Queryable
}
@callback before_update(queryable :: %{__struct__: Ecto.Queryable}) :: %{
__struct__: Ecto.Queryable
}
@callback before_delete(queryable :: %{__struct__: Ecto.Queryable}) :: %{
__struct__: Ecto.Queryable
}
@callback after_get(schema_struct :: struct(), delta :: Delta.t()) :: struct()
@callback after_insert(schema_struct :: struct(), delta :: Delta.t()) :: struct()
@callback after_update(schema_struct :: struct(), delta :: Delta.t()) :: struct()
@callback after_delete(schema_struct :: struct(), delta :: Delta.t()) :: struct()
@optional_callbacks before_insert: 1,
before_update: 1,
before_delete: 1,
after_get: 2,
after_insert: 2,
after_update: 2,
after_delete: 2
@doc """
Alternative interface for initializing EctoHooks when `use`-ed in a module as follows:
```elixir
def MyApp.Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Postgres
use EctoHooks
end
```
Please see `EctoHooks.Repo` for more information about EctoHooks usage, examples,
and considerations.
"""
defmacro __using__(_opts) do
quote do
require EctoHooks.Repo
EctoHooks.Repo.override_repo_callbacks()
end
end
@doc """
Ensures EctoHooks run for all future Repo operations in the current process.
Internally, we use it to scope re-enabling hooks per execution, as we disabled
them as a mitigation for infinitely looping hooks caused by hooks triggering
other hooks. This happens when provided `[global: false]`, which should only be
done internally.
"""
@spec enable_hooks(Keyword.t()) :: :ok
defdelegate enable_hooks(opts \\ [global: true]), to: State
@doc """
Disables EctoHooks running for all future Repo operations in the current process.
Internally, this is called with the options `[global: false]` which gets
automatically cleared after triggering any `Ecto.Repo` callback.
"""
@spec disable_hooks(Keyword.t()) :: :ok
defdelegate disable_hooks(opts \\ [global: true]), to: State
@doc """
Returns a boolean indicating if any `before_*` or `after_*` hooks are allowed
to execute in the current process.
Note that calls to `Ecto.Repo` callbacks may reset or change the state of this flag,
and that this function is primarily exposed for introspection or debugging purposes.
"""
@spec hooks_enabled?() :: boolean()
defdelegate hooks_enabled?, to: State
@doc """
Utility function which returns true if currently executing inside the context of an
Ecto Hook.
"""
@spec in_hook?() :: boolean()
defdelegate in_hook?, to: State
@doc """
Utility function which returns the "nesting" of the current EctoHooks context.
By default, every hook will "acquire" an EctoHook context and increment a ref count.
These ref counts are automatically decremented once a hook finishes running.
This is provided as a lower level alternative the `enable_hooks/1`, `disable_hooks/1`,
and `hooks_enabled?/0` functions.
"""
@spec hooks_ref_count() :: pos_integer()
defdelegate hooks_ref_count, to: State
for hook <- @hooks do
@doc false
def unquote(hook)(struct, caller_function, delta \\ nil)
def unquote(hook)(struct, caller_function, delta) when is_struct(struct) do
hook = unquote(hook)
struct
|> get_schema_module()
|> execute_hook(hook, struct, delta && Delta.new!(caller_function, hook, delta))
end
def unquote(hook)(data, _delta, _caller_function) do
data
end
end
defp get_schema_module(struct) when is_struct(struct) do
if struct.__struct__ == Ecto.Changeset && struct.data do
struct.data.__struct__
else
struct.__struct__
end
end
defp execute_hook(schema, hook, param_1, param_2) do
if State.hooks_enabled?() do
:ok = State.disable_hooks(global: false)
:ok = State.acquire_hook()
apply(schema, hook, [param_1 | (param_2 && [param_2]) || []])
else
param_1
end
rescue
_e in [UndefinedFunctionError, FunctionClauseError] ->
param_1
after
:ok = State.enable_hooks(global: false)
:ok = State.release_hook()
end
end