Lotus.Middleware (Lotus v1.0.0)

Copy Markdown View Source

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

EventTriggeredPayload keys
:before_queryFirst, before sanitization, preflight and execution:statement (%Lotus.Query.Statement{}), :source, :context, :vars
:after_queryAfter execution, before result returned to caller:result, :statement (%Lotus.Query.Statement{}), :source, :context, :vars
:after_list_schemasAfter schema discovery and visibility filtering:schemas, :source, :scope, :context
:after_list_tablesAfter table discovery and visibility filtering:tables, :source, :scope, :context
:after_describe_tableAfter table schema introspection and column visibility:columns, :table_name, :schema, :source, :scope, :context
:after_list_relationsAfter relation discovery and visibility filtering:relations, :source, :scope, :context
:after_discoverAfter 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.

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

compiled_entry()

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

discover_kind()

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

event()

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

middleware_spec()

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

pipeline_result()

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

Functions

compile(middleware_config)

@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(event, payload)

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