storage: add behavior boundary and postgres adapter skeleton

This commit is contained in:
2026-03-13 20:20:58 +01:00
parent 307372fdfe
commit 7ec588805b
13 changed files with 365 additions and 1 deletions

63
lib/parrhesia/storage.ex Normal file
View File

@@ -0,0 +1,63 @@
defmodule Parrhesia.Storage do
@moduledoc """
Storage boundary entrypoint.
Domain/runtime code should resolve behavior modules through this module instead of
depending on concrete adapter implementations directly.
"""
@default_modules [
events: Parrhesia.Storage.Adapters.Postgres.Events,
moderation: Parrhesia.Storage.Adapters.Postgres.Moderation,
groups: Parrhesia.Storage.Adapters.Postgres.Groups,
admin: Parrhesia.Storage.Adapters.Postgres.Admin
]
@spec events() :: module()
def events, do: fetch_module!(:events, Parrhesia.Storage.Events)
@spec moderation() :: module()
def moderation, do: fetch_module!(:moderation, Parrhesia.Storage.Moderation)
@spec groups() :: module()
def groups, do: fetch_module!(:groups, Parrhesia.Storage.Groups)
@spec admin() :: module()
def admin, do: fetch_module!(:admin, Parrhesia.Storage.Admin)
defp fetch_module!(key, behavior) do
module =
:parrhesia
|> Application.get_env(:storage, [])
|> Keyword.get(key, Keyword.fetch!(@default_modules, key))
ensure_behavior!(module, behavior, key)
end
defp ensure_behavior!(module, behavior, key) do
case Code.ensure_loaded(module) do
{:module, _loaded_module} ->
if implements_behavior?(module, behavior) do
module
else
raise ArgumentError,
"configured storage module #{inspect(module)} for #{inspect(key)} " <>
"does not implement #{inspect(behavior)}"
end
{:error, reason} ->
raise ArgumentError,
"configured storage module #{inspect(module)} for #{inspect(key)} could not be loaded: " <>
"#{inspect(reason)}"
end
end
defp implements_behavior?(module, behavior) do
module
|> module_info_attributes()
|> Keyword.get(:behaviour, [])
|> Enum.member?(behavior)
end
defp module_info_attributes(module), do: module.module_info(:attributes)
end

View File

@@ -0,0 +1,19 @@
defmodule Parrhesia.Storage.Adapters.Postgres.Admin do
@moduledoc """
PostgreSQL-backed implementation for `Parrhesia.Storage.Admin`.
Implementation is intentionally staged; callbacks currently return
`{:error, :not_implemented}` until NIP-86 management storage lands.
"""
@behaviour Parrhesia.Storage.Admin
@impl true
def execute(_context, _method, _params), do: {:error, :not_implemented}
@impl true
def append_audit_log(_context, _entry), do: {:error, :not_implemented}
@impl true
def list_audit_logs(_context, _opts), do: {:error, :not_implemented}
end

View File

@@ -0,0 +1,31 @@
defmodule Parrhesia.Storage.Adapters.Postgres.Events do
@moduledoc """
PostgreSQL-backed implementation for `Parrhesia.Storage.Events`.
Implementation is intentionally staged; callbacks currently return
`{:error, :not_implemented}` until migrations and query paths land.
"""
@behaviour Parrhesia.Storage.Events
@impl true
def put_event(_context, _event), do: {:error, :not_implemented}
@impl true
def get_event(_context, _event_id), do: {:error, :not_implemented}
@impl true
def query(_context, _filters, _opts), do: {:error, :not_implemented}
@impl true
def count(_context, _filters, _opts), do: {:error, :not_implemented}
@impl true
def delete_by_request(_context, _event), do: {:error, :not_implemented}
@impl true
def vanish(_context, _event), do: {:error, :not_implemented}
@impl true
def purge_expired(_opts), do: {:error, :not_implemented}
end

View File

@@ -0,0 +1,31 @@
defmodule Parrhesia.Storage.Adapters.Postgres.Groups do
@moduledoc """
PostgreSQL-backed implementation for `Parrhesia.Storage.Groups`.
Implementation is intentionally staged; callbacks currently return
`{:error, :not_implemented}` until group/membership schema lands.
"""
@behaviour Parrhesia.Storage.Groups
@impl true
def put_membership(_context, _membership), do: {:error, :not_implemented}
@impl true
def get_membership(_context, _group_id, _pubkey), do: {:error, :not_implemented}
@impl true
def delete_membership(_context, _group_id, _pubkey), do: {:error, :not_implemented}
@impl true
def list_memberships(_context, _group_id), do: {:error, :not_implemented}
@impl true
def put_role(_context, _role), do: {:error, :not_implemented}
@impl true
def delete_role(_context, _group_id, _pubkey, _role), do: {:error, :not_implemented}
@impl true
def list_roles(_context, _group_id, _pubkey), do: {:error, :not_implemented}
end

View File

@@ -0,0 +1,46 @@
defmodule Parrhesia.Storage.Adapters.Postgres.Moderation do
@moduledoc """
PostgreSQL-backed implementation for `Parrhesia.Storage.Moderation`.
Implementation is intentionally staged; callbacks currently return
`{:error, :not_implemented}` until table design and policy paths land.
"""
@behaviour Parrhesia.Storage.Moderation
@impl true
def ban_pubkey(_context, _pubkey), do: {:error, :not_implemented}
@impl true
def unban_pubkey(_context, _pubkey), do: {:error, :not_implemented}
@impl true
def pubkey_banned?(_context, _pubkey), do: {:error, :not_implemented}
@impl true
def allow_pubkey(_context, _pubkey), do: {:error, :not_implemented}
@impl true
def disallow_pubkey(_context, _pubkey), do: {:error, :not_implemented}
@impl true
def pubkey_allowed?(_context, _pubkey), do: {:error, :not_implemented}
@impl true
def ban_event(_context, _event_id), do: {:error, :not_implemented}
@impl true
def unban_event(_context, _event_id), do: {:error, :not_implemented}
@impl true
def event_banned?(_context, _event_id), do: {:error, :not_implemented}
@impl true
def block_ip(_context, _ip_address), do: {:error, :not_implemented}
@impl true
def unblock_ip(_context, _ip_address), do: {:error, :not_implemented}
@impl true
def ip_blocked?(_context, _ip_address), do: {:error, :not_implemented}
end

View File

@@ -0,0 +1,16 @@
defmodule Parrhesia.Storage.Admin do
@moduledoc """
Storage callbacks used by relay management endpoints (NIP-86 backing).
"""
@type context :: map()
@type method :: atom() | binary()
@type params :: map()
@type result :: map() | list() | term()
@type audit_entry :: map()
@type reason :: term()
@callback execute(context(), method(), params()) :: {:ok, result()} | {:error, reason()}
@callback append_audit_log(context(), audit_entry()) :: :ok | {:error, reason()}
@callback list_audit_logs(context(), keyword()) :: {:ok, [audit_entry()]} | {:error, reason()}
end

View File

@@ -0,0 +1,22 @@
defmodule Parrhesia.Storage.Events do
@moduledoc """
Storage callbacks for event persistence and query operations.
"""
@type context :: map()
@type event_id :: binary()
@type event :: map()
@type filter :: map()
@type query_opts :: keyword()
@type count_result :: non_neg_integer() | %{optional(atom()) => term()}
@type reason :: term()
@callback put_event(context(), event()) :: {:ok, event()} | {:error, reason()}
@callback get_event(context(), event_id()) :: {:ok, event() | nil} | {:error, reason()}
@callback query(context(), [filter()], query_opts()) :: {:ok, [event()]} | {:error, reason()}
@callback count(context(), [filter()], query_opts()) ::
{:ok, count_result()} | {:error, reason()}
@callback delete_by_request(context(), event()) :: {:ok, non_neg_integer()} | {:error, reason()}
@callback vanish(context(), event()) :: {:ok, non_neg_integer()} | {:error, reason()}
@callback purge_expired(query_opts()) :: {:ok, non_neg_integer()} | {:error, reason()}
end

View File

@@ -0,0 +1,22 @@
defmodule Parrhesia.Storage.Groups do
@moduledoc """
Storage callbacks for NIP-29/NIP-43 group membership and role state.
"""
@type context :: map()
@type group_id :: binary()
@type pubkey :: binary()
@type membership :: map()
@type role :: map()
@type reason :: term()
@callback put_membership(context(), membership()) :: {:ok, membership()} | {:error, reason()}
@callback get_membership(context(), group_id(), pubkey()) ::
{:ok, membership() | nil} | {:error, reason()}
@callback delete_membership(context(), group_id(), pubkey()) :: :ok | {:error, reason()}
@callback list_memberships(context(), group_id()) :: {:ok, [membership()]} | {:error, reason()}
@callback put_role(context(), role()) :: {:ok, role()} | {:error, reason()}
@callback delete_role(context(), group_id(), pubkey(), binary()) :: :ok | {:error, reason()}
@callback list_roles(context(), group_id(), pubkey()) :: {:ok, [role()]} | {:error, reason()}
end

View File

@@ -0,0 +1,27 @@
defmodule Parrhesia.Storage.Moderation do
@moduledoc """
Storage callbacks for moderation and access-control state.
"""
@type context :: map()
@type pubkey :: binary()
@type event_id :: binary()
@type ip_address :: binary()
@type reason :: term()
@callback ban_pubkey(context(), pubkey()) :: :ok | {:error, reason()}
@callback unban_pubkey(context(), pubkey()) :: :ok | {:error, reason()}
@callback pubkey_banned?(context(), pubkey()) :: {:ok, boolean()} | {:error, reason()}
@callback allow_pubkey(context(), pubkey()) :: :ok | {:error, reason()}
@callback disallow_pubkey(context(), pubkey()) :: :ok | {:error, reason()}
@callback pubkey_allowed?(context(), pubkey()) :: {:ok, boolean()} | {:error, reason()}
@callback ban_event(context(), event_id()) :: :ok | {:error, reason()}
@callback unban_event(context(), event_id()) :: :ok | {:error, reason()}
@callback event_banned?(context(), event_id()) :: {:ok, boolean()} | {:error, reason()}
@callback block_ip(context(), ip_address()) :: :ok | {:error, reason()}
@callback unblock_ip(context(), ip_address()) :: :ok | {:error, reason()}
@callback ip_blocked?(context(), ip_address()) :: {:ok, boolean()} | {:error, reason()}
end