# `Lotus.Middleware`
[🔗](https://github.com/elixir-lotus/lotus/blob/v1.0.0/lib/lotus/middleware.ex#L1)

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
    end

## Pipeline 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:

1. 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.
2. The unified `:after_discover` event — 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.

# `compiled_entry`

```elixir
@type compiled_entry() :: {module(), term()}
```

# `discover_kind`

```elixir
@type discover_kind() ::
  :list_schemas | :list_tables | :describe_table | :list_relations
```

# `event`

```elixir
@type event() ::
  :before_query
  | :after_query
  | :after_list_schemas
  | :after_list_tables
  | :after_describe_table
  | :after_list_relations
  | :after_discover
```

# `middleware_spec`

```elixir
@type middleware_spec() :: {module(), keyword()}
```

# `pipeline_result`

```elixir
@type pipeline_result() :: {:cont, map()} | {:halt, term()}
```

# `compile`

```elixir
@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.

# `run`

```elixir
@spec run(event(), map()) :: {:cont, map()} | {:halt, term()}
```

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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
