Bier.Config (bier v0.1.0)

Copy Markdown View Source

Defines and validates the internal configuration needed by Bier processes.

The options given to Bier.start_link/1 are validated via new!/2 using the internal schema, if the given options are valid, the configuration will be wrapped in a Elixir.Bier.Config struct, which subsequently will be stored in the Bier.Registry. Internal modules will also consume this configuration to work properly.

Summary

Types

Options given to Bandit

t()

Functions

Decode a base64 jwt-secret (jwt-secret-is-base64), accepting URL-safe characters the way PostgREST does. Shared with the CLI so --dump-config rejects an undecodable secret identically (case 1718).

Translate a server-host value into a :gen_tcp bind address. PostgREST's key is a Warp HostPreference: "*"/"*4"/"!4" bind any IPv4 interface and "*6"/"!6" any IPv6 one (the IPv4-vs-IPv6 preference the * forms express has no counterpart in a single bind call). Anything else is an IP literal or a host name resolved at boot; an unresolvable name raises, which surfaces as a boot failure.

Non-raising variant of new!/2: validates the given options against the schema plus the semantic validators and returns {:ok, config} or {:error, message}. The standalone/CLI boot path uses this to turn a bad config into a clean fatal message instead of a raised exception.

Validates the given options based on the internal schema definition

Parse a socket-mode value the way PostgREST does (Haskell's readOct): the longest leading run of octal digits is the value — so "599" reads as 5 (range error) while "800" has no octal prefix at all — and the result must lie between 0o600 and 0o777. Returns the integer file mode for File.chmod/2.

Render a list of schema names as a search_path GUC value: each name quoted as an identifier, comma-separated — PostgREST's escapeIdentList (Query/PreQuery.hs). search_path takes an identifier list, so the names cannot be bound individually; the value is pre-quoted here and then bound as a single parameter (or sent as a connection parameter).

admin-server-port must differ from the main server port. Mirrors PostgREST (test_cli.py:test_server_port_and_admin_port_same_value; conformance case 1717). NimbleOptions validates fields independently, so this cross-field check lives here, shared by new!/2 and the CLI. Either port may be nil when not configured.

db-channel must be a non-empty channel name of at most 63 bytes (the Postgres identifier limit) and must not contain a null byte. Postgrex.Notifications.listen/3 enforces both the length bound and the null-byte restriction at runtime by raising — validating at boot turns a would-be listener crash-loop into a fast ArgumentError. Library-enforced (PostgREST does not validate this key), like the admin-port collision rule.

db-schemas may not expose Postgres' own catalog schemas. PostgREST v16.0 rejects pg_catalog and information_schema while parsing the key — after the comma split, so a restricted name anywhere in the list aborts startup (Config.hs parseDbSchemas). v14.12 accepted them and failed per request instead. Mirrors conformance cases 1733/1734.

Each events-channels entry must be a usable Postgres notification channel name: non-empty, at most 63 bytes (the identifier limit), no null bytes, and no double quotes (Postgrex.Notifications.listen/3 wraps the name in double quotes without escaping). Validated at boot so a bad entry is a fast ArgumentError instead of a listener crash-loop. Bier-specific key.

events-path is the reserved top-level path segment for the SSE endpoint, so it must be non-empty and must not contain /.

jwt-aud may be any plain string, but a value containing ':' must parse as a valid absolute URI. Mirrors PostgREST conformance case 1709.

A symmetric (text) JWT secret must be at least 32 bytes long — PostgREST counts the secret's octets (BS.length in Config.hs), not characters, so Bier does too. nil (no secret configured) is allowed. Mirrors PostgREST conformance case 1708.

openapi-server-proxy-uri must be an absolute http(s) URI with a host — PostgREST's isMalformedProxyUri check. nil (not configured) is allowed. Mirrors conformance case 1716.

Boolean-style wrapper over parse_socket_mode/2 for the validation chain.

Types

router_opts()

@type router_opts() :: [port: pos_integer(), scheme: :http | :https]

Options given to Bandit

t()

@type t() :: %Bier.Config{
  admin_server_port: pos_integer() | nil,
  app_settings: %{optional(String.t()) => String.t()},
  cancel_on_disconnect: boolean(),
  client_error_verbosity: String.t(),
  database: String.t(),
  db_anon_role: String.t() | nil,
  db_channel: String.t(),
  db_channel_enabled: boolean(),
  db_extra_search_path: [String.t()],
  db_max_rows: pos_integer() | nil,
  db_max_rows_by_schema: %{optional(String.t()) => pos_integer()},
  db_plan_enabled: boolean(),
  db_pool_max_idletime: pos_integer() | nil,
  db_pre_request: String.t() | nil,
  db_profile_default: String.t() | nil,
  db_profile_schemas: [String.t()] | nil,
  db_root_spec: String.t() | nil,
  db_safe_update_tables: [String.t()],
  db_schema_aliases: %{optional(String.t()) => String.t()},
  db_schemas: [String.t(), ...],
  db_tx_end: :commit | :rollback,
  events_channels: [String.t()],
  events_heartbeat_interval: pos_integer(),
  events_path: String.t(),
  hostname: String.t(),
  jwt_aud: String.t() | nil,
  jwt_cache_max_entries: integer(),
  jwt_role_claim_path: Bier.JWT.RoleClaim.path(),
  jwt_secret: String.t() | nil,
  jwt_secret_is_base64: boolean(),
  log_level: :crit | :error | :warn | :info | :debug,
  log_query: boolean(),
  name: module(),
  openapi_mode: String.t(),
  openapi_security_active: boolean(),
  openapi_server_proxy_uri: String.t() | nil,
  openapi_version: String.t(),
  password: String.t() | nil,
  pool_size: pos_integer(),
  port: pos_integer(),
  router: router_opts(),
  server_cors_allowed_origins: String.t() | nil,
  server_host: String.t(),
  server_timing_enabled: boolean(),
  server_trace_header: String.t() | nil,
  server_unix_socket: String.t() | nil,
  server_unix_socket_mode: String.t(),
  ssl: boolean(),
  url_use_legacy_target_names: boolean(),
  username: String.t() | nil
}

Functions

decode_base64_secret(secret)

@spec decode_base64_secret(String.t()) :: {:ok, binary()} | {:error, String.t()}

Decode a base64 jwt-secret (jwt-secret-is-base64), accepting URL-safe characters the way PostgREST does. Shared with the CLI so --dump-config rejects an undecodable secret identically (case 1718).

host_address(host)

@spec host_address(String.t()) :: :inet.socket_address()

Translate a server-host value into a :gen_tcp bind address. PostgREST's key is a Warp HostPreference: "*"/"*4"/"!4" bind any IPv4 interface and "*6"/"!6" any IPv6 one (the IPv4-vs-IPv6 preference the * forms express has no counterpart in a single bind call). Anything else is an IP literal or a host name resolved at boot; an unresolvable name raises, which surfaces as a boot failure.

new(opts, schema)

@spec new(Keyword.t(), Keyword.t()) :: {:ok, t()} | {:error, String.t()}

Non-raising variant of new!/2: validates the given options against the schema plus the semantic validators and returns {:ok, config} or {:error, message}. The standalone/CLI boot path uses this to turn a bad config into a clean fatal message instead of a raised exception.

new!(opts, schema)

@spec new!(Keyword.t(), Keyword.t()) :: t() | no_return()

Validates the given options based on the internal schema definition

In case the given options are valid, it returns a Bier.Config struct, otherwise it will raise an exception.

parse_socket_mode(mode, key \\ "server-unix-socket-mode")

@spec parse_socket_mode(String.t(), String.t()) ::
  {:ok, non_neg_integer()} | {:error, String.t()}

Parse a socket-mode value the way PostgREST does (Haskell's readOct): the longest leading run of octal digits is the value — so "599" reads as 5 (range error) while "800" has no octal prefix at all — and the result must lie between 0o600 and 0o777. Returns the integer file mode for File.chmod/2.

key names the config key the value came from. PostgREST v16.0 applies the same parseSocketFileMode to server-unix-socket-mode and the new admin-server-unix-socket-mode, building both failure messages from the key (Config.hs parseSocketFileMode), so the caller decides which key is reported. Mirrors conformance cases 1714/1715 (server) and 1738 (admin).

search_path(schemas)

@spec search_path([String.t()]) :: String.t() | nil

Render a list of schema names as a search_path GUC value: each name quoted as an identifier, comma-separated — PostgREST's escapeIdentList (Query/PreQuery.hs). search_path takes an identifier list, so the names cannot be bound individually; the value is pre-quoted here and then bound as a single parameter (or sent as a connection parameter).

Duplicates are dropped, keeping first position: the request's schema leads and an extra-search-path entry naming the same schema must not shift it. Returns nil for an empty list, which callers read as "leave search_path alone".

validate_admin_server_port(admin_port, server_port)

@spec validate_admin_server_port(pos_integer() | nil, pos_integer() | nil) ::
  :ok | {:error, String.t()}

admin-server-port must differ from the main server port. Mirrors PostgREST (test_cli.py:test_server_port_and_admin_port_same_value; conformance case 1717). NimbleOptions validates fields independently, so this cross-field check lives here, shared by new!/2 and the CLI. Either port may be nil when not configured.

validate_db_channel(channel)

@spec validate_db_channel(String.t() | nil) :: :ok | {:error, String.t()}

db-channel must be a non-empty channel name of at most 63 bytes (the Postgres identifier limit) and must not contain a null byte. Postgrex.Notifications.listen/3 enforces both the length bound and the null-byte restriction at runtime by raising — validating at boot turns a would-be listener crash-loop into a fast ArgumentError. Library-enforced (PostgREST does not validate this key), like the admin-port collision rule.

validate_db_schemas(schemas)

@spec validate_db_schemas([String.t()]) :: :ok | {:error, String.t()}

db-schemas may not expose Postgres' own catalog schemas. PostgREST v16.0 rejects pg_catalog and information_schema while parsing the key — after the comma split, so a restricted name anywhere in the list aborts startup (Config.hs parseDbSchemas). v14.12 accepted them and failed per request instead. Mirrors conformance cases 1733/1734.

validate_events_channels(channels)

@spec validate_events_channels([String.t()]) :: :ok | {:error, String.t()}

Each events-channels entry must be a usable Postgres notification channel name: non-empty, at most 63 bytes (the identifier limit), no null bytes, and no double quotes (Postgrex.Notifications.listen/3 wraps the name in double quotes without escaping). Validated at boot so a bad entry is a fast ArgumentError instead of a listener crash-loop. Bier-specific key.

validate_events_path(path)

@spec validate_events_path(String.t()) :: :ok | {:error, String.t()}

events-path is the reserved top-level path segment for the SSE endpoint, so it must be non-empty and must not contain /.

validate_jwt_aud(aud)

@spec validate_jwt_aud(String.t() | nil) :: :ok | {:error, String.t()}

jwt-aud may be any plain string, but a value containing ':' must parse as a valid absolute URI. Mirrors PostgREST conformance case 1709.

validate_jwt_secret(secret)

@spec validate_jwt_secret(String.t() | nil) :: :ok | {:error, String.t()}

A symmetric (text) JWT secret must be at least 32 bytes long — PostgREST counts the secret's octets (BS.length in Config.hs), not characters, so Bier does too. nil (no secret configured) is allowed. Mirrors PostgREST conformance case 1708.

validate_proxy_uri(uri)

@spec validate_proxy_uri(String.t() | nil) :: :ok | {:error, String.t()}

openapi-server-proxy-uri must be an absolute http(s) URI with a host — PostgREST's isMalformedProxyUri check. nil (not configured) is allowed. Mirrors conformance case 1716.

validate_socket_mode(mode, key \\ "server-unix-socket-mode")

@spec validate_socket_mode(String.t(), String.t()) :: :ok | {:error, String.t()}

Boolean-style wrapper over parse_socket_mode/2 for the validation chain.