Mechanical cleanup, quick fixes & deduplication closes #531 #543
3 changed files with 124 additions and 113 deletions
98
lib/membership/setting/changes/jsonb_result.ex
Normal file
98
lib/membership/setting/changes/jsonb_result.ex
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
defmodule Mv.Membership.Setting.Changes.JsonbResult do
|
||||||
|
@moduledoc """
|
||||||
|
Shared normalization for the JSONB column values returned by the atomic
|
||||||
|
single-member-field settings updates.
|
||||||
|
|
||||||
|
PostgreSQL may return a JSONB column as an already-decoded map (atom or string
|
||||||
|
keys) or as a raw JSON string depending on the driver path. This helper
|
||||||
|
normalizes either form to a string-keyed map, returning `%{}` for unexpected
|
||||||
|
or undecodable input.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Ash.Error.Invalid
|
||||||
|
alias Ecto.Adapters.SQL
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Runs an atomic single-statement JSONB settings UPDATE inside an after_action
|
||||||
|
and maps the RETURNING row back onto the settings struct.
|
||||||
|
|
||||||
|
Shared by the single-member-field change modules, which differ only in the
|
||||||
|
SQL statement, its parameters, the row-to-settings mapping, and the error
|
||||||
|
labels. The three-branch result handling (row found / not found / SQL error)
|
||||||
|
is identical and lives here.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `:sql` - The UPDATE statement with a RETURNING clause (required).
|
||||||
|
- `:params` - The full parameter list for the statement (required).
|
||||||
|
- `:on_row` - 1-arity function mapping the RETURNING row (a list of column
|
||||||
|
values) to the updated settings struct (required).
|
||||||
|
- `:error_field` - Ash error field for the not-found / failure errors.
|
||||||
|
- `:not_found_message` - Error message when no row matched.
|
||||||
|
- `:error_message` - Error message when the SQL statement failed.
|
||||||
|
- `:log_message` - Log prefix written on SQL failure.
|
||||||
|
"""
|
||||||
|
@spec run_update(keyword()) ::
|
||||||
|
{:ok, map()} | {:error, Exception.t()}
|
||||||
|
def run_update(opts) do
|
||||||
|
sql = Keyword.fetch!(opts, :sql)
|
||||||
|
params = Keyword.fetch!(opts, :params)
|
||||||
|
on_row = Keyword.fetch!(opts, :on_row)
|
||||||
|
error_field = Keyword.fetch!(opts, :error_field)
|
||||||
|
not_found_message = Keyword.fetch!(opts, :not_found_message)
|
||||||
|
error_message = Keyword.fetch!(opts, :error_message)
|
||||||
|
log_message = Keyword.fetch!(opts, :log_message)
|
||||||
|
|
||||||
|
case SQL.query(Mv.Repo, sql, params) do
|
||||||
|
{:ok, %{rows: [row | _]}} ->
|
||||||
|
{:ok, on_row.(row)}
|
||||||
|
|
||||||
|
{:ok, %{rows: []}} ->
|
||||||
|
{:error,
|
||||||
|
Invalid.exception(
|
||||||
|
field: error_field,
|
||||||
|
message: not_found_message
|
||||||
|
)}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
Logger.error("#{log_message}: #{inspect(error)}")
|
||||||
|
|
||||||
|
{:error,
|
||||||
|
Invalid.exception(
|
||||||
|
field: error_field,
|
||||||
|
message: error_message
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Normalizes a JSONB column value to a string-keyed map.
|
||||||
|
"""
|
||||||
|
@spec normalize(map() | binary() | any()) :: map()
|
||||||
|
def normalize(updated_jsonb) do
|
||||||
|
case updated_jsonb do
|
||||||
|
map when is_map(map) ->
|
||||||
|
Enum.reduce(map, %{}, fn
|
||||||
|
{k, v}, acc when is_atom(k) -> Map.put(acc, Atom.to_string(k), v)
|
||||||
|
{k, v}, acc -> Map.put(acc, k, v)
|
||||||
|
end)
|
||||||
|
|
||||||
|
binary when is_binary(binary) ->
|
||||||
|
case Jason.decode(binary) do
|
||||||
|
{:ok, decoded} when is_map(decoded) ->
|
||||||
|
decoded
|
||||||
|
|
||||||
|
{:ok, _} ->
|
||||||
|
%{}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.warning("Failed to decode JSONB: #{inspect(reason)}")
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Logger.warning("Unexpected JSONB format: #{inspect(updated_jsonb)}")
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -19,9 +19,7 @@ defmodule Mv.Membership.Setting.Changes.UpdateSingleMemberField do
|
||||||
"""
|
"""
|
||||||
use Ash.Resource.Change
|
use Ash.Resource.Change
|
||||||
|
|
||||||
alias Ash.Error.Invalid
|
alias Mv.Membership.Setting.Changes.JsonbResult
|
||||||
alias Ecto.Adapters.SQL
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
def change(changeset, _opts, _context) do
|
def change(changeset, _opts, _context) do
|
||||||
with {:ok, field} <- get_and_validate_field(changeset),
|
with {:ok, field} <- get_and_validate_field(changeset),
|
||||||
|
|
@ -118,62 +116,21 @@ defmodule Mv.Membership.Setting.Changes.UpdateSingleMemberField do
|
||||||
|
|
||||||
uuid_binary = Ecto.UUID.dump!(settings.id)
|
uuid_binary = Ecto.UUID.dump!(settings.id)
|
||||||
|
|
||||||
case SQL.query(Mv.Repo, sql, [field, show_in_overview, required, uuid_binary]) do
|
JsonbResult.run_update(
|
||||||
{:ok, %{rows: [[updated_visibility, updated_required] | _]}} ->
|
sql: sql,
|
||||||
vis = normalize_jsonb_result(updated_visibility)
|
params: [field, show_in_overview, required, uuid_binary],
|
||||||
req = normalize_jsonb_result(updated_required)
|
on_row: fn [updated_visibility, updated_required | _] ->
|
||||||
|
%{
|
||||||
updated_settings = %{
|
|
||||||
settings
|
settings
|
||||||
| member_field_visibility: vis,
|
| member_field_visibility: JsonbResult.normalize(updated_visibility),
|
||||||
member_field_required: req
|
member_field_required: JsonbResult.normalize(updated_required)
|
||||||
}
|
}
|
||||||
|
end,
|
||||||
{:ok, updated_settings}
|
error_field: :member_field_required,
|
||||||
|
not_found_message: "Settings not found",
|
||||||
{:ok, %{rows: []}} ->
|
error_message: "Failed to update member field settings",
|
||||||
{:error,
|
log_message: "Failed to atomically update member field settings"
|
||||||
Invalid.exception(
|
)
|
||||||
field: :member_field_required,
|
|
||||||
message: "Settings not found"
|
|
||||||
)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
Logger.error("Failed to atomically update member field settings: #{inspect(error)}")
|
|
||||||
|
|
||||||
{:error,
|
|
||||||
Invalid.exception(
|
|
||||||
field: :member_field_required,
|
|
||||||
message: "Failed to update member field settings"
|
|
||||||
)}
|
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_jsonb_result(updated_jsonb) do
|
|
||||||
case updated_jsonb do
|
|
||||||
map when is_map(map) ->
|
|
||||||
Enum.reduce(map, %{}, fn
|
|
||||||
{k, v}, acc when is_atom(k) -> Map.put(acc, Atom.to_string(k), v)
|
|
||||||
{k, v}, acc -> Map.put(acc, k, v)
|
|
||||||
end)
|
|
||||||
|
|
||||||
binary when is_binary(binary) ->
|
|
||||||
case Jason.decode(binary) do
|
|
||||||
{:ok, decoded} when is_map(decoded) ->
|
|
||||||
decoded
|
|
||||||
|
|
||||||
{:ok, _} ->
|
|
||||||
%{}
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
Logger.warning("Failed to decode JSONB: #{inspect(reason)}")
|
|
||||||
%{}
|
|
||||||
end
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
Logger.warning("Unexpected JSONB format: #{inspect(updated_jsonb)}")
|
|
||||||
%{}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@ defmodule Mv.Membership.Setting.Changes.UpdateSingleMemberFieldVisibility do
|
||||||
"""
|
"""
|
||||||
use Ash.Resource.Change
|
use Ash.Resource.Change
|
||||||
|
|
||||||
alias Ash.Error.Invalid
|
alias Mv.Membership.Setting.Changes.JsonbResult
|
||||||
alias Ecto.Adapters.SQL
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
def change(changeset, _opts, _context) do
|
def change(changeset, _opts, _context) do
|
||||||
with {:ok, field} <- get_and_validate_field(changeset),
|
with {:ok, field} <- get_and_validate_field(changeset),
|
||||||
|
|
@ -106,59 +104,17 @@ defmodule Mv.Membership.Setting.Changes.UpdateSingleMemberFieldVisibility do
|
||||||
# Convert UUID string to binary for PostgreSQL
|
# Convert UUID string to binary for PostgreSQL
|
||||||
uuid_binary = Ecto.UUID.dump!(settings.id)
|
uuid_binary = Ecto.UUID.dump!(settings.id)
|
||||||
|
|
||||||
case SQL.query(Mv.Repo, sql, [field, show_in_overview, uuid_binary]) do
|
JsonbResult.run_update(
|
||||||
{:ok, %{rows: [[updated_jsonb] | _]}} ->
|
sql: sql,
|
||||||
updated_visibility = normalize_jsonb_result(updated_jsonb)
|
params: [field, show_in_overview, uuid_binary],
|
||||||
|
on_row: fn [updated_jsonb | _] ->
|
||||||
# Update the settings struct with the new visibility
|
%{settings | member_field_visibility: JsonbResult.normalize(updated_jsonb)}
|
||||||
updated_settings = %{settings | member_field_visibility: updated_visibility}
|
end,
|
||||||
{:ok, updated_settings}
|
error_field: :member_field_visibility,
|
||||||
|
not_found_message: "Settings not found",
|
||||||
{:ok, %{rows: []}} ->
|
error_message: "Failed to update visibility",
|
||||||
{:error,
|
log_message: "Failed to atomically update member_field_visibility"
|
||||||
Invalid.exception(
|
)
|
||||||
field: :member_field_visibility,
|
|
||||||
message: "Settings not found"
|
|
||||||
)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
Logger.error("Failed to atomically update member_field_visibility: #{inspect(error)}")
|
|
||||||
|
|
||||||
{:error,
|
|
||||||
Invalid.exception(
|
|
||||||
field: :member_field_visibility,
|
|
||||||
message: "Failed to update visibility"
|
|
||||||
)}
|
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_jsonb_result(updated_jsonb) do
|
|
||||||
case updated_jsonb do
|
|
||||||
map when is_map(map) ->
|
|
||||||
# Convert atom keys to strings if needed
|
|
||||||
Enum.reduce(map, %{}, fn
|
|
||||||
{k, v}, acc when is_atom(k) -> Map.put(acc, Atom.to_string(k), v)
|
|
||||||
{k, v}, acc -> Map.put(acc, k, v)
|
|
||||||
end)
|
|
||||||
|
|
||||||
binary when is_binary(binary) ->
|
|
||||||
case Jason.decode(binary) do
|
|
||||||
{:ok, decoded} when is_map(decoded) ->
|
|
||||||
decoded
|
|
||||||
|
|
||||||
# Not a map after decode
|
|
||||||
{:ok, _} ->
|
|
||||||
%{}
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
Logger.warning("Failed to decode JSONB: #{inspect(reason)}")
|
|
||||||
%{}
|
|
||||||
end
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
Logger.warning("Unexpected JSONB format: #{inspect(updated_jsonb)}")
|
|
||||||
%{}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue