Mailglass.Repo (Mailglass v2.0.0)

Copy Markdown View Source

Thin facade over the host-configured Ecto.Repo.

Mailglass does not own a Repo — the host application does. Every context module that needs Postgres routes through this facade, which resolves the real Repo via Application.get_env(:mailglass, :repo) at call time.

Runtime resolution is deliberate: tests inject a test repo through config/test.exs, host apps inject their Repo through config :mailglass, repo: MyApp.Repo, and neither path requires recompiling mailglass.

This module re-exports only what mailglass itself uses. Callers that need lower-level operations call the host Repo directly.

SQLSTATE 45A01 translation

Every write that touches mailglass_events can raise the immutability trigger (BEFORE UPDATE OR DELETE raises SQLSTATE 45A01). The facade rescues %Postgrex.Error{} at every call site and reraises as Mailglass.EventLedgerImmutableError so callers pattern-match a mailglass-owned error, never the raw Postgrex struct. The translation is centralized in translate_postgrex_error/2 — adding new write functions means wiring the same rescue clause.

Schema prefix injection (v2.0)

Every delegated read/write injects prefix: Mailglass.Config.schema() via Keyword.put_new, routing all operations to the configured Postgres schema ("mailglass" by default, "public" for pre-2.0 opt-out). An explicit caller-supplied :prefix wins over the injected default. This is the FACADE-01/FACADE-02 load-bearing mechanism for schema isolation.

transact/2, multi/2, and query!/2 do NOT inject prefix — the prefix must be threaded per-step in Ecto.Multi builders (see multi_opts/1). A future raw caller passing a mailglass table to query!/2 must qualify the table inline; the facade cannot rewrite arbitrary SQL.

Summary

Functions

Delegates to the host Repo's aggregate/4 (with optional opts for schema prefix).

Delegates to the host Repo's all/2.

Delegates to the host Repo's delete/2, translating event-ledger immutability errors.

Delegates to the host Repo's delete_all/2. Used by Mailglass.Webhook.Pruner for retention-policy DELETEs against mailglass_webhook_events.

Delegates to the host Repo's get/3.

Delegates to the host Repo's insert/2, translating event-ledger immutability errors.

Executes an Ecto.Multi against the host-configured repo and returns the canonical {:ok, changes} / {:error, step, reason, changes} shape.

Returns opts with prefix: Mailglass.Config.schema() injected via Keyword.put_new, for use as the step-level opts in Ecto.Multi builders in domain modules (Events, Outbound, Suppression.Escalation).

Delegates to the host Repo's one/2.

Delegates to the host Repo's query!/2. Raw passthrough — no SQLSTATE translation.

Delegates to Ecto.Repo.transact/2 on the host-configured Repo.

Delegates to the host Repo's update/2, translating event-ledger immutability errors.

Functions

aggregate(queryable, aggregate, field, opts \\ [])

(since 1.4.5)
@spec aggregate(Ecto.Queryable.t(), atom(), atom(), keyword()) :: term() | nil

Delegates to the host Repo's aggregate/4 (with optional opts for schema prefix).

all(queryable, opts \\ [])

(since 0.1.0)
@spec all(
  Ecto.Queryable.t(),
  keyword()
) :: [struct()]

Delegates to the host Repo's all/2.

delete(struct_or_changeset, opts \\ [])

(since 0.1.0)
@spec delete(
  struct() | Ecto.Changeset.t(),
  keyword()
) :: {:ok, struct()} | {:error, Ecto.Changeset.t()}

Delegates to the host Repo's delete/2, translating event-ledger immutability errors.

delete_all(queryable, opts \\ [])

(since 0.1.0)
@spec delete_all(
  Ecto.Queryable.t(),
  keyword()
) :: {non_neg_integer(), nil | [term()]}

Delegates to the host Repo's delete_all/2. Used by Mailglass.Webhook.Pruner for retention-policy DELETEs against mailglass_webhook_events.

Does NOT translate SQLSTATE 45A01 — that trigger fires only on mailglass_events UPDATE/DELETE, not mailglass_webhook_events (which is intentionally mutable + prunable per CONTEXT split).

get(queryable, id, opts \\ [])

(since 0.1.0)
@spec get(Ecto.Queryable.t(), term(), keyword()) :: struct() | nil

Delegates to the host Repo's get/3.

insert(struct_or_changeset, opts \\ [])

(since 0.1.0)
@spec insert(
  Ecto.Changeset.t() | struct(),
  keyword()
) :: {:ok, struct()} | {:error, Ecto.Changeset.t()}

Delegates to the host Repo's insert/2, translating event-ledger immutability errors.

multi(multi, opts \\ [])

(since 0.1.0)
@spec multi(
  Ecto.Multi.t(),
  keyword()
) :: {:ok, map()} | {:error, atom(), any(), map()}

Executes an Ecto.Multi against the host-configured repo and returns the canonical {:ok, changes} / {:error, step, reason, changes} shape.

Added in so Mailglass.Outbound can compose two Multis via a public function — repo/0 is deliberately private to keep the facade narrow.

Raises Mailglass.ConfigError{type: :missing} when :repo is not configured (same path as transact/2). Event-ledger-immutability errors (SQLSTATE 45A01) are translated via the same rescue as the other write helpers.

NOTE: multi/2 does NOT inject prefix at the executor level — Ecto.Multi does not propagate executor opts into inner step SQL. Use multi_opts/1 to thread prefix per-step in Multi builder chains.

multi_opts(opts \\ [])

(since 2.0.0)
@spec multi_opts(keyword()) :: keyword()

Returns opts with prefix: Mailglass.Config.schema() injected via Keyword.put_new, for use as the step-level opts in Ecto.Multi builders in domain modules (Events, Outbound, Suppression.Escalation).

Ecto.Multi does NOT propagate the transaction/executor opts into inner step SQL. Every builder step that writes a mailglass table must carry its own :prefix; call multi_opts/1 (or multi_opts()) to build that opts list rather than constructing it by hand.

An explicit caller-supplied :prefix wins over the injected default (Keyword.put_new semantics).

Examples

Ecto.Multi.new()
|> Ecto.Multi.insert(:delivery, changeset, Repo.multi_opts())
|> Ecto.Multi.insert_all(:events, Event, rows, Repo.multi_opts(on_conflict: :nothing))

one(queryable, opts \\ [])

(since 0.1.0)
@spec one(
  Ecto.Queryable.t(),
  keyword()
) :: struct() | nil

Delegates to the host Repo's one/2.

query!(sql, params \\ [])

(since 0.1.0)
@spec query!(String.t(), [term()]) :: term()

Delegates to the host Repo's query!/2. Raw passthrough — no SQLSTATE translation.

Intentionally does NOT rescue %Postgrex.Error{}: the webhook ingest Multi calls query!/2 from inside Repo.transact/1 to run SET LOCAL statement_timeout = '2s' + SET LOCAL lock_timeout = '500ms'. Those SET LOCAL statements never produce SQLSTATE 45A01 — the immutability trigger fires only on UPDATE/DELETE against mailglass_events rows — so translation would add latency for no gain and muddle the semantics. Callers that need the trigger's translated error use insert/2, update/2, delete/2, transact/1, or multi/1 instead.

NOTE: the facade cannot rewrite arbitrary SQL. A future raw caller that touches a mailglass table must schema-qualify the table inline (e.g. "mailglass.mailglass_events") — the current two callers both run schema-agnostic SET LOCAL statements.

transact(fun, opts \\ [])

(since 0.1.0)
@spec transact(
  (-> {:ok, any()} | {:error, any()}),
  keyword()
) :: {:ok, any()} | {:error, any()}

Delegates to Ecto.Repo.transact/2 on the host-configured Repo.

Preferred over Ecto.Repo.transaction/2 because Ecto 3.13+ transact/2 accepts a zero-arity function that returns {:ok, result} or {:error, reason} and rolls the transaction back on :error without requiring Ecto.Repo.rollback/1.

Raises Mailglass.ConfigError of type :missing when :repo is not configured. Raises Mailglass.EventLedgerImmutableError when the mailglass_events immutability trigger fires (SQLSTATE 45A01).

Examples

Mailglass.Repo.transact(fn ->
  {:ok, inserted} = Mailglass.Events.append(multi)
  {:ok, inserted}
end)

update(changeset, opts \\ [])

(since 0.1.0)
@spec update(
  Ecto.Changeset.t(),
  keyword()
) :: {:ok, struct()} | {:error, Ecto.Changeset.t()}

Delegates to the host Repo's update/2, translating event-ledger immutability errors.