Generic middleware pipeline for query execution and schema discovery hooks.
Each middleware module implements init/1 and call/2, following
the standard Plug pattern:
defmodule MyApp.AuditPlug do
def init(opts), do: opts
def call(payload, opts) do
{:cont, payload} # continue to next middleware
# or
{:halt, "reason"} # stop pipeline, Lotus returns {:error, reason}
end
endPipeline Events
| Event | Triggered | Payload keys |
|---|---|---|
:before_query | First, before sanitization, preflight and execution | :statement (%Lotus.Query.Statement{}), :source, :context, :vars |
:after_query | After execution, before result returned to caller | :result, :statement (%Lotus.Query.Statement{}), :source, :context, :vars |
:after_list_schemas | After schema discovery and visibility filtering | :schemas, :source, :scope, :context |
:after_list_tables | After table discovery and visibility filtering | :tables, :source, :scope, :context |
:after_describe_table | After table schema introspection and column visibility | :columns, :table_name, :schema, :source, :scope, :context |
:after_list_relations | After relation discovery and visibility filtering | :relations, :source, :scope, :context |
:after_discover | After any discovery call, following the kind-specific :after_list_* event | :kind, :result, :source, :scope, :context |
:vars is the map of bound query variables, by name, after defaults and
caller-supplied values are merged (%{"start_date" => "2026-01-01"}).
It is %{} for a raw statement run through Lotus.run_statement/3. A plug
can enforce rules on the values a caller picked (date-range limits, tenant
checks) without parsing the statement.
Discovery event ordering
Discovery calls (Lotus.list_schemas/2, list_tables/2, describe_table/3,
list_relations/2) fire two events each:
- The kind-specific event (
:after_list_schemas,:after_list_tables,:after_describe_table, or:after_list_relations) — receives the kind-specific payload with a key matching the returned value. - The unified
:after_discoverevent — receives a uniform payload%{kind:, source:, result:, context:}so a single middleware module can handle every discovery kind by dispatching on:kind.
If any middleware in either phase halts, later middleware do not run and
the caller receives {:error, reason}.
Configuration
config :lotus,
middleware: %{
before_query: [
{MyApp.AccessControlPlug, []},
{MyApp.QueryAuditPlug, [repo: MyApp.AuditRepo]}
],
after_query: [
{MyApp.QueryAuditPlug, [repo: MyApp.AuditRepo]}
],
after_list_tables: [
{MyApp.TableFilterPlug, []}
]
}Context
A :context key carries opaque user data (e.g. the current user) through
to middleware. Lotus never inspects this value.
Summary
Functions
Compiles all middleware by calling init/1 on each module and stores
the result in :persistent_term for fast runtime access.
Runs the middleware pipeline for the given event.
Types
@type discover_kind() ::
:list_schemas | :list_tables | :describe_table | :list_relations
@type event() ::
:before_query
| :after_query
| :after_list_schemas
| :after_list_tables
| :after_describe_table
| :after_list_relations
| :after_discover
Functions
@spec compile(map()) :: :ok
Compiles all middleware by calling init/1 on each module and stores
the result in :persistent_term for fast runtime access.
An empty config clears the compiled pipeline, so reloading a config that no longer declares middleware actually turns it off.
Runs the middleware pipeline for the given event.
Returns {:cont, payload} if all middleware passed, or
{:halt, reason} if any middleware halted the pipeline.
When no middleware is configured for the event, returns {:cont, payload}
with zero overhead beyond the map lookup.