defmodule AshPaperTrail.Resource.Changes.CreateNewVersion do
@moduledoc "Creates a new version whenever a resource is created, deleted, or updated"
use Ash.Resource.Change
require Ash.Query
require Logger
def change(changeset, _, _) do
if changeset.action_type in [:create, :destroy] ||
(changeset.action_type == :update &&
changeset.action.name in AshPaperTrail.Resource.Info.on_actions(changeset.resource)) do
create_new_version(changeset)
else
changeset
end
end
defp create_new_version(changeset) do
Ash.Changeset.after_action(changeset, fn changeset, result ->
if changeset.action_type in [:create, :destroy] ||
(changeset.action_type == :update && changeset.context.changed?) do
{:ok, result, build_notifications(changeset, result)}
else
{:ok, result}
end
end)
end
defp build_notifications(changeset, result) do
version_resource = AshPaperTrail.Resource.Info.version_resource(changeset.resource)
version_resource_attributes =
version_resource |> Ash.Resource.Info.attributes() |> Enum.map(& &1.name)
version_changeset = Ash.Changeset.new(version_resource)
to_skip =
Ash.Resource.Info.primary_key(changeset.resource) ++
AshPaperTrail.Resource.Info.ignore_attributes(changeset.resource)
attributes_as_attributes =
AshPaperTrail.Resource.Info.attributes_as_attributes(changeset.resource)
change_tracking_mode = AshPaperTrail.Resource.Info.change_tracking_mode(changeset.resource)
belongs_to_actors =
AshPaperTrail.Resource.Info.belongs_to_actor(changeset.resource)
actor = changeset.context[:private][:actor]
resource_attributes =
changeset.resource
|> Ash.Resource.Info.attributes()
{input, private} =
resource_attributes
|> Enum.filter(&(&1.name in attributes_as_attributes))
|> Enum.reduce({%{}, %{}}, &build_inputs(changeset, &1, &2))
changes =
resource_attributes
|> Enum.reject(&(&1.name in to_skip))
|> build_changes(change_tracking_mode, changeset)
input =
Enum.reduce(belongs_to_actors, input, fn belongs_to_actor, input ->
with true <- is_struct(actor) && actor.__struct__ == belongs_to_actor.destination,
relationship when not is_nil(relationship) <-
Ash.Resource.Info.relationship(version_resource, belongs_to_actor.name) do
primary_key = Map.get(actor, hd(Ash.Resource.Info.primary_key(actor.__struct__)))
source_attribute = Map.get(relationship, :source_attribute)
Map.put(input, source_attribute, primary_key)
else
_ ->
input
end
end)
|> Map.merge(%{
version_source_id: Map.get(result, hd(Ash.Resource.Info.primary_key(changeset.resource))),
version_action_type: changeset.action.type,
version_action_name: changeset.action.name,
changes: changes
})
{_, notifications} =
version_changeset
|> Ash.Changeset.for_create(:create, input,
tenant: changeset.tenant,
authorize?: false,
actor: actor
)
|> Ash.Changeset.force_change_attributes(Map.take(private, version_resource_attributes))
|> changeset.api.create!(return_notifications?: true)
notifications
end
defp build_inputs(
changeset,
%{private?: true} = attribute,
{input, private}
) do
{input,
Map.put(
private,
attribute.name,
Ash.Changeset.get_attribute(changeset, attribute.name)
)}
end
defp build_inputs(changeset, attribute, {input, private}) do
{
Map.put(
input,
attribute.name,
Ash.Changeset.get_attribute(changeset, attribute.name)
),
private
}
end
defp build_changes(attributes, :changes_only, changeset) do
AshPaperTrail.ChangeBuilders.ChangesOnly.build_changes(attributes, changeset)
end
defp build_changes(attributes, :snapshot, changeset) do
AshPaperTrail.ChangeBuilders.Snapshot.build_changes(attributes, changeset)
end
defp build_changes(attributes, :full_diff, changeset) do
AshPaperTrail.ChangeBuilders.FullDiff.build_changes(attributes, changeset)
end
end