defmodule Plausible.Audit.Entry do @moduledoc """ Persistent Audit Entry schema """ use Ecto.Schema import Ecto.Changeset @type t :: %__MODULE__{ name: String.t(), entity: String.t(), entity_id: String.t(), meta: map(), change: map(), user_id: integer(), team_id: integer(), datetime: NaiveDateTime.t() } @primary_key {:id, :binary_id, autogenerate: true} schema "audit_entries" do field :name, :string field :entity, :string field :entity_id, :string field :meta, :map field :change, :map, default: %{} # default 0 is still useful in tests? field :user_id, :integer, default: 0 field :team_id, :integer, default: 0 field :datetime, :naive_datetime_usec field :actor_type, Ecto.Enum, default: :system, values: [:system, :user] end def changeset(name, params) do context = get_context() params = Map.merge( %{ team_id: context[:current_team] && context.current_team.id, user_id: context[:current_user] && context.current_user.id, actor_type: if(context[:current_user], do: "user", else: "system") }, params ) %__MODULE__{name: name} |> cast(params, [:entity, :entity_id, :meta, :user_id, :team_id, :actor_type]) |> validate_required([:name, :entity, :entity_id, :actor_type]) |> put_change(:datetime, NaiveDateTime.utc_now()) end def new(name, %{__struct__: struct, id: id}, params \\ %{}) do changeset(name, Map.merge(%{entity: to_str(struct), entity_id: to_str(id)}, params)) end def include_change(audit_entry, %Ecto.Changeset{} = related_changeset) do audit_entry |> change() |> put_change(:change, Plausible.Audit.encode(related_changeset)) end def include_change(audit_entry, %{__struct__: _} = struct) do # inserts hardly ever preload associations, so raising on not loaded is not useful audit_entry |> change() |> put_change(:change, Plausible.Audit.encode(struct, raise_on_not_loaded?: false)) end def include_change(audit_entry, map) when is_map(map) do # sometimes a hand-crafted map is passed; we don't need to encode then audit_entry |> change() |> put_change(:change, map) end def persist!(entry) do Plausible.Repo.insert!(entry) end defp get_context() do case :logger.get_process_metadata() do %{:__audit__ => audit_context} -> audit_context %{} -> %{} :undefined -> %{} end end def set_context(kv) when is_map(kv) do :logger.update_process_metadata(%{:__audit__ => kv}) end defp to_str(x) when is_binary(x), do: x defp to_str(x) when is_atom(x), do: inspect(x) defp to_str(x), do: to_string(x) end